Zum Inhalt springen
Michael Malura
570 Github Stars 5810 npm Downloads 53 Projekte

Easing und Tweening

Ein Quadrat von A nach B zu schieben ist schnell gebaut: Anfang plus Differenz mal Fortschritt. Das sieht nach Taschenrechner aus. Ein Menü, das mit gleichbleibender Geschwindigkeit hereinfährt und abrupt stehen bleibt, wirkt hölzern, mit einer Kurve davor ist es sofort lebendiger. Diese Kurven heißen Easing-Funktionen, und ein Tween ist das Stück Code, das sie über die Zeit abfährt. Die Zeit dafür kommt aus Delta Time. Wer seine Simulation mit festem Zeitschritt fährt, reicht den Schritt statt der Frame-Zeit hinein.

Die Demo startet mit einer Galerie aus elf Familien: linear, sine, quad, cubic, quart, quint, expo, circ, back, elastic und bounce. Jede Karte zeichnet die Kurve in Teal, der orange Punkt ist die aktuelle Zeit darauf, und das Quadrat darunter fährt mit demselben Wert zwischen zwei Punkten hin und her. Die Chips unten schalten alle Karten zwischen in, out und inOut um. Ein Klick auf eine Karte öffnet rechts die Detailansicht mit Plot, einem Objekt mit Position, Skalierung und Rotation, Chips für Dauer und loop oder once und einer Spur aus Geisterbildern. Die Geister sitzen im Abstand von 40 ms, wo sie weit auseinander liegen, ist das Objekt schnell. Mit Tab geht es zur Tween-Kette.

Die Zeit wird verbogen

Der Weg bleibt eine gerade Linie. Verbogen wird die Zeit. Eine Easing-Funktion nimmt den linearen Fortschritt t zwischen 0 und 1 und gibt zurück, wie weit der Weg schon gegangen ist. f(0) = 0 und f(1) = 1 gelten für alle, dazwischen darf der Wert aus dem Bereich 0 bis 1 hinausfliegen. Bei back und elastic ist genau das der Zweck, das Objekt schießt übers Ziel und kommt zurück.

template <class T>
T mix(const T& a, const T& b, double k) { return a + (b - a) * k; }

// im Tween:
return mix(from, to, ease(progress));

T kann alles sein, was plus, minus und mal double kann: Die Demo tweent ein double, einen Vec2 und eine Farbe.

Die Formeln

Quad ist t^2, cubic t^3, quart t^4, quint t^5. Je höher die Potenz, desto länger bleibt die Kurve am Anfang flach. Sine ist 1 - cos(t * pi / 2), circ ein Viertelkreis, 1 - sqrt(1 - t^2). Expo ist 2^(10t - 10) und ergibt bei t = 0 nur 0.000977 statt 0, deshalb hat es Sonderfälle an beiden Enden.

Back ist c3 * t^3 - c1 * t^2 mit c1 = 1.70158 und c3 = c1 + 1. Das c3 steht da, damit bei t = 1 wieder 1 herauskommt. Die 1.70158 stammen aus Robert Penners Easing-Gleichungen, dort ist es der Standardwert des Parameters s. Damit überschwingt back um rund 10 Prozent, nachgerechnet liegt der größte Wert von backOut bei 1.1000.

Elastic ist ein Sinus mit abklingender Hülle: -2^(10t - 10) * sin((10t - 10.75) * c4) mit c4 = 2pi / 3. Die Hülle lässt die Schwingung sterben, c4 legt eine Schwingung auf 0.3 der Dauer fest, auch das ist Penners Standard. Bounce sind vier Parabeln, die Konstante 7.5625 ist 2.75 zum Quadrat, die erste Parabel trifft damit bei t = 1/2.75 genau die 1. Die Scheitel der Hüpfer liegen bei 0.75, 0.9375 und 0.984375, jeder Hüpfer ist also ein Viertel so hoch wie der davor. Auch diese Zahlen sind von Penner.

in, out, inOut

Out ist die Spiegelung von in: out(t) = 1 - in(1 - t). Das gilt für jede Familie, auch für die Federn. InOut fährt die erste Hälfte als in(2t) / 2 und die zweite gespiegelt:

inline double polyOut(double t, int n) { return 1.0 - ipow(1.0 - t, n); }
inline double polyInOut(double t, int n) {
    return t < 0.5 ? ipow(2.0, n - 1) * ipow(t, n) : 1.0 - ipow(-2.0 * t + 2.0, n) / 2.0;
}

Back und elastic nehmen für inOut eigene Konstanten (c2 = c1 * 1.525, c5 = 2pi / 4.5), dort stimmt die Zusammensetzung aus in nicht exakt. Symmetrisch um den Punkt (0.5, 0.5) ist es trotzdem, inOut(1 - t) = 1 - inOut(t) prüft der Test für alle 33 Funktionen.

Der Tween

Ein Tween hält Start, Ziel, Dauer, Funktion und eine Uhr. update(dt) schiebt die Uhr um die Frame-Zeit weiter, value() rechnet daraus den Wert. Weil die Uhr Sekunden sammelt, ist das Ergebnis von der Framerate unabhängig. Der Test lässt denselben Tween mit 1000 Schritten zu 1 ms, mit 60 Schritten zu 1/60 s und mit einem Schritt von einer Sekunde laufen, alle drei landen beim selben Wert. In der Tween-Ansicht zeigen das die Chips "20 fps" und "5 fps": Es ruckelt, aber die Kette endet zur selben Zeit.

Hört ein Tween mitten im Frame auf, ist vom dt noch etwas übrig. Dieser Rest gehört dem nächsten Tween, sonst startet jedes Glied einen Frame zu spät, bei 5 fps bis zu 200 ms. Darum gibt update den ungenutzten Teil zurück:

elapsed_ += dt;
if (elapsed_ >= duration) {
    const double rest = elapsed_ - duration;
    elapsed_ = duration;
    done_ = true;
    return rest;
}
return 0.0;

updateChain reicht den Rest von Tween zu Tween. Die Demo hängt move (0.8 s, backOut), grow (0.6 s, elasticOut) und tint (0.6 s, quadInOut) hintereinander, jeder mit einem anderen Typ:

auto step = [&](auto& tween) {
    if (blocked || tween.finished()) return;
    rest = tween.update(rest);
    if (!tween.finished()) blocked = true;
};
(step(tweens), ...);

Das blocked ist nötig. Ohne die Zeile würde ein Tween mit Dauer 0 schon feuern, solange sein Vorgänger läuft, weil er ein update(0) abbekommt. Ich hab sie probehalber rausgenommen, der Test schlägt dann an.

Wofür und wofür nicht

Menüs, die hereinrutschen, ein Fokuswechsel der Kamera, ein Pickup, das beim Einsammeln aufploppt, ein Gegner, der beim Treffer aufschwillt. Alles hat feste Dauer, Start und Ziel.

Für Physik taugt es nicht. Easing kennt weder Masse noch Kräfte, bounce sieht aus wie ein hüpfender Ball, spielt aber nur eine Kurve ab und prallt nirgends ab. Auch ein bewegliches Ziel passt schlecht: Startet man jeden Frame einen neuen Tween vom aktuellen Wert, beginnt jeder bei einem Ease-in wieder mit Tempo 0, und die Kamera kriecht nur noch.

Stolperfallen

Die Funktionen erwarten t zwischen 0 und 1, deshalb klemmt progress(). Bei Dauer 0 gäbe es eine Division durch null, progress() gibt dann 1 zurück und update reicht die ganze Zeit weiter. Farben tweene ich als doubles von 0 bis 255 und klemme erst beim Zeichnen, sonst läuft ein Überschwinger in einem Byte über. Die Mitte zwischen Teal und Orange ist übrigens ein ziemlich braunes Etwas, das sieht man in der Kette.

Den Code findest du im Repo unter easing. Alle Demos der Serie sammelt das Projekt Game Dev Snippets.

30.10.2026 aktualisiert 06.11.2026
Teilen X Reddit Hacker News LinkedIn E-Mail
Neue Beiträge abonnieren RSS JSON