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

Le barre di scorrimento personalizzate sono estremamente rare e ciò è dovuto principalmente al fatto che sono uno degli elementi rimanenti sul Web e sono praticamente in stili non stilibili (ti stiamo guardando). Puoi usare JavaScript per crearne uno personalizzato, che però è costoso, a bassa fedeltà e può risultare lento. In questo articolo, sfrutteremo alcune matrici CSS non convenzionali per creare uno scroller personalizzato che non richiede JavaScript durante lo scorrimento, ma solo un po' di codice di configurazione.

TL;DR

Non ti interessano le piccole cose? Vuoi guardare la demo del gatto Nyan e scaricare la raccolta? Puoi trovare il codice della demo nel nostro repository GitHub.

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

Qualche tempo fa abbiamo creato uno scroller Parallasse (hai letto quell'articolo? È davvero buono, ne vale la pena!). Spostando gli elementi indietro utilizzando le trasformazioni 3D CSS, gli elementi si spostavano più lentamente rispetto alla velocità di scorrimento effettiva.

Riepilogo

Iniziamo con un riepilogo di come funzionava lo scorrimento con parallasse.

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

Ovviamente, se sposti di nuovo un elemento nello spazio, l'elemento sembrerà più piccolo, che correggiamo ridimensionando l'elemento verso l'alto. Abbiamo capito i calcoli esatti quando abbiamo creato lo scroller con parallasse, quindi non ripeto tutti i dettagli.

Passaggio 0: cosa vogliamo fare?

Barre di scorrimento. È quello che creeremo. Ma hai mai pensato a cosa fanno? Certo che no. Le barre di scorrimento indicano quanto i contenuti disponibili sono attualmente visibili e quanti progressi hai fatto il lettore. Se scorri verso il basso, la barra di scorrimento indica che stai procedendo verso la fine. Se tutti i contenuti rientrano nell'area visibile, solitamente la barra di scorrimento è nascosta. Se i contenuti hanno il doppio dell'altezza dell'area visibile, la barra di scorrimento riempie la metà dell'altezza dell'area visibile. I contenuti che valgono il triplo dell'altezza dell'area visibile ridimensionano la barra di scorrimento a 1⁄3 dell'area visibile e così via. Vedi il pattern. Invece di scorrere, puoi anche fare clic e trascinare la barra di scorrimento per spostarti più velocemente nel sito. È una quantità sorprendente di comportamento per un elemento così poco appariscente. Combattiamo una battaglia alla volta.

Passaggio 1: invertire il valore

Possiamo far muovere gli elementi più lentamente della velocità di scorrimento con le trasformazioni CSS 3D, come descritto nell'articolo relativo allo scorrimento con parallasse. Possiamo anche invertire la direzione? Abbiamo scoperto che è possibile creare una barra di scorrimento personalizzata e perfetta per il frame. Per capire come funziona, dobbiamo prima trattare alcuni concetti di base di CSS 3D.

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 considerarle come le coordinate 3D con una quarta coordinata aggiuntiva chiamata w. Questa coordinata deve essere 1, tranne nel caso in cui tu voglia avere una distorsione della prospettiva. Non dobbiamo preoccuparci dei dettagli di w, in quanto utilizzeremo soltanto un valore diverso da 1. D'ora in poi tutti i punti sono vettori quadridimensionali [x, y, z, w=1] e di conseguenza anche le matrici dovranno essere 4x4.

Ad esempio, puoi vedere che il CSS utilizza coordinate omogenee in generale quando definisci le tue matrici 4 x 4 in una proprietà transform utilizzando la funzione matrix3d(). matrix3d accetta 16 argomenti (perché la matrice è 4 x 4), specificando una colonna dopo l'altra. Possiamo quindi utilizzare questa funzione per specificare manualmente rotazioni, traslazioni e così via. Ma quello che ci permette anche di spegnere la coordinata w.

Prima di poter utilizzare matrix3d(), è necessario un contesto 3D, perché senza un contesto 3D non ci sarebbe alcuna distorsione prospettica e non sarebbero necessarie coordinate omogenee. Per creare un contesto 3D, è necessario un container con un elemento perspective e alcuni elementi al suo interno da trasformare nello spazio 3D appena creato. Ad esempio:

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

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

  • Trasforma ogni angolo (vertice) di un elemento in coordinate omogenee [x,y,z,w], relative al contenitore della prospettiva.
  • Applica tutte le trasformazioni dell'elemento come matrici da destra a sinistra.
  • Se l'elemento della prospettiva è scorrevole, applica una matrice di scorrimento.
  • Applicare 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 verso l'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 possono apparire più piccoli quando sono più indietro e li rallentano anche quando vengono tradotti. Quindi, se un elemento viene respinto, una traslazione di 400 px farà sì che l'elemento si sposti solo di 300 px sullo schermo.

Se vuoi conoscere tutti i dettagli, ti consigliamo di leggere le spec del modello di rendering trasformativo del CSS. Tuttavia, ai fini di questo articolo, ho semplificato l'algoritmo riportato sopra.

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

Matrice di prospettiva per matrice di scorrimento per matrice di trasformazione dell'elemento
  è uguale a quattro per quattro matrice di identità con meno uno su p nella quarta riga
  terza colonna per quattro volte la matrice identità con meno n nella seconda riga
  quarta colonna per la matrice di trasformazione dell'elemento.

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 qui il segno negativo.

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

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

Questa matrice non farà altro che negare w. Di conseguenza, quando il motore CSS ha trasformato ogni angolo in un vettore nel formato [x,y,z,1], la matrice lo convertirà in [x,y,z,-1].

quattro per quattro, meno uno su p nella quarta riga
 terza colonna per quattro volte la matrice di identità con meno n nella seconda riga
 quarta colonna per quattro per quattro la matrice di identità con meno uno nella
 quarta riga quarta colonna per quattro volte il vettore dimensionale x, y, z, 1 uguale a quattro
 per quattro matrice di identità con meno uno su p nella quarta colonna
 nella quarta colonna meno n, nella quarta colonna meno n nella quarta colonna

Ho elencato un passaggio intermedio per mostrare l'effetto della nostra matrice di trasformazione. Se non hai dimestichezza con i calcoli della matrice, non c'è problema. Il momento Eureka è che nell'ultima riga aggiungiamo l'offset di scorrimento n alla nostra coordinata y invece di sottrarlo. 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é le specifiche CSS richiedono che qualsiasi vertice con w < 0 blocchi la visualizzazione dell'elemento. Inoltre, poiché la nostra coordinata z è attualmente 0 e p è 1, w sarà -1.

Fortunatamente, possiamo scegliere il valore z. Per assicurarci di ottenere 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 box è tornato.

Passaggio 2: fai movimento

Ora il nostro dispositivo è lì e funziona come se fosse senza trasformazioni. Al momento, il contenitore della prospettiva non è scorrevole, quindi non è possibile visualizzarlo, ma sappiamo che l'elemento andrà nell'altra direzione quando si scorre. Facciamo scorrere il container, ok? 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 la casella. La casella rossa si sposta verso il basso.

Passaggio 3: specifica una taglia

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

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

<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 di contenuto visibile. Le proporzioni tra lo spazio verticale coperto dal pollice deve corrispondere alle proporzioni dei contenuti visibili:

Stile punto di pollice Altezza punto sopra scrollerHeight uguale all&#39;altezza di scorrimento
 rispetto all&#39;altezza di scorrimento del punto di scorrimento se e solo se l&#39;altezza del punto in stile punto del pollice
 è uguale all&#39;altezza di scorrimento per l&#39;altezza di scorrimento rispetto all&#39;altezza di scorrimento del punto di scorrimento.
<script>
    // …
    thumb.style.height =
    scrollerHeight * scrollerHeight / scroller.scrollHeight + 'px';
    // Accommodate for native scrollbars
    thumb.style.right =
    (scroller.clientWidth - scroller.getBoundingClientRect().width) + 'px';
</script>

Le dimensioni del pollice sono buone, ma si spostano troppo velocemente. Qui possiamo estrarre la nostra tecnica dallo scroller parallasse. Se spostiamo l'elemento più indietro, si sposterà più lentamente durante lo scorrimento. Possiamo correggere le dimensioni aumentandole. Ma quanto dovremmo respingerla esattamente? Facciamo un po' di matematica, l'hai indovinato! Questa è l'ultima volta, prometto.

L'aspetto più importante è che il bordo inferiore del pollice sia allineato al bordo inferiore dell'elemento scorrevole quando viene fatto scorrere fino in fondo. In altre parole: se abbiamo fatto scorrere scroller.scrollHeight - scroller.height pixel, vogliamo che il nostro pollice venga tradotto da scroller.height - thumb.height. Per ogni pixel di scorrimento, vogliamo che il nostro pollice muova di una frazione di pixel:

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

Questo è il nostro fattore di scalabilità. Ora dobbiamo convertire il fattore di scala in una traslazione lungo l'asse z, come abbiamo già fatto nell'articolo sullo scorrimento con parallasse. In base alla sezione pertinente nelle specifiche: il fattore di scala è uguale a p/(p - z). Possiamo risolvere l'equazione di z per capire quanto dobbiamo traslare il pollice sull'asse z. Ma tieni presente che, a causa dei nostri imbrogli delle coordinate, dobbiamo tradurre un ulteriore -2px insieme alla lettera z. Inoltre, tieni presente 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 saranno tutte le traduzioni dopo la nostra matrice speciale. Codifichiamo la procedura.

<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 gli stili come vogliamo. Una cosa importante da fare 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 evitare che questo post del blog sia ancora più lungo, non spiegherò i dettagli di quella parte. Se vuoi vedere come si fa, dai un'occhiata al codice libreria per i dettagli.

E iOS?

Ah, il mio vecchio amico iOS Safari. Come per lo scorrimento della parallasse, in questo caso riscontriamo un problema. Poiché stiamo scorrendo su un elemento, dobbiamo specificare -webkit-overflow-scrolling: touch, ma questo causa la visualizzazione appiattita 3D e l'intero effetto di scorrimento smette di funzionare. Abbiamo risolto questo problema nello scorrimento con parallasse rilevando Safari iOS e affidandoci a position: sticky come soluzione alternativa; farò esattamente la stessa cosa qui. Dai un'occhiata all'articolo sulla parallasse per rinfrescarti la memoria.

E la barra di scorrimento del browser?

In alcuni sistemi, dobbiamo avere a che fare con una barra di scorrimento nativa permanente. Storicamente, la barra di scorrimento non può essere nascosta (tranne con uno pseudo-selettore non standard). Quindi, per nasconderla, dobbiamo ricorrere a un certo tipo di hacker (senza matematica). Aggregamo l'elemento di scorrimento in un container con overflow-x: hidden e lo inseriamo più largo del container. La barra di scorrimento nativa del browser non è visibile.

Alette

Riassumendo, ora possiamo creare una barra di scorrimento personalizzata perfetta per il frame, come quella della nostra demo del gatto Nyan.

Se non vedi il gatto Nyan, stai riscontrando un bug che abbiamo trovato e segnalato durante la creazione di questa demo (fai clic sul pollice per visualizzare il gatto Nyan). Chrome è davvero bravo a evitare lavori inutili come dipingere o animare elementi fuori schermo. La cattiva notizia è che i nostri imbrogli della matrice fanno credere a Chrome che la GIF del gatto Nyan sia in realtà fuori schermo. Ci auguriamo che il problema venga risolto a breve.

Ecco fatto. È stato molto lavoro. Ti applauso per aver letto tutto. Questo è un vero inganno per ottenere questo risultato e probabilmente non ne vale la pena, tranne quando una barra di scorrimento personalizzata è una parte essenziale dell'esperienza. Ma è bello sapere che è possibile, no? Il fatto che una barra di scorrimento personalizzata sia così complicata dimostra che è necessario lavorare sul lato CSS. Ma non temere. In futuro, il progetto AnimationWorklet di Houdini semplificherà molto più facilmente gli effetti collegati allo scorrimento perfetto per i fotogrammi come questo.