Files
hermes-sync/skills/creative/p5js/references/animation.md

10 KiB

Animation

Frame-Based Animation

The Draw Loop

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

// 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

// 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

// 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

// 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

// 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.

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

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.

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).

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.

// 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

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

Slide

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

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

// 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():

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.

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:

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(dx*dx + dy*dy) > revealRadius) continue;
// Soft edge:
let edgeFade = constrain((revealRadius - dist) / 40, 0, 1);

Parameter Sweep

Slowly change a parameter to show the algorithm evolving:

// 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:

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:

// Static background
image(staticResult, 0, 0);
// Dynamic overlay
for (let p of ambientParticles) {
  p.update();
  p.display();  // slow-moving specks add life
}