Een onscherpte animeren

Vervagen is een geweldige manier om de focus van een gebruiker te verleggen. Door sommige visuele elementen wazig te maken terwijl andere elementen scherp blijven, wordt de focus van de gebruiker op natuurlijke wijze verlegd. Gebruikers negeren de wazige content en richten zich in plaats daarvan op de content die ze wel kunnen lezen. Een voorbeeld hiervan is een lijst met pictogrammen die details over de afzonderlijke items weergeven wanneer u eroverheen zweeft. Gedurende die tijd kunnen de resterende keuzes wazig worden gemaakt om de gebruiker naar de nieuw weergegeven informatie te leiden.

Kortom

Een blur animeren is niet echt een optie, omdat het erg traag is. Bereken in plaats daarvan een reeks steeds waziger wordende versies en laat ze overvloeien. Mijn collega Yi Gu heeft een bibliotheek geschreven die alles voor je regelt! Bekijk onze demo .

Deze techniek kan echter behoorlijk schokkend zijn wanneer deze zonder overgangsperiode wordt toegepast. Het animeren van een onscherpe afbeelding – de overgang van onscherp naar onscherp – lijkt een redelijke keuze, maar als je dit ooit online hebt geprobeerd, heb je waarschijnlijk gemerkt dat de animaties allesbehalve vloeiend zijn, zoals deze demo laat zien als je geen krachtige machine hebt. Kunnen we het beter doen?

Het probleem

Markup wordt door de CPU omgezet in texturen. Texturen worden geüpload naar de GPU. De GPU tekent deze texturen met behulp van shaders naar de framebuffer. De vervaging vindt plaats in de shader.

Op dit moment kunnen we het animeren van een vervaging niet efficiënt laten werken. We kunnen echter wel een oplossing vinden die er goed genoeg uitziet, maar technisch gezien geen geanimeerde vervaging is. Laten we eerst eens kijken waarom de geanimeerde vervaging traag is. Om elementen op het web te vervagen, zijn er twee technieken: de CSS- filter en SVG-filters. Dankzij de verbeterde ondersteuning en het gebruiksgemak worden CSS-filters doorgaans gebruikt. Helaas, als u Internet Explorer moet ondersteunen, hebt u geen andere keuze dan SVG-filters te gebruiken, aangezien IE 10 en 11 die wel ondersteunen, maar geen CSS-filters. Het goede nieuws is dat onze oplossing voor het animeren van een vervaging met beide technieken werkt. Laten we dus proberen de bottleneck te vinden door naar DevTools te kijken.

Als je "Paint Flashing" inschakelt in DevTools, zie je helemaal geen flashes. Het lijkt erop dat er geen repaints plaatsvinden. En dat klopt technisch gezien, want een "repaint" verwijst naar het feit dat de CPU de textuur van een gepromoveerd element opnieuw moet schilderen. Wanneer een element zowel gepromoveerd als vervaagd is, wordt de vervaging door de GPU toegepast met behulp van een shader.

Zowel SVG- als CSS-filters gebruiken convolutiefilters om vervaging toe te passen. Convolutiefilters zijn vrij duur, omdat voor elke uitvoerpixel een aantal invoerpixels in aanmerking moet worden genomen. Hoe groter de afbeelding of hoe groter de vervagingsradius, hoe duurder het effect.

En daar zit het probleem: we draaien bij elk frame een vrij dure GPU-bewerking, waardoor we ons framebudget van 16 ms overschrijden en dus ruim onder de 60 fps uitkomen.

Het konijnenhol in

Dus wat kunnen we doen om dit soepel te laten verlopen? We kunnen goocheltrucs gebruiken! In plaats van de werkelijke vervagingswaarde (de straal van de vervaging) te animeren, berekenen we vooraf een aantal vervaagde kopieën waarbij de vervagingswaarde exponentieel toeneemt, en we laten ze vervolgens overvloeien met behulp van opacity .

Crossfade is een reeks overlappende fade-ins en fade-outs met een dekkingsgraad. Als we bijvoorbeeld vier blur-fasen hebben, faden we de eerste fase uit en faden we tegelijkertijd de tweede fase in. Zodra de tweede fase 100% dekking heeft bereikt en de eerste 0%, faden we de tweede fase uit en faden we de derde fase in. Als dat is gebeurd, faden we uiteindelijk de derde fase uit en faden we de vierde en laatste versie in. In dit scenario zou elke fase een kwart van de gewenste duur in beslag nemen. Visueel lijkt dit sterk op een echte, geanimeerde blur.

In onze experimenten leverde het exponentieel vergroten van de vervagingsradius per fase de beste visuele resultaten op. Voorbeeld: Als we vier vervagingsfasen hebben, passen we filter: blur(2^n) toe op elke fase, d.w.z. fase 0: 1px, fase 1: 2px, fase 2: 4px en fase 3: 8px. Als we elk van deze vervaagde kopieën op een eigen laag forceren ('promoten' genoemd) met behulp van will-change: transform , zou het wijzigen van de dekking van deze elementen supersnel moeten gaan. In theorie zou dit ons in staat stellen om het kostbare werk van het vervagen voor te zijn. Het blijkt dat de logica gebrekkig is. Als je deze demo uitvoert, zie je dat de framerate nog steeds onder de 60 fps ligt en dat de vervaging zelfs erger is dan voorheen.

DevTools toont een tracering waarbij de GPU langere tijd bezet is.

Een snelle blik in DevTools laat zien dat de GPU nog steeds extreem druk is en elk frame uitrekt tot ongeveer 90 ms. Maar waarom? We veranderen de vervagingswaarde niet meer, alleen de dekking, dus wat gebeurt er? Het probleem ligt, wederom, in de aard van het vervagingseffect: zoals eerder uitgelegd, als het element zowel gepromoot als vervaagd is, wordt het effect toegepast door de GPU. Dus zelfs als we de vervagingswaarde niet meer animeren, is de textuur zelf nog steeds niet vervaagd en moet deze elk frame opnieuw door de GPU worden vervaagd. De reden dat de framesnelheid nog slechter is dan voorheen, komt doordat de GPU, vergeleken met de naïeve implementatie, feitelijk meer werk heeft dan voorheen, omdat er meestal twee texturen zichtbaar zijn die onafhankelijk van elkaar vervaagd moeten worden.

Wat we bedachten is niet mooi, maar het maakt de animatie razendsnel. We gaan terug naar het niet promoten van het te vervagen element, maar promoten in plaats daarvan een bovenliggende wrapper. Als een element zowel vervaagd als gepromoot is, wordt het effect door de GPU toegepast. Dit is wat onze demo traag maakte. Als het element vervaagd is maar niet gepromoot, wordt de vervaging gerasterd naar de dichtstbijzijnde bovenliggende textuur. In ons geval is dat het gepromote bovenliggende wrapperelement. De vervaagde afbeelding is nu de textuur van het bovenliggende element en kan voor alle toekomstige frames worden hergebruikt. Dit werkt alleen omdat we weten dat de vervaagde elementen niet geanimeerd zijn en dat het cachen ervan juist voordelig is. Hier is een demo die deze techniek implementeert. Ik ben benieuwd wat de Moto G4 van deze aanpak vindt? Spoiler alert: hij vindt het geweldig:

DevTools toont een trace waar de GPU veel inactiviteit heeft.

Nu hebben we veel speling op de GPU en een zijdezachte 60 fps. Het is ons gelukt!

Productioniseren

In onze demo hebben we een DOM-structuur meerdere keren gedupliceerd om kopieën van de content te laten vervagen met verschillende intensiteiten. Je vraagt je misschien af hoe dit zou werken in een productieomgeving, aangezien dit onbedoelde bijwerkingen zou kunnen hebben met de CSS-stijlen van de auteur of zelfs met hun JavaScript. Je hebt gelijk. Maak kennis met Shadow DOM!

Hoewel de meeste mensen Shadow DOM zien als een manier om "interne" elementen aan hun aangepaste elementen te koppelen, is het ook een isolatie- en prestatie-primitief! JavaScript en CSS kunnen de grenzen van Shadow DOM niet doorbreken, waardoor we content kunnen dupliceren zonder de stijlen of applicatielogica van de ontwikkelaar te verstoren. We hebben al een <div> -element voor elke kopie om op te rasteren en gebruiken deze <div> 's nu als shadow hosts. We maken een ShadowRoot met behulp van attachShadow({mode: 'closed'}) en koppelen een kopie van de content aan de ShadowRoot in plaats van aan de <div> zelf. We moeten ervoor zorgen dat we ook alle stylesheets naar de ShadowRoot kopiëren om te garanderen dat onze kopieën dezelfde stijl hebben als het origineel.

Sommige browsers ondersteunen Shadow DOM v1 niet. In die gevallen vallen we terug op het dupliceren van de content en hopen we op het beste dat er niets kapotgaat. We zouden de Shadow DOM-polyfill met ShadyCSS kunnen gebruiken, maar we hebben dit niet in onze bibliotheek geïmplementeerd.

En voilà. Na onze verkenning van de rendering-pipeline van Chrome, hebben we ontdekt hoe we blurs efficiënt in verschillende browsers kunnen animeren!

Conclusie

Dit soort effect moet je niet lichtvaardig gebruiken. Doordat we DOM-elementen kopiëren en op een eigen laag forceren, kunnen we de grenzen van low-end apparaten verleggen. Het kopiëren van alle stylesheets naar elke ShadowRoot brengt ook een potentieel prestatierisico met zich mee, dus je moet beslissen of je je logica en stijlen wilt aanpassen zodat ze niet worden beïnvloed door kopieën in de LightDOM , of dat je onze ShadowDOM -techniek wilt gebruiken. Maar soms kan onze techniek een waardevolle investering zijn. Bekijk de code in onze GitHub-repository en de demo en neem contact met me op via Twitter als je vragen hebt!