Animazione di una sfocatura

La sfocatura è un ottimo modo per reindirizzare l'attenzione dell'utente. Se fai in modo che alcuni elementi visivi appaiano sfocati e altri elementi a fuoco, l'utente viene indirizzato in modo naturale. Gli utenti ignorano i contenuti sfocati e si concentrano invece sui contenuti che possono leggere. Un esempio è un elenco di icone che mostrano i dettagli dei singoli elementi quando ci passi il mouse sopra. Durante questo periodo, le opzioni rimanenti potrebbero essere sfocate per reindirizzare l'utente alle nuove informazioni visualizzate.

TL;DR

L'animazione di una sfocatura non è un'opzione perché è molto lenta. Puoi invece precalcolare una serie di versioni sempre più sfocate e creare una dissolvenza incrociata tra le versioni. Il mio collega Yi Gu ha scritto una libreria per occuparsi di tutto. Dai un'occhiata alla nostra demo.

Tuttavia, questa tecnica può risultare abbastanza scioccante quando viene applicata senza un periodo di transizione. Animare una sfocatura, ovvero passare da un'immagine nitida a una sfocata, sembra una scelta ragionevole, ma se hai mai provato a farlo sul web, probabilmente hai scoperto che le animazioni sono tutt'altro che fluide, perché questa demo mostra se non si dispone di una macchina potente. Possiamo fare di meglio?

Il problema

Il markup viene
convertito in texture dalla CPU. Le texture vengono caricate nella GPU. La GPU disegna queste texture nel framebuffer utilizzando gli mesh. La sfocatura avviene nello
sfumatura.

Al momento, non è possibile fare in modo che l'animazione di una sfocatura funzioni in modo efficiente. Tuttavia, possiamo trovare una soluzione alternativa che sembri abbastanza buona, ma che, tecnicamente, non sia una sfocatura animata. Per iniziare, vediamo innanzitutto perché la sfocatura animata è lenta. Per sfocare gli elementi sul web, esistono due tecniche: la proprietà filter CSS e i filtri SVG. Grazie a un maggiore supporto e a una maggiore facilità d'uso, vengono solitamente utilizzati filtri CSS. Purtroppo, se devi supportare Internet Explorer, non hai altra scelta che utilizzare i filtri SVG, in quanto IE 10 e 11 supportano questi filtri, ma non i filtri CSS. La buona notizia è che la soluzione alternativa per animare una sfocatura funziona con entrambe le tecniche. Proviamo a individuare il collo di bottiglia guardando DevTools.

Se attivi l'opzione "Lampeggiamento della pittura" in DevTools, non vedrai alcun lampeggiamento. Sembra che non sia in corso alcuna ricolorazione. Questo è tecnicamente corretto, in quanto il termine "ricolorazione" indica che la CPU deve ridipingere la texture di un elemento promosso. Ogni volta che un elemento viene promosso e sfocato, la sfocatura viene applicata dalla GPU utilizzando uno ombreggiatore.

Sia i filtri SVG sia i filtri CSS utilizzano filtri di convoluzione per applicare una sfocatura. I filtri di convoluzione sono abbastanza costosi poiché per ogni pixel di output è necessario prendere in considerazione una serie di pixel di input. Più grande è l'immagine o maggiore è il raggio di sfocatura, più costoso sarà l'effetto.

Ed è qui che sta il problema: eseguiamo operazioni GPU piuttosto costose per ogni frame, esaurindo il budget per i frame di 16 ms e, di conseguenza, arrivando ben al di sotto dei 60 fps.

Nella tana del coniglio

Cosa possiamo fare affinché tutto funzioni senza intoppi? Possiamo usare la gioco di prestigio! Invece di animare l'effettivo valore di sfocatura (il raggio della sfocatura), calcoliamo un paio di copie sfocate in cui il valore di sfocatura aumenta in modo esponenziale, quindi applichiamo una dissolvenza incrociata utilizzando opacity.

La dissolvenza incrociata consiste in una serie di dissolvenze in entrata e in uscita dell'opacità sovrapposte. Se, ad esempio, abbiamo quattro fasi di sfocatura, usiamo la dissolvenza in uscita nella prima fase e contemporaneamente nella seconda fase. Una volta che la seconda fase ha raggiunto il 100% di opacità e la prima ha raggiunto lo 0%, usiamo la seconda fase, mentre nel terzo viene fatta una dissolvenza in uscita. Fatto questo, usciamo in dissolvenza la terza fase e la dissolvenza nella quarta e ultima versione. In questo scenario, ogni fase richiederebbe un quarto della durata totale desiderata. A livello visivo, sembra molto simile a una sfocatura reale e animata.

Nei nostri esperimenti, l'aumento esponenziale del raggio di sfocatura per fase ha prodotto i migliori risultati visivi. Esempio: se abbiamo quattro fasi di sfocatura, applichiamo filter: blur(2^n) a ciascuna fase, ad esempio fase 0: 1 px, fase 1: 2 px, fase 2: 4 px e fase 3: 8 px. Se forziamo ciascuna di queste copie sfocate sul proprio livello (chiamato "promozione") utilizzando will-change: transform, la modifica dell'opacità di questi elementi dovrebbe essere estremamente rapida. In teoria, questo ci permetterebbe di affrontare il costoso lavoro di sfocatura. A quanto pare, la logica è imperfetta. Se esegui questa demo, noterai che la frequenza fotogrammi è ancora inferiore a 60 fps e la sfocatura è peggiore rispetto a prima.

DevTools mostra una traccia in cui la GPU ha lunghi periodi di tempo di attività.

Una rapida occhiata a DevTools rivela che la GPU è ancora estremamente impegnata e allunga ogni frame a circa 90 ms. Perché? Non modifichiamo più il valore di sfocatura, ma solo l'opacità. Cosa succede? Il problema risiede, ancora una volta, nella natura dell'effetto di sfocatura: come spiegato in precedenza, se l'elemento è sia promosso sia sfocato, l'effetto viene applicato dalla GPU. Pertanto, anche se non stiamo più animando il valore di sfocatura, la texture stessa è ancora non sfocata e deve essere nuovamente sfocata ogni fotogramma da parte della GPU. Il motivo per cui la frequenza fotogrammi è ancora peggiore rispetto a prima deriva dal fatto che, rispetto all'implementazione ingenua, la GPU in realtà ha più lavoro di prima, poiché la maggior parte delle volte sono visibili due texture che devono essere sfocate in modo indipendente.

Quello che abbiamo scoperto non è bello, ma rende l'animazione incredibilmente veloce. Torniamo a non promuovere l'elemento da sfocare, bensì un wrapper principale. Se un elemento è sfocato e promosso, l'effetto viene applicato dalla GPU. Questo è ciò che ha reso lenta la nostra demo. Se l'elemento è sfocato ma non promosso, la sfocatura viene invece rasterizzata con la texture principale più vicina. Nel nostro caso, si tratta dell'elemento wrapper principale promosso. L'immagine sfocata è ora la texture dell'elemento principale e può essere riutilizzata per tutti i frame futuri. Funziona solo perché sappiamo che gli elementi sfocati non sono animati e memorizzarli nella cache è davvero vantaggioso. Ecco una demo che implementa questa tecnica. Chissà cosa ne pensa la Moto G4 di questo approccio. Spoiler: pensa che sia fantastico:

DevTools
  mostra una traccia in cui la GPU ha molto tempo di inattività.

Ora abbiamo molto margine sulla GPU e 60 fps fluidi come la seta. Ce l'abbiamo fatta!

Produzione

Nella nostra demo, abbiamo duplicato più volte una struttura DOM per fare in modo che le copie dei contenuti vengano sfocati con punti di forza diversi. Forse ti starai chiedendo come funzionerebbe in un ambiente di produzione, dal momento che potrebbero verificarsi effetti collaterali indesiderati con gli stili CSS dell'autore o persino con il codice JavaScript. Hai ragione. Inserisci il DOM Shadow!

Sebbene la maggior parte delle persone pensi al DOM Shadow come a un modo per collegare elementi "interni" ai propri elementi personalizzati, è anche una primitiva di isolamento e prestazioni. JavaScript e CSS non possono superare i confini del DOM Shadow, consentendo di duplicare i contenuti senza interferire con gli stili o la logica dell'applicazione dello sviluppatore. Abbiamo già un elemento <div> per ogni copia in cui eseguire il rasterizzazione e ora utilizziamo questi <div> come host shadow. Creiamo un ShadowRoot utilizzando attachShadow({mode: 'closed'}) e alleghiamo una copia dei contenuti a ShadowRoot anziché a <div>. Dobbiamo assicurarci di copiare anche tutti i fogli di stile in ShadowRoot per garantire che le nostre copie abbiano lo stesso stile dell'originale.

Alcuni browser non supportano Shadow DOM v1 e, per quelli, torniamo a limitarci a duplicare i contenuti e a sperare che nulla si rompa. Potremmo utilizzare il polyfill DOM Shadow con ShadyCSS, ma non l'abbiamo implementato nella nostra libreria.

Ecco fatto. Dopo il percorso nella pipeline di rendering di Chrome, abbiamo capito come poter animare le sfocature in modo efficiente tra i browser.

Conclusione

Questo tipo di effetto non deve essere usato con leggerezza. Poiché gli elementi DOM vengono copiati e forzati sul proprio livello, possiamo superare i limiti dei dispositivi di fascia inferiore. Anche la copia di tutti i fogli di stile in ogni ShadowRoot rappresenta un potenziale rischio per il rendimento, quindi devi decidere se modificare la logica e gli stili in modo che non vengano influenzati dalle copie nella LightDOM o utilizzare la nostra tecnica ShadowDOM. A volte, però, la nostra tecnica può essere un investimento utile. Dai un'occhiata al codice nel nostro repository GitHub e alla demo e contattaci su Twitter per qualsiasi domanda.