Parallasse performante

Paul Lewis
Robert Flack
Robert Flack

Che si tratti di un amore o di un'odio, la parallasse è destinata a durare. Se utilizzato con accortezza, può aggiungere profondità e sottigliezza a un'app web. Il problema, tuttavia, è che implementare la parallasse in modo efficace può essere difficile. In questo articolo parleremo di una soluzione efficace e che funziona anche su più browser.

Illustrazione con parallasse.

TL;DR

  • Non utilizzare eventi di scorrimento o background-position per creare animazioni con parallasse.
  • Utilizza le trasformazioni 3D CSS per creare un effetto parallasse più accurato.
  • In Safari per dispositivi mobili, utilizza position: sticky per garantire che l'effetto parallasse venga propagato.

Se vuoi la soluzione integrata, vai al repository GitHub di esempi di elementi UI e scarica l'helper Parallasse JS. Puoi vedere una demo dal vivo dello scorrimento parallasse nel repository GitHub.

Parallasse di problemi

Per cominciare, diamo un'occhiata a due modi comuni per ottenere un effetto parallasse, e in particolare perché non sono adatti ai nostri scopi.

Male: vengono utilizzati gli eventi di scorrimento

Il requisito chiave della parallasse è che deve essere accoppiato tramite scorrimento; per ogni singola modifica nella posizione di scorrimento della pagina, la posizione dell'elemento di parallasse dovrebbe aggiornarsi. Anche se sembra semplice, un meccanismo importante dei browser moderni è la loro capacità di funzionare in modo asincrono. Questo vale, nel nostro caso particolare, per gli eventi di scorrimento. Nella maggior parte dei browser, gli eventi di scorrimento vengono pubblicati secondo il criterio "best effort" e non è garantito che vengano pubblicati in ogni frame dell'animazione di scorrimento.

Questa importante informazione ci spiega perché dobbiamo evitare una soluzione basata su JavaScript che sposti gli elementi in base a eventi di scorrimento: JavaScript non garantisce che la parallasse mantenga il passo con la posizione di scorrimento della pagina. Nelle versioni precedenti di Safari Mobile, gli eventi di scorrimento in realtà venivano inviati alla fine dello scorrimento, il che rendeva impossibile creare un effetto di scorrimento basato su JavaScript. Le versioni più recenti offrono eventi di scorrimento durante l'animazione ma, come per Chrome, secondo il criterio del "best effort". Se il thread principale è occupato con qualsiasi altro lavoro, gli eventi di scorrimento non verranno pubblicati immediatamente, il che significa che l'effetto Parallasse andrà perso.

Errore: aggiornamento di background-position in corso...

Un'altra situazione che vogliamo evitare è dipingere su ogni fotogramma. Molte soluzioni tentano di modificare background-position per fornire il look parallasse, il che causa la revisione da parte del browser delle parti interessate della pagina durante lo scorrimento, il che può essere abbastanza costoso da bloccare significativamente l'animazione.

Se vogliamo mantenere la promessa del movimento di parallasse, vogliamo qualcosa che possa essere applicato come proprietà accelerata (che oggi significa rispettare le trasformazioni e l'opacità) e che non si basi su eventi di scorrimento.

CSS in 3D

Sia Scott Kellum che Keith Clark hanno svolto un lavoro significativo nell'ambito dell'utilizzo di CSS 3D per ottenere il movimento con parallasse. La tecnica utilizzata è effettivamente questa:

  • Configura un elemento contenitore per scorrere con overflow-y: scroll (e probabilmente overflow-x: hidden).
  • Allo stesso elemento applica un valore perspective e un perspective-origin impostato su top left o 0 0.
  • Ai figli di quell'elemento viene applicata una traslazione in Z e poi vengono ridimensionate per generare un movimento di parallasse senza influenzarne le dimensioni sullo schermo.

Il CSS per questo approccio ha il seguente aspetto:

.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);
}

Il che presuppone uno snippet di codice HTML simile al seguente:

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

Regolazione della scala per la prospettiva

Se respingi l'elemento secondario, questo diventerà più piccolo in proporzione al valore prospettico. Puoi calcolare di quanto sarà necessario fare lo scale up con questa equazione: (prospettiva - distanza) / prospettiva. Dal momento che molto probabilmente vogliamo che l'elemento di parallasse diventi in parallasse, ma appaia nelle dimensioni da noi create, dovrebbe essere ridimensionato in questo modo, anziché essere lasciato così com'è.

Nel caso del codice riportato sopra, la prospettiva è 1 px e la distanza Z di parallax-child è -2 px. Ciò significa che l'elemento dovrà essere sottoposto a scale up di 3x. Come puoi vedere, il valore inserito nel codice: scale(3).

Per tutti i contenuti a cui non è applicato un valore translateZ, puoi sostituire un valore pari a zero. Ciò significa che la scala è (prospettiva - 0)/prospettiva, che equivale a un valore pari a 1, il che significa che non è stata scalata né in alto né in basso. Molto utile, davvero.

Come funziona questo approccio

È importante essere chiari sul motivo, dato che utilizzeremo a breve queste conoscenze. Lo scorrimento è in pratica una trasformazione, ed è per questo che può essere accelerato; prevede principalmente lo spostamento degli strati all'interno della GPU. In uno scorrimento tipico, che non ha alcuna nozione di prospettiva, lo scorrimento avviene in modo 1:1 quando si confronta l'elemento di scorrimento e i suoi elementi secondari. Se fai scorrere un elemento verso il basso di 300px, i relativi elementi secondari vengono trasformati in avanti della stessa quantità: 300px.

Tuttavia, l'applicazione di un valore di prospettiva all'elemento di scorrimento influisce in questo processo; cambia le matrici alla base della trasformazione di scorrimento. Ora uno scorrimento di 300 px può spostare solo gli elementi secondari di 150 px, a seconda dei valori perspective e translateZ che hai scelto. Se un elemento ha un valore translateZ pari a 0, lo scorrimento avverrà in 1:1 (come in precedenza), ma un elemento figlio spostato in Z lontano dall'origine della prospettiva la scorrerà con una velocità diversa. Risultato netto: movimento di parallasse. Inoltre, cosa molto importante, questa funzionalità 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.

Una mosca nell'unguento: Safari mobile

Per ogni effetto sono necessarie delle avvertenze e una importante per le trasformazioni riguarda la conservazione degli effetti 3D agli elementi secondari. Se ci sono elementi nella gerarchia tra l'elemento con una prospettiva e i suoi elementi secondari con parallasse, la prospettiva 3D è "appiattita", nel senso 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à in modo efficace il valore perspective, perdendo l'effetto parallasse. La soluzione, nella maggior parte dei casi, è piuttosto semplice: si aggiunge transform-style: preserve-3d all'elemento, in modo che propaga eventuali effetti 3D (come il valore di prospettiva) applicati più in alto nell'albero.

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

Nel caso di Safari Mobile, tuttavia, la situazione è un po' più contorta. L'applicazione di overflow-y: scroll all'elemento contenitore funziona tecnicamente, ma a costo di far scorrere l'elemento di scorrimento. La soluzione è aggiungere -webkit-overflow-scrolling: touch, ma verrà anche appiattito il perspective e non otterrai parallasse.

Da un punto di vista del miglioramento progressivo, probabilmente non si tratta di un problema. Se non riusciamo a eseguire il parallasse in ogni situazione, la nostra app continuerà a funzionare, ma sarebbe utile trovare una soluzione alternativa.

position: sticky in soccorso!

Esistono, infatti, alcuni elementi di aiuto sotto forma di position: sticky, che esistono per consentire agli elementi di "attaccarsi" alla parte superiore dell'area visibile o a un determinato elemento principale durante lo scorrimento. Le specifiche, come la maggior parte di esse, sono piuttosto pesanti, ma contengono un piccolo gioiello al suo interno:

Questo potrebbe non significare molto a prima vista, ma un punto chiave in questa frase è quando si riferisce a come, esattamente, viene calcolata la rigidità di un elemento: "l'offset viene calcolato con riferimento al predecessore più vicino con una casella di scorrimento". In altre parole, la distanza per spostare l'elemento fisso (in modo che appaia attaccato a un altro elemento o all'area visibile) viene calcolata prima di applicare qualsiasi altra trasformazione, non dopo. Ciò significa che, proprio come nell'esempio di scorrimento precedente, se l'offset è stato calcolato a 300 px, c'è una nuova opportunità di utilizzare le prospettive (o qualsiasi altra trasformazione) per manipolare il valore di offset di 300 px prima che venga applicato a qualsiasi elemento fisso.

Se applichi position: -webkit-sticky all'elemento di parallasse, possiamo invertire efficacemente l'effetto di appiattimento di -webkit-overflow-scrolling: touch. Ciò garantisce che l'elemento di parallasse faccia riferimento al predecessore più vicino con una casella di scorrimento, in questo caso .container. Quindi, 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 Safari per dispositivi mobili, che è un'ottima notizia a tutto tondo.

Avvertenze sul posizionamento persistente

Tuttavia, c'è una differenza in questo caso: position: sticky modifica i meccanismi della parallasse. Il posizionamento persistente cerca di fissare l'elemento al contenitore di scorrimento, mentre una versione non persistente no. Ciò significa che la parallasse con persistente diventa l'inversa di quella senza:

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

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

Screenshot della prospettiva Parallasse

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

Diversi bug e soluzioni alternative

Come per ogni altra cosa, però, ci sono ancora protuberanze e protuberanze da leviare:

  • Il supporto fisso non è coerente. Il supporto è ancora in fase di implementazione in Chrome, Edge non supporta completamente e Firefox presenta bug di disegno quando il sistema permanente è combinato con trasformazioni di prospettiva. In questi casi, vale la pena aggiungere un piccolo codice per aggiungere position: sticky (la versione con prefisso -webkit-) solo quando necessario, solo per Safari per dispositivi mobili.
  • L'effetto non "funziona solo" in Edge. Edge cerca di gestire lo scorrimento a livello di sistema operativo, il che in genere è una cosa positiva, ma in questo caso gli impedisce di rilevare i cambiamenti di prospettiva durante lo scorrimento. Per risolvere il problema, puoi aggiungere un elemento con posizione fissa, che sembra passare da Edge a un metodo di scorrimento non del sistema operativo e garantisce che tenga conto delle modifiche di prospettiva.
  • "I contenuti della pagina sono diventati davvero vasti!" Molti browser tengono conto della scala quando decidono le dimensioni dei contenuti della pagina, ma purtroppo Chrome e Safari non tengono conto delle prospettive. Pertanto, se a un elemento è applicata una scala pari a 3x, si possono vedere barre di scorrimento e simili, anche se l'elemento è a 1x dopo l'applicazione di perspective. È possibile risolvere questo problema scalando gli elementi dall'angolo in basso a destra (con transform-origin: bottom right), il che funziona perché gli elementi di grandi dimensioni finiscono nell'"area esclusa" (in genere l'angolo in alto a sinistra) dell'area scorrevole. Le regioni scorrevoli non ti consentono mai di visualizzare o scorrere ai contenuti di quella regione esclusa.

Conclusione

Il parallasse è un effetto divertente se usato in modo intelligente. Come puoi vedere, è possibile implementarlo in un modo efficace, con accoppiamento a scorrimento e cross-browser. Poiché sono necessarie alcune operazioni matematiche e una piccola quantità di boilerplate per ottenere l'effetto desiderato, abbiamo raggruppato una piccola libreria di supporto con un esempio, che puoi trovare nel nostro repository GitHub di esempi di elementi UI.

Gioca e facci sapere cosa ne pensi.