Wie viel wissen Sie über Box-Shadows?
(dgerrells.com)- CSS-box-shadow ist normalerweise eine Eigenschaft, die UIs Tiefe verleiht. Stapelt man jedoch Hunderte bis Tausende davon auf einem einzelnen
div, verhält sie sich wie eine Art Zeichen-API - Trotz der Beschränkung auf Rechtecke lassen sich mit abgerundeten Ecken, mehreren Shadow-Ketten sowie Kombinationen aus Farbe, Blur und Transparenz kreisförmige Punkte und Layer-Effekte erzeugen
- Im Experiment reduzierten Blur und Transparenz die Anzahl der animierbaren Elemente stark; ohne diese konnte auf einem M1-Laptop Tausende von box-shadows gezeichnet werden
- Ballsimulationen, Bildpixel, Punkte auf Würfel- und Kugeloberflächen sowie musikreaktive Visualisierungen werden alle umgesetzt, indem pro Frame der box-shadow-String aktualisiert wird
- Am Ende wird ein CPU-basierter Raytracer mit Web Workers parallelisiert und zeigt, dass sich Szenen allein mit einem einzelnen
divund box-shadow rendern lassen
box-shadow als Zeichenwerkzeug verwenden
- CSS
box-shadowist eine Form von Drop Shadow und wird ursprünglich verwendet, um hinter Bildern oder UI-Elementen Schatten zu zeichnen und so Tiefe zu erzeugen - Ein gewöhnlicher Drop Shadow verschiebt das Raster eines Bildes auf der x-/y-Achse und zeichnet es in einer einzelnen Farbe hinter das Original, um einen Tiefeneindruck zu schaffen
- CSS
filter: drop-shadow()akzeptiert x-/y-Offsets und eine Farbe und unterstützt optional auch einen Blur-Wert - Durch das Layering mehrerer Drop-Shadow-Filter lässt sich der Bildkomposition zusätzliche Tiefe geben
Eigenschaften und Einschränkungen von box-shadow
- Das „Box“ in
box-shadowbedeutet, dass die Schattenform auf die Bounding Box des Containers beschränkt ist - Diese Einschränkung wirkt begrenzend, ist in echten UIs aber nützlich, da die meisten UIs aus Boxen bestehen
- Die CSS-box-shadow-Implementierung unterstützt mathematische Optimierungen, um abgerundete Boxen günstig zu zeichnen, und solche abgerundeten Boxen können wie Kreise aussehen
- Designer können mit box-shadow größere Downloads vermeiden, ohne auf vorgerenderte Bilder zurückgreifen zu müssen
- An ein einzelnes
divlassen sich mehrere box-shadows wie eine Kette anhängen, und die Beispiele bauen Farben und Schatten genau auf diese Weise auf - Anpassungen des border-radius lassen sich mit dem CSS border-radius generator ausprobieren
„Falsche“ Verwendung von box-shadow
- Im gewöhnlichen UI-Design werden Schatten zusammen mit Abständen, Padding, Typografie und Barrierefreiheit als unterstützendes Mittel eingesetzt, um Zustände und Interaktionen zu unterscheiden
- box-shadow lässt sich aber auch wie eine Painting-API behandeln, die eine beliebige Anzahl Rechtecke auf den Bildschirm malt und optional Blur anwendet
- Die ersten Experimente begannen mit minimalistischer Kunst aus einem früheren Artikel und erzeugten bereits mit einer einfachen Konfiguration farbiger Blöcke visuelle Ergebnisse
- Später wurde ein Visualisierungstool gebaut, um zu sehen, wie sich mehr box-shadows, Blur und Transparenz auf die Performance auswirken
- Es erzeugt einen großen box-shadow-String und setzt ihn auf ein einzelnes
div - Die Animation begann damit, alle 300 ms den box-shadow-String auszutauschen und den Rest
transition: allzu überlassen - Dieser Ansatz ruckelte und war langsamer, als pro Frame direkt box-shadow zu setzen
- Es erzeugt einen großen box-shadow-String und setzt ihn auf ein einzelnes
- Das Beispiel mit 100 box-shadows remixt per Tap die Farbpalette und zeigt links einen Verlauf der letzten 10 Paletten
- Mit Blur sank die Anzahl animierbarer Elemente, und auch transparente Farben reduzierten die Zahl der darstellbaren Elemente deutlich
- Auch die Größe des
divbeeinflusste die Performance, und bei Animationen schien der Software-Rasterizer beteiligt zu sein - Ohne Transparenz und Blur konnte ein M1-Laptop Tausende von box-shadows zeichnen
Eine Ballsimulation mit box-shadow bauen
- box-shadow kann nicht rotieren, aber mit border-radius lässt sich ein kreisartiges Erscheinungsbild erzeugen
- Mehrere kreisförmige Schatten wurden wie Bälle behandelt, und ihre Größe wurde abhängig vom z-Wert skaliert, um einen künstlichen 3D-Tiefeneindruck zu erzeugen
- Die Implementierung aktualisiert den Spielzustand in
requestAnimationFrameund setzt pro Frame einen großen box-shadow-String auf dasdiv - Der Rendering-Prozess sieht so aus
- Die Bälle werden nach ihrem z-Wert sortiert
- Ihre Größe wird abhängig vom z-Wert berechnet
- Für jeden Ball werden x-/y-Position, Spread und Farbe in einen box-shadow-Eintrag umgewandelt
- Die Einträge werden mit Kommas zu einem einzigen String verbunden
- Im Beispiel mit 50 Bällen lassen sich die Bälle per Drag bewegen und innerhalb der Box abprallen
- Die künstliche 3D-Skalierung vermittelt etwas Tiefe, aber wenn Bälle der „Kamera“ nahe kommen, können die Kreise sichtbar auseinanderfallen
- Das liegt daran, dass das
div, von dem die box-shadows ausgehen, im Verhältnis zur Skalierung zu klein ist - Eine größere Containergröße behebt das Problem, verlangsamt aber die Performance
- Das liegt daran, dass das
- In einer Version mit Kollisionen zwischen Bällen wird eine
n^2-Kollisionsprüfung verwendet und bei einer Kollision nur die Geschwindigkeit reflektiert- Eine echte physikalische Wechselwirkung wird nicht simuliert
- Für bessere Sichtbarkeit wurde die z-Position fixiert, sodass das Ganze 2D ist
- Es wurde auch ein Beispiel gebaut, in dem die Bälle zu zufälligen Startpositionen zurückwollen; zieht man sie mit einer Touch-Kraft an, entsteht ein Effekt, als würde man Stücke aus einem Schwamm reißen
Bilder und 3D-Punktwolken
- Das nächste Experiment mappt Bildpixel auf Punkte in einer 2D-Ebene und zeichnet jeden Punkt als box-shadow
- Position und Farbe der Bildpixel werden ausgelesen und in
state.particlesgespeichert; jedes Pixel wird als einzelnes Partikel verwendet - Der Sourcecode ist im CodeSandbox-Beispiel verfügbar
- Dieses Beispiel rendert Tausende box-shadows im 3D-Raum und erlaubt per Drag eine Interaktion, bei der das Bild auseinanderzustieben scheint
- Eine Live-Version mit automatischer Rotation ist unter
/box-shadow/v3?width=80&size=5&autoRotate=1verfügbar und trägt beim Start einen Hinweis auf möglichen Batterieverbrauch - Ein Beispiel mit höherer Anzahl arbeitete mit etwa 12.000 box-shadows, und auf diesem Niveau zeigte sich deutliches Stottern
- Auf einem M1 konnten sehr viele box-shadows verarbeitet werden, aber Desktop-Systeme, iPhones und ältere Android-Geräte schafften dieses Niveau nicht
Punkte auf Würfel- und Kugeloberflächen platzieren
- Es folgten weitere Experimente, bei denen Punkte gleichmäßig auf die Oberfläche von Meshes projiziert wurden
- Der Würfel wurde umgesetzt, indem Punkte entlang jeder Fläche platziert werden
- Es wird angenommen, dass alle Kanten gleich lang sind
- Jede Fläche wird durchlaufen und entsprechend der vorgegebenen Größe mit Punkten gefüllt
- Mehr Punkte erhöhen die Genauigkeit des Würfels
- Das Würfelbeispiel lässt sich per Tap bedienen und ergänzt einen kleinen Lichtpunkt, der der Mausposition folgt und den Tiefeneindruck verstärkt
- Die Lichtberechnung ist nicht exakt und verwendet mehrere „magic constants“
- Die Mapping-Funktion für Würfelpartikel und verschiedene mathematische Hilfsfunktionen wurden mit gypity erzeugt
- Das erste Ergebnis war eine zufällige Verteilung
- Im zweiten Versuch gelang eine gleichmäßige Verteilung auf der Würfeloberfläche
- Die gleichmäßige Verteilung auf einer Kugeloberfläche ist komplexer, daher wurde eine spiral discretization verwendet
- Die Idee ist, Punkte gleichmäßig auf einer Linie zu platzieren, die die Kugel von unten nach oben umwickelt
- Verglichen wird das mit einem Seil, das einen Ball umspannt
- Auf Basis dieses linearen Mappings wurde auch eine Animation erstellt, die an ein Array von Frequenzwerten aus Musik gekoppelt ist
- Auf die Punkte wurde eine Gradient-Palettenanimation angewendet, die über die Zeit interpoliert; dieser Teil war schwieriger als die vorherigen Arbeiten
- Im Kugelbeispiel mit wenigen Umdrehungen nähert die Spirale eine gleichmäßige Verteilung an, bricht aber sichtbar auf, wenn die Zahl der Umdrehungen nicht ausreicht
box-shadow-Raytracer
- Der Gedanke, dass sich mit box-shadows Dreiecke zeichnen lassen und damit fast jede CGI erzeugbar sein müsste, führte zum Experiment mit Raytracing
- Das Ziel war, eine Szene nur mit einem einzelnen
divund box-shadow zu zeichnen - Die späteren Beispiele kombinieren Live-Rendering mit niedriger Präzision und Bild-Rendering mit hoher Präzision; einige Beispiele haben keine Präzisionsbegrenzung, daher ist beim Ausführen Vorsicht geboten
- Raytracer und Ray Marcher sind exakte, aber langsame Verfahren zur Bilderzeugung und werden in der gesamten CGI-Branche eingesetzt
- Hier wird ein CPU-basierter Tracer statt der GPU verwendet
- Mit GPU würde der Sinn des Experiments verwässert und die Umsetzung wäre schwieriger
- Der erste Tracer wurde so gebaut, dass er nur Kugeln rendert; das Szenendatenmodell enthält Kamera, mehrere Kugeln und Materialinformationen
- Der anfängliche Code von gypity lief in Echtzeit, hatte aber Probleme
- Bei einer Sampling-Optimierung fehlte die Zeile, die den nötigen Bias-Koeffizienten addiert
- Ein entsprechender Kommentar war vorhanden, aber die eigentliche Zeile fehlte; nach Recherche zur Sampling-Technik wurde das korrigiert
Progressive Rendering und Three.js
- Später wurde der Code auf eine Progressive-Rendering-Struktur umgestellt
- Progressive Rendering verteilt viele Ray-Berechnungen auf mehrere Frames und zeigt, wie das Rendering schrittweise näher an die „ground truth“ heranrückt
- Diese Struktur passt gut zu einer interaktiv bewegbaren Kamera
- Für Kamera und Orbit Controls wird die Bibliothek Three.js verwendet
- So mussten Matrixmathematik und Mobile-Support nicht selbst implementiert werden
- Diese Version kann nur Kugeln rendern, und alle Elemente der Szene sind in gewissem Maße skalierte Kugeln
- Das Beispiel läuft standardmäßig mit etwas mehr als 6 % der vollen Auflösung
- Je weiter man sich auf dem Bildschirm entfernt, desto schärfer wirkt die niedrig aufgelöste Szene; aus der Nähe wird der Mangel an Präzision deutlicher sichtbar
- Über Query-Parameter lassen sich Auflösungsskalierung,
pixelSize, Anzahl der Ray-Bounces und maximale Sample-Zahl anpassen - Höhere Auflösung und mehr Samples verbessern das Ergebnis, machen es aber sehr langsam
Tracer-Implementierung und Performance-Engpässe
- Das Rendering folgt dem Standardansatz: Für jedes Pixel wird relativ zur Kamera ein Ray ausgesendet, die Farbe berechnet und anschließend über Frames gemittelt
- Da der Tracer Three.js nutzt, werden viele neue Objekte erzeugt, die schnell zu Garbage werden
- Es wurde teilweise versucht, Objekte wiederzuverwenden, aber für maximale Performance wäre es besser gewesen, Three.js gar nicht zu verwenden
- Laut Profiler machte die Garbage Collection keinen entscheidenden Unterschied, daher blieb die Nutzung von Three.js bestehen
- Die Grundidee des Tracers ist, dass ein Ray auf Objekte in der Szene trifft, bis zum Licht weiterbounct und abhängig von Objekt- und Lichteigenschaften eine Farbe zurückliefert
- Viele Rays sind nötig, weil manche das Licht erreichen und andere nicht
- Dieser Tracer verwendet ein einfaches Beleuchtungsmodell
- Keine physikalisch korrekte BRDF
- Keine Texturen
- Kein subsurface scattering
- Einfache diffuse Beleuchtung und specular reflection
- Als Lernmaterial für Raytracing wird Ray Tracing in One Weekend empfohlen
- Später konnten auch Planes gerendert werden, und nach der Korrektur eines Fehlers bei Bitmanipulationen von Floating-Point-Werten ließen sich auch rechteckige Lichtquellen rendern
Mit Web Workers parallelisieren
- Die Performance-Verbesserung wurde nicht über die Behebung von Three.js-GC-Problemen angegangen, sondern über Multithreading mit Web Workers
- Raytracing eignet sich gut für Parallelisierung, da jede Berechnung ein einzelnes Ergebnis liefert und keine Seiteneffekte hat
- Ein Worker-Manager erzeugt einen Worker-Pool und bietet die beiden Methoden
renderundupdateScene, damit sich die Szene zur Laufzeit ändern lässt - Der Worker-Code ist eine Kopie des bestehenden Tracers; zum Austausch der Szene war Data Marshalling nötig
- Um den
postMessage-Overhead zu verringern, rendert jeder Worker nicht ein einzelnes Pixel, sondern ein ganzes Frame - Das Vollbild-Beispiel läuft dadurch deutlich schneller als zuvor
- Der Nachteil ist, dass bei Änderungen an Kamera oder Szene alle bisherigen Ray-Berechnungsergebnisse ungültig werden und der Bildschirm schwarz erscheinen kann
- Gelöst wurde das über ein
isDirty-Flag bei Eingabeereignissen und eine Prüfung im Update-Loop, ob das Frame gelöscht werden soll - Es kann vorkommen, dass ein Worker direkt nach einem Szenenupdate noch ein Frame sendet, das auf alten Szenendaten basiert
- Mit Timestamp oder Scene-ID in
postMessageließe sich das verwerfen - Im Experiment wurde es so belassen, weil die falschen Ray-Daten eines einzelnen Frames schnell herausgemittelt werden
- Mit Timestamp oder Scene-ID in
- Insgesamt funktionierte box-shadow-basiertes Raytracing ausreichend gut; der Sourcecode ist auf CodeSandbox verfügbar
- Die Performance-Statistik zeigt eine geschätzte Gesamtzahl an Rays, die Sample-Anzahl und die Renderauflösung
- Standardmäßig stoppt das Sampling bei 1200, lässt sich aber konfigurieren
- Ein schnelleres, aber ungenaueres alternatives Beleuchtungsmodell lässt sich ebenfalls umschalten
- Die Szenendaten liegen als JSON vor und können leicht direkt bearbeitet werden
Fazit: möglich, aber nicht empfehlenswert
- Allein mit box-shadow eines einzelnen
divlassen sich Ballsimulationen, Punktwolken, bildbasierte Partikel, Würfel- und Kugeloberflächen bis hin zu Raytracing-Szenen zeichnen - Das gesamte Experiment bleibt jedoch ein Ansatz, den man eher „nicht tun sollte“, ohne praktischen Anwendungsfall
- Mit dem Laden von Triangle Meshes, Acceleration Structures und genaueren Beleuchtungsmodellen gäbe es noch Spielraum für Verbesserungen
- gypity antwortete anfangs, ein box-shadow-Raytracer sei unmöglich, doch ein reales Beispiel wurde dennoch gebaut
- CSS ist nicht intuitiv, hat aber manchmal seltsame Möglichkeiten, die offensichtlich trotzdem funktionieren
1 Kommentare
Hacker-News-Kommentare
Die Verwendung transparenter Farben macht es langsamer, weil sie GPU-Draw-Batching einschränkt.
Undurchsichtiges Zeichnen kann den Tiefenpuffer nutzen, die Reihenfolge beliebig ändern und das Batching maximieren. Bei Transparenz muss in Painter's Order gezeichnet werden, damit das Blending korrekt ist.
Insbesondere Chromium versucht, die Gesamtzahl der Layer zu minimieren, rendert jeden Layer als Pixelmap und setzt dann in jedem Frame die sichtbaren Layer zum finalen Bild zusammen. In der Praxis ändern sich bei vielen Layern nicht die Pixel, sondern nur ihre Position, sodass man sie nicht in jedem Frame rasterisieren muss, sondern nur compositen kann.
Bei vielen box-shadows ohne Transparenz kann Chromium alles auf einmal in einen einzigen Layer rasterisieren, bei vielen transparenten box-shadows könnte es sie dagegen jeweils zu separaten Layern machen. Das kann ineffizient sein, ist aber auch die notwendige Methode, wenn sich halbtransparente box-shadows auf der Seite unabhängig bewegen müssen.
Man startet mit einem vollständig transparenten Buffer (α=0.0), geht jede Fläche von vorn nach hinten durch und blendet für jeden Pixel
1.0-buffer.αdes neuen Pixels in den bestehenden Buffer. Wennbuffer.α == 1.0ist, kann man ihn wie mit einem Tiefenpuffer vollständig überspringen.Allerdings müsste man die Mathematik noch einmal prüfen, wenn sich hinter einem transparenten Objekt ein weiteres transparentes Objekt befindet; außerdem werden Fälle schwierig, in denen Flächen sich zyklisch überlappen oder einander durchdringen.
Das ist auch der Grund, warum es im Allgemeinen besser ist, mehr Dreiecke zu zeichnen als transparente Texturen zu verwenden.
Undurchsichtiges Rendering kann von vorn nach hinten zeichnen, sodass nur das gerendert wird, was im finalen Framebuffer tatsächlich sichtbar ist; die Anzahl der Pixel nach dem Depth Pass ist proportional zur Größe des Framebuffers.
Transparentes Rendering muss von hinten nach vorn zeichnen und dabei viele Szenenteile rendern, die später teilweise von anderen Polygonen verdeckt werden. Deshalb kann die Zahl der Pixel, die durch die Shader-Pipeline laufen, bis hin zur Proportionalität zur Mesh-Größe anwachsen.
Wenn sich Elemente nicht überlappen, dürfte Transparenz kaum langsamer sein. Schließlich muss man jeden Pixel ohnehin einmal anfassen, und geändert hat sich nur die Shader-Formel.
Wenn undurchsichtiges Zeichnen über anderem Zeichnen liegt, können im besten Fall alle überlappenden Zeichenoperationen entfernt werden; im schlechtesten Fall braucht man nur so viel Bandbreite wie für die einzelne Zeichenoperation.
Bei Transparenz muss, sofern man die Operationen nicht irgendwie zusammenführen kann, der gesamte überlappende Bereich erneut gelesen werden. Dadurch wandert für jede transparente Zeichenoperation ein Bitmap von mindestens der doppelten Größe des finalen Framebuffers über den Speicherbus.
Wenn man bedenkt, dass vielen Mobilgeräten schon die Speicherbandbreite fehlte, um innerhalb der Zeit für 60 fps den gesamten Bildschirm zweimal zu blitten, ist das ein ziemlich großes Problem.
Das war eine wirklich interessante Erkundung.
Dass Layering hier ein wichtiges Wort ist, stimmt ebenfalls. Auch bei einem Textschatten-Projekt, das ich vor 14 Jahren gemacht habe und das absurde, aber manchmal ziemlich cool aussehende Effekte erzeugte, war Layering der Kern: https://paulirish.github.io/mothereffingtextshadow/
Es war mir etwas peinlich, dass ich erst im letzten Absatz bemerkt habe, dass
gypityChatGPT meint.gyp.Gutes altmodisches unpraktisches Hacking ist absolut willkommen. Man sollte nur im Hinterkopf behalten, dass es bereits canvas gibt und dass canvas solche Dinge einfacher, schneller und besser kann.
Die Musikvisualisierung war definitiv cool. Ich vermisse die alten Winamp-Zeiten wirklich, als man Musik abgespielt und die Visualisierung im Vollbild laufen ließ.
Ich wünschte, heutige Streaming-Audio-Player hätten auch so eine Funktion.
Dass Streaming-Audio-Player wirklich nur Software mit den allernötigsten Grundfunktionen sind, stimmt allerdings.
Am Ende rendern Firefox und Chrome einen 1px box-shadow bei 150 % Browser-Zoom immer noch unterschiedlich.
Ich setze meine Hoffnungen auf Baseline 2025.
Ich frage mich, ob es gute Referenzen gibt, um mehr über „mathematische Hacks zum sehr günstigen Zeichnen der abgerundeten Boxen, die UI-Leute so mögen“ zu lernen.
https://www.folklore.org/Round_Rects_Are_Everywhere.html
http://wg20.criticalcodestudies.com/index.php?p=/discussion/...
Für moderne 3D-beschleunigte Ansätze gibt es einen Artikel, der SDFs verwendet, wie jemand anderes vermutet hat.
https://mortoray.com/quickly-drawing-a-rounded-rectangle-wit...
Das ist genau die Art von Hack, die ich mag.
Es wirkt fast wie das genaue Gegenteil der Josh-Comeau-Artikel, die ich zu diesem Thema gelesen habe: https://www.google.com/search?q=josh+comeau+shadows
Ein hervorragender Artikel, vielleicht der beste, den ich dieses Jahr gelesen habe, endete am Schluss mit your welcome statt
you're.Das sollte schnell korrigiert werden. Oder ich habe den Witz nicht verstanden.
In den letzten 30 Jahren bin ich zwar gut im Programmieren geworden, habe aber kaum Grafik gemacht, weil ich keine Spiele mag.
Inzwischen sehe ich das als riesigen Fehler und versuche seit über einem Jahr, aufzuholen; es ist wirklich schwer.