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 principalmente al fatto che le barre di scorrimento sono uno dei pochi elementi sul web che non possono essere personalizzati (mi riferisco a te, selettore della data). Puoi utilizzare JavaScript per crearne uno tuo, ma è costoso, ha una bassa fedeltà e può risultare lento. In questo articolo, sfrutteremo alcune matrici CSS non convenzionali per creare uno scorrevole personalizzato che non richiede JavaScript durante lo scorrimento, ma solo un po' di codice di configurazione.

TL;DR

Non ti interessano i dettagli? Vuoi solo guardare la demo di Nyan cat e scaricare la libreria? Puoi trovare il codice della demo nel nostro repository GitHub.

LAM;WRA (Lungo e matematico; verrà letto comunque)

Qualche tempo fa abbiamo creato uno scorrimento con effetto parallasse. Hai letto questo articolo? È davvero interessante, vale la pena dedicarci del tempo. Se li spostiamo indietro utilizzando le trasformazioni 3D CSS, gli elementi si muovono più lentamente rispetto alla velocità di scorrimento effettiva.

Riepilogo

Iniziamo con un riepilogo del funzionamento dello scorrimento con effetto 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 è in pratica una traduzione lungo l'asse Y. Quindi, se scorriamo verso il basso, ad esempio di 100 px, ogni elemento verrà spostato verso l'alto di 100 px. Questo vale per tutti gli elementi, anche per quelli "più lontani". Tuttavia, poiché sono più lontani dalla fotocamera, il loro movimento osservato sullo schermo sarà inferiore a 100 pixel, producendo così l'effetto parallasse desiderato.

Ovviamente, se sposti un elemento all'indietro nello spazio, questo apparirà anche più piccolo, ma lo correggeremo ridimensionandolo. Abbiamo risolto i problemi matematici esatti quando abbiamo creato il selettore con effetto parallasse, quindi non ripeterò tutti i dettagli.

Passaggio 0: che cosa vogliamo fare?

Barre di scorrimento. È quello che stiamo per creare. Ma hai mai pensato davvero a cosa fanno? Io di sicuro no. Le barre di scorrimento indicano quanto dei contenuti disponibili è attualmente visibile e quanto hai letto. Se scorri verso il basso, lo fa anche la barra di scorrimento per indicare che stai procedendo verso la fine. Se tutti i contenuti rientrano nell'area visibile, la barra di scorrimento è solitamente nascosta. Se i contenuti hanno il doppio dell'altezza dell'area visibile, la barra di scorrimento riempie metà dell'altezza dell'area visibile. Contenuti pari a tre volte l'altezza dell'area visibile, la barra di scorrimento viene ridimensionata a 1/3 dell'area visibile e così via. Hai capito il pattern. Anziché scorrere, puoi anche fare clic e trascinare la barra di scorrimento per spostarti più velocemente nel sito. È una quantità sorprendente di comportamenti per un elemento poco appariscente come questo. Affrontiamo una battaglia alla volta.

Passaggio 1: inserisci la retromarcia

Bene, possiamo far muovere gli elementi più lentamente rispetto alla velocità di scorrimento con le trasformazioni 3D CSS come descritto nell'articolo sullo scorrimento con parallasse. Possiamo anche invertire la direzione? A quanto pare possiamo, ed è il nostro modo per creare una barra di scorrimento personalizzata perfetta per i frame. Per capire come funziona, dobbiamo prima esaminare alcune nozioni di base del CSS 3D.

Per ottenere qualsiasi tipo di proiezione prospettica in senso matematico, molto probabilmente finirai per utilizzare le coordinate omogenee. Non entrerò nei dettagli su cosa sono e perché funzionano, ma puoi immaginarle come coordinate 3D con una quarta coordinata aggiuntiva chiamata w. Questa coordinata deve essere 1, a meno che tu non voglia applicare una distorsione prospettica. Non dobbiamo preoccuparci dei dettagli di w perché non utilizzeremo un valore diverso da 1. Pertanto, d'ora in poi tutti i punti sono vettori di 4 dimensioni [x, y, z, w=1] e di conseguenza anche le matrici devono essere di dimensione 4x4.

Un caso in cui puoi vedere che il CSS utilizza le coordinate omogenee sotto il cofano è quando definisci le tue matrici 4x4 in una proprietà di trasformazione utilizzando la funzione matrix3d(). matrix3d accetta 16 argomenti (poiché la matrice è 4x4), specificando una colonna dopo l'altra. Quindi possiamo utilizzare questa funzione per specificare manualmente rotazioni, traslazioni e così via. Ma ci consente anche di giocare con la coordinata w.

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

Un frammento di codice CSS che distorce un div utilizzando l'attributo prospettiva del CSS.

Gli elementi all'interno di un contenitore prospettico vengono elaborati dal motore CSS come segue:

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

La matrice di scorrimento è una traslazione lungo l'asse y. Se scorri verso il basso di 400 px, tutti gli elementi devono essere spostati verso l'alto di 400 px. La matrice di prospettiva è una matrice che "attira" i punti verso il punto di fuga man mano che si allontanano nello spazio 3D. In questo modo, gli elementi appaiono più piccoli quando sono più lontani e si "muovono più lentamente" durante la traduzione. Pertanto, se un elemento viene spinto indietro, una traslazione di 400 pixel farà sì che l'elemento si sposti di soli 300 pixel sullo schermo.

Se vuoi conoscere tutti i dettagli, devi leggere la specifica sul modello di rendering delle trasformazioni del CSS, ma per il bene di questo articolo ho semplificato l'algoritmo riportato sopra.

La nostra casella si trova all'interno di un contenitore prospettico con valore p per l'attributo perspective. Supponiamo che il contenitore sia scorrevole e che lo scorrimento verso il basso sia di n pixel.

La matrice prospettiva moltiplicata per la matrice di scorrimento moltiplicata per la matrice di trasformazione dell'elemento equivale alla matrice identità quattro per quattro con meno uno su p nella quarta riga e terza colonna moltiplicata per la matrice identità quattro per quattro con meno n nella seconda riga e quarta colonna moltiplicata per la matrice di trasformazione dell'elemento.

La prima matrice è la matrice di prospettiva, la seconda è la matrice di scorrimento. Per riepilogare: il compito della matrice di scorrimento è far spostare verso l'alto un elemento quando scorriamo verso il basso, da qui il segno negativo.

Per la barra di scorrimento, però, vogliamo l'opposto: vogliamo che il nostro elemento si sposti verso il basso quando scorriamo verso il basso. Qui possiamo utilizzare un trucco: invertire la coordinata w degli angoli della nostra scatola. Se la coordinata w è -1, tutte le traslazioni verranno applicate nella direzione opposta. Come si fa? Il motore CSS si occupa di convertire i vertici della nostra scatola in coordinate omogenee e imposta w su 1. È 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. Pertanto, quando il motore CSS ha trasformato ogni angolo in un vettore del tipo [x,y,z,1], la matrice lo convertirà in [x,y,z,-1].

La matrice di identità quattro per quattro con meno uno su p nella terza colonna della quarta riga moltiplicata per la matrice di identità quattro per quattro con meno n nella quarta colonna della seconda riga moltiplicata per la matrice di identità quattro per quattro con meno uno nella quarta colonna della quarta riga moltiplicata per il vettore a quattro dimensioni x, y, z, 1 è uguale alla matrice di identità quattro per quattro con meno uno su p nella terza colonna della quarta riga, meno n nella quarta colonna della seconda riga e meno uno nella quarta colonna della quarta riga è uguale al vettore a quattro dimensioni x, y più n, z, meno z su p meno 1.

Ho elencato un passaggio intermedio per mostrare l'effetto della nostra matrice di trasformazione degli elementi. Se non hai dimestichezza con la matematica delle matrici, non preoccuparti. Il momento Eureka è che nell'ultima riga finiamo per aggiungere l'offset di scorrimento n alla nostra coordinata y anziché 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 il rendering dell'elemento. Poiché la nostra coordinata z è attualmente 0 e p è 1, w sarà -1.

Fortunatamente, possiamo scegliere il valore di 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);
}

E indovinate un po', la nostra casella è tornata.

Passaggio 2: fai in modo che si muova

Ora la nostra scatola è lì e ha lo stesso aspetto che avrebbe avuto senza alcuna trasformazione. Al momento il contenitore prospettico non è scorrevole, quindi non possiamo visualizzarlo, ma sappiamo che il nostro elemento andrà nell'altra direzione quando scorreremo. Quindi facciamo scorrere il contenitore, ok? Possiamo semplicemente aggiungere un elemento di spaziatura che occupi 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>

Ora scorri la casella. La casella rossa si sposta verso il basso.

Passaggio 3: assegna una dimensione

Abbiamo un elemento che si sposta verso il basso quando la pagina scorre verso il basso. La parte difficile è stata completata. Ora dobbiamo impostare lo stile in modo che assomigli a una barra di scorrimento e lo rendiamo un po' più interattivo.

Una barra di scorrimento è solitamente composta da un "cursore" e da una "barra", mentre quest'ultima non è sempre visibile. L'altezza del mini-ritratto è 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 di contenuti visibili. Il rapporto tra lo spazio verticale coperto dall'anteprima e il rapporto tra i contenuti visibili deve essere uguale:

l&#39;altezza del punto dello stile del cursore rispetto all&#39;altezza del cursore è uguale all&#39;altezza del cursore rispetto all&#39;altezza dello scorrimento del punto del cursore se e solo se l&#39;altezza del punto dello stile del cursore è uguale all&#39;altezza del cursore moltiplicata per l&#39;altezza del cursore rispetto all&#39;altezza dello scorrimento del punto del cursore.
<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 sembrano buone, ma si muove troppo velocemente. È qui che possiamo recuperare la nostra tecnica dall'elemento scorrevole con effetto parallasse. Se spostiamo l'elemento più indietro, si muoverà più lentamente durante lo scorrimento. Possiamo correggere le dimensioni aumentandole. Ma quanto dobbiamo spingerla indietro esattamente? Facciamo un po' di matematica. È l'ultima volta, lo prometto.

L'informazione fondamentale è che vogliamo che il bordo inferiore del cursore sia allineato al bordo inferiore dell'elemento scorrevole quando si scorre completamente verso il basso. In altre parole: se abbiamo scorretto scroller.scrollHeight - scroller.height pixel, vogliamo che il pollice venga tradotto di scroller.height - thumb.height. Per ogni pixel del cursore, vogliamo che il pollice si sposti di una frazione di pixel:

Il fattore è uguale all&#39;altezza del punto del cursore meno l&#39;altezza del punto del cursore sull&#39;altezza del punto del cursore meno l&#39;altezza del punto del cursore.

Questo è il nostro fattore di scala. Ora dobbiamo convertire il fattore di scala in una traduzione lungo l'asse z, come abbiamo già fatto nell'articolo sullo scorrimento parallattico. In base alla sezione pertinente della specifica: Il fattore di scalabilità è uguale a p/(p − z). Possiamo risolvere questa equazione per z per capire quanto dobbiamo tradurre il pollice lungo l'asse z. Tuttavia, tieni presente che, a causa delle nostre manfrine con le coordinate w, dobbiamo tradurre un altro -2px lungo z. Tieni inoltre presente che le trasformazioni di un elemento vengono applicate da destra a sinistra, il che significa che tutte le traslazioni prima della nostra matrice speciale non verranno invertite, ma tutte le traslazioni dopo la nostra matrice speciale sì. Andiamo 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>

Abbiamo una barra di scorrimento. Inoltre, è solo un elemento DOM a cui possiamo applicare gli stili che preferiamo. Un aspetto importante in termini di accessibilità è fare in modo che il cursore risponda al trascinamento, poiché molti utenti sono abituati a interagire con una barra di scorrimento in questo modo. Per non allungare ulteriormente questo post del blog, non spiegherò i dettagli di questa parte. Dai un'occhiata al codice della libreria per maggiori dettagli su come funziona.

E per iOS?

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

E la barra di scorrimento del browser?

Su alcuni sistemi dovremo gestire una barra di scorrimento nativa permanente. In passato, la barra di scorrimento non poteva essere nascosta (tranne con un pseudo-selettore non standard). Per nasconderlo, dobbiamo ricorrere a qualche trucco (senza matematica). Inseriamo il nostro elemento di scorrimento in un contenitore con overflow-x: hidden e lo rendiamo più largo del contenitore. La barra di scorrimento nativa del browser non è più visibile.

Pinna

Mettendo tutto insieme, ora possiamo creare una barra di scorrimento personalizzata perfetta per i frame, come quella nella nostra demo di Nyan Cat.

Se non riesci a vedere Nyan Cat, hai riscontrato un bug che abbiamo rilevato e registrato durante la creazione di questa demo (fai clic sulla miniatura per visualizzare Nyan Cat). Chrome è molto bravo a evitare lavori non necessari, come dipingere o animare elementi fuori dallo schermo. La cattiva notizia è che i nostri escamotage con le matrici fanno pensare a Chrome che la GIF di Nyan Cat sia effettivamente fuori dallo schermo. Ci auguriamo che il problema venga risolto a breve.

Ecco fatto. È stato un bel po' di lavoro. Ti applaudo per aver letto tutto. Per far funzionare questa funzionalità sono necessari alcuni passaggi complicati e probabilmente raramente vale la pena, tranne quando una barra di scorrimento personalizzata è parte essenziale dell'esperienza. Ma è bello sapere che è possibile, no? Il fatto che sia così difficile creare una barra di scorrimento personalizzata dimostra che c'è del lavoro da fare sul lato CSS. Ma non temere. In futuro, AnimationWorklet di Houdini semplificata moltissimo la realizzazione di effetti legati allo scorrimento con frame perfetti come questo.