CSS Deep-Dive - matrix3d() per una barra di scorrimento personalizzata perfetta per il frame

Le barre di scorrimento personalizzate sono estremamente rare e questo è dovuto soprattutto al fatto che le barre di scorrimento sono una delle parti restanti sul web che sono piuttosto instilabili (ti guardo, selettore di date). Puoi utilizzare JavaScript per crearne uno tuo, ma è costoso, a bassa fedeltà e può sembrare lento. In questo articolo, sfrutteremo alcune matrici CSS non convenzionali per creare uno scorrimento personalizzato che non richiede JavaScript durante lo scorrimento, ma solo del codice di configurazione.

TL;DR

Non ti interessano le piccole cose? Vuoi solo dare un'occhiata alla demo del gatto Nyan e prendere la raccolta? Puoi trovare il codice della demo nel nostro repository GitHub.

LAM;WRA (Lungo e matematica; leggerà comunque)

Qualche tempo fa, abbiamo costruito uno scorrimento parallasse. Hai letto quell'articolo? È un'esperienza che vale davvero la pena. Se respingendo gli elementi utilizzando le trasformazioni CSS 3D, gli elementi si sono spostati più lentamente rispetto alla velocità di scorrimento effettiva.

Riepilogo

Iniziamo con un riassunto di come funzionava lo scorrimento parallasse.

Come mostrato nell'animazione, abbiamo ottenuto l'effetto parallasse spingendo gli elementi "indietro" nello spazio 3D, lungo l'asse Z. Lo scorrimento di un documento è una traslazione lungo l'asse Y. Pertanto, se scorriamo verso il basso di 100 px, ogni elemento viene tradotto verso l'alto di 100 px. Questo vale per tutti gli elementi, anche quelli "più indietro". Tuttavia, perché sono più lontani dalla fotocamera, il loro movimento osservato sullo schermo sarà inferiore a 100 px, producendo l'effetto di parallasse desiderato.

Naturalmente, anche lo spostamento di un elemento di nuovo nello spazio lo renderà più piccolo, cosa che correggiamo ridimensionando l'elemento. Abbiamo capito il calcolo esatto durante la creazione dello scorrimento con parallasse, quindi non ripeterò tutti i dettagli.

Passaggio 0: cosa vogliamo fare?

Barre di scorrimento Ecco cosa realizzeremo. Ma ti è mai capitato di pensare a cosa fanno? Di sicuro non l'ho fatto. Le barre di scorrimento sono un indicatore di quanti contenuti disponibili sono attualmente visibili e quanti progressi hai compiuti dal lettore. Se scorri verso il basso, lo stesso vale per la barra di scorrimento per indicare che stai procedendo verso la fine. Se tutti i contenuti rientrano nell'area visibile, solitamente la barra di scorrimento è nascosta. Se i contenuti hanno un'altezza pari al doppio dell'altezza dell'area visibile, la barra di scorrimento riempie metà dell'altezza dell'area visibile. I contenuti che valgono tre volte l'altezza dell'area visibile scalano la barra di scorrimento a 1⁄3 dell'area visibile e così via. Vedi il motivo. Invece di scorrere, puoi anche fare clic e trascinare la barra di scorrimento per spostarti più velocemente nel sito. Si tratta di un comportamento sorprendente per un elemento poco appariscente come questo. Combattiamo una battaglia alla volta.

Passaggio 1: inversione

Con la trasformazione CSS 3D, possiamo far spostare gli elementi più lentamente della velocità di scorrimento come descritto nell'articolo sullo scorrimento con parallasse. Possiamo anche invertire la direzione? A quanto pare possiamo creare una barra di scorrimento personalizzata perfetta per il frame. Per capire come funziona, occorre prima avere alcune nozioni di base sul 3D di CSS.

Per ottenere qualsiasi tipo di proiezione prospettica in senso matematico, molto probabilmente finirai per utilizzare coordinate omogenee. Non spiegherò nel dettaglio cosa sono e perché funzionano, ma puoi pensare a queste come coordinate 3D con una quarta coordinata aggiuntiva chiamata w. Questa coordinata deve essere 1, a meno che non si voglia ottenere una distorsione della prospettiva. Non dobbiamo preoccuparci dei dettagli di w perché non utilizzeremo alcun valore diverso da 1. Pertanto, d'ora in poi tutti i punti hanno vettori quadridimensionali [x, y, z, w=1] e di conseguenza anche le matrici devono essere 4 x 4.

Un'occasione in cui puoi vedere che CSS utilizza coordinate omogenee si verifica quando definisci le tue matrici 4x4 in una proprietà di trasformazione utilizzando la funzione matrix3d(). matrix3d accetta 16 argomenti (perché la matrice è 4x4), specificando una colonna dopo l'altra. Possiamo quindi utilizzare questa funzione per specificare manualmente rotazioni, traslazioni e così via. Tuttavia, possiamo anche usare questa funzione per modificare la coordinata w.

Prima di poter utilizzare matrix3d(), abbiamo bisogno di un contesto 3D, perché senza un contesto 3D non ci sarebbe alcuna distorsione della prospettiva e non saranno necessarie coordinate omogenee. Per creare un contesto 3D, abbiamo bisogno di un container con un perspective e alcuni elementi al suo interno che possiamo trasformare nello spazio 3D appena creato. Ad esempio:

Una porzione di codice CSS che distorce un div utilizzando l'attributo di prospettiva del CSS.

Gli elementi all'interno di un container in prospettiva vengono elaborati dal motore CSS nel seguente modo:

  • Trasforma ogni angolo (vertice) di un elemento in coordinate omogenee [x,y,z,w], rispetto al container prospettico.
  • Applica tutte le trasformazioni dell'elemento come matrici da destra a sinistra.
  • Se l'elemento prospettico è scorrevole, applica una matrice di scorrimento.
  • Applica la matrice prospettica.

La matrice di scorrimento è una traslazione lungo l'asse y. Se scorriamo verso il basso di 400 px, tutti gli elementi devono essere spostati in alto di 400 px. La matrice prospettica è una matrice che "tira" i punti più vicini al punto di fuga più indietro nello spazio 3D. In questo modo, gli elementi appaiono più piccoli quando sono più indietro e "lo si muovono più lentamente" durante la traduzione. Quindi, se un elemento viene spostato indietro, una traduzione di 400 px farà sì che l'elemento si sposti solo di 300 px sullo schermo.

Se vuoi conoscere tutti i dettagli, dovresti leggere le spec sul modello di rendering in modalità Transform del CSS, ma ai fini di questo articolo abbiamo semplificato l'algoritmo di cui sopra.

Il riquadro si trova all'interno di un container in prospettiva con valore p per l'attributo perspective. Supponiamo che il container sia scorrevole e che venga fatto scorrere verso il basso di n pixel.

Matrice prospettica moltiplicata per scorrere la matrice per elemento trasforma matrice
  è uguale a quattro per quattro, matrice identità con meno uno su p nella quarta riga
  terza colonna moltiplicata per 4 volte la matrice identità con meno n nella seconda
  riga quarta colonna per volta l'elemento trasforma la matrice.

La prima matrice è la matrice prospettica, la seconda è la matrice di scorrimento. Ricapitolando, il compito della matrice di scorrimento è far spostare un elemento verso l'alto quando scorriamo verso il basso, da cui deriva il segno negativo.

Per la barra di scorrimento, invece, vogliamo l'opposto: vogliamo che l'elemento si sposti verso il basso quando scorriamo verso il basso. Ecco dove possiamo usare un trucco: invertendo la coordinata w degli angoli del rettangolo. Se la coordinata w è -1, tutte le traduzioni verranno applicate nella direzione opposta. Come possiamo farlo? Il motore CSS si occupa di convertire gli angoli del riquadro in coordinate omogenee e imposta w su 1. È arrivato il momento di far brillare matrix3d()!

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

Questa matrice non serve altro che negare w. Pertanto, quando il motore CSS ha convertito ogni angolo in un vettore del formato [x,y,z,1], la matrice lo convertirà in [x,y,z,-1].

Quattro per quattro colonne di identità con meno uno su p nella quarta riga terza colonna volte quattro per quattro matrice identità con meno n nella seconda riga la quarta colonna per quattro colonne di identità con meno uno nella quarta riga quarta colonna per quattro dimensioni.

Ho elencato un passaggio intermedio per mostrare l'effetto della nostra matrice di trasformazione degli elementi. Se non hai dimestichezza con i calcoli matematici matriciali, va bene così. Il momento Eureka è che nell'ultima riga finiamo per aggiungere l'offset di scorrimento n alla coordinata y, invece di sottrarla. L'elemento verrà tradotto verso il basso se scorriamo verso il basso.

Tuttavia, se inseriamo questa matrice nel nostro esempio, l'elemento non verrà visualizzato. Questo perché la specifica CSS richiede che qualsiasi vertice con w < 0 blocchi il rendering dell'elemento. Inoltre, poiché la nostra coordinata z è attualmente 0 e p è 1, w sarà -1.

Fortunatamente, possiamo scegliere il valore di z! Per avere w=1, dobbiamo impostare z = -2.

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

Ecco, il nostro articolo è tornato!

Passaggio 2: fai muovere i primi passi

Ora il nostro riquadro è pronto e ha lo stesso aspetto che avrebbe senza alcuna trasformazione. Al momento il container in prospettiva non è scorrevole, quindi non possiamo vederlo, ma sappiamo che l'elemento andrà nell'altra direzione se lo facciamo scorrere. Facciamo scorrere il container? Possiamo semplicemente aggiungere un elemento distanziatore che occupa spazio:

<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>

E ora scorri il riquadro. La casella rossa si sposta verso il basso.

Passaggio 3: specifica una dimensione

Abbiamo un elemento che si sposta verso il basso quando la pagina scorre verso il basso. Questa è la parte difficile, davvero. Ora dobbiamo applicare lo stile desiderato a una barra di scorrimento e renderlo un po' più interattivo.

Una barra di scorrimento di solito è composta da un "pollice" e una "traccia", mentre la traccia non è sempre visibile. L'altezza del pollice è direttamente proporzionale alla quantità di contenuti visibili.

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

scrollerHeight è l'altezza dell'elemento scorrevole, mentre scroller.scrollHeight è l'altezza totale dei contenuti scorrevoli. scrollerHeight/scroller.scrollHeight è la frazione dei contenuti visibili. Il rapporto tra lo spazio verticale delle copertine dei pollici deve corrispondere a quello dei contenuti visibili:

Altezza puntino stile punto del pollice sopra scorrimentoAltezza è uguale altezza
altezza scorrimento punto di scorrimento se e solo se altezza punto punto stile puntino
 uguale altezza altezza scorrimento volte altezza altezza scorrimento
 altezza scorrimento punto
<script>
    // …
    thumb.style.height =
    scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
    // Accommodate for native scrollbars
    thumb.style.right =
    (scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>

La dimensione del pollice è bella agli occhi, ma si sta muovendo troppo velocemente. È qui che possiamo recuperare la tecnica dallo scorrimento del parallasse. Se spostiamo l'elemento più indietro, si sposterà più lentamente durante lo scorrimento. Possiamo correggere le dimensioni scalandole verso l'alto. Ma quanto dobbiamo respingere esattamente? Facciamo un po' di matematica, come avrai indovinato. Questa è l'ultima volta, prometto.

L'informazione cruciale è che il bordo inferiore del pollice deve essere allineato al bordo inferiore dell'elemento scorrevole quando l'utente è fatto scorrere completamente verso il basso. In altre parole: se abbiamo fatto scorrere scroller.scrollHeight - scroller.height pixel, vogliamo che il pollice venga tradotto da scroller.height - thumb.height. Per ogni pixel dello scorrimento, vogliamo spostare il pollice di una parte:

Il fattore è uguale all&#39;altezza del punto dello scorrimento meno l&#39;altezza del punto del pollice rispetto all&#39;altezza del punto dello scorrimento meno l&#39;altezza del punto della barra di scorrimento.

Questo è il nostro fattore di scala. Ora dobbiamo convertire il fattore di scala in una traslazione lungo l'asse z, come abbiamo già fatto nell'articolo sullo scorrimento parallasse. In base alla sezione pertinente della specifica: il fattore di scala è uguale a p/(p - z). Possiamo risolvere l'equazione di z per capire di quanto dobbiamo tradurre il pollice lungo l'asse z. Tuttavia, tieni presente che, a causa delle nostre imbroglioni del lavoro, dobbiamo tradurre un elemento -2px aggiuntivo in Z. Nota inoltre che le trasformazioni di un elemento vengono applicate da destra a sinistra, il che significa che tutte le traduzioni prima della nostra matrice speciale non verranno invertite, ma lo faranno tutte le traduzioni dopo la nostra matrice speciale. Codifichiamo questo!

<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>

Abbiamo una barra di scorrimento. È solo un elemento DOM a cui possiamo applicare lo stile desiderato. Una cosa importante in termini di accessibilità è fare in modo che il pollice risponda al clic e al trascinamento, dato che molti utenti sono abituati a interagire con una barra di scorrimento in questo modo. Per non allungare ulteriormente il post del blog, non vi spiegherò i dettagli di quella parte. Per sapere come si fa, dai un'occhiata al codice libreria per maggiori dettagli.

E iOS?

Ah, il mio vecchio amico Safari iOS. Come con lo scorrimento Parallasse, in questo caso si verifica un problema. Poiché stiamo scorrendo su un elemento, dobbiamo specificare -webkit-overflow-scrolling: touch, ma questo provoca l'appiattimento 3D e l'intero effetto di scorrimento smette di funzionare. Abbiamo risolto il problema nello scorrimento del parallasse rilevando iOS Safari e affidandoci a position: sticky come soluzione alternativa, mentre faremo esattamente la stessa cosa qui. Dai un'occhiata all'articolo sul parallasse per rinfrescarti la memoria.

E la barra di scorrimento del browser?

In alcuni sistemi, dovremo fare i conti con una barra di scorrimento nativa permanente. In passato, la barra di scorrimento non può essere nascosta, tranne che con uno pseudo-selettore non standard. Per nasconderlo, dobbiamo ricorrere ad hacker che non usano i fondamenti matematici. Aggreghiamo l'elemento di scorrimento in un contenitore con overflow-x: hidden e lo rendiamo più ampio rispetto al contenitore. La barra di scorrimento nativa del browser non è più visibile.

Pinna

Combinando tutto, ora possiamo creare una barra di scorrimento personalizzata perfetta per il fotogramma, come quella della demo del gatto Nyan.

Se non vedi Nyan cat, significa che stai riscontrando un bug che abbiamo trovato e segnalato durante la creazione di questa demo (fai clic sul pollice per visualizzarlo). Chrome è bravo a evitare lavori non necessari, come dipingere o animare elementi fuori schermo. La cattiva notizia è che le nostre imbroglie alla matrice fanno pensare a Chrome che la GIF del gatto Nyan sia in realtà fuori dallo schermo. Speriamo che il problema venga risolto a breve.

Ecco fatto. È stato un bel po' di lavoro. Ti ringrazio per aver letto tutto. Questo è un vero inganno per far funzionare tutto e probabilmente raramente vale la pena, tranne quando una barra di scorrimento personalizzata è una parte essenziale dell'esperienza. Ma è bello sapere che è possibile. Il fatto che sia difficile creare una barra di scorrimento personalizzata indica che c'è molto lavoro da fare lato CSS. Ma non temere! In futuro, AnimationWorklet di Houdini semplificherà molto più facilmente gli effetti di scorrimento del frame perfetti come questo.