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

Las barras de desplazamiento personalizadas son muy poco frecuentes y eso se debe, principalmente, a que son uno de los bits restantes en la Web que son prácticamente imposibles de estilo (los veo, el selector de fecha). Puedes usar JavaScript para compilar el tuyo, pero eso 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 requiera JavaScript durante el desplazamiento, sino solo algo de código de configuración.

Resumen

¿No te importan los pequeños detalles? ¿Solo quieres ver 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 con paralaje (¿Leíste ese artículo? Es muy bueno y vale la pena tu tiempo). Al enviar elementos hacia atrás con las transformaciones CSS 3D, estos se mueven más lento que nuestra velocidad de desplazamiento real.

Resumen

Empecemos con un resumen de cómo funcionó el desplazador de paralaje.

Como se muestra en la animación, logramos el efecto de paralaje empujando los elementos "hacia atrás" en un espacio 3D, a lo largo del eje Z. Desplazarse por un documento es, efectivamente, una traducción en el eje Y. Por lo tanto, si nos desplazamos hacia abajo, por ejemplo, 100 px, cada elemento se traducirá hacia arriba en 100 px. Eso se aplica a todos los elementos, incluso a los que están "más alejados". Sin embargo, debido a que están más lejos de la cámara, el movimiento observado en pantalla 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 cuando vuelves a escalar el elemento. Descubrimos los cálculos exactos cuando compilamos 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 compilar. Pero, ¿alguna vez has pensado en lo que hacen? Claro que no. Las barras de desplazamiento son un indicador de cuánto contenido disponible está visible en el momento y cuánto progreso hiciste como lector. Si te desplazas hacia abajo, también lo hace la barra de desplazamiento para indicar que estás avanzando hacia el final. Si todo el contenido cabe en el viewport, la barra de desplazamiento suele estar oculta. Si el contenido tiene el doble de altura del viewport, la barra de desplazamiento ocupa 1⁄2 de la altura del viewport. El contenido que vale el triple de la altura del viewport escala la barra de desplazamiento a 1⁄3 del viewport, etcétera. 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. Es una cantidad sorprendente de comportamiento para un elemento discreto como ese. Vamos a pelear una batalla a la vez.

Paso 1: Invierte

De acuerdo, podemos hacer que los elementos se muevan más lento que la velocidad de desplazamiento con las transformaciones 3D de CSS, como se describe en el artículo sobre el desplazamiento de paralaje. ¿Podemos invertir la dirección? Resulta que sí podemos, y ese es nuestro camino para crear una barra de desplazamiento personalizada con un fotograma perfecto. Para entender cómo funciona, primero debemos abarcar algunos conceptos básicos de CSS 3D.

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

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

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

Fragmento de código CSS que distorsiona un 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 las esquinas (vértices) de un elemento en coordenadas homogéneas [x,y,z,w], en relación con el 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 por 400 px, todos los elementos deberán moverse hacia arriba 400 px. La matriz de perspectiva es una matriz que "tira" a los puntos más cerca del punto de fuga cuanto más atrás en el espacio 3D estén. De esta manera, se logran los dos efectos de hacer que los elementos parezcan más pequeños cuando están más atrás y, además, los hace "mover más lento" cuando se traducen. Por lo tanto, si se aleja 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 del modelo de renderización de transformaciones de CSS, pero, para los fines de este artículo, simplifiqué el algoritmo anterior.

Nuestro cuadro está dentro de un contenedor de perspectiva con valor p para el atributo perspective. Supongamos que el contenedor es desplazable y se desplaza hacia abajo en n píxeles.

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

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

Para nuestra barra de desplazamiento, queremos lo opuesto; queremos que el elemento se desplace hacia abajo cuando nos desplacemos hacia abajo. Podemos usar un truco: invertir la coordenada w de las esquinas de nuestro cuadro. Si la coordenada w es -1, todas las traducciones se aplicarán en la dirección opuesta. ¿Cómo lo logramos? El motor de CSS se encarga de convertir las esquinas de nuestro cuadro en coordenadas homogéneas y establece w en 1. ¡Es hora de brillar matrix3d()!

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

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

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

Enumeramos 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 abajo si nos desplazamos hacia abajo.

Sin embargo, si solo 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 el elemento para que no se renderice. Y 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, tenemos que configurar 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 volvió.

Paso 2: Haz que se mueva

Ahora nuestra caja está ahí y se ve igual que 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, ¿de acuerdo? 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 cuadro 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 darle un estilo para que parezca una barra de desplazamiento y hacer que sea un poco más interactivo.

Por lo general, una barra de desplazamiento está formada por un círculo y un "recorrido", mientras que el recorrido no siempre está visible. La altura del pulgar es directamente proporcional a la porción del 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 de contenido visible:

thumb punto style punto altura sobrescrollerHeight es igual a altura de desplazamiento sobre la altura de desplazamiento de punto si y solo si la altura de desplazamiento de estilo de punto del pulgar es igual a la altura de la barra de desplazamiento por la altura de la barra de desplazamiento sobre la altura de desplazamiento de los puntos.
<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 pulgar se ve bien, pero se mueve demasiado rápido. Aquí es donde podemos tomar nuestra técnica del desplazamiento de paralaje. Si mueves el elemento más atrás, se moverá más lento mientras te desplazas. Podemos corregir el tamaño agrandándolo. Pero, ¿hasta qué punto debemos hacerlo hacia atrás exactamente? Hagamos algo de matemáticas, ya lo adivinaste. Te prometo que esta es la última vez.

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

El factor es igual a la altura de los puntos de desplazamiento menos la altura de los puntos del pulgar sobre la altura de desplazamiento de los puntos menos la altura de los puntos de desplazamiento.

Ese es nuestro factor de escala. Ahora, debemos convertir el factor de escala en una traslación junto al eje z, lo que ya hicimos en el artículo de desplazamiento de paralaje. Según la sección relevante en la especificación: el factor de escala es igual a p/(p − z). Podemos resolver esta ecuación para z para averiguar cuánto necesitamos trasladar nuestro pulgar a lo largo del eje z. Sin embargo, ten en cuenta que, debido a nuestros trucos 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 no se invertirán todas las traducciones anteriores a nuestra matriz especial, pero sí lo harán todas las traducciones posteriores. 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 diseño de la forma que queramos. Una cosa que es importante hacer en términos de accesibilidad es hacer que el 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 no extender esta entrada de blog, no voy a explicar los detalles de esa parte. Si deseas ver cómo se hace, consulta el código de la biblioteca para obtener más información.

¿Qué sucede con iOS?

Ah, mi viejo amigo Safari iOS. Al igual que con el desplazamiento con paralaje, nos encontramos con un problema aquí. Debido a que nos desplazamos sobre un elemento, debemos especificar -webkit-overflow-scrolling: touch, pero eso provoca que se acople en 3D y todo el efecto de desplazamiento deja de funcionar. Resolvimos este problema en la barra de desplazamiento paralaje detectando iOS Safari y utilizando position: sticky como solución alternativa. Haremos exactamente lo mismo aquí. Consulta el artículo de 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 un hackeo (sin matemáticas). Unimos el 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

Haciendo una revisión general, ahora podemos compilar una barra de desplazamiento personalizada con un fotograma perfecto, como la de nuestra demostración del gato Nyan.

Si no ves el gato Nyan, significa que experimentas un error que encontramos y presentamos durante la compilación de esta demostración (haz clic en el pulgar para que aparezca el gato Nyan). Chrome es muy bueno para evitar el trabajo innecesario, como pintar o animar elementos fuera de la pantalla. La mala noticia es que nuestras travesuras matriciales hacen que Chrome crea que el GIF del gato Nyan está realmente fuera de la pantalla. Esperamos que esto se solucione pronto.

Ahí lo tienes. Eso fue mucho trabajo. Te aplaudo por leer todo el material. Este es un verdadero truco para que esto funcione y probablemente no valga la pena el esfuerzo, excepto cuando una barra de desplazamiento personalizada es una parte esencial de la experiencia. Pero es bueno saber que sí es posible, ¿no? El hecho de que sea tan difícil crear una barra de desplazamiento personalizada muestra que hay trabajo por hacer en el lado del CSS. ¡Pero no temas! En el futuro, AnimationWorklet de Houdini hará que sean mucho más fáciles efectos vinculados con el desplazamiento de un fotograma perfecto.