Animazione di una sfocatura

La sfocatura è un ottimo modo per reindirizzare l'attenzione di un utente. La sfocatura di alcuni elementi visivi, mentre altri rimangono a fuoco, attira naturalmente l'attenzione dell'utente. Gli utenti ignorano i contenuti sfocati e si concentrano su quelli che possono leggere. Un esempio è un elenco di icone che mostrano i dettagli sui singoli elementi quando vengono passate con il mouse. Durante questo periodo, le scelte rimanenti potrebbero essere sfocate per reindirizzare l'utente alle informazioni appena visualizzate.

TL;DR

L'animazione di una sfocatura non è una vera opzione, in quanto è molto lenta. In alternativa, precalcola una serie di versioni sempre più sfocate e applica una dissolvenza incrociata tra queste. Il mio collega Yi Gu ha scritto una libreria per occuparsi di tutto. Dai un'occhiata alla nostra demo.

Tuttavia, questa tecnica può essere piuttosto brusca se applicata senza alcun periodo di transizione. Animare una sfocatura, ovvero la transizione da non sfocato a sfocato, sembra una scelta ragionevole, ma se hai mai provato a farlo sul web, probabilmente hai notato che le animazioni non sono per niente fluide, come mostra questa demo se non hai una macchina potente. Possiamo fare di meglio?

Il problema

Il markup viene
trasformato in texture dalla CPU. Le texture vengono caricate sulla GPU. La GPU
disegna queste texture nel framebuffer utilizzando gli shader. La sfocatura avviene nello shader.

Al momento, non possiamo animare una sfocatura in modo efficiente. Tuttavia, possiamo trovare una soluzione alternativa che sembri abbastanza buona, ma che, tecnicamente parlando, non sia un effetto sfocatura animato. Per iniziare, capiamo innanzitutto perché la sfocatura animata è lenta. Per sfocare gli elementi sul web, esistono due tecniche: la proprietà CSS filter e i filtri SVG. Grazie al maggiore supporto e alla facilità d'uso, vengono in genere utilizzati i filtri CSS. Purtroppo, se devi supportare Internet Explorer, non hai altra scelta che utilizzare i filtri SVG, in quanto IE 10 e 11 li supportano, ma non i filtri CSS. La buona notizia è che la nostra soluzione alternativa per animare una sfocatura funziona con entrambe le tecniche. Quindi, proviamo a trovare il collo di bottiglia esaminando DevTools.

Se attivi "Evidenziazione rendering" in DevTools, non vedrai alcun flash. Sembra che non siano in corso riverniciature. E questo è tecnicamente corretto in quanto "ridisegno" si riferisce al fatto che la CPU deve ridisegnare la trama di un elemento promosso. Ogni volta che un elemento viene sia promosso che sfocato, la sfocatura viene applicata dalla GPU utilizzando uno shader.

Sia i filtri SVG sia quelli CSS utilizzano filtri di convoluzione per applicare una sfocatura. I filtri di convoluzione sono piuttosto costosi, in quanto per ogni pixel di output deve essere preso in considerazione un numero di pixel di input. Più grande è l'immagine o il raggio di sfocatura, più costoso è l'effetto.

Ed è qui che risiede il problema: eseguiamo un'operazione GPU piuttosto costosa ogni frame, superando il budget di 16 ms e quindi ottenendo un risultato ben inferiore a 60 fps.

Down the rabbit hole

Cosa possiamo fare per far sì che tutto vada liscio? Possiamo usare un gioco di prestigio. Anziché animare il valore di sfocatura effettivo (il raggio della sfocatura), precalcoliamo un paio di copie sfocate in cui il valore di sfocatura aumenta in modo esponenziale, quindi eseguiamo la dissolvenza incrociata tra queste copie utilizzando opacity.

La dissolvenza incrociata è una serie di dissolvenze in entrata e in uscita sovrapposte. Se abbiamo quattro fasi di sfocatura, ad esempio, la prima fase svanisce mentre la seconda appare contemporaneamente. Una volta che la seconda fase raggiunge il 100% di opacità e la prima ha raggiunto lo 0%, la seconda fase svanisce mentre la terza diventa visibile. Una volta fatto, dissolviamo la terza fase e visualizziamo la quarta e ultima versione. In questo scenario, ogni fase durerà un quarto della durata totale desiderata. Visivamente, questo effetto è molto simile a una sfocatura animata reale.

Nei nostri esperimenti, l'aumento esponenziale del raggio di sfocatura per ogni fase ha prodotto i migliori risultati visivi. Esempio: se abbiamo quattro fasi di sfocatura, applicheremo filter: blur(2^n) a ogni fase, ovvero fase 0: 1 px, fase 1: 2 px, fase 2: 4 px e fase 3: 8 px. Se forziamo ognuna di queste copie sfocate sul proprio livello (operazione chiamata "promozione") utilizzando will-change: transform, la modifica dell'opacità di questi elementi dovrebbe essere molto rapida. In teoria, questo ci consentirebbe di anticipare il costoso lavoro di sfocatura. A quanto pare, la logica è errata. Se esegui questa demo, vedrai che il framerate è ancora inferiore a 60 fps e che la sfocatura è peggiore di prima.

DevTools
  che mostra una traccia in cui la GPU ha lunghi periodi di tempo occupato.

Un rapido sguardo a DevTools rivela che la GPU è ancora estremamente occupata e allunga ogni frame a circa 90 ms. Ma perché? Non modifichiamo più il valore di sfocatura, ma solo l'opacità, quindi cosa sta succedendo? Il problema risiede, ancora una volta, nella natura dell'effetto sfocatura: come spiegato in precedenza, se l'elemento viene sia promosso che sfocato, l'effetto viene applicato dalla GPU. Quindi, anche se non animiamo più il valore di sfocatura, la texture stessa non è ancora sfocata e deve essere sfocata di nuovo a ogni frame dalla GPU. Il motivo per cui il frame rate è ancora peggiore di prima è che, rispetto all'implementazione naïve, la GPU ha più lavoro da fare, poiché la maggior parte delle volte sono visibili due texture che devono essere sfocate in modo indipendente.

Il risultato non è bellissimo, ma l'animazione è velocissima. Torniamo a non promuovere l'elemento da sfocare, ma a promuovere un wrapper principale. Se un elemento è sfocato e promosso, l'effetto viene applicato dalla GPU. Questo è ciò che ha rallentato la nostra demo. Se l'elemento è sfocato ma non promosso, la sfocatura viene rasterizzata nella 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 fotogrammi futuri. Questo funziona solo perché sappiamo che gli elementi sfocati non sono animati e la memorizzazione nella cache è effettivamente vantaggiosa. Ecco una demo che implementa questa tecnica. Chissà cosa ne pensa Moto G4 di questo approccio. Spoiler: pensa di essere fantastico:

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

Ora abbiamo molto spazio sulla GPU e una fluidità di 60 fps. Ce l'abbiamo fatta!

Messa in produzione

Nella nostra demo, abbiamo duplicato più volte una struttura DOM per avere copie dei contenuti da sfocare con intensità diverse. Ti starai chiedendo come funzionerebbe in un ambiente di produzione, in quanto potrebbe avere alcuni effetti collaterali indesiderati con gli stili CSS dell'autore o persino con il suo JavaScript. Hai ragione. Inserisci Shadow DOM.

Anche se la maggior parte delle persone pensa a Shadow DOM come a un modo per collegare elementi "interni" ai propri elementi personalizzati, si tratta anche di una primitiva di isolamento e prestazioni. JavaScript e CSS non possono superare i limiti del DOM ombra, il che ci consente di duplicare i contenuti senza interferire con gli stili o la logica dell'applicazione dello sviluppatore. Abbiamo già un elemento <div> per ogni copia da rasterizzare e ora utilizziamo questi <div> come host ombra. Creiamo un ShadowRoot utilizzando attachShadow({mode: 'closed'}) e allegando una copia dei contenuti al ShadowRoot anziché al <div> stesso. 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 questi, torniamo semplicemente a duplicare i contenuti e speriamo che non si verifichino problemi. Potremmo utilizzare il polyfill Shadow DOM con ShadyCSS, ma non l'abbiamo implementato nella nostra libreria.

Ed ecco fatto. Dopo il nostro viaggio nella pipeline di rendering di Chrome, abbiamo capito come animare le sfocature in modo efficiente su tutti i browser.

Conclusione

Questo tipo di effetto non deve essere usato con leggerezza. Poiché copiamo gli elementi DOM e li forziamo sul proprio livello, possiamo superare i limiti dei dispositivi di fascia bassa. Anche la copia di tutti i fogli di stile in ogni ShadowRoot è un potenziale rischio per il rendimento, quindi devi decidere se preferisci modificare la logica e gli stili in modo che non siano interessati dalle copie in LightDOM o utilizzare la nostra tecnica ShadowDOM. A volte, però, la nostra tecnica potrebbe essere un investimento valido. Dai un'occhiata al codice nel nostro repository GitHub, nonché alla demo e contattami su Twitter se hai domande.