Parallasse performante

Che ti piaccia o meno, l'effetto parallasse è destinato a durare. Se utilizzato con giudizio, può aggiungere profondità e sottigliezza a un'app web. Il problema, tuttavia, è che l'implementazione dell'effetto parallasse in modo efficiente può essere difficile. In questo articolo parleremo di una soluzione che sia performante e, cosa altrettanto importante, funzioni su più browser.

Illustrazione con parallasse.

TL;DR

  • Non utilizzare gli eventi di scorrimento o background-position per creare animazioni parallasse.
  • Utilizza le trasformazioni 3D CSS per creare un effetto parallasse più preciso.
  • Per Safari mobile, utilizza position: sticky per assicurarti che l'effetto parallasse venga propagato.

Se vuoi la soluzione drop-in, vai al repository GitHub di esempi di elementi UI e prendi Parallax helper JS. Puoi vedere una demo dal vivo dello scorrimento parallasse nel repository GitHub.

Problem parallaxers

Per iniziare, diamo un'occhiata a due modi comuni per ottenere un effetto parallax e, in particolare, al motivo per cui non sono adatti ai nostri scopi.

Errato: utilizzo degli eventi di scorrimento

Il requisito fondamentale della parallasse è che deve essere accoppiata allo scorrimento: per ogni singola modifica della posizione di scorrimento della pagina, la posizione dell'elemento di parallasse deve essere aggiornata. Anche se sembra semplice, un meccanismo importante dei browser moderni è la loro capacità di funzionare in modo asincrono. Nel nostro caso specifico, ciò vale per gli eventi di scorrimento. Nella maggior parte dei browser, gli eventi di scorrimento vengono inviati con il "miglior tentativo" e non è garantito che vengano inviati a ogni frame dell'animazione di scorrimento.

Questa importante informazione ci dice perché dobbiamo evitare una soluzione basata su JavaScript che sposta gli elementi in base agli eventi di scorrimento: JavaScript non garantisce che l'effetto parallasse rimanga in sincronia con la posizione di scorrimento della pagina. Nelle versioni precedenti di Safari mobile, gli eventi di scorrimento venivano effettivamente inviati alla fine dello scorrimento, il che rendeva impossibile creare un effetto di scorrimento basato su JavaScript. Le versioni più recenti forniscono eventi di scorrimento durante l'animazione, ma, come Chrome, in base al "miglior sforzo". Se il thread principale è occupato con altre attività, gli eventi di scorrimento non verranno inviati immediatamente, il che significa che l'effetto parallasse andrà perso.

Errato: aggiornamento di background-position

Un'altra situazione che vorremmo evitare è la pittura su ogni fotogramma. Molte soluzioni tentano di modificare background-position per fornire l'aspetto parallasse, il che fa sì che il browser ridisegni le parti interessate della pagina durante lo scorrimento, e questo può essere abbastanza costoso da compromettere in modo significativo l'animazione.

Se vogliamo mantenere la promessa del movimento parallasse, abbiamo bisogno di qualcosa che possa essere applicato come proprietà accelerata (il che oggi significa attenersi a trasformazioni e opacità) e che non si basi sugli eventi di scorrimento.

CSS in 3D

Sia Scott Kellum che Keith Clark hanno svolto un lavoro significativo nell'area dell'utilizzo di CSS 3D per ottenere un movimento parallasse, e la tecnica che utilizzano è effettivamente questa:

  • Configura un elemento contenitore da scorrere con overflow-y: scroll (e probabilmente overflow-x: hidden).
  • Allo stesso elemento applica un valore perspective e un valore perspective-origin impostato su top left o 0 0.
  • Ai figli di questo elemento viene applicata una traslazione in Z e vengono ridimensionati per fornire un movimento parallasse senza influire sulle loro dimensioni sullo schermo.

Il CSS per questo approccio è il seguente:

.container {
  width: 100%;
  height: 100%;
  overflow-x: hidden;
  overflow-y: scroll;
  perspective: 1px;
  perspective-origin: 0 0;
}

.parallax-child {
  transform-origin: 0 0;
  transform: translateZ(-2px) scale(3);
}

che presuppone uno snippet di codice HTML come questo:

<div class="container">
    <div class="parallax-child"></div>
</div>

Regolazione della scala per la prospettiva

Se spingi indietro l'elemento secondario, questo diventerà più piccolo in proporzione al valore della prospettiva. Puoi calcolare di quanto dovrà essere scalato con questa equazione: (prospettiva - distanza) / prospettiva. Poiché molto probabilmente vogliamo che l'elemento di parallasse abbia questo effetto, ma che appaia nelle dimensioni in cui l'abbiamo creato, deve essere scalato in questo modo, anziché essere lasciato così com'è.

Nel caso del codice precedente, la prospettiva è 1 px e la distanza Z di parallax-child è -2 px. Ciò significa che l'elemento dovrà essere scalato di 3x, come puoi vedere dal valore inserito nel codice: scale(3).

Per i contenuti a cui non è stato applicato un valore translateZ, puoi sostituire un valore pari a zero. Ciò significa che la scala è (prospettiva - 0) / prospettiva, che si traduce in un valore pari a 1, il che significa che non è stata aumentata né diminuita. Molto utile, davvero.

Come funziona questo approccio

È importante capire perché funziona, perché useremo queste informazioni a breve. Lo scorrimento è effettivamente una trasformazione, motivo per cui può essere accelerato; consiste principalmente nello spostamento dei livelli con la GPU. In uno scorrimento tipico, ovvero senza alcuna nozione di prospettiva, lo scorrimento avviene in modo 1:1 quando si confrontano l'elemento di scorrimento e i relativi elementi secondari. Se scorri un elemento verso il basso di 300px, i relativi elementi secondari vengono trasformati verso l'alto dello stesso importo: 300px.

Tuttavia, l'applicazione di un valore di prospettiva all'elemento di scorrimento interferisce con questo processo, in quanto modifica le matrici alla base della trasformazione dello scorrimento. Ora uno scorrimento di 300 px potrebbe spostare i figli solo di 150 px, a seconda dei valori di perspective e translateZ che hai scelto. Se un elemento ha un valore translateZ pari a 0, lo scorrimento avverrà in rapporto 1:1 (come in passato), ma un elemento secondario spostato in Z lontano dall'origine della prospettiva scorrerà a una velocità diversa. Risultato netto: movimento parallasse. Inoltre, cosa molto importante, questa operazione viene gestita automaticamente come parte del meccanismo di scorrimento interno del browser, il che significa che non è necessario ascoltare gli eventi scroll o modificare background-position.

Un neo: Safari mobile

Esistono avvertenze per ogni effetto e una importante per le trasformazioni riguarda la conservazione degli effetti 3D sugli elementi secondari. Se nella gerarchia sono presenti elementi tra l'elemento con una prospettiva e i relativi elementi secondari con effetto parallasse, la prospettiva 3D viene "appiattita", il che significa che l'effetto viene perso.

<div class="container">
    <div class="parallax-container">
    <div class="parallax-child"></div>
    </div>
</div>

Nel codice HTML riportato sopra, .parallax-container è nuovo e appiattirà il valore di perspective, quindi l'effetto parallasse andrà perso. Nella maggior parte dei casi, la soluzione è piuttosto semplice: aggiungi transform-style: preserve-3d all'elemento, in modo che propaghi gli effetti 3D (come il nostro valore di prospettiva) applicati più in alto nell'albero.

.parallax-container {
  transform-style: preserve-3d;
}

Nel caso di Safari mobile, invece, le cose sono un po' più complicate. L'applicazione di overflow-y: scroll all'elemento contenitore funziona tecnicamente, ma a costo di non poter scorrere l'elemento di scorrimento. La soluzione è aggiungere -webkit-overflow-scrolling: touch, ma appiattirà anche perspective e non otterremo alcun effetto parallasse.

Dal punto di vista del miglioramento progressivo, probabilmente non si tratta di un problema eccessivo. Se non riusciamo a creare l'effetto parallasse in ogni situazione, la nostra app continuerà a funzionare, ma sarebbe bello trovare una soluzione alternativa.

position: sticky in soccorso!

Esiste, infatti, un aiuto sotto forma di position: sticky, che consente agli elementi di "aderire" alla parte superiore del riquadro visibile o a un determinato elemento principale durante lo scorrimento. La specifica, come la maggior parte, è piuttosto corposa, ma contiene una piccola gemma utile:

A prima vista, questa frase potrebbe non sembrare molto significativa, ma un punto chiave è il riferimento a come viene calcolata esattamente l'aderenza di un elemento: "l'offset viene calcolato in riferimento all'elemento padre più vicino con una casella di scorrimento". In altre parole, la distanza da percorrere per spostare l'elemento sticky (in modo che appaia collegato a un altro elemento o al riquadro) viene calcolata prima dell'applicazione di qualsiasi altra trasformazione, non dopo. Ciò significa che, proprio come nell'esempio di scorrimento precedente, se l'offset è stato calcolato a 300 px, esiste una nuova opportunità di utilizzare le prospettive (o qualsiasi altra trasformazione) per manipolare il valore dell'offset di 300 px prima che venga applicato a qualsiasi elemento sticky.

Applicando position: -webkit-sticky all'elemento di parallasse, possiamo "invertire" efficacemente l'effetto di appiattimento di -webkit-overflow-scrolling: touch. In questo modo, l'elemento di parallasse fa riferimento all'elemento principale più vicino con una casella di scorrimento, che in questo caso è .container. Poi, come in precedenza, .parallax-container applica un valore perspective, che modifica l'offset di scorrimento calcolato e crea un effetto parallasse.

<div class="container">
    <div class="parallax-container">
    <div class="parallax-child"></div>
    </div>
</div>
.container {
  overflow-y: scroll;
  -webkit-overflow-scrolling: touch;
}

.parallax-container {
  perspective: 1px;
}

.parallax-child {
  position: -webkit-sticky;
  top: 0px;
  transform: translate(-2px) scale(3);
}

In questo modo viene ripristinato l'effetto parallasse per Mobile Safari, il che è un'ottima notizia per tutti.

Avvertenze sul posizionamento fisso

Tuttavia, qui c'è una differenza: position: sticky modifica la meccanica del parallasse. Il posizionamento fisso tenta di fissare l'elemento al contenitore di scorrimento, mentre una versione non fissa non lo fa. Ciò significa che la parallasse con sticky risulta essere l'inverso di quella senza:

  • Con position: sticky, più l'elemento è vicino a z=0, meno si muove.
  • Senza position: sticky, più l'elemento è vicino a z=0, più si sposta.

Se tutto questo ti sembra un po' astratto, dai un'occhiata a questa demo di Robert Flack, che mostra come gli elementi si comportano in modo diverso con e senza il posizionamento fisso. Per vedere la differenza, devi utilizzare Chrome Canary (versione 56 al momento della stesura) o Safari.

Screenshot con prospettiva parallasse

Una demo di Robert Flack che mostra come position: sticky influisce sullo scorrimento parallasse.

Bug e soluzioni alternative assortiti

Come per qualsiasi cosa, però, ci sono ancora dei problemi da risolvere:

  • Il supporto per la funzionalità Sticky è incoerente. Il supporto è ancora in fase di implementazione in Chrome, Edge non supporta completamente la funzionalità e Firefox presenta bug di rendering quando la proprietà sticky viene combinata con le trasformazioni prospettiche. In questi casi, vale la pena aggiungere un po' di codice per aggiungere solo position: sticky (la versione con prefisso -webkit-) quando è necessario, ovvero solo per Mobile Safari.
  • L'effetto non "funziona e basta" in Edge. Edge tenta di gestire lo scorrimento a livello di sistema operativo, il che è generalmente una buona cosa, ma in questo caso impedisce di rilevare i cambiamenti di prospettiva durante lo scorrimento. Per risolvere il problema, puoi aggiungere un elemento a posizione fissa, in quanto sembra che Edge passi a un metodo di scorrimento non OS, e garantisce che tenga conto delle modifiche alla prospettiva.
  • "I contenuti della pagina sono diventati enormi!" Molti browser tengono conto della scala quando decidono le dimensioni dei contenuti della pagina, ma purtroppo Chrome e Safari non tengono conto della prospettiva. Quindi, se, ad esempio, a un elemento viene applicata una scala di 3x, potresti vedere barre di scorrimento e simili, anche se l'elemento è a 1x dopo l'applicazione di perspective. È possibile aggirare questo problema scalando gli elementi dall'angolo in basso a destra (con transform-origin: bottom right), il che funziona perché gli elementi sovradimensionati crescono nella "regione negativa" (in genere in alto a sinistra) dell'area scorrevole; le regioni scorrevoli non consentono mai di visualizzare o scorrere i contenuti nella regione negativa.

Conclusione

L'effetto parallasse è divertente se utilizzato con criterio. Come puoi vedere, è possibile implementarlo in modo efficiente, accoppiato allo scorrimento e cross-browser. Poiché richiede un po' di manipolazione matematica e una piccola quantità di boilerplate per ottenere l'effetto desiderato, abbiamo creato una piccola libreria helper e un esempio, che puoi trovare nel nostro repository GitHub di esempi di elementi UI.

Prova e facci sapere come va.