Skip to the plate
FORMA PUBLIC DOMAIN GENERATIVE ATLAS / ED. 0.28
Plate 33, Easing Curves: a still of the interpolation / polynomial plate as the atlas renders it, in the colour accent.

PL. 33  ·  COLOUR / INTERPOLATION / POLYNOMIAL

Easing Curves

Robert Penner, 2001 · the polynomials are older

OPEN THE LIVE PLATE ▸

DEFINITION

quad  f(t) = t²
cubic f(t) = t³
sine  f(t) = 1 − cos(πt/2)
expo  f(t) = 2^(10(t−1))

NOTES

The interpolation curves that make motion read as physical rather than mechanical. Penner catalogued and named them for Flash designers in 2001; the underlying functions are just polynomials and trigonometry, some of them centuries old. The names stuck so thoroughly that they are now effectively an interface standard.

PROVENANCE

Origin
Named set popularised by Robert Penner, 2001
Standing
The functions are elementary mathematics — public domain
Constants
Every curve here passes through (0,0) and (1,1)

HOUDINI · VEX

The same published mathematics as a Detail Wrangle body. Paste it into a Wrangle with Run Over set to Detail; every constant is the published value plus a tweak channel, so Create Spare Parameters gives a slider that starts where the paper does.

// FORMA — PL. 33 · EASING CURVES — Robert Penner, 2001 · the polynomials are older
//   quad  f(t) = t²
//   cubic f(t) = t³
//   sine  f(t) = 1 − cos(πt/2)
//   expo  f(t) = 2^(10(t−1))
// Paste into a Detail Wrangle (Run Over: Detail), no inputs needed.
// Written from the published mathematics, not adapted from any code.
// Constants arrive at their published values. Press the node's Create
// Spare Parameters button and every tweak becomes a slider — starting
// at 0, the published figure, and moving in the constant's own units.
// https://forma-gen.com/#plate=easing

float p_power = 6 + chf('power_tweak');       // polynomial power — sweep ceiling · live 1.5 .. 9
float p_back  = 1.7 + chf('back_tweak');      // overshoot · live 0 .. 3

// The plate's own colour: FORMA's COLOUR accent as a cosine ramp,
// brightest near t = 0 and t = 1, near-black around t = 0.5.
vector forma_ramp(float t){
  return set(
    0.46 + 0.5 * cos(6.28318530718 * (t + 0)),
    0.2002 + 0.2176 * cos(6.28318530718 * (t + 0.05)),
    0.3896 + 0.4235 * cos(6.28318530718 * (t + 0.1)));
}

// Penner's catalogue as plotted curves: ease-in, ease-out and ease-in-out on
// the polynomial, the sine ease, and the back ease with its overshoot. Each
// is a polyline through the plate's own 121 samples in the unit box, y
// negated onto Houdini's up so "easing in" still rises to the right.
//
// The plate breathes its exponent from 1 up to the slider's ceiling and
// back on a 16-second pingpong, and runs a playhead dot along every curve —
// both are clock animation a cook does not have. The exponent is frozen at
// the ceiling, which is the number the slider names ("sweep ceiling"), and
// the playhead is not ported. Curve colours: the plate draws the first two
// in the page's accent and contrast marks, which a port bakes as the ramp's
// own brightest steps; the last three are the plate's own literal channel
// colours.
int forma_samples = 120;

float power = p_power;
float bk = p_back;

for (int c = 0; c < 5; c++){
    vector col;
    if      (c == 0) col = forma_ramp(0.0);                  // TRACE — the accent itself
    else if (c == 1) col = forma_ramp(0.92);                 // HILITE's role: the bright mark
    else if (c == 2) col = set(0.482, 0.890, 0.627);         // #7BE3A0
    else if (c == 3) col = set(1.000, 0.620, 0.478);         // #FF9E7A
    else             col = set(0.725, 0.627, 1.000);         // #B9A0FF
    int prim = addprim(0, "polyline");
    for (int s = 0; s <= forma_samples; s++){
        float u = float(s) / float(forma_samples);
        float f;
        if      (c == 0) f = pow(u, power);                              // ease in
        else if (c == 1) f = 1.0 - pow(1.0 - u, power);                  // ease out
        else if (c == 2) f = u < 0.5 ? pow(2.0 * u, power) / 2.0         // in-out
                                     : 1.0 - pow(2.0 - 2.0 * u, power) / 2.0;
        else if (c == 3) f = 1.0 - cos(u * M_PI / 2.0);                  // sine
        else             f = u * u * ((bk + 1.0) * u - bk);              // back, overshooting
        // the plate clamps the overshoot to its own quarter-frame of air
        f = min(1.25, max(-0.25, f));
        // canvas y runs down and the plate flips f onto it; Houdini's y is
        // already up, so f plots directly and the curves rise to the right
        int pt = addpoint(0, set(u, f, 0.0));
        setpointattrib(0, "Cd", pt, col);
        setpointattrib(0, "Alpha", pt, 0.9);
        addvertex(0, prim, pt);
    }
}

AFTER EFFECTS · EXPRESSION

The same published mathematics as a Shape Layer path expression. Paste it onto a Path property; every constant is the published value plus a Slider Control named _tweak, so a bare paste already draws the figure and each slider moves one constant in its own units. Trim Paths is the comet.

// FORMA — PL. 33 · EASING CURVES — Robert Penner, 2001 · the polynomials are older
//   quad  f(t) = t²
//   cubic f(t) = t³
//   sine  f(t) = 1 − cos(πt/2)
//   expo  f(t) = 2^(10(t−1))
// After Effects port — paste onto a Shape Layer's Path property
// (Contents › Shape › Path). Written from the published mathematics, not
// adapted from any code. Constants arrive at their published values; add a
// Slider Control (Effect › Expression Controls) named <k>_tweak and that
// constant moves in its own units, starting at 0 — the published figure.
// The plate's comet and its reveal are Trim Paths; the stroke colour is
// FORMA's COLOUR accent, #FF6FD8. Animation runs on time.
// This plate draws 6 separate paths at its published constants:
// duplicate the group (Contents › Group) that many times and each copy draws
// its own part, read from its position in the layer. A Slider Control named
// "part" on the layer pins one instead.
// https://forma-gen.com/#plate=easing

// A missing slider reads 0, so a bare paste already draws the figure.
function forma_tweak(n){ try { return effect(n)("Slider"); } catch (e){ return 0; } }
var p_power = 6 + forma_tweak("power_tweak");     // polynomial power — sweep ceiling · live 1.5 .. 9
var p_back  = 1.7 + forma_tweak("back_tweak");    // overshoot · live 0 .. 3

// The frame: the plate's W × H canvas is this comp, with the origin at the
// layer's anchor; canvas y already runs down, as After Effects' does.
var forma_W = thisComp.width, forma_H = thisComp.height, forma_t = time;
var forma_phase = 0.698725682683289;   // this plate's own fixed phase, as the page has it
function forma_pt(x, y){ return [x - forma_W / 2, y - forma_H / 2]; }
function forma_partIndex(){
  try { return Math.round(effect("part")("Slider")); } catch (e){}
  try { return thisProperty.propertyGroup(3).propertyIndex - 1; } catch (e){ return 0; }
}
var forma_part = forma_partIndex();

// Five easing curves over the unit square, as the page draws them: ease-in
// u^n, ease-out 1 − (1 − u)^n, ease-in-out, the sine ease 1 − cos(πu/2), and
// the back ease u²((s + 1)u − s) with its overshoot s. The exponent breathes
// from linear up to the slider's power and back over 16 s — the slider is the
// ceiling, not a fixed exponent — so the curves stay alive. Plotted values
// are clamped to −0.25 .. 1.25 as on the page, so the overshoot reads without
// leaving the frame. One curve per part, 0 .. 4, each in its own colour on
// the page (the accent, the contrast mark, mint, coral, lavender); part 5 is
// the unit square itself. The travelling dot is Trim Paths with Start = End.
// parts: 6
var m = Math.min(forma_W, forma_H) * 0.1, bw = forma_W - m * 2, bh = forma_H - m * 2;
var u16 = ((forma_t / 16) + forma_phase) % 1, tri = u16 < 0.5 ? u16 * 2 : 2 - u16 * 2;
var power = 1 + tri * (p_power - 1);
function forma_ease(i, u){
  if (i === 0) return Math.pow(u, power);
  if (i === 1) return 1 - Math.pow(1 - u, power);
  if (i === 2) return u < 0.5 ? Math.pow(2 * u, power) / 2 : 1 - Math.pow(2 - 2 * u, power) / 2;
  if (i === 3) return 1 - Math.cos(u * Math.PI / 2);
  return u * u * ((p_back + 1) * u - p_back);
}
var pts = [];
if (forma_part >= 5){
  pts.push(forma_pt(m, m), forma_pt(m + bw, m), forma_pt(m + bw, m + bh), forma_pt(m, m + bh));
  createPath(pts, [], [], true);
} else {
  for (var s = 0; s <= 120; s++){
    var u = s / 120, v = forma_ease(forma_part, u);
    v = Math.min(1.25, Math.max(-0.25, v));
    pts.push(forma_pt(m + u * bw, m + bh - v * bh));
  }
  createPath(pts, [], [], false);
}