Animer un floutage

Le floutage est un excellent moyen de rediriger l'attention de l'utilisateur. Le fait de rendre certains éléments visuels floutés tout en laissant la mise au point d'autres éléments dirige naturellement l'attention de l'utilisateur. Les utilisateurs ignorent le contenu flouté et se concentrent sur le contenu qu'ils peuvent lire. Il peut s'agir, par exemple, d'une liste d'icônes qui affichent des détails sur les éléments individuels lorsque l'utilisateur pointe dessus. Pendant ce temps, les options restantes peuvent être floutées pour rediriger l'utilisateur vers les informations nouvellement affichées.

Résumé

Animer un flou n'est pas vraiment une option, car cette méthode est très lente. Précalculez plutôt une série de versions de plus en plus floues et effectuez un fondu croisé entre elles. Mon collègue Yi Gu a écrit une bibliothèque pour s'occuper de tout ! Découvrez notre démonstration.

Cependant, cette technique peut être assez difficile à mettre en œuvre lorsqu'elle est appliquée sans période de transition. L'animation d'un floutage (passage du mode non flouté au floutage) semble être un choix raisonnable, mais si vous avez déjà essayé de le faire sur le Web, vous avez probablement constaté que les animations sont tout sauf fluides, comme le montre cette démonstration si vous ne disposez pas d'une machine puissante. Peut-on faire mieux ?

Problème

Le balisage est transformé en textures par le processeur. Les textures sont importées dans le GPU. Le GPU dessine ces textures dans le tampon de frames à l'aide de nuanceurs. Le floutage se produit dans le nuanceur.

Pour le moment, l'animation d'un floutage ne peut pas être efficace. Nous pouvons cependant trouver une solution de contournement qui semble suffisamment adaptée, mais qui n'est techniquement pas un flou animé. Pour commencer, voyons d'abord pourquoi le flou animé est lent. Pour flouter des éléments sur le Web, il existe deux techniques: la propriété CSS filter et les filtres SVG. Grâce à une meilleure prise en charge et à une simplicité d'utilisation accrue, les filtres CSS sont généralement utilisés. Malheureusement, si vous devez prendre en charge Internet Explorer, vous n'avez pas d'autre choix que d'utiliser les filtres SVG, car ils sont compatibles avec IE 10 et 11, mais pas avec les filtres CSS. La bonne nouvelle est que notre solution pour animer un flou fonctionne avec les deux techniques. Essayons donc d'identifier le goulot d'étranglement grâce aux outils de développement.

Si vous activez l'option "Paint Flashing" dans les outils de développement, aucun flash ne s'affiche. Apparemment, aucun repein n'est effectué. Et cela est techniquement correct, car un "repeinture" fait référence au processeur devant repeindre la texture d'un élément mis en avant. Chaque fois qu'un élément est à la fois mis en avant et flouté, le floutage est appliqué par le GPU à l'aide d'un nuanceur.

Les filtres SVG et CSS utilisent tous deux des filtres de convolution pour flouter. Les filtres de convolution sont assez coûteux, car pour chaque pixel de sortie, un nombre de pixels d'entrée doit être pris en compte. Plus l'image ou le rayon de flou sont grands, plus l'effet est coûteux.

C'est là que le problème se situe : nous exécutons une opération GPU plutôt coûteuse à chaque image, ce qui fait exploser notre budget de frames de 16 ms et finit donc bien en dessous de 60 FPS.

Dans le terrier du lapin

Que pouvons-nous donc faire pour que cela fonctionne correctement ? On peut se servir du tour de tour ! Au lieu d'animer la valeur de flou réelle (le rayon du flou), nous précalculons quelques copies floutées où la valeur du flou augmente de manière exponentielle, puis nous effectuons un fondu croisé entre elles à l'aide de opacity.

Le fondu enchaîné est une série de fondus d'ouverture et de fondu en sortie d'opacité qui se chevauchent. Par exemple, si nous avons quatre étapes de floutage, nous fondons la première étape et la deuxième en même temps. Une fois que la deuxième étape atteint une opacité de 100% et que la première a atteint 0%, nous supprimons la deuxième étape et la troisième étape en fondu. Une fois l'opération terminée, nous supprimons en fondu la troisième étape, puis la quatrième et dernière version. Dans ce scénario, chaque étape correspondrait à un quart de la durée totale souhaitée. Visuellement, cela ressemble à un véritable flou animé.

Lors de nos tests, l'augmentation du rayon de floutage de manière exponentielle par phase a permis d'obtenir les meilleurs résultats visuels. Exemple: Si nous avons quatre étapes de floutage, nous appliquerons filter: blur(2^n) à chacune d'elles, c'est-à-dire l'étape 0: 1px, l'étape 1: 2px, l'étape 2: 4px et l'étape 3: 8px. Si nous forçons chacune de ces copies floutées sur sa propre couche (appelée "promoting") à l'aide de will-change: transform, la modification de l'opacité de ces éléments devrait être extrêmement rapide. En théorie, cela nous permettrait de prélever le travail de floutage coûteux. Il s'avère que la logique est erronée. Si vous exécutez cette démonstration, vous constaterez que la fréquence d'images est toujours inférieure à 60 FPS et que le floutage est en fait pire qu'avant.

Les outils de développement affichent une trace pour laquelle le GPU présente de longues périodes d'activité.

Un examen rapide des outils de développement révèle que le GPU est toujours extrêmement occupé et étire chaque image jusqu'à environ 90 ms. Mais pourquoi ? Nous ne modifions plus la valeur de floutage, mais seulement l'opacité. Que se passe-t-il ? Là encore, le problème réside dans la nature de l'effet de flou. Comme expliqué précédemment, si l'élément est à la fois promu et flouté, l'effet est appliqué par le GPU. Ainsi, même si nous n'animons plus la valeur de flou, la texture elle-même n'est toujours pas floue et doit être floutée à nouveau à chaque image par le GPU. Si la fréquence d'images est encore pire qu'avant, c'est parce que par rapport à l'implémentation naïve, le GPU a plus de travail qu'auparavant, car la plupart du temps deux textures sont visibles et doivent être floutées indépendamment.

Le résultat n'est pas très esthétique, mais l'animation est incroyablement rapide. Nous revenons à ne pas promouvoir l'élément à flouter, mais à promouvoir un wrapper parent. Si un élément est à la fois flouté et mis en avant, l'effet est appliqué par le GPU. C'est ce qui a ralenti notre démonstration. Si l'élément est flouté, mais n'est pas mis en avant, le flou est rastérisé à la texture parente la plus proche. Ici, il s'agit de l'élément wrapper parent promu. L'image floutée est désormais la texture de l'élément parent et peut être réutilisée pour toutes les futures images. Cela ne fonctionne que parce que nous savons que les éléments floutés ne sont pas animés et que leur mise en cache est en fait bénéfique. Voici une démonstration qui met en œuvre cette technique. Je me demande ce que le Moto G4 pense de cette approche. Alerte spoiler: elle pense que c'est génial:

Les outils de développement affichent une trace où le GPU présente un temps d'inactivité important.

Nous avons maintenant une grande marge de manœuvre sur le GPU et une fluidité de 60 FPS. On a réussi !

Mettre un modèle en production

Dans notre démonstration, nous avons dupliqué une structure DOM à plusieurs reprises afin que des copies du contenu soient floutées à des niveaux différents. Vous vous demandez peut-être comment cela fonctionnerait dans un environnement de production, car cela pourrait avoir des effets secondaires inattendus avec les styles CSS de l'auteur ou même leur code JavaScript. Tu as raison. C'est là que Shadow DOM est lancé !

Bien que la plupart des utilisateurs considèrent le Shadow DOM comme un moyen d'associer des éléments "internes" à leurs éléments personnalisés, il constitue également une primitive de l'isolation et des performances. JavaScript et CSS ne peuvent pas percer les limites du Shadow DOM, ce qui nous permet de dupliquer le contenu sans interférer avec les styles ou la logique d'application du développeur. Nous disposons déjà d'un élément <div> pour chaque copie à rastériser et nous utilisons désormais ces <div>s en tant qu'hôtes fictifs. Nous créons un ShadowRoot à l'aide de attachShadow({mode: 'closed'}) et joignons une copie du contenu au ShadowRoot au lieu de l'<div> elle-même. Nous devons également veiller à copier toutes les feuilles de style dans le ShadowRoot pour garantir que nos copies appliquent le même style que l'original.

Certains navigateurs ne sont pas compatibles avec Shadow DOM v1. Dans ces cas, nous nous contentons de dupliquer le contenu en espérant que tout fonctionne. Nous avons pu utiliser le polyfill Shadow DOM avec ShadyCSS, mais nous ne l'avons pas implémenté dans notre bibliothèque.

Et voilà. Après avoir exploré le pipeline de rendu de Chrome, nous avons découvert comment animer efficacement les floutages dans tous les navigateurs.

Conclusion

Ce type d'effet ne doit pas être utilisé à la légère. Comme nous copions les éléments DOM et les forceons sur leur propre couche, nous pouvons repousser les limites des appareils bas de gamme. Le fait de copier toutes les feuilles de style dans chaque ShadowRoot représente également un risque potentiel sur les performances. Vous devez donc décider si vous préférez ajuster votre logique et vos styles pour que les copies dans LightDOM n'affectent pas les copies, ou utiliser notre technique ShadowDOM. Mais parfois, notre technique peut être un investissement rentable. Jetez un coup d'œil au code de notre dépôt GitHub ainsi que de la démonstration. N'hésitez pas à me contacter sur Twitter si vous avez des questions.