Additive Animation mit der Web Animations API

Avatar of Dan Wilson
Dan Wilson am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Zum Zeitpunkt des Verfassens sind diese Funktionen in keinem stabilen Browser verfügbar. Alles, was besprochen wird, ist jedoch bereits standardmäßig in Firefox Nightly enthalten und wichtige Teile sind in Chrome Canary (mit aktiviertem Experimental Web Platform Features Flag) enthalten. Daher empfehle ich die Verwendung eines dieser Browser (beim Lesen dieses Artikels), um so viele der Funktionen wie möglich in Aktion zu sehen.

Unabhängig von Ihrer bevorzugten Methode der Animation im Web wird es Zeiten geben, in denen Sie dieselbe Eigenschaft mit separaten Animationen animieren müssen. Vielleicht haben Sie einen Hover-Effekt, der ein Bild skaliert, und ein Klickereignis, das eine Translation auslöst – beide beeinflussen die transform. Standardmäßig wissen diese Animationen nichts voneinander, und es wird nur eine visuell angewendet (da sie dieselbe CSS-Eigenschaft beeinflussen und der andere Wert überschrieben wird).

element.animate({
  transform: ['translateY(0)', 'translateY(10px)']
}, 1000);

/* This will completely override the previous animation */
element.animate({
  transform: ['scale(1)', 'scale(1.15)']
}, 1500);

Die zweite Animation in diesem Beispiel der Web Animations API ist die einzige, die in diesem Beispiel visuell gerendert würde, da beide Animationen gleichzeitig abgespielt werden und die zweite zuletzt definiert wurde.

Manchmal haben wir sogar größere Ideen, bei denen wir eine grundlegende Animation haben möchten und dann basierend auf einer Benutzerinteraktion oder einer Zustandsänderung die Animation auf halbem Weg sanft modifizieren möchten, ohne ihre bestehende Dauer, Keyframes oder Easing zu beeinträchtigen. CSS Animations und die aktuelle Web Animations API in stabilen Browsern können dies nicht sofort leisten.

Eine neue Option

Die Spezifikation der Web Animations führt die composite-Eigenschaft (und die verwandte iterationComposite) ein. Der Standardwert für composite ist 'replace' und hat das Verhalten, das wir seit Jahren kennen, bei dem der Wert einer aktiv animierten Eigenschaft einfach jeden zuvor gesetzten Wert ersetzt – entweder aus einer Regel oder einer anderen Animation.

Der Wert 'add' ist, wo sich die Dinge von den bisherigen Normen ändern.

element.animate({
  transform: ['scale(1)', 'scale(1.5)']
}, {
  duration: 1000,
  fill: 'both'
});
element.animate({
  transform: ['rotate(0deg)', 'rotate(180deg)']
}, {
  duration: 1500,
  fill: 'both',
  composite: 'add'
});

Nun werden beide Animationen gesehen, da der Browser im laufenden Betrieb die entsprechende Transformation zu einem bestimmten Zeitpunkt in der Timeline des Elements ermittelt und dabei beide Transformationen berücksichtigt. In unseren Beispielen ist das Easing standardmäßig 'linear' und die Animationen beginnen gleichzeitig, sodass wir aufschlüsseln können, wie die effektive transform zu einem bestimmten Zeitpunkt aussieht. Zum Beispiel

  • 0ms: scale(1) rotate(0deg)
  • 500ms: scale(1.25) rotate(60deg) (auf halbem Weg der ersten Animation, 1/3 der zweiten)
  • 1000ms: scale(1.5) rotate(120deg) (Ende der ersten, 2/3 der zweiten)
  • 1500ms: scale(1.5) rotate(180deg) (Ende der zweiten)

Siehe den Pen Animation Composite von Dan Wilson (@danwilson) auf CodePen.

Also lass uns kreativ werden

Eine einzelne Animation besteht nicht nur aus einem Start- und einem Endzustand – sie kann ihr eigenes Easing, ihre Anzahl an Wiederholungen, Dauer und weitere Keyframes in der Mitte haben. Während sich ein Element mitten in einer Animation befindet, können Sie ihm eine zusätzliche Transformation mit eigenen Timing-Optionen hinzufügen.

Siehe den Pen Add more transform animations von Dan Wilson (@danwilson) auf CodePen.

Dieses Beispiel ermöglicht es Ihnen, mehrere Animationen auf demselben Element anzuwenden, die alle die transform-Eigenschaft beeinflussen. Um in diesem Beispiel nicht ausfallend zu werden, beschränken wir jede Animation auf eine einzige Transformationsfunktion gleichzeitig (z. B. nur eine scale), beginnend mit einem Standardwert (z. B. scale(1) oder translateX(0)) und endend mit einem vernünftigen zufälligen Wert für dieselbe Transformationsfunktion, wiederholt unendlich. Die nächste Animation beeinflusst eine andere Funktion mit ihrer eigenen zufälligen Dauer und ihrem eigenen Easing.

element.animate(getTransform(), //e.g. { transform: ['rotate(0deg), 'rotate(45deg)'] }
{
  duration: getDuration(), //between 1000 and 6000ms
  iterations: Infinity,
  composite: 'add',
  easing: getEasing() //one of two options
});

Wenn jede Animation beginnt, ermittelt der Browser effektiv, wo sie sich in den zuvor angewendeten Animationen befindet, und startet eine neue Rotationsanimation mit den angegebenen Timing-Optionen. Selbst wenn bereits eine Rotation in die entgegengesetzte Richtung läuft, wird der Browser die Mathematik durchführen, um herauszufinden, wie viel Rotation stattfinden muss.
Da jede Animation ihre eigenen Timing-Optionen hat, werden Sie in diesem Beispiel wahrscheinlich nicht dieselbe Bewegung wiederholt sehen, sobald Sie ein paar hinzugefügt haben. Dies verleiht der Animation ein frisches Gefühl, während Sie sie betrachten.

Da jede Animation in unserem Beispiel mit dem Standardwert (0 für Translationen und 1 für Skalierungen) beginnt, erhalten wir einen sanften Start. Hätten wir stattdessen Keyframes wie { transform: ['scale(.5)', 'scale(.8)'] }, würden wir einen Sprung bekommen, da diese scale vorher nicht vorhanden war und plötzlich mit halber Skalierung beginnt.

Wie werden Werte addiert?

Transformationswerte folgen der Syntax von in der Spezifikation, und wenn Sie eine Transformation hinzufügen, hängen Sie sie an eine Liste an.

Für die transform-Animationen A, B und C ist der resultierende berechnete transform-Wert [aktueller Wert in A] [aktueller Wert in B] [aktueller Wert in C]. Angenommen, die folgenden drei Animationen

element.animate({
  transform: ['translateX(0)', 'translateX(10px)']
}, 1000);

element.animate({
  transform: ['translateY(0)', 'translateY(-20px)']
}, { 
  duration:1000,
  composite: 'add'
});

element.animate({
  transform: ['translateX(0)', 'translateX(300px)']
}, { 
  duration:1000,
  composite: 'add'
});

Jede Animation läuft 1 Sekunde lang mit linearem Easing, also wären zur Hälfte der Animationen die resultierenden transform-Werte translateX(5px) translateY(-10px) translateX(150px). Easings, Dauern, Verzögerungen und mehr beeinflussen den Wert im Laufe der Zeit.

Wir können nicht nur Transformationen animieren. Filter (hue-rotate(), blur() usw.) folgen einem ähnlichen Muster, bei dem die Elemente an eine Filterliste angehängt werden.

Einige Eigenschaften verwenden eine Zahl als Wert, wie z. B. opacity. Hier addieren sich die Zahlen zu einer einzigen Summe.

element.animate({
  opacity: [0, .1]
}, 1000);

element.animate({
  opacity: [0, .2]
}, { 
  duration:1000,
  composite: 'add'
});

element.animate({
  opacity: [0, .4]
}, { 
  duration:1000,
  composite: 'add'
});

Da jede Animation wieder 1 Sekunde Dauer mit linearem Easing hat, können wir den resultierenden Wert zu jedem Zeitpunkt dieser Animation berechnen.

  • 0ms: opacity: 0 (0 + 0 + 0)
  • 500ms: opacity: .35 (.05 + .1 + .2)
  • 1000ms: opacity: .7 (.1 + .2 + .4)

Daher werden Sie nicht viel sehen, wenn Sie mehrere Animationen mit dem Wert 1 als Keyframe haben. Das ist ein Maximalwert für seinen visuellen Zustand, daher sehen Additionen über diesen Wert hinaus genauso aus, als wäre es nur ein 1.

Siehe den Pen Add more opacity animations von Dan Wilson (@danwilson) auf CodePen.

Ähnlich wie bei Opazität und anderen Eigenschaften, die Zahlenwerte akzeptieren, werden auch Eigenschaften, die Längen, Prozentsätze oder Farben akzeptieren, zu einem einzigen Ergebniswert summiert. Bei Farben müssen Sie bedenken, dass auch sie einen Maximalwert haben (entweder ein Maximum von 255 in rgb() oder 100 % für Sättigung/Helligkeit in hsl()), sodass Ihr Ergebnis zu einem Weiß maximiert werden könnte. Bei Längen können Sie zwischen Einheiten wechseln (z. B. px zu vmin), als ob es sich innerhalb eines calc() befände.

Weitere Details finden Sie in der Spezifikation, die die verschiedenen Arten von Animationen und die Berechnung des Ergebnisses beschreibt.

Arbeiten mit Füllmodi

Wenn Sie keine unendliche Animation durchführen (unabhängig davon, ob Sie composite verwenden oder nicht), behält die Animation standardmäßig ihren Endzustand nicht bei, wenn sie endet. Die fill-Eigenschaft ermöglicht es uns, dieses Verhalten zu ändern. Wenn Sie einen reibungslosen Übergang wünschen, wenn Sie eine endliche Animation hinzufügen, möchten Sie wahrscheinlich einen Füllmodus von forwards oder both, um sicherzustellen, dass der Endzustand erhalten bleibt.

Siehe den Pen Spiral: Composite Add + Fill Forwards von Dan Wilson (@danwilson) auf CodePen.

Dieses Beispiel hat eine Animation mit einem spiralförmigen Pfad, indem eine Rotation und eine Translation angegeben werden. Es gibt zwei Schaltflächen, die neue Ein-Sekunden-Animationen mit einer zusätzlichen kleinen Translation hinzufügen. Da sie fill: 'forwards' angeben, bleibt jede zusätzliche Translation effektiv Teil der Transformationsliste. Die sich erweiternde (oder schrumpfende) Spirale passt sich mit jeder Translationsanpassung reibungslos an, da es sich um eine additive Animation von translateX(0) zu einem neuen Betrag handelt und bei diesem neuen Betrag verbleibt.

Akkumulierende Animationen

Die neue composite-Option hat einen dritten Wert – 'accumulate'. Sie ist konzeptionell mit 'add' verwandt, mit dem Unterschied, dass bestimmte Arten von Animationen anders reagieren. Bleiben wir bei unserer transform und beginnen mit einem neuen Beispiel, das 'add' verwendet, und besprechen dann, wie sich 'accumulate' unterscheidet.

element.animate({
  transform: ['translateX(0)', 'translateX(20px)']
}, {
  duration: 1000,
  composite: 'add'
});
element.animate({
  transform: ['translateX(0)', 'translateX(30px)']
}, {
  duration: 1000,
  composite: 'add'
});
element.animate({
  transform: ['scale(1)', 'scale(.5)']
}, {
  duration: 1000,
  composite: 'add'
});

Bei der 1-Sekunden-Markierung (dem Ende der Animationen) ist der effektive Wert

transform: translateX(20px) translateX(30px) scale(.5)

Dies wird ein Element visuell um 50 Pixel nach rechts verschieben und es dann auf halbe Breite und halbe Höhe skalieren.

Wenn jede Animation stattdessen 'accumulate' verwendet hätte, wäre das Ergebnis

transform: translateX(50px) scale(.5)

Dies wird ein Element visuell um 50 Pixel nach rechts verschieben und es dann auf halbe Breite und halbe Höhe skalieren.

Kein Grund zur erneuten Überprüfung, die visuellen Ergebnisse sind tatsächlich exakt gleich – wie unterscheidet sich also 'accumulate'?

Technisch gesehen hängen wir bei der Akkumulation einer transform-Animation nicht immer an eine Liste an. Wenn eine Transformationsfunktion bereits existiert (wie z. B. die translateX() in unserem Beispiel), hängen wir den Wert nicht an, wenn wir die zweite Animation starten. Stattdessen werden die inneren Werte (d. h. die Längenwerte) addiert und in die bestehende Funktion eingefügt.

Wenn unsere visuellen Ergebnisse gleich sind, warum existiert dann die Option, innere Werte zu akkumulieren?

Im Fall von transform ist die Reihenfolge der Funktionsliste wichtig. Die Transformation translateX(20px) translateX(30px) scale(.5) ist anders als translateX(20px) scale(.5) translateX(30px), da jede Funktion das Koordinatensystem der nachfolgenden Funktionen beeinflusst. Wenn Sie in der Mitte eine scale(.5) durchführen, werden die letzteren Funktionen ebenfalls auf halber Skalierung ausgeführt. Daher wird mit diesem Beispiel die translateX(30px) visuell als 15-Pixel-Translation nach rechts gerendert.

Siehe den Pen Visual Reference: Transform Coordinate Systems von Dan Wilson (@danwilson) auf CodePen.

Daher können wir mit Akkumulation eine andere Reihenfolge haben als wenn wir immer die Werte an die Liste anhängen.

Akkumulation für jede Iteration

Ich habe zuvor erwähnt, dass es auch eine verwandte iterationComposite-Eigenschaft gibt. Sie bietet die Möglichkeit, einige der bereits besprochenen Verhaltensweisen durchzuführen, außer bei einer einzelnen Animation von einer Iteration zur nächsten.

Im Gegensatz zu composite hat diese Eigenschaft nur zwei gültige Werte: 'replace' (das Standardverhalten, das Sie kennen und lieben) und 'accumulate'. Bei 'accumulate' folgen die Werte dem bereits besprochenen Akkumulationsprozess für Listen (wie bei transform) oder werden für zahlenbasierte Eigenschaften wie opacity addiert.

Als einführendes Beispiel wären die visuellen Ergebnisse für die folgenden beiden Animationen identisch

intervals.animate([{ 
  transform: `rotate(0deg) translateX(0vmin)`,
  opacity: 0
}, { 
  transform: `rotate(50deg) translateX(2vmin)`,
  opacity: .5
}], {
  duration: 2000,
  iterations: 2,
  fill: 'forwards',
  iterationComposite: 'accumulate'
});

intervals2.animate([{ 
  transform: `rotate(0deg) translateX(0vmin)`,
  opacity: 0
},{ 
  transform: `rotate(100deg) translateX(4vmin)`,
  opacity: 1
}], {
  duration: 4000,
  iterations: 1,
  fill: 'forwards',
  iterationComposite: 'replace' //default value
});

Die erste Animation erhöht ihre Opazität nur um 0,5, dreht sich um 50 Grad und bewegt sich 2000 Millisekunden lang um 2vmin. Sie hat unseren neuen Wert iterationComposite und ist so eingestellt, dass sie 2 Iterationen läuft. Daher hat sie am Ende der Animation (2 * 2000 ms) eine opacity von 1 (2 * 0,5) erreicht, sich um 100 Grad gedreht (2 * 50deg) und sich um 4vmin (2 * 2vmin) bewegt.

Siehe den Pen Spiral with WAAPI iterationComposite von Dan Wilson (@danwilson) auf CodePen.

Großartig! Wir haben gerade eine neue Eigenschaft verwendet, die nur in Firefox Nightly unterstützt wird, um das nachzubilden, was wir bereits mit der Web Animations API (oder CSS) tun können!
Die interessanteren Aspekte von iterationComposite kommen zum Tragen, wenn Sie sie mit anderen bald erscheinenden Elementen der Web Animations-Spezifikation kombinieren (und die bereits in Firefox Nightly enthalten sind).

Neue Effektoptionen festlegen

Die Web Animations API, wie sie heute in stabilen Browsern verfügbar ist, ist weitgehend mit CSS Animations gleichzusetzen, mit einigen zusätzlichen Vorteilen wie einer playbackRate-Option und der Möglichkeit, zu verschiedenen Punkten zu springen/zu seeken. Das Animation-Objekt erhält jedoch die Möglichkeit, Effekt- und Timing-Optionen für bereits laufende Animationen zu aktualisieren.

Siehe den Pen WAAPI iterationComposite & composite von Dan Wilson (@danwilson) auf CodePen.

Hier haben wir ein Element mit zwei Animationen, die die transform-Eigenschaft beeinflussen und sich auf composite: 'add' verlassen – eine, die das Element horizontal über den Bildschirm bewegt, und eine, die es gestaffelt vertikal bewegt. Der Endzustand ist etwas höher auf dem Bildschirm als der Startzustand dieser zweiten Animation, und mit iterationComposite: 'accumulate' wird es immer höher und höher. Nach acht Iterationen endet die Animation und kehrt sich für weitere acht Iterationen zurück zum unteren Bildschirmrand, wo der Prozess von neuem beginnt.

Wir können ändern, wie weit nach oben sich die Animation bewegt, indem wir die Anzahl der Iterationen unterwegs ändern. Diese Animationen laufen unbegrenzt, aber Sie können das Dropdown-Menü mitten in der Animation auf eine andere Iterationszahl ändern. Wenn Sie zum Beispiel von sieben Iterationen zu neun wechseln und gerade die sechste Iteration sehen, läuft Ihre Animation weiter, als ob nichts passiert wäre. Sie werden jedoch sehen, dass sie nach der nächsten (siebten) Iteration nicht umkehrt, sondern noch zwei weitere durchläuft. Sie können auch neue Keyframes einfügen, und das Timing der Animation bleibt unverändert.

animation.effect.timing.iterations = 4;
animation.effect.setKeyframes([
  { transform: 'scale(1)' },
  { transform: 'scale(1.2)' }
]);

Das Modifizieren von Animationen unterwegs ist vielleicht nichts, was Sie jeden Tag verwenden werden, aber da es sich um etwas Neues auf Browserebene handelt, werden wir seine Möglichkeiten kennenlernen, sobald die Funktionalität breiter verfügbar ist. Das Ändern von Iterationszahlen könnte für ein Spiel nützlich sein, wenn ein Spieler eine Bonusrunde erhält und das Gameplay länger dauert als ursprünglich beabsichtigt. Unterschiedliche Keyframes können sinnvoll sein, wenn ein Benutzer von einem Fehlerzustand zu einem Erfolgzustand wechselt.

Wo geht es von hier aus weiter?

Die neuen composite-Optionen und die Fähigkeit, Timing-Optionen und Keyframes zu ändern, eröffnen neue Möglichkeiten für reaktive und choreografierte Animationen. Es gibt auch eine laufende Diskussion in der CSS Working Group über die Hinzufügung dieser Funktionalität zu CSS, sogar über den Kontext von Animationen hinaus – sie beeinflusst die Kaskade auf neue Weise. Es wird noch einige Zeit dauern, bis dies in einem stabilen Hauptbrowser landet, aber es ist aufregend, neue Optionen zu sehen und noch aufregender, sie noch heute ausprobieren zu können.