7 Punkte von GN⁺ 2024-05-06 | 1 Kommentare | Auf WhatsApp teilen
  • Mit der erweiterten Unterstützung für CSS-Mathefunktionen wird ein Ansatz wieder praktikabel, bei dem der Animationszustand statt über Keyframes über die Zeit-Tick-Variable --t berechnet wird
  • Kombiniert man @property --t mit einer Root-Animation von 86400000ms Länge, steigt die Zeit in Millisekunden an und lässt sich mit Berechnungen für Eigenschaften wie rotate oder translate verknüpfen
  • Mit step() lässt sich die Framerate auf etwa 8fps, 24fps oder 60fps begrenzen, und dank Browser-Optimierungen sind selbst Aktualisierungsraten oberhalb von 60fps meist kein Problem
  • Mit min(), mod(), sin(), pow() und round() lassen sich Zeitwerte begrenzen, wiederholen, oszillieren und glätten, sodass benutzerdefinierte Easing-Funktionen und Positionsänderungen ohne Keyframes ausgedrückt werden können
  • css-doodle bietet Kurzschreibweisen wie @t, @T und @TS, mit denen sich Demos mit vielen mathematischen Ausdrücken kürzer schreiben und schneller ausprobieren lassen

Einen Zeit-Tick als CSS-Variable erzeugen

  • Früher war dieser Ansatz nur eingeschränkt einsetzbar, weil sich komplexe mathematische Berechnungen in CSS nur schwer umsetzen ließen
  • Heute unterstützt CSS die nötigen mathematischen Funktionen wie mod(), round() und trigonometrische Funktionen, sodass zeitbasierte Animationen wieder interessant werden
  • Zum Ansehen der Demos muss möglicherweise Experimental feature flags aktiviert werden

Grundlegende Funktionsweise

  • CSS kann wie JavaScript nicht direkt einen Timer starten, aber mit der CSS Houdini API lässt sich die benutzerdefinierte Variable --t definieren, um Zeit in Millisekunden zu verfolgen
  • Im Grundbeispiel wird @property --t als <integer> deklariert, in @keyframes tick von 0 bis 86400000 verändert und auf :root eine Animation 86400000ms linear infinite angewendet
  • --t steigt jede Millisekunde um 1 und erreicht nach 1 Sekunde den Wert 1000
  • Mit counter() lässt sich der aktuelle Wert von --t auf dem Bildschirm anzeigen
  • Verknüpft man eigenschaftsseitige Berechnungen wie rotate: calc(var(--t) * .001turn) mit --t, ändern sich die Werte mit der Zeit und ergeben eine Animation

Steuerung der Framerate

  • Für flüssige Animationen reicht eine Aktualisierungsrate von 60fps aus
  • Durch Browser-Rendering-Optimierungen ist auch eine höhere Aktualisierungsrate als 60fps in der Regel unproblematisch
  • Bei Bedarf lässt sich die Framerate mit step() in animation-timing-function direkt begrenzen
    • 8fps: step(calc(86400000/(1000/8)))
    • 24fps: step(calc(86400000/(1000/24)))
    • 60fps: step(calc(86400000/(1000/60)))

Einen steigenden Zeitwert in Animationswerte umwandeln

  • Der Wert von --t steigt fortlaufend in eine Richtung an
  • Ein angle-Wert darf auch über 360deg hinausgehen, aber nicht alle CSS-Eigenschaften behandeln Werte zyklisch
  • translate: calc(var(--t) * .001px) verschiebt eine Box immer weiter nach rechts
  • Mit min() anhalten

    • Soll die Bewegung beim Erreichen eines bestimmten Offsets stoppen, kann min() verwendet werden
    • translate: min(270px, calc(var(--t) * .5px)) begrenzt den Verschiebungswert auf 270px
    • Für eine exakt steuerbare Dauer kann stattdessen --t selbst begrenzt werden
    • translate: calc(min(3000, var(--t)) * (270px / 3000))
    • Diese Berechnung bewegt das Element in 3 Sekunden um 270px
  • Mit mod() wiederholen

    • Soll die Box nach der Bewegung nach rechts wieder von vorn beginnen, wird mod() verwendet
    • Mit translate: calc(mod(var(--t)/4, 270) * 1px) lässt sich der Offset wiederholen
  • Mit sin() eine Hin-und-zurück-Bewegung erzeugen

    • Eine Hin-und-zurück-Bewegung lässt sich mit sin() ausdrücken
    • Dabei wird der Zeitwert mit einem trigonometrischen Ausdruck verknüpft, etwa in translate: calc(sin(mod(var(--t)/135, 270)) * 135px)

Benutzerdefinierte Easing-Funktionen

  • Mit CSS-Mathefunktionen und der Variablen --t lassen sich benutzerdefinierte Easing-Funktionen erzeugen, die mit cubic-bezier() schwer oder gar nicht erreichbar sind
  • Zunächst wird --t auf einen Wert zwischen 0 und 1 begrenzt
    • --t01: calc(min(1000, var(--t)) / 1000) verändert sich innerhalb von 1 Sekunde von 0 auf 1
  • ease-out-cubic

    • ease-out-cubic basiert auf der Formel 1 - pow(1 - t, 3)
    • Nach dem Erzeugen des Werts --ease-out-cubic wird er mit translate: calc(var(--ease-out-cubic) * 270px) verknüpft
  • ease-out-elastic

    • ease-out-elastic basiert auf der Formel pow(2, -10t) * sin((10t - .75) * 2/3 * PI) + 1
    • Auf dieselbe Weise wird --ease-out-elastic gebildet und für eine translate-Bewegung über 270px verwendet

Formeln mit css-doodle verkürzen

  • Je komplexer die Formeln werden, desto schwerer lesbar machen var() und calc() den Code
  • Die neueste Version von css-doodle ergänzt die @t-Funktion zur Darstellung von --t und akzeptiert darin direkt einfache mathematische Ausdrücke
  • Ein rotate-Ausdruck lässt sich so verkürzen
    • bisher: rotate: calc(mod(var(--t) / 1000, 10) * 5deg)
    • mit css-doodle: rotate: @t(/1000, %10, *5deg)
  • Auch ohne geschriebene Keyframes lassen sich so mathematikbasierte Animationen mit kurzem Code erstellen
  • Durch das Ändern von Parametern kann man neue Ergebnisse schnell ausprobieren
  • @T und @TS

    • Zusätzlich zu @t steht das großgeschriebene @T für die Time Ticks seit Tagesbeginn
    • @TS ist die Kurzform von @t(/1000) und verfolgt die Zeit in Sekunden
    • Ein Uhr-Beispiel ist auf CodePen zu sehen
    • Sekundenzeiger: rotate: @TS(*6, %360deg)
    • Minutenzeiger: rotate: @TS(/60, *6, %360deg)
    • Stundenzeiger: rotate: @TS(/60, /12, *6, %360deg)
  • Sprungbewegung des Sekundenzeigers mit round()

    • Soll sich der Sekundenzeiger springend bewegen, kann round() verwendet werden
    • Der dritte Parameter von round() legt das Rundungsintervall fest
    • Bei einer Uhr entspricht ein Schritt 360 / 60 = 6deg, daher wird rotate: round(down, @TS(*6, %360deg), 6deg) verwendet

Demos und Anwendungspunkte

  • Ein Beispiel, das Farbe und Position gemeinsam animiert, ist auf CodePen zu sehen
  • Ein Beispiel für Textfarben-Animationen gibt es auf CodePen
  • Keyframes können intuitiver wirken, aber in Demo-Szenen mit vielen mathematischen Berechnungen und Eingabevariablen lässt sich mit dem Ansatz, Zeit als Variable zu verwenden, leichter eine größere Vielfalt an Ergebnissen erzeugen
  • Mit dem Update vom 17. Mai 2024 wurde eine Demo für Textfarben-Animationen hinzugefügt

1 Kommentare

 
GN⁺ 2024-05-06
Hacker-News-Kommentare
  • Wenn man den Fortschritt per JS steuern möchte, empfiehlt sich auch ein negativer animation-delay
    Zum Beispiel startet animation-delay: -1500ms die Animation sofort, springt aber an die Stelle bei 1,5 Sekunden
    Steuert man diesen Wert per JS, kann man CSS-Animationen aus JS heraus praktisch scrubben und so alle Animationen mit einer Game-Engine-artigen Compute-Update-Render-Tick-Schleife synchronisieren
    https://developer.mozilla.org/en-US/docs/Web/CSS/animation-d...

    • Würden damit nicht auch Animationen auf Basis des Scroll-Offsets im iOS-Stil möglich?
  • Sobald es auch nur etwas komplexer wird als einfache Easing-Funktionen oder ein, zwei grundlegende Keyframe-Kanäle, stößt dieser Ansatz schnell an seine Grenzen
    Ich nutze seit einigen Jahren Theatre.js und bin recht zufrieden damit
    Die Bibliothek ist aufgeteilt in eine Studio-UI zum Bearbeiten von Keyframes und Bézierkurven auf einer Timeline sowie eine Runtime, die diese Keyframes relativ zur Timeline interpoliert
    Wenn mehrere Animationen aufeinander abgestimmt laufen sollen, ist es einen Blick wert
    https://www.theatrejs.com/

  • Sehr beeindruckend, aber irgendwie fühlt es sich auch so an, als kämen wir erst jetzt in die Nähe dessen, was vor Jahrzehnten mit Flash möglich war
    Ich wünsche mir Flash nicht zurück, aber ich hätte gern benutzerfreundlichere Werkzeuge zum Erstellen von CSS-Animationen

  • CSS unterstützt inzwischen ausreichend mathematische Funktionen wie mod(), round() und trigonometrische Funktionen
    CSS kann zwar nicht wie JavaScript einen Timer starten, aber heute kann man mit der CSS Houdini API benutzerdefinierte Variablen definieren, die Zeit im Millisekundenbereich verfolgen
    Aber wozu braucht man all das, wenn es JavaScript gibt?
    Sollte sich die Darstellungsebene nicht auf Darstellungsprimitive konzentrieren? Warum werden dort immer mehr höherstufige Funktionen eingebaut?

    • Man darf nicht vergessen, dass CSS-Animationen hardwarebeschleunigt sind
      Wenn man in JS für Animationen trigonometrische Funktionen ausführt, manipuliert man vermutlich auch bei jeder Millisekunde das DOM, um die UI zu aktualisieren, und das ist sehr teuer
    • CSS ist keine Darstellungsebene
      Das Rendering wird vom DOM angetrieben, das aus HTML besteht oder per JS aktualisiert wird
      CSS liegt nur darüber, indem es der Painting-Engine Parameter dafür übergibt, was mit dem DOM zu tun ist, und dabei Standardwerte überschreibt
      Man kann DOM-Blöcke hinzufügen, das Positionierungsverhalten mit komplexen Algorithmen verändern, und selbst Margins sind in Wahrheit nicht so simpel
      Ich halte es nicht für eine großartige Lösung, aber offenbar begann es damit, dass man HTML-Code nicht aktualisieren musste, nur um beim DOM-Rendering Farben zu ändern
      Vermutlich, weil das Aktualisieren von HTML-Code überhaupt keinen Spaß machte; meiner Ansicht nach wurde von Anfang an das falsche Problem gelöst
      Allerdings war das eben auch die Zeit der „Webmaster“
    • Ein Vorteil, der mir einfällt, ist die Trennung von Anwendungslogik und Darstellung
      Es ist leichter zu lesen, wenn in der Fetch-Logik nicht auch die geometrische Fortschrittslogik eines Loader-DOM-Elements steckt, etwa das Berechnen des Rotationswinkels und das Anwenden von Inline-Styles
      Ich weiß, dass es heutzutage nicht beliebt ist, Schokolade und Erdnussbutter getrennt zu halten, aber solche Konventionen ziehen Grenzen, die es leichter machen abzuleiten, wo Code zu finden ist und wo er hingehört
    • JS ist grundsätzlich single-threaded; natürlich gibt es Worker, aber um die UI bei stabiler Framerate zu halten, ist alles hilfreich, was man an anderer Stelle erledigen lassen kann
    • Warten wir, bis WebSocket in CSS landet!
      Spaß beiseite: CSS ist deklarativ, und es ist natürlich, dass sich die Probleme, die man mit JS und CSS lösen kann, überschneiden
      Ich freue mich wirklich über die neuen Funktionen, die zuletzt zu CSS hinzugekommen sind, und weniger interpretierter Code ist eine gute Sache
  • Das im Artikel verwendete benutzerdefinierte CSS-@property ist großartig, und die Browser-Unterstützung liegt ebenfalls bei 88% [1]
    Allerdings sollte man beim Setzen eines alternativen initial-value auf Unterschiede im Browserverhalten achten
    Chrome verwendet initial-value, wenn eine CSS-Variable nicht definiert ist oder auf einen ungültigen Wert gesetzt wurde
    Firefox verwendet initial-value nur, wenn die Variable nicht definiert ist
    Für die meisten Projekte ist das wohl kein Problem, aber in einem aktuellen Projekt mussten wir den Standardwert in Firefox per JavaScript setzen, um die Unterschiede zwischen den Browser-Implementierungen auszugleichen
    [1] https://caniuse.com/?search=%40property

    • Wenn man die Demo in Firefox ausprobieren möchte, kann man about:config öffnen und layout.css.properties-and-values.enabled aktivieren
    • So kann man bei Bedarf Eigenschaftswerte in JavaScript setzen
      Einen in :root{} definierten Eigenschaftswert abrufen:
      getComputedStyle(document.documentElement).getPropertyValue("--some-custom-css-var");
      Eigenschaftswert setzen:
      document.documentElement.style.setProperty("--some-custom-css-var", "some value");
      In Browsern, in denen CSS-@property noch nicht aktiviert ist, kann man das wie einen Polyfill verwenden
  • Im aktuellen Android Chrome funktionieren die Demos vor mod() interessanterweise auch ohne Experimental feature flags, aber mod() und sin() bewirken gar nichts

  • Ich wollte so etwas schon länger, weil ich etwas wie box-breathing bauen wollte
    Ich wollte die Phasen Einatmen–Anhalten–Ausatmen–Anhalten jeweils passend zum Körper und zum gewünschten Effekt anpassen können
    Ohne einen Ansatz wie auf dieser Seite habe ich keinen Weg gefunden, das ohne JS oder ohne absurde Komplexität umzusetzen
    Meine Güte, ein Stylesheet! Die Demos auf dieser Seite sind wirklich kreativ

    • Ist das nicht einfach eine Animationskurve in CSS?
  • Der Artikel ist wirklich sehr gut geschrieben
    Chrome unterstützt CSS-mod() noch nicht in einem offiziellen Release, daher werden die meisten Beispiele auf der Seite nicht animiert, sofern man keine Chrome-Preview-Version nutzt
    Wenn man keine Tick-Steuerung braucht und nur flüssige CSS-Animationen in allen Browsern möchte, ist die FLIP-Technik nützlich
    https://medium.com/outsystems-experts/flip-your-60-fps-anima...

  • Die Animation am Ende der Seite ist sehr beeindruckend
    Sie wirkt eher wie ein Video als wie einzelne Elemente

  • https://RTCode.io ist ein Live-Web-Playground, der ohne Neuladen und ohne Zurücksetzen des Zustands funktioniert, und kann daher für alle, die mit CSS-Animationen oder Transitions arbeiten, sehr hilfreich sein
    Die Demo gibt es auf https://RTCode.io
    Um einen Pen zu importieren, muss man den Hostnamen in der URL in xcodepen.io ändern

    • Hat jemand RTCode ausprobiert? Es sieht interessant aus, aber auf der Homepage ist etwas unklar, ob es ein CodePen-Ersatz mit Live-Unterstützung ist oder ob man es auch in Production verwenden kann