📄 animation.md

← Vault

Animation

Frame-Based Animation

The Draw Loop

`javascript

function draw() {

// Called ~60 times/sec by default

// frameCount — integer, starts at 1

// deltaTime — ms since last frame (use for framerate-independent motion)

// millis() — ms since sketch start

}

`

Time-Based vs Frame-Based

`javascript

// Frame-based (speed varies with framerate)

x += speed;

// Time-based (consistent speed regardless of framerate)

x += speed * (deltaTime / 16.67); // normalized to 60fps

`

Normalized Time

`javascript

// Progress from 0 to 1 over N seconds

let duration = 5000; // 5 seconds in ms

let t = constrain(millis() / duration, 0, 1);

// Looping progress (0 → 1 → 0 → 1...)

let period = 3000; // 3 second loop

let t = (millis() % period) / period;

// Ping-pong (0 → 1 → 0 → 1...)

let raw = (millis() % (period * 2)) / period;

let t = raw <= 1 ? raw : 2 - raw;

`

Easing Functions

Built-in Lerp

`javascript

// Linear interpolation — smooth but mechanical

let x = lerp(startX, endX, t);

// Map for non-0-1 ranges

let y = map(t, 0, 1, startY, endY);

`

Common Easing Curves

`javascript

// Ease in (slow start)

function easeInQuad(t) { return t * t; }

function easeInCubic(t) { return t t t; }

function easeInExpo(t) { return t === 0 ? 0 : pow(2, 10 * (t - 1)); }

// Ease out (slow end)

function easeOutQuad(t) { return 1 - (1 - t) * (1 - t); }

function easeOutCubic(t) { return 1 - pow(1 - t, 3); }

function easeOutExpo(t) { return t === 1 ? 1 : 1 - pow(2, -10 * t); }

// Ease in-out (slow both ends)

function easeInOutCubic(t) {

return t < 0.5 ? 4 t t t : 1 - pow(-2 t + 2, 3) / 2;

}

function easeInOutQuint(t) {

return t < 0.5 ? 16 t t t t t : 1 - pow(-2 t + 2, 5) / 2;

}

// Elastic (spring overshoot)

function easeOutElastic(t) {

if (t === 0 || t === 1) return t;

return pow(2, -10 t) sin((t 10 - 0.75) (2 * PI / 3)) + 1;

}

// Bounce

function easeOutBounce(t) {

if (t < 1/2.75) return 7.5625 t t;

else if (t < 2/2.75) { t -= 1.5/2.75; return 7.5625 t t + 0.75; }

else if (t < 2.5/2.75) { t -= 2.25/2.75; return 7.5625 t t + 0.9375; }

else { t -= 2.625/2.75; return 7.5625 t t + 0.984375; }

}

// Smooth step (Hermite interpolation — great default)

function smoothstep(t) { return t t (3 - 2 * t); }

// Smoother step (Ken Perlin)

function smootherstep(t) { return t t t (t (t * 6 - 15) + 10); }

`

Applying Easing

`javascript

// Animate from startVal to endVal over duration ms

function easedValue(startVal, endVal, startTime, duration, easeFn) {

let t = constrain((millis() - startTime) / duration, 0, 1);

return lerp(startVal, endVal, easeFn(t));

}

// Usage

let x = easedValue(100, 700, animStartTime, 2000, easeOutCubic);

`

Spring Physics

More natural than easing — responds to force, overshoots, settles.

`javascript

class Spring {

constructor(value, target, stiffness = 0.1, damping = 0.7) {

this.value = value;

this.target = target;

this.velocity = 0;

this.stiffness = stiffness;

this.damping = damping;

}

update() {

let force = (this.target - this.value) * this.stiffness;

this.velocity += force;

this.velocity *= this.damping;

this.value += this.velocity;

return this.value;

}

setTarget(t) { this.target = t; }

isSettled(threshold = 0.01) {

return abs(this.velocity) < threshold && abs(this.value - this.target) < threshold;

}

}

// Usage

let springX = new Spring(0, 0, 0.08, 0.85);

function draw() {

springX.setTarget(mouseX);

let x = springX.update();

ellipse(x, height/2, 50);

}

`

2D Spring

`javascript

class Spring2D {

constructor(x, y) {

this.pos = createVector(x, y);

this.target = createVector(x, y);

this.vel = createVector(0, 0);

this.stiffness = 0.08;

this.damping = 0.85;

}

update() {

let force = p5.Vector.sub(this.target, this.pos).mult(this.stiffness);

this.vel.add(force).mult(this.damping);

this.pos.add(this.vel);

return this.pos;

}

}

`

State Machines

For complex multi-phase animations.

`javascript

const STATES = { IDLE: 0, ENTER: 1, ACTIVE: 2, EXIT: 3 };

let state = STATES.IDLE;

let stateStart = 0;

function setState(newState) {

state = newState;

stateStart = millis();

}

function stateTime() {

return millis() - stateStart;

}

function draw() {

switch (state) {

case STATES.IDLE:

// waiting...

break;

case STATES.ENTER:

let t = constrain(stateTime() / 1000, 0, 1);

let alpha = easeOutCubic(t) * 255;

// fade in...

if (t >= 1) setState(STATES.ACTIVE);

break;

case STATES.ACTIVE:

// main animation...

break;

case STATES.EXIT:

let t2 = constrain(stateTime() / 500, 0, 1);

// fade out...

if (t2 >= 1) setState(STATES.IDLE);

break;

}

}

`

Timeline Sequencing

For timed multi-scene animations (motion graphics, title sequences).

`javascript

class Timeline {

constructor() {

this.events = [];

}

at(timeMs, duration, fn) {

this.events.push({ start: timeMs, end: timeMs + duration, fn });

return this;

}

update() {

let now = millis();

for (let e of this.events) {

if (now >= e.start && now < e.end) {

let t = (now - e.start) / (e.end - e.start);

e.fn(t);

}

}

}

}

// Usage

let timeline = new Timeline();

timeline

.at(0, 2000, (t) => {

// Scene 1: title fade in (0-2s)

let alpha = easeOutCubic(t) * 255;

fill(255, alpha);

textSize(48);

text("Hello", width/2, height/2);

})

.at(2000, 1000, (t) => {

// Scene 2: title fade out (2-3s)

let alpha = (1 - easeInCubic(t)) * 255;

fill(255, alpha);

textSize(48);

text("Hello", width/2, height/2);

})

.at(3000, 5000, (t) => {

// Scene 3: main content (3-8s)

renderMainContent(t);

});

function draw() {

background(0);

timeline.update();

}

`

Noise-Driven Motion

More organic than deterministic animation.

`javascript

// Smooth wandering position

let x = map(noise(frameCount * 0.005, 0), 0, 1, 0, width);

let y = map(noise(0, frameCount * 0.005), 0, 1, 0, height);

// Noise-driven rotation

let angle = noise(frameCount 0.01) TWO_PI;

// Noise-driven scale (breathing effect)

let s = map(noise(frameCount * 0.02), 0, 1, 0.8, 1.2);

// Noise-driven color shift

let hue = map(noise(frameCount * 0.003), 0, 1, 0, 360);

`

Transition Patterns

Fade In/Out

`javascript

function fadeIn(t) { return constrain(t, 0, 1); }

function fadeOut(t) { return constrain(1 - t, 0, 1); }

`

Slide

`javascript

function slideIn(t, direction = 'left') {

let et = easeOutCubic(t);

switch (direction) {

case 'left': return lerp(-width, 0, et);

case 'right': return lerp(width, 0, et);

case 'up': return lerp(-height, 0, et);

case 'down': return lerp(height, 0, et);

}

}

`

Scale Reveal

`javascript

function scaleReveal(t) {

let et = easeOutElastic(constrain(t, 0, 1));

push();

translate(width/2, height/2);

scale(et);

translate(-width/2, -height/2);

// draw content...

pop();

}

`

Staggered Entry

`javascript

// N elements appear one after another

let staggerDelay = 100; // ms between each

for (let i = 0; i < elements.length; i++) {

let itemStart = baseTime + i * staggerDelay;

let t = constrain((millis() - itemStart) / 500, 0, 1);

let alpha = easeOutCubic(t) * 255;

let yOffset = lerp(30, 0, easeOutCubic(t));

// draw element with alpha and yOffset

}

`

Recording Deterministic Animations

For frame-perfect export, use frame count instead of millis():

`javascript

const TOTAL_FRAMES = 300; // 10 seconds at 30fps

const FPS = 30;

function draw() {

let t = frameCount / TOTAL_FRAMES; // 0 to 1 over full duration

if (t > 1) { noLoop(); return; }

// Use t for all animation timing — deterministic

renderFrame(t);

// Export

if (CONFIG.recording) {

saveCanvas('frame-' + nf(frameCount, 4), 'png');

}

}

`

Scene Fade Envelopes (Video)

Every scene in a multi-scene video needs fade-in and fade-out. Hard cuts between visually different generative scenes are jarring.

`javascript

const SCENE_FRAMES = 150; // 5 seconds at 30fps

const FADE = 15; // half-second fade

function draw() {

let lf = frameCount - 1; // 0-indexed local frame

let t = lf / SCENE_FRAMES; // 0..1 normalized progress

// Fade envelope: ramp up at start, ramp down at end

let fade = 1;

if (lf < FADE) fade = lf / FADE;

if (lf > SCENE_FRAMES - FADE) fade = (SCENE_FRAMES - lf) / FADE;

fade = fade fade (3 - 2 * fade); // smoothstep for organic feel

// Apply fade to all visual output

// Option 1: multiply alpha values by fade

fill(r, g, b, alpha * fade);

// Option 2: tint entire composited image

tint(255, fade * 255);

image(sceneBuffer, 0, 0);

noTint();

// Option 3: multiply pixel brightness (for pixel-level scenes)

pixels[i] = r * fade;

}

`

Animating Static Algorithms

Some generative algorithms produce a single static result (attractors, circle packing, Voronoi). In video, static content reads as frozen/broken. Techniques to add motion:

Progressive Reveal

Expand a mask from center outward to reveal the precomputed result:

`javascript

let revealRadius = easeOutCubic(min(t 1.5, 1)) (width * 0.8);

// In the render loop, skip pixels beyond revealRadius from center

let dx = x - width/2, dy = y - height/2;

if (sqrt(dxdx + dydy) > revealRadius) continue;

// Soft edge:

let edgeFade = constrain((revealRadius - dist) / 40, 0, 1);

`

Parameter Sweep

Slowly change a parameter to show the algorithm evolving:

`javascript

// Attractor with drifting parameters

let a = -1.7 + sin(t 0.5) 0.2; // oscillate around base value

let b = 1.3 + cos(t 0.3) 0.15;

`

Slow Camera Motion

Apply subtle zoom or rotation to the final image:

`javascript

push();

translate(width/2, height/2);

scale(1 + t * 0.05); // slow 5% zoom over scene duration

rotate(t * 0.1); // gentle rotation

translate(-width/2, -height/2);

image(precomputedResult, 0, 0);

pop();

`

Overlay Dynamic Elements

Add particles, grain, or subtle noise on top of static content:

`javascript

// Static background

image(staticResult, 0, 0);

// Dynamic overlay

for (let p of ambientParticles) {

p.update();

p.display(); // slow-moving specks add life

}

`