Análisis detallado de CSS: matrix3d() para obtener una barra de desplazamiento personalizada perfecta para el marco.

Las barras de desplazamiento personalizadas son muy inusuales y se debe, en su mayoría, a que son una de las partes restantes de la Web que no son estilizadas (Te estoy mirando, selector de fecha). Puedes usar JavaScript para compilar el tuyo, pero es costoso, de baja fidelidad y puede parecer lento. En este artículo, aprovecharemos algunas matrices de CSS no convencionales para compilar un desplazador personalizado que no requiere JavaScript durante el desplazamiento, solo algunos códigos de configuración.

Resumen

¿No te interesan las pequeñas cosas? ¿Solo quieres mirar la demostración del gato Nyan y obtener la biblioteca? Puedes encontrar el código de la demostración en nuestro repositorio de GitHub.

LAM;WRA (largo y matemático; se leerá de todos modos)

Hace un tiempo, creamos un desplazador de paralaje. (¿Leíste ese artículo? Es realmente bueno, vale la pena. Al enviar los elementos hacia atrás con transformaciones CSS en 3D, estos se movieron más lento que nuestra velocidad de desplazamiento real.

Resumen

Empecemos con un resumen de cómo funcionaba la barra de desplazamiento con paralaje.

Como se muestra en la animación, logramos el efecto de paralaje empujando los elementos "hacia atrás" en el espacio 3D, a lo largo del eje Z. Desplazarse un documento es efectivamente una traducción a lo largo del eje Y. Por lo tanto, si nos desplazamos hacia abajo, por ejemplo, 100 px, cada elemento se traducirá hacia arriba por 100 px. Eso se aplica a todos los elementos, incluso a los que están "más atrás". Sin embargo, dado que están más alejados de la cámara, su movimiento en pantalla observado será de menos de 100 px, lo que producirá el efecto de paralaje deseado.

Por supuesto, mover un elemento de vuelta al espacio también hará que parezca más pequeño, lo que corregimos escalando una copia de seguridad del elemento. Descubrimos la matemática exacta cuando creamos el desplazamiento de paralaje, por lo que no repetiré todos los detalles.

Paso 0: ¿Qué queremos hacer?

Barras de desplazamiento. Eso es lo que vamos a crear. Pero, ¿alguna vez has pensado realmente en lo que hacen? Claro que no. Las barras de desplazamiento son un indicador de cuánto del contenido disponible está visible actualmente y cuánto progreso hiciste como lector. Si te desplazas hacia abajo, también lo hará la barra de desplazamiento para indicar que estás progresando hacia el final. Si todo el contenido se ajusta al viewport, la barra de desplazamiento suele estar oculta. Si el contenido tiene el doble de la altura del viewport, la barra de desplazamiento cubre la mitad de la altura del viewport. El contenido equivalente a 3 veces la altura del viewport escala la barra de desplazamiento a 1⁄3 del viewport, etc. Verás el patrón. En lugar de desplazarte, también puedes hacer clic y arrastrar la barra de desplazamiento para desplazarte por el sitio más rápido. Esta es una cantidad sorprendente de comportamiento para un elemento discreto como ese. Peleemos de a una batalla por vez.

Paso 1: Invertir la situación

De acuerdo, podemos hacer que los elementos se muevan más lento que la velocidad de desplazamiento con las transformaciones CSS 3D, como se describe en el artículo sobre desplazamiento con paralaje. ¿Podemos invertir también la dirección? Resulta que sí, y esa es nuestra manera de crear una barra de desplazamiento personalizada perfecta para fotogramas. Para entender cómo funciona esto, primero tenemos que cubrir algunos conceptos básicos de CSS 3D.

Para obtener cualquier tipo de proyección de perspectiva en el sentido matemático, lo más probable es que termines usando coordenadas homogéneas. No explicaré qué son ni por qué funcionan, pero puedes considerarlas como coordenadas en 3D con una cuarta coordenada adicional llamada w. Esta coordenada debe ser 1, excepto si quieres tener distorsión de perspectiva. No debemos preocuparnos por los detalles de w, ya que no usaremos otro valor que no sea 1. Por lo tanto, a partir de ahora, todos los puntos son vectores de 4 dimensiones [x, y, z, w=1] y, en consecuencia, las matrices también deben ser de 4x4.

Una ocasión en la que puedes ver que CSS usa coordenadas homogéneas de forma interna es cuando defines tus propias matrices 4 × 4 en una propiedad de transformación con la función matrix3d(). matrix3d toma 16 argumentos (porque la matriz es 4 x 4) y especifica una columna después de la otra. Así que podemos usar esta función para especificar manualmente rotaciones, traslaciones, etc. Pero lo que también nos permite hacer es usar esa coordenada w.

Antes de poder usar matrix3d(), necesitamos un contexto 3D, ya que sin él no habría distorsión de perspectiva ni necesidad de coordenadas homogéneas. Para crear un contexto 3D, necesitamos un contenedor con una perspective y algunos elementos dentro que podamos transformar en el espacio 3D recién creado. Por ejemplo:

Es un fragmento de código CSS que distorsiona un elemento div mediante el atributo de perspectiva de CSS.

El motor de CSS procesa los elementos dentro de un contenedor de perspectiva de la siguiente manera:

  • Convierte cada esquina (vertex) de un elemento en coordenadas homogéneas [x,y,z,w], relativas al contenedor de perspectiva.
  • Aplica todas las transformaciones de los elementos como matrices de derecha a izquierda.
  • Si el elemento de perspectiva se puede desplazar, aplica una matriz de desplazamiento.
  • Aplica la matriz de perspectiva.

La matriz de desplazamiento es una traslación a lo largo del eje y. Si nos desplazamos hacia abajo 400 px, todos los elementos deben mover hacia arriba 400 px. La matriz de perspectiva es una matriz que "atraye" los puntos más cerca del punto de fuga a medida que se alejan en el espacio 3D. Esto logra ambos efectos de hacer que los elementos parezcan más pequeños cuando están más atrás y también hace que "se muevan más lento" cuando se traducen. Por lo tanto, si se retrocede un elemento, una traducción de 400 px hará que el elemento se mueva solo 300 px en la pantalla.

Si quieres conocer todos los detalles, debes leer la spec sobre el modelo de procesamiento de transformaciones de CSS. Sin embargo, para este artículo, simplificamos el algoritmo anterior.

Nuestro cuadro se encuentra dentro de un contenedor de perspectiva con el valor p para el atributo perspective. Supongamos que el contenedor se puede desplazar y se desplaza hacia abajo n píxeles.

La matriz de perspectiva multiplicada por la matriz de desplazamiento multiplicada por la matriz de transformación de elementos es igual a cuatro por cuatro matrices de identidad con menos uno sobre p en la tercera fila de la tercera columna por cuatro por cuatro matrices de identidad con menos n en la segunda fila por cuarta columna por la matriz de transformación de elementos.

La primera matriz es la matriz de perspectiva y la segunda es la matriz de desplazamiento. En resumen: el trabajo de la matriz de desplazamiento es hacer que un elemento se mueva hacia arriba cuando nos desplazamos hacia abajo, de ahí que aparezca el signo negativo.

Sin embargo, para la barra de desplazamiento, queremos lo opuesto: queremos que el elemento se desplace hacia abajo cuando nos desplacemos hacia abajo. Aquí es donde podemos usar un truco: invertir la coordenada w de las esquinas de la caja. Si la coordenada w es -1, todas las traducciones tendrán efecto en la dirección opuesta. Entonces, ¿cómo lo hacemos? El motor de CSS se encarga de convertir las esquinas del cuadro en coordenadas homogéneas y establece w en 1. Es hora de que matrix3d() se destaque

.box {
  transform:
    matrix3d(
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, -1
    );
}

Esta matriz no hará más que negar w. Por lo tanto, cuando el motor de CSS convierte cada esquina en un vector con la forma [x,y,z,1], la matriz lo convierte en [x,y,z,-1].

Matriz de identidad de cuatro por cuatro con menos uno sobre p en la tercera fila de la tercera columna por cuatro por cuatro matriz de identidad con menos n en la segunda fila, cuarta columna por cuatro por cuatro matrices de identidad con menos uno en la cuarta fila, la cuarta columna por el vector de cuatro dimensiones x, y, z, 1 es igual a cuatro por cuatro filas de la matriz de identidad con menos uno sobre p en la cuarta fila de la columna y tercera columna, menos uno sobre la cuarta fila de p en la cuarta columna y la tercera columna, menos uno sobre la cuarta fila

Enumeré un paso intermedio para mostrar el efecto de nuestra matriz de transformación de elementos. Si no te sientes cómodo con la matemática de matrices, no hay problema. El momento Eureka es que en la última línea terminamos agregando el desplazamiento n a nuestra coordenada y en lugar de restarlo. El elemento se traducirá hacia hacia abajo si nos desplazamos hacia abajo.

Sin embargo, si colocamos esta matriz en nuestro ejemplo, el elemento no se mostrará. Esto se debe a que la especificación de CSS requiere que cualquier vértice con w < 0 bloquee la renderización del elemento. Además, como nuestra coordenada z actualmente es 0 y p es 1, w será -1.

Por suerte, podemos elegir el valor de z. Para asegurarnos de terminar con w=1, necesitamos establecer z = -2.

.box {
  transform:
    matrix3d(
      1, 0, 0, 0,
      0, 1, 0, 0,
      0, 0, 1, 0,
      0, 0, 0, -1
    )
    translateZ(-2px);
}

Mira, nuestra caja está de vuelta.

Paso 2: Haz que se mueva

Ahora nuestra caja está allí y se ve de la misma manera que lo haría sin ninguna transformación. En este momento, no se puede desplazar el contenedor de perspectiva, por lo que no podemos verlo, pero sabemos que nuestro elemento irá en otra dirección cuando se desplace. Hagamos que el contenedor se desplace. Podemos agregar un elemento espaciador que ocupe espacio:

<div class="container">
    <div class="box"></div>
    <span class="spacer"></span>
</div>

<style>
/* … all the styles from the previous example … */
.container {
    overflow: scroll;
}
.spacer {
    display: block;
    height: 500px;
}
</style>

Ahora, desplázate por el cuadro. El recuadro rojo se mueve hacia abajo.

Paso 3: Asígnale un tamaño

Tenemos un elemento que se mueve hacia abajo cuando la página se desplaza hacia abajo. Esa es la parte difícil. Ahora debemos ajustar el diseño para que se vea como una barra de desplazamiento y hacer que sea un poco más interactivo.

Por lo general, una barra de desplazamiento consta de un círculo y una pista, mientras que la pista no siempre está visible. La altura del pulgar es directamente proporcional a la cantidad de contenido visible.

<script>
    const scroller = document.querySelector('.container');
    const thumb = document.querySelector('.box');
    const scrollerHeight = scroller.getBoundingClientRect().height;
    thumb.style.height = /* ??? */;
</script>

scrollerHeight es la altura del elemento desplazable, mientras que scroller.scrollHeight es la altura total del contenido desplazable. scrollerHeight/scroller.scrollHeight es la fracción del contenido visible. La proporción del espacio vertical que cubre el pulgar debe ser igual a la proporción del contenido visible:

La altura del punto de estilo de punto de pulgar sobre ScrollerHeight es igual a la altura de desplazamiento del punto de desplazamiento sobre el punto de desplazamiento solo si la altura del punto es igual a la altura del punto de desplazamiento sobre la altura de desplazamiento del punto de desplazamiento.
<script>
    // …
    thumb.style.height =
    scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
    // Accommodate for native scrollbars
    thumb.style.right =
    (scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>

El tamaño del dedo pulgar se ve bien, pero se mueve demasiado rápido. Aquí es donde podemos tomar nuestra técnica del desplazamiento de paralaje. Si retrocedemos más el elemento, se moverá más lento durante el desplazamiento. Podemos corregir el tamaño escalando verticalmente. Pero, ¿cuánto debemos evitar exactamente? Hagamos algunas matemáticas, como ya lo habrás adivinado. Prometo que es la última vez.

La información fundamental es que queremos que el borde inferior del pulgar se alinee con el borde inferior del elemento desplazable cuando se desplaza hacia abajo. En otras palabras, si nos desplazamos scroller.scrollHeight - scroller.height píxeles, queremos que scroller.height - thumb.height traduzca nuestro dedo pulgar. Para cada píxel de desplazamiento, queremos que nuestro pulgar se mueva una fracción de un píxel:

Factoriza la altura del punto de desplazamiento menos la altura del punto del pulgar sobre la altura del punto de desplazamiento menos la altura del punto de desplazamiento.

Ese es nuestro factor de escala. Ahora, debemos convertir el factor de escala en una traducción a lo largo del eje z, como lo hicimos en el artículo sobre desplazamiento con paralaje. Según la sección relevante en la especificación: el factor de escala es igual a p/(p − z). Podemos resolver la ecuación de z para averiguar cuánto necesitamos trasladar nuestro pulgar junto al eje z. Sin embargo, ten en cuenta que, debido a nuestras travesuras de coordenadas w, debemos traducir un -2px adicional junto con z. Además, ten en cuenta que las transformaciones de un elemento se aplican de derecha a izquierda, lo que significa que todas las traducciones antes de nuestra matriz especial no se invertirán, todas las traducciones después de nuestra matriz especial, sin embargo, sí. Codifiquemos esto.

<script>
    // ... code from above...
    const factor =
    (scrollerHeight - thumbHeight)/(scroller.scrollHeight - scrollerHeight);
    thumb.style.transform = `
    matrix3d(
        1, 0, 0, 0,
        0, 1, 0, 0,
        0, 0, 1, 0,
        0, 0, 0, -1
    )
    scale(${1/factor})
    translateZ(${1 - 1/factor}px)
    translateZ(-2px)
    `;
</script>

¡Tenemos una barra de desplazamiento! Y es solo un elemento del DOM al que podemos aplicar ajustes de estilo. Algo importante en términos de accesibilidad es hacer que el dedo pulgar responda al hacer clic y arrastrar, ya que muchos usuarios están acostumbrados a interactuar con una barra de desplazamiento de esa manera. Para que esta entrada de blog no sea aún más larga, no explicaré los detalles de esa parte. Consulta el código de la biblioteca para obtener más detalles si deseas ver cómo se hace.

¿Qué ocurre con iOS?

Ah, mi viejo amigo iOS Safari. Al igual que con el desplazamiento con paralaje, nos encontramos con un problema. Como nos desplazamos en un elemento, debemos especificar -webkit-overflow-scrolling: touch, pero eso causa la aplanación en 3D y todo nuestro efecto de desplazamiento deja de funcionar. Para solucionar este problema en la barra de desplazamiento de paralaje, detectamos Safari de iOS y usamos position: sticky como solución alternativa. Haremos lo mismo aquí. Consulta el artículo sobre paralaje para refrescar tu memoria.

¿Qué ocurre con la barra de desplazamiento del navegador?

En algunos sistemas, tendremos que lidiar con una barra de desplazamiento nativa permanente. Históricamente, la barra de desplazamiento no se puede ocultar (excepto con un pseudoselector no estándar). Así que para ocultarlo, tenemos que recurrir a una piratería (sin matemática). Unimos nuestro elemento de desplazamiento en un contenedor con overflow-x: hidden y hacemos que el elemento de desplazamiento sea más ancho que el contenedor. La barra de desplazamiento nativa del navegador ahora está fuera de la vista.

Aleta

Cuando se junta todo, ahora podemos crear una barra de desplazamiento personalizada perfecta para el fotograma, como la de nuestra demostración del gato Nyan.

Si no puedes ver el gato Nyan, significa que estás experimentando un error que encontramos y archivamos mientras compilabas esta demostración (haz clic en el pulgar para que aparezca el gato Nyan). Chrome es muy bueno para evitar trabajos innecesarios, como pintar o animar elementos fuera de la pantalla. La mala noticia es que nuestras travesuras de matriz hacen que Chrome piense que el GIF del gato Nyan está realmente fuera de la pantalla. Esperamos que esto se solucione pronto.

Ahí tienes. Eso fue mucho trabajo. Te felicito por haber leído todo. Se trata de un verdadero truco para que funcione, y es probable que el esfuerzo no valga la pena, excepto cuando una barra de desplazamiento personalizada es una parte esencial de la experiencia. Pero es bueno saber que es posible, ¿no? El hecho de que es difícil crear una barra de desplazamiento personalizada demuestra que aún hay trabajo por hacer en CSS. ¡Pero no temas! En el futuro, AnimationWorklet de Houdini facilitará mucho los efectos vinculados al desplazamiento de fotogramas perfectos, como este.