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 una delle parti restanti sul Web che sono praticamente non stilizzabile (ti guardo, selettore di date). Puoi usare JavaScript per crearne uno tuo, ma è costoso, economico fedeltà e possono sembrare in ritardo. In questo articolo, sfrutteremo alcuni matrici CSS non convenzionali per creare uno scorrimento personalizzato che non richiede alcuna JavaScript durante lo scorrimento, solo un codice di configurazione.

TL;DR

Non ti interessano le piccole cose? Devi solo esaminare Demo di Nyan Cat e ottenere la raccolta? Puoi trovare il codice della demo nel nostro Repository GitHub.

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

Qualche tempo fa, abbiamo creato uno scorrimento parallasse (Hai letto) l'articolo? È un'esperienza che vale davvero la pena. Respingendo gli elementi utilizzando CSS 3D trasformazioni, gli elementi sono stati spostati più lentamente della 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 è di fatto una traslata lungo l'asse Y. Quindi se scorriamo verso il basso di 100 px, ogni verrà tradotto verso l'alto di 100 px. Questo vale per tutti gli elementi, anche quelle "più indietro". Ma perché sono più distanti la videocamera, il movimento osservato sullo schermo sarà inferiore a 100 px, generando l'effetto parallasse desiderato.

Naturalmente, anche se sposti un elemento di nuovo nello spazio, lo renderà più piccolo, che correggiamo scalando di nuovo l'elemento. Abbiamo capito il calcolo esatto quando abbiamo creato Scorrimento Parallasse quindi non ripeto tutti i dettagli.

Passaggio 0: cosa vogliamo fare?

Barre di scorrimento Ecco cosa realizzeremo. Ma hai mai pensato davvero di cosa fanno? Di sicuro non l'ho fatto. Le barre di scorrimento sono indicatori quanto dei contenuti disponibili sono attualmente visibili e quanto avanza come l'ha creato il lettore. Se scorri verso il basso, lo stesso accade con la barra di scorrimento indicano che stai facendo progressi verso la fine. Se tutti i contenuti sono adatti nell'area visibile la barra di scorrimento di solito è nascosta. Se i contenuti hanno il doppio dell'altezza dell'area visibile, la barra di scorrimento riempie metà dell'altezza dell'area visibile. Contenuti con un valore triplo dell'altezza di nell'area visibile la barra di scorrimento viene ridimensionata 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 più velocemente il sito. È una quantità sorprendente di comportamento per una persona poco appariscente di questo tipo. Combattiamo una battaglia alla volta.

Passaggio 1: inversione

Ok, con CSS 3D possiamo far spostare gli elementi più lentamente della velocità di scorrimento si trasforma come descritto nell'articolo sullo scorrimento con parallasse. Possiamo anche invertire la direzione? E a quanto pare possiamo creare ed è così barra di scorrimento personalizzata per il frame perfetta. Per capire come funziona, dobbiamo affrontare una alcune nozioni di base su CSS 3D.

Per ottenere qualsiasi tipo di proiezione prospettica in senso matematico, dovrai soprattutto probabilmente finiranno per utilizzare coordinate omogenee. Non spiegherò nel dettaglio cosa sono e perché funzionano, ma puoi pensare come le coordinate 3D, con la quarta coordinata aggiuntiva chiamata w. Questo dovrebbe essere 1, a meno che non si voglia ottenere una distorsione della prospettiva. Me non devi preoccuparti dei dettagli di w perché non utilizzeremo alcuna diverso da 1. Perciò, d'ora in poi tutti i punti provengono da vettori a 4 dimensioni [x, y, z, w=1] e di conseguenza le matrici devono anche 4 x 4.

Un'occasione in cui puoi vedere che il CSS utilizza coordinate omogenee sotto il "hood" è quando si definiscono le proprie matrici 4x4 in una proprietà di trasformazione utilizzando Funzione matrix3d(). matrix3d accetta 16 argomenti (perché la matrice è 4 x 4), specificando una colonna dopo l'altra. Quindi possiamo utilizzare questa funzione specificare manualmente rotazioni, traslazioni, ecc. Ma ciò che ci consente anche è andare in giro con la coordinata w!

Prima di poter utilizzare matrix3d(), abbiamo bisogno di un contesto 3D, perché senza un Con un contesto 3D non ci sarebbe alcuna distorsione della prospettiva e non c'è bisogno di coordinate omogenee. Per creare un contesto 3D, abbiamo bisogno di un container con perspective e alcuni elementi all'interno che possiamo trasformare nel nuovo spazio 3D creato. Per esempio:

Una porzione di codice CSS che distorce un div utilizzando
    .

Gli elementi all'interno di un container in prospettiva vengono elaborati dal motore CSS come segue:

  • Convertire ogni angolo (vertice) di un elemento in coordinate omogenee [x,y,z,w], rispetto al container in prospettiva.
  • 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 400 px, tutti gli elementi devono essere spostati in alto di 400 px. La matrice prospettica è matrice che "tira" i punti più vicini al punto di fuga più indietro in 3D nello spazio in cui sono. In questo modo, entrambi gli elementi sono più piccoli quando sono più indietro e li "mossa più lentamente" durante la traduzione. Pertanto, se un elemento viene spostato indietro, una traslazione di 400 px lo causerà per spostare solo 300 px sullo schermo.

Se vuoi conoscere tutti i dettagli, devi leggere l' spec sul foglio di lavoro di trasformare il modello di rendering, ma ai fini di questo articolo ho semplificato dall'algoritmo riportato sopra.

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

Matrice prospettica per volta matrice di scorrimento per elemento trasforma matrice
  è uguale a 4x4, matrice delle identità con meno uno su p nella quarta riga
  la terza colonna moltiplicata per 4 per 4 una matrice identità con meno n nella seconda
  riga quarta colonna per volta la matrice di trasformazione dell'elemento.

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

Per la barra di scorrimento, invece, vogliamo l'opposto, ovvero che l'elemento Sposta verso il basso quando scorri verso il basso. Ecco dove possiamo usare un trucco: Invertire la coordinata w degli angoli del rettangolo. Se la coordinata w è -1, tutte le traduzioni avranno effetto nella direzione opposta. Come facciamo che? Il motore CSS si occupa di convertire gli angoli della scatola in 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. Quindi, quando il motore CSS trasformerà ogni angolo in un vettore della forma [x,y,z,1], la matrice convertilo in [x,y,z,-1].

Matrice identità 4x4 con meno uno su p nella quarta riga
  la terza colonna moltiplicata per 4 per 4 una matrice identità con meno n nella seconda
  riga quarta colonna moltiplicata per quattro per quattro, con meno uno nella
  quarta riga quarta colonna moltiplicata per 4 vettori x, y, z, 1 è uguale a 4
  una matrice di identità con meno uno su p nella quarta riga e nella terza colonna,
  meno n nella seconda riga nella quarta colonna e meno uno nella quarta riga
  la quarta colonna è uguale al vettore a quattro dimensioni x, y più n, z, meno z sopra
  p meno 1.

Ho elencato un passaggio intermedio per mostrare l'effetto della trasformazione dell'elemento . Se non hai dimestichezza con la matematica matriciale, va bene lo stesso. Eureka è che nell'ultima riga finiamo per aggiungere l'offset di scorrimento n alla colonna y per la coordinata, invece di sottrarla. L'elemento verrà tradotto verso il basso se scorriamo verso il basso.

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

Fortunatamente, possiamo scegliere il valore di z! Per assicurarci di finire con w=1, dobbiamo per 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, le nostre è tornato il decoder!

Passaggio 2: fai muovere i primi passi

Ora la nostra scatola è lì ed ha lo stesso aspetto che avrebbe senza e piccole trasformazioni. Al momento il contenitore della prospettiva non è scorrevole, quindi non lo vediamo, ma sappiamo che il nostro elemento andrà nella direzione diversa quando hai fatto scorrere la pagina. Facciamo scorrere il container? Possiamo semplicemente aggiungere spazio 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. Questo è il problema un po' fuori strada, davvero. Ora dobbiamo definire lo stile in modo che abbia l'aspetto di una barra di scorrimento rendilo un po' più interattivo.

Una barra di scorrimento di solito è composta da un "pollice" e una "traccia", mentre la traccia non è sempre visibili. L'altezza del pollice è direttamente proporzionale a quanto siano 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 che visibile. Il rapporto tra lo spazio verticale in cui il coperchio per i pollici deve essere uguale proporzione dei contenuti visibili:

stile punto del pollice altezza punto sopra scorrimentoAltezza è uguale altezza a scorrimento
  sopra l&#39;altezza scorrimento del punto di scorrimento se e solo se l&#39;altezza del punto stile punto del pollice
  è uguale all&#39;altezza dello scorrimento volte all&#39;altezza della barra sopra la barra di scorrimento del punto
  altezza.
<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 è che hanno un bell'aspetto, ma si sta muovendo troppo velocemente. Qui possiamo prendere la nostra tecnica scorrimento parallasse. Se spostiamo l'elemento più indietro, si sposterà più lentamente scorrere. Possiamo correggere le dimensioni scalandole verso l'alto. Ma quanto dobbiamo spingere a tornare esattamente? Facciamo un po' di matematica, come avrai indovinato. Questa è l'ultima volta la promessa.

L'informazione cruciale è che vogliamo che il bordo inferiore del pollice allinealo con il bordo inferiore dell'elemento scorrevole quando l'hai fatto scorrere fino in fondo verso il basso. In altre parole: se abbiamo fatto scorrere scroller.scrollHeight - scroller.height pixel, vogliamo che il pollice sia tradotto da scroller.height - thumb.height. Per ogni pixel di scorrimento, vogliamo che il pollice sposti una frazione di pixel:

Fattore uguale all&#39;altezza del punto dello scorrimento meno l&#39;altezza del punto rispetto alla barra a scorrimento
  altezza scorrimento punto meno altezza punto scorrimento.

Questo è il nostro fattore di scala. Ora dobbiamo convertire il fattore di scala traslante lungo l'asse z, come già fatto nello scorrimento del parallasse . In base alle sezione pertinente della specifica: Il fattore di scala è uguale a p/(p - z). Possiamo risolvere questa equazione per z dobbiamo capire quanto dobbiamo tradurre il pollice lungo l'asse z. Ma mantieni ricorda che, a causa delle nostre truffe, dobbiamo tradurre -2px aggiuntivi in z. Nota anche che le trasformazioni di un elemento vengono applicate da destra a sinistra, il che significa che tutte le traduzioni prima della nostra matrice speciale essere invertita, tuttavia, tutte le traduzioni dopo la nostra matrice speciale lo faranno! Iniziamo codificalo.

<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 un barra di scorrimento. È solo un elemento DOM a cui possiamo applicare lo stile desiderato. Una cosa è importante in termini di accessibilità è far sì che il pollice risponda fare clic e trascinare, poiché molti utenti sono abituati a interagire in questo modo con una barra di scorrimento. Per non allungare ulteriormente questo post, non vi spiegherò i dettagli di quella parte. Dai un'occhiata codice libreria per maggiori dettagli, se vuoi vedere come si fa.

E iOS?

Ah, il mio vecchio amico Safari iOS. Come con lo scorrimento in parallasse, ci imbattiamo in un problema. Poiché stiamo scorrendo un elemento, dobbiamo specificare -webkit-overflow-scrolling: touch, ma questo causa l'appiattimento 3D e l'intera l'effetto di scorrimento smette di funzionare. Abbiamo risolto il problema nello scorrimento di parallasse rilevando Safari in iOS e affidandosi a position: sticky come soluzione alternativa. faremo esattamente la stessa cosa. Dai un'occhiata 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 con un pseudo-selettore non standard). Per nasconderlo, dobbiamo ricorrere ad hacker che non usano i fondamenti matematici. Concludiamo elemento di scorrimento in un contenitore con overflow-x: hidden e rende dell'elemento di scorrimento più ampio del contenitore. La barra di scorrimento nativa del browser ora non visibili.

Fin

Combinando tutto, ora possiamo creare una configurazione perfetta barra di scorrimento, come quella della finestra Demo di Nyan Cat.

Se non vedi Nyan cat, significa che stai un bug che abbiamo trovato e segnalato durante la creazione della demo (fai clic sul pollice per far apparire Nyan cat). Chrome è davvero bravo a evitare di lavorare inutilmente come dipingere o animare elementi fuori schermo. La cattiva notizia è che i nostri matrix 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 faccio i complimenti per aver letto tutto cosa. Ecco alcuni esempi un vero inganno per far funzionare tutto questo e probabilmente raramente vale la pena fare lo sforzo, tranne nei casi in cui una barra di scorrimento personalizzata è una parte essenziale dell'esperienza. Ma bello sapere che è possibile, no? Il fatto che sia difficile la barra di scorrimento personalizzata mostra che il CSS sta svolgendo attività. Ma non temere! In futuro, Houdini AnimationWorklet renderà effetti di scorrimento perfetti come questo è molto più semplice.