Parallasse performante

Paul Lewis
Robert Flack
Robert Flack

Che ti piaccia o meno, l'effetto parallasse è destinato a durare. Se utilizzata con giudizio, può aggiungere profondità e raffinatezza a un'app web. Il problema, però, è che implementare la parallasse in modo efficiente può essere complicato. In questo articolo discuteremo di una soluzione che è sia performante sia, altrettanto importante, cross-browser.

Illustrazione con parallasse.

TL;DR

  • Non utilizzare gli eventi di scorrimento o background-position per creare animazioni in 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 plug-in, vai al repository GitHub di UI Element Samples e scarica il parametro JS di assistenza per la parallasse. Puoi vedere una demo dal vivo del selettore con effetto parallasse nel repository GitHub.

Problemi di parallasse

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

Sbagliato: utilizzo di eventi di scorrimento

Il requisito principale della parallasse è che deve essere accoppiata allo scorrimento; per ogni singola modifica della posizione di scorrimento della pagina, la posizione dell'elemento parallasse deve aggiornarsi. Sembra semplice, ma un meccanismo importante dei browser moderni è la loro capacità di lavorare in modo asincrono. Nel nostro caso specifico, questo vale per gli eventi di scorrimento. Nella maggior parte dei browser, gli eventi di scorrimento vengono pubblicati come "best effort" e non è garantito che vengano pubblicati in 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 la parallasse rimanga in linea con la posizione di scorrimento della pagina. Nelle versioni precedenti di Safari per il 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 generano eventi di scorrimento durante l'animazione, ma, come in Chrome, in base al criterio "best effort". Se il thread principale è occupato da altri lavori, gli eventi di scorrimento non verranno inviati immediatamente, il che significa che l'effetto parallasse andrà perso.

Errore: aggiornamento di background-position

Un'altra situazione che vogliamo evitare è la pittura su ogni fotogramma. Molte soluzioni tentano di modificare background-position per fornire l'effetto parallasse, il che fa sì che il browser ridisegnerà le parti interessate della pagina durante lo scorrimento e questo può essere abbastanza costoso da causare un'animazione notevolmente discontinua.

Se vogliamo mantenere la promessa del movimento parallattico, ci serve qualcosa che possa essere applicato come proprietà accelerata (oggi significa attenersi a trasformazioni e 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 parallattico e la tecnica che utilizzano è in pratica la seguente:

  • Configura un elemento contenitore per lo scorrimento 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.
  • Agli elementi secondari di quell'elemento applica una traslazione in Z e riduci la scala per creare un movimento parallattico senza modificare le 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 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 quanto dovrà essere aumentato il livello di zoom con questa equazione: (prospettiva - distanza) / prospettiva. Poiché molto probabilmente vogliamo che l'elemento con effetto parallasse abbia questo effetto, ma venga visualizzato nelle dimensioni in cui è stato creato, dovrà essere aumentato di dimensioni in questo modo, anziché essere lasciato invariato.

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

Per tutti 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 ha un valore netto di 1, il che significa che non è stata scalata né verso l'alto né verso il basso. Molto utile, in effetti.

Come funziona questo approccio

È importante capire chiaramente perché funziona, dato che useremo queste informazioni a breve. Lo scorrimento è in effetti una trasformazione, motivo per cui può essere accelerato; consiste principalmente nello spostare i livelli con la GPU. In un scorrimento tipico, ovvero senza alcuna nozione di prospettiva, lo scorrimento avviene in modo 1:1 se 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 con scorrimento influisce su questo processo; modifica le matrici alla base della trasformazione di scorrimento. Ora uno scorrimento di 300 pixel potrebbe spostare gli elementi secondari solo di 150 pixel, a seconda dei valori perspective e translateZ che hai scelto. Se un elemento ha un valore translateZ pari a 0, verrà visualizzato con uno scorrimento 1:1 (come in precedenza), ma un elemento secondario allontanato in Z dall'origine della prospettiva verrà visualizzato con una frequenza diversa. Risultato netto: movimento parallasse. Inoltre, è molto importante sottolineare che questo viene gestito 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'ombra: Safari mobile

Esistono delle limitazioni per ogni effetto e una importante per le trasformazioni riguarda la conservazione degli effetti 3D negli elementi secondari. Se nella gerarchia sono presenti elementi tra l'elemento con una prospettiva e i relativi elementi secondari con parallasse, la prospettiva 3D viene "appiattata", 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 appiattisce efficacemente il valore perspective, con la conseguente perdita dell'effetto parallasse. La soluzione, nella maggior parte dei casi, è abbastanza semplice: aggiungi transform-style: preserve-3d all'elemento, in modo da propagare eventuali effetti 3D (come il nostro valore di prospettiva) che sono stati applicati più in alto nell'albero.

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

Nel caso di Safari mobile, però, le cose sono un po' più complicate. L'applicazione di overflow-y: scroll all'elemento contenitore funziona tecnicamente, ma con il costo di non poter far scorrere l'elemento. La soluzione è aggiungere-webkit-overflow-scrolling: touch, ma appiattisce anche perspectivee non avremo parallasse.

Dal punto di vista del miglioramento progressivo, probabilmente non è un problema troppo grave. Se non possiamo usare la parallasse in ogni situazione, la nostra app continuerà a funzionare, ma sarebbe bello trovare una soluzione alternativa.

position: sticky accorre in soccorso.

In effetti, esiste un'opzione di assistenza sotto forma di position: sticky, che consente di "bloccare" gli elementi nella parte superiore dell'area visibile o di un determinato elemento principale durante lo scorrimento. La specifica, come la maggior parte di queste, è piuttosto complessa, ma contiene un piccolo gioiello utile:

A prima vista, questa frase potrebbe non sembrare molto importante, ma un punto chiave è quando si riferisce a come viene calcolata esattamente la coerenza di un elemento: "l'offset viene calcolato in base all'antenato più vicino con una casella di scorrimento". In altre parole, la distanza per spostare l'elemento fisso (in modo che venga visualizzato attaccato a un altro elemento o al viewport) viene calcolata prima dell'applicazione di qualsiasi altra trasformazione, non dopo. Ciò significa che, molto simile all'esempio di scorrimento precedente, se l'offset è stato calcolato su 300px, esiste una nuova opportunità di utilizzare le prospettive (o qualsiasi altra trasformazione) per manipolare il valore dell'offset di 300px prima che venga applicato a qualsiasi elemento fisso.

Applicando position: -webkit-sticky all'elemento con parallasse, possiamo effettivamente "invertire" l'effetto di appiattimento di -webkit-overflow-scrolling: touch. In questo modo, l'elemento con effetto parallasse fa riferimento all'antenato 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 Safari mobile, quindi è un'ottima notizia.

Limitazioni del posizionamento fisso

Tuttavia, esiste una differenza: position: sticky modifica la dinamica della parallasse. Il posizionamento fisso tenta di attaccare l'elemento al contenitore scorrevole, mentre una versione non fissa non lo fa. Ciò significa che la parallasse con effetto sticky finisce per 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 muove.

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

Screenshot della prospettiva con parallasse

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

Bug e soluzioni alternative assortiti

Tuttavia, come per qualsiasi cosa, ci sono ancora dei problemi da risolvere:

  • Il supporto delle schede fisse non è coerente. Il supporto è ancora in fase di implementazione in Chrome, Edge non lo supporta del tutto e Firefox presenta bug di pittura quando l'elemento sticky è combinato con le trasformazioni prospettiche. In questi casi, vale la pena aggiungere un po' di codice per aggiungere position: sticky (la versione con prefisso -webkit-) solo quando necessario, solo per Safari su dispositivi mobili.
  • L'effetto non "funziona e basta" in Edge. Edge tenta di gestire lo scorrimento a livello di sistema operativo, il che in genere è una buona cosa, ma in questo caso gli impedisce di rilevare le variazioni di prospettiva durante lo scorrimento. Per risolvere il problema, puoi aggiungere un elemento di posizione fissa, in quanto sembra che passi Edge a un metodo di scorrimento non OS e garantisce che vengano prese in considerazione le modifiche della 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. Pertanto, se a un elemento viene applicata una scala di 3 volte, potresti vedere barre di scorrimento e simili, anche se l'elemento è a 1x dopo l'applicazione di perspective. È possibile aggirare il problema ridimensionando gli elementi dall'angolo in basso a destra (con transform-origin: bottom right). In questo modo, gli elementi di grandi dimensioni verranno visualizzati nella "regione negativa" (in genere in alto a sinistra) dell'area scorrevole. Le regioni scorrevoli non ti consentono mai di vedere o scorrere fino ai contenuti nella regione negativa.

Conclusione

La parallasse è un effetto divertente se usato con attenzione. Come puoi vedere, è possibile implementarlo in modo efficiente, accoppiato allo scorrimento e cross-browser. Poiché richiede un po' di matematica e una piccola quantità di codice boilerplate per ottenere l'effetto desiderato, abbiamo creato una piccola libreria di assistenza e un sample, che puoi trovare nel nostro repo GitHub di esempi di elementi dell'interfaccia utente.

Prova e facci sapere come va.