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, tuttavia, è che l'implementazione del parallasse in modo efficiente può essere complicata. In questo articolo discuteremo di una soluzione che è sia performante sia, altrettanto importante, cross-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.
  • 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 JS di Parallax helper. Puoi vedere una demo dal vivo dello scorrimento 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. Anche se sembra semplice, un meccanismo importante dei browser moderni è la loro capacità di funzionare 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 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

Un'altra situazione che vogliamo evitare è la pittura su ogni fotogramma. Molte soluzioni tentano di modificare background-position per fornire l'aspetto della 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 a scatti.

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

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 spingi indietro l'elemento secondario, questo diventerà più piccolo in proporzione al valore della prospettiva. Puoi calcolare quanto dovrà essere ridimensionato 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 è 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 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, 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, 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.

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 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 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 al costo di non poter far scorrere l'elemento. La soluzione è aggiungere -webkit-overflow-scrolling: touch, ma verrà anche appiattito il perspective e non otterrai 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, 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.

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.

Avvertenze sul posizionamento persistente

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 in parallasse.

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 piccolo codice per aggiungere position: sticky (la versione con prefisso -webkit-) solo quando necessario, solo per Safari per 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 passare 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 pari a 3x, si possono 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 un modo efficace, con accoppiamento a scorrimento e su più 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 esempio, che puoi trovare nel nostro repo GitHub di esempi di elementi dell'interfaccia utente.

Prova e facci sapere come va.