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 esto se debe principalmente al hecho de que las barras de desplazamiento son uno de los elementos restantes en la Web que prácticamente no se pueden diseñar (te estoy mirando, selector de fecha). Puedes usar JavaScript para crear tu propio mapa, pero es costoso, de baja fidelidad y puede generar retrasos. En este artículo, aprovecharemos algunas matrices de CSS no convencionales para crear un control deslizante personalizado que no requiera ningún código JavaScript mientras se desplaza, solo un código de configuración.

A modo de resumen

¿No te importan los detalles? ¿Solo quieres ver la demo de Nyan Cat 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 desplazamiento de paralaje (¿leíste ese artículo? Es muy bueno, vale la pena dedicarle tiempo. Cuando se empujan los elementos hacia atrás con las transformaciones 3D de CSS, estos se mueven más lento que nuestra velocidad de desplazamiento real.

Resumen

Comencemos con un repaso de cómo funcionaba el control deslizante de 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. El desplazamiento de un documento es, en realidad, una traducción a lo largo del eje Y. Por lo tanto, si nos desplazamos hacia abajo, por ejemplo, 100 px, cada elemento se desplazará hacia arriba 100 px. Eso se aplica a todos los elementos, incluso a los que están “más atrás”. Sin embargo, debido a que están más lejos de la cámara, su movimiento observado en pantalla será inferior a 100 px, lo que generará el efecto de paralaje deseado.

Por supuesto, mover un elemento hacia atrás en el espacio también hará que parezca más pequeño, lo que corregimos ajustando el tamaño del elemento hacia arriba. Descubrimos la matemática exacta cuando compilamos el deslizador de paralaje, por lo que no repetiré todos los detalles.

Paso 0: ¿Qué queremos hacer?

Barras de desplazamiento. Eso es lo que compilaremos. Pero, ¿alguna vez pensaste en lo que hacen? Por supuesto que no. Las barras de desplazamiento son un indicador de cuánto del contenido disponible está visible en un momento dado y cuánto progreso realizaste como lector. Si te desplazas hacia abajo, la barra de desplazamiento también se desplaza para indicar que estás avanzando 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 ocupa la mitad de la altura del viewport. El contenido que vale 3 veces la altura de la vista del puerto ajusta la barra de desplazamiento a ⅓ de la vista del puerto, etcétera. Ya conoces el patrón. En lugar de desplazarte, también puedes hacer clic y arrastrar la barra de desplazamiento para moverte por el sitio más rápido. Es una cantidad sorprendente de comportamiento para un elemento discreto como ese. Luchemos una batalla a la vez.

Paso 1: Pon el vehículo en reversa

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. ¿También podemos revertir la dirección? Resulta que sí podemos, y esa es nuestra forma de crear una barra de desplazamiento personalizada con un marco perfecto. Para comprender cómo funciona, primero debemos analizar algunos aspectos básicos de CSS 3D.

Para obtener cualquier tipo de proyección de perspectiva en el sentido matemático, es probable que termines usando coordenadas homogéneas. No entraré en detalles sobre qué son y por qué funcionan, pero puedes pensar en ellas como coordenadas 3D con una cuarta coordenada adicional llamada w. Esta coordenada debe ser 1, a menos que desees tener una distorsión de perspectiva. No necesitamos 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 son vectores de 4 dimensiones [x, y, z, w=1] y, en consecuencia, las matrices también deben ser de 4 × 4.

Una ocasión en la que puedes ver que CSS usa coordenadas homogéneas en segundo plano 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 4 × 4) y especifica una columna después de la otra. Por lo tanto, podemos usar esta función para especificar manualmente rotaciones, traducciones, etc., pero también nos permite jugar con esa coordenada w.

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

Un fragmento de código CSS que distorsiona un div con el atributo perspectiva de CSS.

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

  • Convierte cada esquina (vértice) de un elemento en coordenadas homogéneas [x,y,z,w], en relación con el contenedor de perspectiva.
  • Aplica todas las transformaciones del elemento como matrices de derecha a izquierda.
  • Si el elemento de perspectiva es desplazable, aplica una matriz de desplazamiento.
  • Aplica la matriz de perspectiva.

La matriz de desplazamiento es una traducción a lo largo del eje y. Si nos desplazamos hacia abajo 400 px, todos los elementos deben moverse hacia arriba 400 px. La matriz de perspectiva es una matriz que “atrae” los puntos más cerca del punto de fuga cuanto más atrás están en el espacio 3D. Esto logra ambos efectos de hacer que los elementos se vean 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 empuja hacia atrás un elemento, una traslación de 400 px hará que el elemento solo se mueva 300 px en la pantalla.

Si quieres conocer todos los detalles, debes leer la especificación sobre el modelo de renderización de transformación del CSS, pero, para este artículo, simplifiqué el algoritmo anterior.

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

La matriz de perspectiva por la matriz de desplazamiento por la matriz de transformación de elementos es igual a la matriz identidad de cuatro por cuatro con menos uno sobre p en la cuarta fila, tercera columna por la matriz identidad de cuatro por cuatro con menos n en la segunda fila, 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, la función de la matriz de desplazamiento es hacer que un elemento se mueva hacia arriba cuando nos desplazamos hacia abajo, de ahí el signo negativo.

Sin embargo, para nuestra barra de desplazamiento, queremos lo contrario: queremos que nuestro elemento se mueva hacia abajo cuando nos desplazamos hacia abajo. Aquí es donde 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. Entonces, ¿cómo lo hacemos? 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 que matrix3d() brille!

.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 negar w. Por lo tanto, cuando el motor de CSS haya convertido cada esquina en un vector del formulario [x,y,z,1], la matriz lo convertirá en [x,y,z,-1].

La matriz de identidad de cuatro por cuatro con menos uno sobre p en la cuarta fila, tercera columna, por la matriz de identidad de cuatro por cuatro con menos n en la segunda fila, cuarta columna, por la matriz de identidad de cuatro por cuatro con menos uno en la cuarta fila, cuarta columna, por el vector de cuatro dimensiones x, y, z, 1 es igual a la matriz de identidad de cuatro por cuatro con menos uno sobre p en la cuarta fila, tercera columna, menos n en la segunda fila, cuarta columna y menos uno en la cuarta fila, cuarta columna, es igual al vector de cuatro dimensiones x, y más n, z, menos z sobre p menos 1.

Incluí un paso intermedio para mostrar el efecto de nuestra matriz de transformación de elementos. Si no te sientes cómodo con las matemáticas matriciales, no te preocupes. El momento Eureka es que, en la última línea, terminamos agregando el desplazamiento del desplazamiento n a nuestra coordenada y en lugar de restarlo. El elemento se desplazará hacia abajo si nos desplazamos hacia abajo.

Sin embargo, si solo ponemos 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. Y como nuestra coordenada z es actualmente 0 y p es 1, w será -1.

Por suerte, podemos elegir el valor de z. Para asegurarnos de obtener w=1, debemos configurar z = -2.

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

¡Sorpresa!, nuestra caja volvió.

Paso 2: Haz que se mueva

Ahora, nuestro cuadro está allí y se ve de la misma manera que lo haría sin ninguna transformación. En este momento, el contenedor de perspectiva no se puede desplazar, por lo que no podemos verlo, pero sabemos que nuestro elemento irá en la otra dirección cuando se desplace. Hagamos que el contenedor se desplace, ¿te parece? Podemos agregar un elemento de espacio 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: Asigna un tamaño

Tenemos un elemento que se mueve hacia abajo cuando la página se desplaza hacia abajo. Eso es lo más difícil. Ahora debemos aplicarle diseño para que se vea como una barra de desplazamiento y hacerla un poco más interactiva.

Por lo general, una barra de desplazamiento consta de un “círculo” y una “barra”, mientras que la barra no siempre es visible. La altura del pulgar es directamente proporcional a la cantidad de contenido que es 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 que es visible. La proporción de espacio vertical que cubre la miniatura debe ser igual a la proporción de contenido que es visible:

La altura del punto del estilo de punto de SliderThumb sobre scrollerHeight es igual a la altura del scroller sobre la altura de desplazamiento del punto del scroller solo si la altura del punto del estilo de punto de SliderThumb es igual a la altura del scroller por la altura del scroller sobre la altura de desplazamiento del punto del scroller.
<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 deslizador de paralaje. Si movemos el elemento más atrás, se moverá más lento mientras se desplaza. Podemos corregir el tamaño aumentándolo. Pero ¿cuánto deberíamos retroceder exactamente? Hagamos algunos cálculos. Te prometo que esta 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 desplace hacia abajo. En otras palabras, si desplazamos scroller.scrollHeight - scroller.height píxeles, queremos que el pulgar se translate en scroller.height - thumb.height. Para cada píxel del control deslizante, queremos que el pulgar mueva una fracción de un píxel:

El factor es igual a la altura del punto del control deslizante menos la altura del punto del control deslizante de la barra de desplazamiento sobre la altura del desplazamiento del punto del control deslizante menos la altura del punto del control deslizante.

Ese es nuestro factor de escalamiento. Ahora debemos convertir el factor de escala en una translación a lo largo del eje z, lo que ya hicimos en el artículo sobre el desplazamiento de paralaje. Según la sección relevante de la especificación, el factor de escala es igual a p/(p − z). Podemos resolver esta ecuación para z y averiguar cuánto debemos desplazar el pulgar a lo largo del eje z. Sin embargo, ten en cuenta que, debido a nuestros trucos con las coordenadas w, debemos traducir un -2px adicional a lo largo de z. También 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 antes de nuestra matriz especial, pero sí todas las traducciones después de nuestra matriz especial. Vamos a codificarlo.

<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 DOM al que podemos aplicarle el estilo que queramos. Algo que es importante hacer en términos de accesibilidad es hacer que el pulgar responda al clic y arrastre, ya que muchos usuarios están acostumbrados a interactuar con una barra de desplazamiento de esa manera. Para no alargar esta entrada de blog, no explicaré los detalles de esa parte. Consulta el código de la biblioteca para obtener más detalles si quieres ver cómo se hace.

¿Qué sucede con iOS?

Ah, mi viejo amigo iOS Safari. Al igual que con el desplazamiento de paralaje, nos encontramos con un problema aquí. Como nos desplazamos por un elemento, debemos especificar -webkit-overflow-scrolling: touch, pero eso causa el aplanamiento en 3D y todo nuestro efecto de desplazamiento deja de funcionar. Para resolver este problema en el control deslizante de paralaje, detectamos iOS Safari y usamos position: sticky como solución alternativa, y haremos exactamente lo mismo aquí. Consulta el artículo sobre el paralaje para actualizar.

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

En algunos sistemas, tendremos que lidiar con una barra de desplazamiento nativa y permanente. Históricamente, la barra de desplazamiento no se puede ocultar (excepto con un pseudoselector no estándar). Por lo tanto, para ocultarlo, debemos recurrir a algunos trucos de hackeo (sin matemáticas). 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.

Fin

Si juntamos todo, ahora podemos crear una barra de desplazamiento personalizada con un marco perfecto, como la de nuestra demo de Nyan Cat.

Si no puedes ver a Nyan Cat, estás experimentando un error que encontramos y registramos mientras compilabas esta demostración (haz clic en la miniatura para que aparezca Nyan Cat). Chrome es muy bueno para evitar el trabajo innecesario, como pintar o animar elementos que están fuera de la pantalla. La mala noticia es que nuestras travesuras con matrices hacen que Chrome piense que el GIF de Nyan Cat está fuera de la pantalla. Esperamos que se solucione pronto.

Ahí lo tienes. Eso fue mucho trabajo. Te felicito por leer todo. Esta es una técnica bastante complicada para que funcione y, probablemente, rara vez valga la pena el esfuerzo, a menos que una barra de desplazamiento personalizada sea una parte esencial de la experiencia. Pero es bueno saber que es posible, ¿no? El hecho de que sea tan difícil crear una barra de desplazamiento personalizada demuestra que hay trabajo por hacer en el CSS. Pero no te preocupes. En el futuro, AnimationWorklet de Houdini facilitará mucho los efectos vinculados al desplazamiento con una precisión de fotogramas como este.