Que savez-vous vraiment de `box-shadow` ?
(dgerrells.com)- En CSS,
box-shadowest normalement une propriété qui donne de la profondeur à une UI, mais en en superposant des centaines à des milliers sur un seuldiv, elle peut fonctionner comme une sorte d'API de dessin - Malgré la contrainte de la boîte, il est possible de créer des points circulaires et des effets de calque avec des coins arrondis, des chaînes de plusieurs ombres, et des combinaisons de couleurs, de flou et de transparence
- Lors des tests, le flou et la transparence ont fortement réduit le nombre d'éléments pouvant être animés, alors qu'en les retirant, il était possible de dessiner des milliers de
box-shadowsur un portable M1 - La simulation de balles, les pixels d'image, les points sur la surface d'un cube ou d'une sphère, ainsi que des visualisations réactives à la musique, sont tous implémentés en mettant à jour à chaque frame une chaîne
box-shadow - Enfin, un ray tracer basé CPU a été parallélisé avec des Web Workers, montrant qu'il est possible de rendre une scène avec un seul
divet uniquementbox-shadow
Utiliser box-shadow comme un outil de dessin
- En CSS,
box-shadowest une forme de drop shadow à l'origine utilisée pour dessiner une ombre derrière une image ou un élément d'UI afin de créer une impression de profondeur - Une drop shadow classique décale le raster d'une image sur les axes x/y et le dessine derrière l'original avec une seule couleur pour donner du relief
filter: drop-shadow()en CSS accepte des décalages x/y et une couleur, avec éventuellement une valeur de blur- En superposant plusieurs filtres de drop shadow, on peut renforcer la profondeur d'une composition à l'écran
Caractéristiques et limites de box-shadow
- Le terme « Box » dans
box-shadowsignifie que la forme de l'ombre est limitée à la bounding box du conteneur - Cette contrainte peut sembler restrictive, mais comme la plupart des UI sont construites à partir de boîtes, elle reste utile en pratique
- L'implémentation CSS de
box-shadowbénéficie d'optimisations mathématiques peu coûteuses pour dessiner des boîtes arrondies, et une boîte arrondie peut ressembler à un cercle - Les designers peuvent ainsi éviter d'augmenter la taille des téléchargements en utilisant
box-shadowau lieu d'images pré-rendues - Un seul
divpeut chaîner plusieursbox-shadow, et les exemples s'appuient sur cette méthode pour composer couleurs et ombres - On peut expérimenter le réglage de
border-radiusavec le générateur CSS border-radius
Une utilisation « incorrecte » de box-shadow
- Dans un design d'UI classique, les ombres servent d'élément secondaire pour distinguer états et interactions, avec les marges, le padding, la typographie et l'accessibilité
- Mais
box-shadowpeut aussi être traité comme une API de peinture capable de colorer à l'écran un nombre arbitraire de rectangles et d'y appliquer éventuellement du blur - Les premiers essais sont partis du minimal art d'un article précédent, qui produisait déjà un résultat visuel avec de simples blocs de couleur
- Un outil de visualisation a ensuite été créé pour observer l'impact de davantage de
box-shadow, de blur et de transparence sur les performances- Il génère une grande chaîne
box-shadowet l'applique à un seuldiv - L'animation a d'abord consisté à changer la chaîne
box-shadowtoutes les 300 ms et à laissertransition: allfaire le reste - Cette approche provoquait du jank et était plus lente que le fait de définir
box-shadowà chaque frame
- Il génère une grande chaîne
- L'exemple avec 100
box-shadowremixe une palette de couleurs au tap et affiche à gauche l'historique des 10 palettes les plus récentes - L'application de blur réduisait le nombre d'éléments animables, et les couleurs transparentes réduisaient elles aussi fortement le nombre d'éléments pouvant être rendus
- La taille du
divinfluait également sur les performances, et lors de l'animation, le rasterizer logiciel semblait être impliqué - Sans transparence ni blur, un portable M1 pouvait dessiner des milliers de
box-shadow
Créer une simulation de balles avec box-shadow
box-shadowne peut pas pivoter, mais avecborder-radius, il peut prendre l'apparence d'un cercle- Plusieurs ombres circulaires ont été traitées comme des balles, avec une mise à l'échelle selon la valeur z pour produire une fausse profondeur 3D
- L'implémentation met à jour l'état du jeu dans
requestAnimationFrame, puis affecte à chaque frame une grande chaînebox-shadowà undiv - Le rendu suit les étapes suivantes
- Trier les balles selon leur valeur z
- Calculer leur taille en fonction de z
- Convertir la position x/y, le spread et la couleur de chaque balle en entrée
box-shadow - Joindre les entrées par des virgules pour former une seule chaîne
- L'exemple avec 50 balles permet de les déplacer par glisser-déposer et de les faire rebondir dans la boîte
- Cette fausse mise à l'échelle 3D donne un certain relief, mais quand les balles s'approchent de la « caméra », les cercles peuvent paraître cassés
- Le
divde base servant à créer lebox-shadowest trop petit par rapport au mode de mise à l'échelle - Augmenter la taille du conteneur corrige le problème, mais plus il est grand, plus les performances baissent
- Le
- Une version avec collisions entre balles utilise une détection de collisions en
n^2et ne fait que réfléchir la vitesse au moment du choc- Elle ne simule pas de véritables interactions physiques
- Pour rester lisible, la position en z a été figée afin d'en faire une scène 2D
- Un autre exemple fait revenir les balles vers leurs positions initiales aléatoires, et le fait de les attirer avec une force tactile donne un effet rappelant un morceau arraché à une éponge
Images et nuages de points 3D
- L'expérience suivante consiste à mapper les pixels d'une image en points sur un plan 2D, puis à dessiner chaque point avec
box-shadow - La position et la couleur des pixels de l'image sont lues puis stockées dans
state.particles, chaque pixel devenant une particule - Le code source est disponible dans cet exemple CodeSandbox
- Cet exemple rend des milliers de
box-shadowdans un espace 3D, avec une interaction qui disperse l'image lorsqu'on la fait glisser - Une version live en rotation automatique est proposée via
/box-shadow/v3?width=80&size=5&autoRotate=1, avec un avertissement sur la consommation de batterie - L'exemple avec un plus grand nombre montait à environ 12 000
box-shadow, niveau où des saccades devenaient visibles - Sur M1, un très grand nombre de
box-shadowpassait, mais pas sur desktop, iPhone ou anciens appareils Android au même niveau
Placer des points sur la surface d'un cube et d'une sphère
- L'expérimentation s'est poursuivie avec la projection uniforme de points sur la surface de maillages
- Le cube est implémenté en plaçant des points sur chacune de ses faces
- Toutes les arêtes sont supposées avoir la même longueur
- Chaque face est parcourue puis remplie de points selon la taille choisie
- Plus on augmente le nombre de points, plus la précision du cube augmente
- L'exemple du cube est interactif au tap et ajoute une petite lumière qui suit la position de la souris pour accentuer la profondeur
- Le calcul de l'éclairage n'est pas exact et utilise plusieurs « magic constants »
- La fonction de mapping des particules sur le cube et divers helpers mathématiques ont été obtenus avec gypity
- Le premier résultat donnait une distribution aléatoire
- La deuxième tentative a permis d'obtenir une répartition uniforme sur la surface du cube
- La répartition uniforme sur une sphère est plus complexe et utilise une méthode de spiral discretization
- L'idée est de placer uniformément des points le long d'une ligne qui enroule la sphère du bas vers le haut
- L'image utilisée est celle d'une corde enroulée autour d'une balle
- Ce mapping linéaire a aussi servi à une animation connectée à un tableau de valeurs de fréquences musicales
- Une animation de palette en dégradé, interpolée dans le temps, a été appliquée aux points, et cette partie s'est révélée plus difficile que les travaux précédents
- L'exemple de sphère avec peu de rotations montre que la spirale approxime une distribution uniforme, mais se casse visuellement quand le nombre de tours est insuffisant
Un ray tracer en box-shadow
- L'idée que si l'on peut dessiner des triangles avec
box-shadow, on peut presque produire n'importe quel CGI, a mené à une expérience de ray tracing - L'objectif était de dessiner une scène entière avec un seul
divetbox-shadowuniquement - Les exemples suivants utilisent à la fois un rendu live basse précision et un rendu d'image haute précision, et certains ne limitent pas la précision, donc prudence à l'exécution
- Les ray tracers et ray marchers sont des techniques exactes mais lentes de génération d'images, largement utilisées dans l'industrie du CGI
- Ici, le tracer repose sur le CPU plutôt que sur le GPU
- Utiliser le GPU aurait affaibli l'intérêt de l'expérience et compliqué l'implémentation
- Le premier tracer ne rendait que des sphères, et le modèle de données de la scène comprenait une caméra, plusieurs sphères et des informations de matériau
- Le code initial obtenu via gypity fonctionnait en temps réel, mais avec des problèmes
- Une ligne ajoutant le bias coefficient nécessaire à l'optimisation de l'échantillonnage manquait
- Le commentaire correspondant était bien là, mais pas la ligne effective ; après vérification de la technique d'échantillonnage, le problème a été corrigé
Rendu progressif et Three.js
- Le code a ensuite évolué vers une architecture de progressive rendering
- Le progressive rendering répartit un grand nombre de calculs de rayons sur plusieurs frames, montrant un rendu qui se rapproche progressivement de la « ground truth »
- Cette structure se marie bien avec un déplacement interactif de la caméra
- La caméra et les orbit controls utilisent la bibliothèque Three.js
- Pour éviter d'implémenter à la main les maths matricielles et la prise en charge mobile
- Cette version ne peut rendre que des sphères, et tous les éléments de la scène sont en pratique des sphères plus ou moins redimensionnées
- L'exemple s'exécute par défaut à un peu plus de 6 % de la résolution totale
- Plus la scène est éloignée à l'écran, plus la faible résolution semble nette ; de près, le manque de précision devient beaucoup plus visible
- Des paramètres de requête permettent d'ajuster l'échelle de résolution,
pixelSize, le nombre de rebonds des rayons et le nombre maximal d'échantillons - Augmenter la résolution et les échantillons améliore le résultat, mais ralentit énormément l'exécution
Implémentation du tracer et goulets d'étranglement de performance
- Le rendu suit l'approche standard : lancer un rayon par pixel depuis la caméra, calculer une couleur, puis moyenner le résultat entre plusieurs frames
- Comme le tracer utilisait Three.js, de nombreux nouveaux objets étaient créés puis rapidement abandonnés au garbage collector
- Une certaine réutilisation d'objets a été tentée, mais pour pousser les performances au maximum, il aurait mieux valu se passer de Three.js
- D'après le profiler, le garbage collection ne changeait pas énormément la donne, et l'usage de Three.js a été conservé
- L'idée de base du tracer est qu'un rayon rebondit sur les objets de la scène jusqu'à atteindre la lumière, puis renvoie une couleur selon les propriétés des objets et des sources lumineuses
- Il faut lancer beaucoup de rayons parce que certains atteignent une source lumineuse, et d'autres non
- Ce tracer utilise un modèle d'éclairage simple
- Pas de BRDF physiquement correcte
- Pas de textures
- Pas de subsurface scattering
- Seulement une lumière diffuse simple et de la specular reflection
- Pour apprendre le ray tracing, on peut consulter Ray Tracing in One Weekend
- Plus tard, il a pu rendre des plans, puis des rectangles lumineux après correction d'une erreur de manipulation des bits sur des valeurs en floating point
Paralléliser avec des Web Workers
- Au lieu de corriger les problèmes de GC liés à Three.js, l'amélioration des performances est passée par le multithreading avec Web Workers
- Le ray tracing se prête bien à la parallélisation, car chaque calcul retourne un résultat unique sans effet de bord
- Le gestionnaire de workers crée un pool de workers et expose deux méthodes,
renderetupdateScene, pour modifier la scène à l'exécution - Le code côté worker est une copie du tracer existant, avec du data marshalling nécessaire pour pouvoir remplacer la scène
- Pour réduire le coût de
postMessage, chaque worker rend une frame entière plutôt qu'un pixel isolé - L'exemple plein écran fonctionne bien plus vite que la version précédente
- L'inconvénient est qu'en cas de changement de caméra ou de scène, tous les résultats de calcul de rayons précédents deviennent invalides, ce qui peut rendre l'écran noir
- La solution consiste à lever un drapeau
isDirtylors des événements d'entrée, puis à décider dans la boucle de mise à jour s'il faut effacer la frame - Il peut arriver qu'un worker envoie une frame calculée avec les anciennes données de scène juste après une mise à jour
- On pourrait inclure un timestamp ou un identifiant de scène dans
postMessagepour l'ignorer - Dans l'expérience, cela a été laissé tel quel, car une frame contenant de mauvais rayons finit vite absorbée dans la moyenne
- On pourrait inclure un timestamp ou un identifiant de scène dans
- Au final, le ray tracing basé sur
box-shadowfonctionne suffisamment bien, et son code source est disponible sur CodeSandbox - Les statistiques de performances affichent une estimation du nombre total de rayons, le nombre d'échantillons et la résolution de rendu
- Le nombre d'échantillons s'arrête par défaut à 1200, mais peut être configuré
- Un modèle d'éclairage alternatif, plus rapide mais moins précis, peut aussi être activé
- Les données de scène sont en JSON, donc faciles à modifier directement
Conclusion : une expérience possible, mais déconseillée
- Avec seulement
box-shadowsur un uniquediv, il est possible de dessiner des simulations de balles, des nuages de points, des particules issues d'images, des surfaces de cube ou de sphère, et même des scènes en ray tracing - Cela reste cependant proche d'une approche « à ne pas faire », sans réel cas d'usage pratique
- Il y aurait encore des améliorations possibles avec le chargement de triangle meshes, une acceleration structure et un modèle d'éclairage plus précis
- Au départ, gypity avait répondu qu'un ray tracer en
box-shadowétait impossible, mais un exemple concret a finalement été réalisé - Le CSS n'est pas intuitif, mais il recèle parfois des possibilités étranges qui fonctionnent manifestement
1 commentaires
Commentaires Hacker News
Utiliser une couleur transparente ralentit les choses, car cela limite le traitement par lots du dessin GPU.
Pour les dessins opaques, on peut utiliser le tampon de profondeur pour réordonner librement les opérations et maximiser le batching. Dans le cas transparent, il faut dessiner dans l’ordre du peintre pour que le blending soit correct.
Chromium, en particulier, essaie de minimiser le nombre total de calques : il rend chaque calque sous forme de carte de pixels, puis compose à chaque frame les calques visibles pour produire l’image finale. En pratique, beaucoup de calques ne changent pas de pixels mais seulement de position, donc il suffit de les composer à chaque frame sans les rastériser à nouveau.
Avec beaucoup de box-shadow sans transparence, Chromium peut tout rastériser d’un coup dans un seul calque ; avec beaucoup de box-shadow transparents, il peut en faire des calques séparés. Cela peut être inefficace, mais c’est aussi nécessaire si des box-shadow semi-transparents doivent se déplacer indépendamment dans la page.
Il suffit de partir d’un tampon totalement transparent (α=0.0), de parcourir chaque face de l’avant vers l’arrière, puis, pour chaque pixel, de mélanger au tampon existant une proportion
1.0-buffer.αdu nouveau pixel. Sibuffer.α == 1.0, on peut l’ignorer complètement comme avec un tampon de profondeur.Cela dit, il faudrait revérifier les maths quand un autre objet transparent se trouve derrière un objet transparent, et les cas où les faces se recouvrent cycliquement ou s’interpénètrent sont délicats.
C’est aussi pour cette raison qu’il est généralement préférable de dessiner davantage de triangles plutôt que d’utiliser des textures transparentes.
Le rendu opaque peut être fait de l’avant vers l’arrière, de sorte qu’on ne rend réellement que ce qui est visible dans le framebuffer final ; après le passage de profondeur, le nombre de pixels est proportionnel à la taille du framebuffer.
Le rendu transparent doit être fait de l’arrière vers l’avant, et il faut rendre beaucoup de scènes qui seront ensuite partiellement masquées par d’autres polygones. Le nombre de pixels traversant le pipeline de shaders peut donc devenir proportionnel à la taille du maillage.
Si les éléments ne se chevauchent pas, la transparence ne devrait quasiment pas ralentir les choses. De toute façon, il faut toucher chaque pixel une fois, et seule la formule du shader a changé.
Si un dessin opaque est placé au-dessus d’un autre, dans le meilleur des cas on peut éliminer toutes les opérations de dessin qui se chevauchent, et dans le pire des cas on ne consomme que la bande passante d’un dessin individuel.
Avec la transparence, si l’on ne peut pas fusionner les opérations d’une façon ou d’une autre, il faut relire toute la zone de chevauchement ; à chaque dessin transparent, un bitmap d’au moins deux fois la taille du framebuffer final transite donc sur le bus mémoire.
Quand on pense que beaucoup d’appareils mobiles n’avaient même pas assez de bande passante mémoire pour blitter tout l’écran deux fois dans le temps nécessaire pour maintenir 60 fps, c’est un vrai problème.
C’était une exploration vraiment amusante.
Layering est effectivement le mot important. C’était aussi la clé d’un projet d’ombres de texte que j’ai fait il y a 14 ans, avec des effets absurdes mais parfois assez chouettes : https://paulirish.github.io/mothereffingtextshadow/
J’ai eu un peu honte de ne comprendre qu’au dernier paragraphe que
gypitydésignait ChatGPT.gyp.Les bons vieux hacks impraticables sont totalement bienvenus. Il faut juste se rappeler qu’il existe déjà canvas, et que canvas fait ce genre de choses plus facilement, plus vite et mieux.
La visualisation musicale était vraiment cool. L’époque de Winamp, où l’on lançait de la musique puis la visualisation en plein écran, me manque beaucoup.
J’aimerais que les lecteurs audio en streaming actuels proposent ce genre de fonction.
Mais oui, les lecteurs audio en streaming sont vraiment des logiciels aux fonctionnalités très basiques.
Au final, Firefox et Chrome rendent encore différemment une box-shadow de 1 px avec un zoom navigateur à 150 %.
Je mise sur Baseline 2025.
Je me demande s’il existe de bonnes références pour en apprendre davantage sur ces « hacks mathématiques permettant de dessiner à très bas coût les boîtes arrondies que les gens de l’UI adorent ».
https://www.folklore.org/Round_Rects_Are_Everywhere.html
http://wg20.criticalcodestudies.com/index.php?p=/discussion/...
Pour une approche moderne avec accélération 3D, comme quelqu’un l’a supposé, il y a cet article qui utilise des SDF :
https://mortoray.com/quickly-drawing-a-rounded-rectangle-wit...
C’est le genre de hack que j’aime.
Cela semble presque à l’opposé des articles de Josh Comeau que j’ai lus sur le sujet : https://www.google.com/search?q=josh+comeau+shadows
Un excellent article, peut-être le meilleur que j’aie lu cette année, se terminait par your welcome au lieu de
you're.Il faut corriger ça vite. Ou alors je n’ai peut-être pas compris la blague.
Ces 30 dernières années, je suis devenu bon en programmation, mais comme je n’aime pas les jeux, je n’ai presque pas fait de graphisme.
Aujourd’hui, je considère ça comme une énorme erreur et j’essaie de rattraper mon retard depuis plus d’un an, mais c’est vraiment difficile.