Das Animieren von Objekten auf einem Bildschirm gehört zu den Standardwerkzeugen eines Font-End-Entwicklers. Sei es das Öffnen eines Menüs, das Minimieren eines Fensters oder das Einblenden einer Bildschirmtastatur; wir sehen diese Animationen hunderte Male über den Verlauf eines Tages. Easing-Funktionen helfen bei der Gestaltung von Animationen. Sie kontrollieren die Beschleunigung der Animation und können diese somit sanft, schwungvoll oder gar verspielt wirken lassen.

Dieser Artikel beschreibt wie Easing funktioniert, welche Arten von Easing-Funktionen es gibt und wie diese in Code implementiert werden.

Funktionsweise

Eine Easing-Funktion nimmt einen Zahlenwert im Bereich von 0 bis 1 entgegen, führt damit eine Berechnung durch und gibt das Resultat wieder zurück. Dieser Zahlenwert wird meist als t für time notiert. t ist anfangs 0, wird über den Verlauf des Übergangs erhöht (z. B. 0.25 bei 25 % der Animation) und erreicht 1 zum Endzeitpunkt. Bei einer Animation, die sich über 120 Frames1 erstreckt, wird t bei jedem Frame um 1120 erhöht. Nach besagten 120 Frames hat t einen Wert von 1 und die Animation ist vollendet.2

let duration  = 120
let increment = 1 / duration

func animate(t: Float) {
  balloon.altitude = 720 * t
  drawFrame()

  if t < 1 {
    animate(t: t + increment)
  }
}

animate(t: 0) // Animation starten

Im obigen Code-Beispiel wird die Flughöhe eines Ballons (balloon.altitude) animiert. Der Ballon steigt dabei von 0 auf 720 Meter3 Höhe. Bei jeden Aufruf der animate-Funktion wird t um 1120 erhöht. So schwebt der Ballon bei jedem Frame um 720120=6 Meter weiter nach oben.

Hierbei handelt es sich um eine Animation mit linearem Easing; nach ihrem Start verläuft sie mit beständiger Geschwindigkeit und kommt dementsprechend abrupt zum Stillstand. Da der Parameter t nicht verändert wird, spricht man von einer identischen Abbildung.

Die identische Abbildung

f(x) = x

Die identische Abbildung oder Identität-Funktion, f(x)=x, kann als triviale Easing-Funktion betrachtet werden. Der Parameter der Funktion wird unverändert zurückgegeben. Aus diesem Grund wird im obigen Code an keiner Stelle einer Easing-Funktion aufgerufen; der Wert t würde schließlich nicht verändern werden und kann so direkt eingesetzt werden. Daher ist es in den allermeisten Fällen am besten, keinen Funktionsaufruf einer identischen Abbildung zu bemühen. Stattdessen ist es einfacher, den Wert direkt zu benutzen. Für jene Umstände, in welchen ein Aufruf unabdingbar ist, lässt sich folgender Code nutzen.

func identity(t: Float) -> Float {
  return t
}

Eine Animation, welche die Identität-Funktion verwendet,4 ist schnell berechnet und spart Speicher. Das kann selbst bei alter, langsamer oder überlasteter Hardware zu einer flüssigen Animation führen. Dafür wirkt die visuelle Präsentation nicht besonders liebevoll umgesetzt. Gerade das abrupte Starten und Enden der Animation lässt sie als nachträglichen Einfall erscheinen.

Mathematische Ausdrücke

Einzelne Ausdrücke

Jede Easing-Funktion muss drei Bedingungen erfüllen.

  1. Zahlen von 0 bis 1 entgegennehmen,
  2. als Resultat eine Zahl zurückgeben und dabei
  3. für Null 0 und für Eins 1 zurückgeben.

Es gibt viele mathematische Funktionen, welche die ersten beiden Bedingungen erfüllen, f(t)=t2 oder f(t)=sin(t) zum Beispiel. Die dritte Bedingung hingegen stellt sich als eine wahre Hürde heraus. Glücklicherweise gibt es einige Funktion-Familien, die eben jene Hürde bewältigen.

Potenzfunktionen

f(t) = t^3

Eine Funktion-Familie, welche alle drei Bedingungen erfüllt, sind Potenzfunktionen der Art f(t)=tn, bei welchen n größer als Null ist. Diese Funktionen können weiter wie folgt unterteilt werden:

  • Bei Funktionen der Art 0<n<1 startet die Animation schnell und endet sanft, wohingegen sie bei
  • Funktionen mit einem Wert von n größer als 1 langsam startet, Schwung aufbaut und abrupt endet.

Sprachen wie Python, Ruby oder Haskell verfügen über einen eigenen Potenz-Operator.

object_width = 500 * (t ** 3)
Die Breite eines Objekts wird von 0 auf 500 animiert.

Allgemein können Potenzfunktion als Kette von Multiplikationen implementiert werden. Zudem wird von den meisten Standard-Bibliotheken eine Potenzfunktion bereitgestellt.

object.width = 500 * (t * t * t)
object.width = 500 * pow(t, 3)

Trigonometrische Funktionen

f(t) = (1 – cos(πt)) / 2

Wer indes eine Easing-Funktion braucht, die sanft startet, beschleunigt und wieder sanft endet, ist mit trigonometrischen Funktionen gut bedient. Der Einsatz von Sinus- bzw. Kosinus-Funktionen ermöglicht ansonsten unerreichbar weiche Bewegungen. Als Draufgabe fließen Sinus-Funktionen weich in ihr Spiegelbild über. Das erlaubt es, die Animation direkt nach ihrem Ende entgegengesetzt ablaufen zu lassen.

Trigonometrische Funktionen werden bei den meisten Programmiersprachen in der Standard-Bibliothek mitgeliefert.

object.width = 500 * Math.sin(t * Math.PI / 2)
Ease-Out-Sine in JavaScript
from math import *
object_width = 500 * sin(t * pi / 2)
Ease-Out-Sine in Python

Wie bereits erwähnt ist es nicht so einfach, mathematische Funktionen zu finden, die alle drei Bedingungen erfüllen.5 Daher kommen oft andere Methoden zum Einsatz, solcherlei Kurven zu generieren. Eine Methode besteht darin, mehrere mathematische Funktionen zu kombinieren und so weitere Easing-Funktionen zu generieren.6

Mehrere Ausdrücke

Die Ease-In/Out-Cubic-Funktion ist ein gutes Beispiel für eine Easing-Funktion mit mehreren Ausdrücken. Ab 50 % (also ab t=0.5) verwendet sie eine andere Formel, was der Kurve erlaubt ab der Hälfte wieder zu verlangsamen.

func easeInOutCubic(t: Float) -> Float {
  if t < 0.5 {
    return pow(t, 3) * 4
  } else {
    return pow(2 * t - 2, 2) * (t - 1) + 1
  }
}

Die Kombination mehrerer Ausdrücke erlaubt für eine Vielzahl von neuen Easing-Funktionen. Es bleibt jedoch weiterhin kompliziert, eigene Easing-Funktionen zu kreieren. Wäre es nicht fantastisch, maßgeschneiderter Easing-Funkionen mithilfe einer grafischen Oberfläche zu entwerfen?

Bézierkurven

Aus der Automobildesign-Branche geboren, aus Vektor-Programmen bekannt; Bézierkurven sind intuitiv zu bedienen und können beinahe jede Kurve darstellen. Wer noch nicht mit dem Konzept von Kontrollpunkten vertraut ist, kann die obige Grafik mit dem Finger oder der Maus manipulieren.

y(t) = {{0.6, 0.1}, {0.2, 0.95}}

Werden Bézierkurven als Easing-Funktionen eingesetzt, so handelt es sich dabei meist um Cubic-Bézierkurven. Diese haben vier Kontrollpunkte, welche frei positioniert werden können und eine weich verlaufende Kurve aufspannen. Damit die drei Bedingungen erfüllt sind, werden die beiden äußeren Kontrollpunkte an den Koordinaten (0,0) beziehungsweise (1,1) fixiert. Somit bleiben noch zwei Kontrollpunkte übrig, die frei positioniert werden können.7

Die animierte Grafik zum Beginn des Kapitels zeigt, wie sich aus den vier Kontrollpunkten die finale Bézierkurve ergibt. Zunächst werden die vier Kontrollpunkte miteinander verbunden, entlang der dabei entstehenden Linien werden drei neue Punkte geführt, welche wiederum zwei Punkte ergeben, zwischen welchen der finale Punkt platziert wird. Dieser letzte Punkt gibt die Koordinaten der Kurve zum Zeitpunkt t an. Ist t=0.3 gegeben, so finden sich alle Punkte 30 % entlang ihrer Linie.

Implementation einer Bézierkurve

Bézierkurven bestehen aus drei Bausteinen: den vier Kontrollpunkten, der lerp-Funktion und einem Mechanismus, der die ersten beiden Bausteine miteinander kombiniert.

Die vier Kontrollpunkte sind schnell und einfach implementiert. Sie setzen sich aus einer x- und einer y-Koordinate zusammen.

let controlPoints = [
  Point(x: 0  , y: 0),   // fixiert links unten
  Point(x: 0.3, y: 0.1), // frei positionierbar
  Point(x: 0.5, y: 0.7), // frei positionierbar
  Point(x: 1  , y: 1),   // fixiert rechts oben
]
Wie zuvor besprochen sind die beiden äußeren Punkte fixiert.

Als nächstes benötigen wir eine Funktion, welche die Position der Punkte auf den Linien berechnet. Diese Funktion nennt sich lerp, kurz für lineare Interpolation.

func lerp(v0: Float, v1: Float, t: Float) -> Float {
  return v0 * (1 - t) + v1 * t
}

lerp nimmt zwei Zahlen (v0 und v1) sowie einen Zeitpunkt t entgegen und gibt einen neuen Wert zurück, der zu t Anteilen zwischen den beiden Zahlen liegt. Für t == 0.2 gibt lerp 80 % des Startwertes und 20 % des Endwertes zurück.

lerp(10, 70, 0)   // = 10 +  0 = 10
lerp(10, 70, 0.2) // =  8 + 14 = 22
lerp(10, 70, 0.5) // =  5 + 35 = 40
lerp(10, 70, 1)   // =  0 + 70 = 70

Mit der linearen Interpolation können die Koordinaten der neuen Punkte berechnet werden, indem jeweils die x- und y-Koordinate zweier Punkte interpoliert werden.

let p3 = Point(x: lerp(p1.x, p2.x, t), y: lerp(p1.y, p2.y, t))

Da diese Operation, das Erstellen neuer Punkte zwischen bestehenden Punkten, relativ häufig angewendet werden muss, ist es sinnvoll, die Aufgabe in eine eigene Funktion zu packen.

func connectPoints(points: [Point], t: Float) -> [Point] {
  let newPoints: [Point] = []

  for i in 0..<points.length {
    let point     = points[i]
    let nextPoint = points[i + 1]
    let newX      = lerp(point.x, nextPoint.x, t)
    let newY      = lerp(point.y, nextPoint.y, t)

    newPoints.append(Point(x: newX, y: newY))
  }

  return newPoints
}

connectPoints nimmt ein Array an Punkten der Länge n entgegen und gibt ein Array der Länge n1 von neuen Punkten zurück. Die neuen Punkte liegen zu t Teilen zwischen den übergebenen Punkten.

Nun verwenden wir connectPoints um den finalen Punkt zu errechnen.

// Array mit vier Punkten -> Array mit drei Punkten
let level2points = connectPoints(controlPoints, t)
// Array mit drei Punkten -> Array mit zwei Punkten
let level3points = connectPoints(level2points,  t)
// Array mit zwei Punkten -> Array mit einem Punkt
let finalPoint   = connectPoints(level3points,  t)

Nach drei Aufrufen bleibt ein Array mit einem einzelnen Punkt übrig. Die Koordinaten dieses finalen Punktes beschreiben die Bézierkurve zum Zeitpunkt t. Visuell kann man sich das gut vorstellen, wenn man den grünen Punkt in der obigen Animation beobachtet.

In den meisten Fällen ist es jedoch nicht von Bedarf, eigene Bézierkurven zu berechnen. Stattdessen bieten die meisten Animation-Bibliotheken vordefinierte Funktionalitäten an, welche lediglich die Position der zwei freien Kontrollpunkte benötigen. Im Folgenden zeige ich einige Beispiele in CSS, Core Animation und Velocity.js, welche eine Bézierkurve mit den freien Kontrollpunkten (0.6,0.1) und (0.2,0.95) beschreiben.

dl { transition: all 250ms cubic-bezier(0.6, 0.1, 0.2, 0.95) }
Eine Bézierkurve als timing-function in CSS
let easing = CAMediaTimingFunction(controlPoints: 0.6, 0.1, 0.2, 0.95)
Instanziierung einer Core Animation CAMediaTimingFunction
const easing = [0.6, 0.1, 0.2, 0.95]
Definition von Kontrollpunkten für das Velocity.js-Framework

Über die Grenzen hinweg

f(t) = {{0.5, 0.4}, {0.3, 1.12}}

Die drei Bedingungen einer Easing-Funktion geben nicht vor, dass der Rückgabewert der Easing-Funktion sich während der Animation im Bereich 0 bis 1 befinden muss. Wird f(t) zwischenzeitlich größer als 1, so wirkt die Animation zu hüpfen. Für f(t)<0 läuft die Animation anfangs rückwärts, fast als müsste sie Schwung holen.

Während dieses Verhalten bei den wenigsten mathematischen Funktionen auftrifft, lässt es sich einfach in eine Bézierkurve einbauen. Dazu muss mindestens einer der beiden freien Kontrollpunkte oberhalb 1 oder unterhalb 0 platziert werden.

Zusammenfassung

Das Animieren von Objekten mithilfe von Easing-Funktionen verleiht der Animation einen Charakter. Während lineares Easing oft einfallslos wirken kann, ist es am schnellten berechnet und einfach einzusetzen. Mathematische Ausdrücke wie Potenzfunktionen oder Trigonometrischer Funktionen liefern ansprechendere Ergebnisse, sind jedoch nicht an die eigenen Vorstellungen anpassbar. Bézierkurven hingegen sind etwas komplizierter in ihrer Berechnung, bieten dafür aber freie Kontrolle über das Easing-Verhalten.

Weiterführende Informationen

Fußnoten & Quellen
  1. Als Frame wird im Bereich der Computergrafik ein einzelnes Bild beschrieben, das ein Bildschirm darstellt. Handelsübliche Bildschirme stellen 60 Frames pro Sekunde dar. Das wiederum ist wichtig, um die Dauer der Animation zu bestimmen. Eine Animation, die aus 120 Frames besteht, dauert bei 60 fps (frames per second) zwei Sekunden.

  2. Technisch korrekt wäre es, von 121 Frames zu sprechen. Der erste Frame (t=0) entspricht jedoch meist dem Zustand vor der Animation und wird daher selten als Teil der Animation angesehen.

  3. Die Einheit ist nicht im obigen Code gegeben; es könnte sich also auch um Pixel, Seemeilen, Lichtjahre … handeln. Der Satz würde ohne Einheit nur etwas merkwürdig klingen, warum also nicht Meter?

  4. oder eben genau keine Easing-Funktion verwendet

  5. Du kannst ja selbst auf die Suche nach weiteren Funktionen gehen, die für Null 0 und für Eins 1 ergeben. Bearbeite dazu die Formeln der Funktionsgraphen auf dieser Webseite.

  6. Ähnlich, wie mehrere Funktionen kombiniert werden können, um eine Batman Kurve zu zeichnen. Und warum nicht eine Zoidberg Kurve?

  7. Auch hier sind die Grafiken auf der Webseite interaktiv. Tippe auf die Legende einer Grafik und teste deinen eigenen Werten. Schema: y(x)={{x1,y1},{x2,y2}}