440 lines
10 KiB
Markdown
440 lines
10 KiB
Markdown
# 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(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:
|
|
|
|
```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
|
|
}
|
|
```
|