- Exponential Smoothing ist eine einfache Technik, die sich breit für Animationen einsetzen lässt, die einem Zielwert sanft folgen sollen, etwa bei Toggle-Buttons, Kameras, UI-Elementen oder der Audio-Lautstärke
- Die zentrale Formel lautet
position += (target - position) * (1 - exp(- speed * dt)) und reagiert allein mit aktueller Position und Zielposition natürlich auf Zieländerungen
- Lineare Bewegung und übliches Easing benötigen bei Klicks mitten in der Animation, schnellen Kameraeingaben oder großem
dt oft zusätzliche Behandlung für Sprünge, Zittern oder Input-Queues, während Exponential Smoothing die Komplexität mit derselben Struktur reduziert
1 - exp(- speed * dt) stammt aus der Lösung der Differentialgleichung, die die proportionale Aktualisierungsformel beschreibt; bei kleinem dt ist es fast identisch zur bisherigen Formel und vermeidet auch bei großem dt Overshooting
- Den Zielwert erreicht man mathematisch nicht exakt, aber wegen der Grenzen von Fließkommapräzision und der minimal wahrnehmbaren Änderungsmenge für Nutzer wirkt die Animation in der Praxis dennoch vollständig abgeschlossen
Das Problem, sichtbar an einem Toggle-Button
- Die Schalterposition eines Toggle-Buttons lässt sich zwar einfach als
turned_on ? max_x : min_x berechnen, aber beim Zustandswechsel teleportiert die Position sofort, was wenig Lebendigkeit vermittelt
- Eine lineare Animation wird umgesetzt, indem die aktuelle Position mit konstanter Geschwindigkeit aktualisiert und auf den erlaubten Bereich begrenzt wird
position.x += (turned_on ? 1 : -1) * speed * dt;
position.x = clamp(position.x, min_x, max_x);
- Bewegung mit konstanter Geschwindigkeit macht die Position zu einer linearen Funktion der Zeit, wodurch die Bewegung träge wirken kann
- Mit einer Easing-Funktion lässt sich die Bewegung weicher machen
- klassisches cubic smoothstep:
3t² - 2t³
- Quadratwurzel-Easing:
sqrt(t)
smoothstep besitzt die Symmetrie 1 - f(t) = f(1 - t), sodass sich für Vorwärts- und Rückwärtsanimation derselbe Code verwenden lässt
sqrt braucht je nach Richtung unterschiedliche Formeln
- beim Einschalten:
sqrt(t)
- beim Ausschalten:
1 - sqrt(1 - t)
sqrt bewegt sich anfangs schnell und wird in Zielnähe sanft langsamer, aber selbst bei einem simplen 2-Zustands-Toggle braucht man Zustandsverwaltung für t, Richtung und Easing-Berechnung
- Klickt der Nutzer während der laufenden Animation erneut, kann der bisherige Easing-Ansatz eine Diskontinuität erzeugen, bei der die Position plötzlich springt
Die Formel für Exponential Smoothing
- Bei Exponential Smoothing wird zunächst eine Zielposition festgelegt, danach wird die aktuelle Position schrittweise in Richtung Ziel gezogen
target = (state.value ? max_x : min_x);
position.x += (target - position.x) * (1 - exp(- dt * speed));
- Die Änderungsmenge richtet sich nach
target - position.x, also der Differenz zwischen aktueller Position und Ziel
1 - exp(- dt * speed) ist der Interpolationsfaktor, der bestimmt, wie weit pro Frame in Richtung Ziel bewegt wird
- Ändert sich das Ziel unterwegs, setzt die Bewegung ohne separate Zustände für Fortschrittszeit oder Animationsrichtung direkt in Richtung des neuen Ziels fort
- Im Toggle-Beispiel beginnt die Bewegung ähnlich wie beim
sqrt-Easing schnell und wird in Zielnähe langsamer, reduziert aber Sprungprobleme bei Klicks mitten in der Animation
Noch größere Vorteile bei Kamerabewegungen
- Dasselbe Problem tritt auch bei einer Kamera auf, die sich über eine Karte bewegt
- Implementiert man Interpolation mit konstanter Geschwindigkeit naiv, verwendet man pro Achse das Vorzeichen der Zielrichtung
position.x += sign(target.x - position.x) * speed * dt;
position.y += sign(target.y - position.y) * speed * dt;
- Am Ende der Animation kann das Vorzeichen von
target - position zwischen positiv und negativ wechseln, was zu Zittern führt
- Um das zu verhindern, braucht man eine separate Update-Funktion, die
delta auf den Bereich max_delta begrenzt
float update(float & value, float target, float max_delta)
{
float delta = target - value;
delta = min(delta, max_delta);
delta = max(delta, -max_delta);
value += delta;
}
- Möchte man cubic Easing auf die Kamerabewegung anwenden, muss man angeforderte Bewegungsereignisse in eine Queue legen und nacheinander verarbeiten, was die Struktur komplexer macht
- Ein Ansatz, bei dem Benutzereingaben während der Animation ignoriert werden, fühlt sich aus Nutzersicht sehr frustrierend an
- Mit Exponential Smoothing lässt sich Kamerabewegung mit fast demselben Code wie beim Toggle-Button umsetzen
position.x += (target.x - position.x) * (1.0 - exp(- speed * dt));
position.y += (target.y - position.y) * (1.0 - exp(- speed * dt));
- Klickt der Nutzer schnell hintereinander, wächst die Differenz zwischen Ziel und aktueller Position, wodurch die Kamera ganz natürlich schneller wird
Warum 1 - exp(- speed * dt)?
- Eine einfache proportionale Aktualisierung lässt sich so schreiben
position += (target - position) * speed * dt;
- Diese Formel bewegt sich umso schneller, je größer die Differenz zwischen Ziel und aktueller Position ist, und benötigt außer aktueller Position und Zielposition keinen zusätzlichen Zustand
- Mit
lerp geschrieben ergibt sich dieselbe Formel
position = lerp(position, target, speed * dt);
- Ist
speed * dt nahe 0, bewegt sich die Animation langsam; ist es nahe 1, nähert sie sich dem Ziel schnell an
- Wenn
speed * dt größer als 1 wird, überschreitet die Interpolation das Ziel, es kommt also zu Overshooting
- Im Beispiel ist bei
speed = 220, dt = 1 / 125 der Wert speed * dt größer als 1, wodurch Zittern entsteht
- Für
speed * dt < 2 kann sich der absolute Abstand zwar noch verkleinern, aber bei speed * dt > 2 ergibt sich kein sinnvolles Verhalten mehr
- Man könnte den Interpolationsfaktor auf
min(1, speed * dt) begrenzen, aber das ist keine Lösung, die auch Situationen mit großem dt wirklich glatt behandelt
- Der Code könnte langsam werden und die Framerate einbrechen
- Der Nutzer könnte zu einem anderen Tab oder Fenster wechseln, wodurch der Code stoppt und erst mit einem
dt von mehreren Sekunden wieder aufwacht
- In Physiksimulationen kann man großes
dt begrenzen oder auf mehrere Updates verteilen, aber bei Animationen ist es für die User Experience besser, wenn Kamera und Buttons auch bei großem dt natürlich funktionieren
Die Lösung aus Sicht der Differentialgleichung
- Eine Form vom Typ
A += B * dt ist im Allgemeinen eine numerische Lösung der Differentialgleichung dA/dt = B
- Die einfache Update-Formel entspricht dem Lösen der folgenden Gleichung
d(position) / dt = (target - position) * speed
- Setzt man
x = position, a = target, c = speed, erhält man
dx / dt = (a - x) * c
- Löst man diese Gleichung direkt, ergibt sich folgende Form
x = x0 + (a - x0) * (1 - exp(-c * t))
- Wenn also bei kleinem
dt die korrekte Formel position += (target - position) * speed * dt ist, dann lautet die Formel für beliebiges dt
position += (target - position) * (1 - exp(- speed * dt));
- Aus der Taylor-Entwicklung
exp(x) ≈ 1 + x folgt, dass bei kleinem dt auch 1 - exp(-speed * dt) ≈ speed * dt gilt, also dieselbe einfache Formel wie zuvor
- Selbst wenn
speed * dt sehr groß wird, nähert sich exp(-speed * dt) der 0 und 1 - exp(...) der 1, sodass sich die Position stabil auf einen zielnahen Wert zubewegt
- Dieselbe Formel lässt sich auch mit
lerp schreiben
position = lerp(position, target, 1 - exp(- speed * dt));
position = lerp(target, position, exp(- speed * dt));
Wahl des Geschwindigkeitswerts
- Bei gewöhnlichen Animationen denkt man oft in Dauer, etwa „Bewegung in 0,125 Sekunden“, aber Exponential Smoothing benötigt technisch unendlich viel Zeit, um das Ziel exakt zu erreichen
exp(- speed * time) wird mit der Zeit kleiner, wird aber nie 0; solange Startwert und Zielwert verschieden sind, wird position mathematisch also nie vollständig gleich target
- In der Praxis stößt man jedoch an die Grenzen der Fließkommapräzision oder die Differenz der Kameraposition wird so klein, dass Nutzer sie nicht mehr wahrnehmen, sodass die Animation als beendet erscheint
- Die Bedeutung von
speed ist, dass position innerhalb von 1 / speed Zeiteinheiten dem target exakt um den Faktor e = 2.71828... näher kommt
- In der Praxis liegt
speed meist im Bereich 5..50; für lineare oder cubic Animationen mit ähnlichem Gefühl kann sich bei Exponential Smoothing ungefähr 2 * speed passend anfühlen
Verbindung zur Signalverarbeitung
- Sucht man nach „exponential smoothing“ oder „exponential moving average“, findet man den Artikel Wikipedia: Exponential smoothing
- Bei festem
dt, wenn sich target in jeder Iteration ändert, wird der Wert bezogen auf den Iterationsindex ungefähr so aktualisiert
factor = 1 - exp(- speed * dt)
- Häufig setzt man
factor auch direkt als Wert zwischen 0 und 1 fest
- Diskretes Exponential Smoothing ist das diskrete Analogon der für Animationen verwendeten Methode
- Es wird auch in der Signalverarbeitung genutzt, weil nur der aktuelle Mittelwert gebraucht wird und keine Liste früherer Werte oder komplexe Zustände erhalten bleiben müssen
- In digitalem Audio ist
dt gewöhnlich fest auf den Kehrwert der Sampling-Frequenz 1 / freq gesetzt
1 Kommentare
Hacker-News-Kommentare
Der Kern wird hier meiner Meinung nach nicht ausreichend behandelt. Das ist nicht einfach nur eine weitere Easing-Kurve zwischen 0 und 1 oder
smoothstep(), sondern eine zustandslose Methode, die fast jede Eingabe regelmäßig verarbeitet, und das macht sie wirklich nützlich.Wenn man CSS-Transitions verwendet hat, kennt man dieses Problem. Wenn man die Dauer auf 400 ms setzt: Warum ausgerechnet 400 ms? Sollte das nicht von der zurückzulegenden Strecke abhängen?
Wie andere schon gesagt haben, hat exponentielle Glättung das Problem, dass sie sich dem Ziel nur asymptotisch nähert und es nie erreicht. Es gibt die naheliegende Lösung, die Animation zu stoppen, wenn der Schritt unter einen Schwellenwert fällt, aber elegant ist das nicht.
Als ich einen ähnlichen Ansatz für Inertial Scrolling verwendet habe, war es hilfreich, einen pseudo-Reibungsterm hinzuzufügen. Dieser Term hebt den exponentiellen Term auf und verhält sich im Grunde wie eine Mindestgeschwindigkeit. Ein Desmos-Beispiel gibt es hier: https://www.desmos.com/calculator/98ufbuzxhj
Dzum Ziel interpretiert. Exponentielle Glättung ist ein Euler-Update vondD/dt=-C*D, und die Lösung istD(t)=A*exp(-C*t), nähert sich also 0 asymptotisch an, ohne sie zu erreichen.Man kann das durch eine Gleichung ersetzen, die in endlicher Zeit auf 0 geht, zum Beispiel
dD/dt=-C*sqrt(D). Die Lösung bewegt sich wie eine halbe quadratische Funktion und bleibt dort, sobald sie 0 erreicht. Auch das Euler-Update dieser Gleichung ist wie gewünscht zustandslos.Als Spieleentwickler finde ich, dass für die meisten UIs Easing-Tweens mit vordefinierter Dauer besser sind. Aber wenn man kontinuierliche, unvorhersehbare Bewegungen glätten möchte, bei denen Start- und Endpunkt nicht klar sind, ist diese andere Art von Animation sehr nützlich.
Beispiele wären, wenn der Spieler mit der Maus Kacheln über ein Raster zieht und sie am Raster einrasten sollen, oder wenn man wie im Beispiel des Artikels eine Kamera bewegt.
In solchen Fällen ist der Trick mit exponentieller Interpolation sehr nützlich, aber nicht besonders bekannt. Viele Spiele verwenden stattdessen eine ungenauere lineare Interpolation und bekommen dann das Problem, dass sich die Animation völlig falsch anfühlt, wenn jemand sie auf einem 240-Hz-Monitor ausführt – anders als zu der Zeit, als 60 fps der Standard waren.
Deshalb freue ich mich über diesen Artikel. Solches übermäßig spezifische Wissen wird meist im Team von Senior-Leuten an Junior-Leute in einer Art mündlicher Lehrlingsweitergabe vermittelt und ist daher schwer zugänglich.
Mir gefällt der Artikel, aber ich möchte sagen, dass die Einschätzung des Autors,
sqrtsei bei einem Kippschalter besser als eine kubische Funktion, objektiv falsch ist. Wenn man sich anschaut, wie echte Kippschalter in der Regel funktionieren, ist eine kubische Funktion für diese Situation die bessere Wahl.Man denke an Sicherungsschalter im Haus oder an Schalter, wie man sie oft an analogen Synthesizern und Audio-Equipment findet. Das sind Geräte mit einer bestimmten Ästhetik; sogar mein kleiner Hughes-&-Kettner-Gitarrenverstärker hat zwei Schalter mit einem angenehmen Druckgefühl.
Solche Schalter haben anfangs etwas Widerstand und schnappen dann wegen der Federmechanik plötzlich in die neue Position. Dieses Verhalten modelliert eine kubische Funktion besser als
sqrtoder exponentielle Glättung.Abgesehen von diesem kleinen Einwand war der Artikel sehr gut. Er zeigt gut, dass gut eingesetzte Animationen, etwa mit einer passenden Easing-Funktion, die User Experience verbessern können, während unbedachte Implementierungen wie im Beispiel mit linearer Interpolation stören und die Erfahrung verschlechtern können.
[0] Es hängt natürlich von der Art des Kippschalters ab. Aber auch die Sorte, die man etwa bei einem Minimoog sieht, zeigt dieses „erst Widerstand, dann in die neue Position schnappen“-Verhalten und macht die Bedienung angenehm. Und nur zur Klarstellung: Das ist kein Gear-Flex, ich besitze keinen Minimoog.
Ich bin immer wieder erstaunt, wie oft einfache nichtlineare Tricks Online-Interaktionen angenehmer machen. Bei der Farbwahrnehmung sind sie sogar zentral, um zu verstehen, warum zwei Farben für manche Menschen nicht ausreichend unterscheidbar sind.
Seltsam ist, dass Menschen Beschleunigung nicht immer gut verstehen. Man sollte nicht einen Hügel hinauf fliehen in dem Glauben, Feuer bewege sich dort mit ungefähr konstanter Geschwindigkeit wie auf ebenem Gelände. Feuer beschleunigt, wenn es bergauf läuft.
Kinder lernen schnell, wie schnell sich ein geworfener Ball entlang des Bodens bewegt, verstehen aber nicht immer gut, wie schnell er wegen der Schwerkraft unterwegs ist, wenn er ihre Hand trifft.
Interessant ist, dass ein Großteil dieses Artikels am Ende auf Easing hinausläuft. Es wirkt, als müsste jede neue Generation das für sich selbst wiederentdecken.
Ich erinnere mich, dass ich Ende der 90er von den experimentellen Websites von Yugo Nakamura fasziniert war. Das war eine der ersten Websites, die ich gesehen habe, die Easing frei einsetzte, um ein organisches Gefühl zu erzeugen: https://www.youtube.com/watch?v=NLt7Gwnt3WY
Irgendwie hätte ich gern so einen Toggle. Während man ihn berührt oder anklickt, bewegt er sich langsam bis etwa 75 % des Ziels, und beim Loslassen schnellt er den Rest des Weges.
Was das aus UX-Sicht bedeuten würde, weiß ich nicht genau. Es könnte heißen, dass die Einstellung erst am Ende tatsächlich angewendet oder gespeichert wird.
Oder es könnte Teil eines „Sind Sie wirklich sicher?“-Dialogs sein. Solange man gedrückt hält, wird die Einstellung angewendet, aber bevor sie einrastet, kann man sie mit Escape zurücknehmen.
input:checked + .slider:active:before { transform: translateX(8px); transition: 1s; }input:not(:checked) + .slider:active:before { transform: translateX(18px); transition: 1s; }Mir gefällt dieser Artikel. Ich habe vor etwa 10 Jahren eine fast exakt gleiche Technik geschrieben, sie damals
lazy-easygenannt und nutze sie immer noch.Manchmal möchte man einfach nur eine sanfte Animation, ohne das ganze State-Management zu betreiben: https://www.hailpixel.com/articles/lazy-animation-with-lazy-...
Der Artikel selbst ist wirklich gut. Die Demos scheinen in Chrome gut zu funktionieren, aber in Firefox bleiben sie beim Scrollen hängen und das Rendering der Seite stoppt vollständig.
Das ist tatsächlich ein ziemlich guter Ansatz und auch ein guter Proof of Concept für Animations- und Easing-Techniken. Erinnert stark an Flickity.
https://metafizzy.co/blog/initial-demos/
https://metafizzy.co/blog/math-time-resting-position/
https://metafizzy.co/blog/particle-to-slider/
https://metafizzy.co/blog/flickity-begins/
Besonders diese Demo: https://codepen.io/desandro/pen/myXdej
Diese Technik ist nicht nur für Switches nützlich. In der Praxis würde man so ein Element auch gar nicht für einen Switch verwenden, und man würde nicht 20
requestAnimationFrame-Loops parallel über die ganze Site laufen lassen. Man würde auch nicht absichtlich kaputte Elemente einbauen.Außerdem gibt es keine Optimierung, die das Rendering stoppt, sobald das Delta klein genug wird, und vermutlich gibt es noch Dutzende kleiner Anpassungen, um das produktionsreif zu machen.
Wenn ich mir die Reaktionen hier ansehe, wirkt es so, als hätten manche den Artikel nicht gelesen und würden Annahmen treffen, den Wald vor lauter Bäumen nicht sehen oder seien so voreingenommen und zynisch, dass sie fachlich fragwürdige Urteile teilen, um klug zu wirken.
Seit wann ist HN wie Reddit geworden?
Was den Teil angeht, dass HN wie Reddit geworden sei: Vielleicht gibt es in der Tech-Welt insgesamt eine vage zynische Grundstimmung. Dahinter steckt Misstrauen gegenüber dem, was Regierungen und Unternehmen mit mächtigen neuen Technologien vorhaben – ob sie sie nicht eher bauen, um Individuen einzuschränken und zu verwalten, statt ihre persönliche Freiheit zu vergrößern.
Das kommt dem Kern von Emotional Design (https://en.wikipedia.org/wiki/Emotional_Design) ziemlich nahe. Selbst hinter winzigen Animationen gibt es viel zu sagen.