Modal · David-grade · animation patterns lab
The five patterns from David Umoru's mascot.
Studied his Claude mascot animation source (extracted from his Next.js bundle, prettified, saved at design/_research/david-umoru-claude-mascot-extracted.js) and isolated five techniques that make the difference between "GSAP works" and "this character has weight." Each pattern below has a working Modal demo. The last one is a walking Modal — the same `linear` translate + alternating squash rhythm David uses, ported to v2 silhouette.
Function-based values for asymmetric multi-element animation.
GSAP lets you pass a function instead of a static value. The function receives each target's index and returns the per-element value. Lets you stagger one timeline across multiple elements with bespoke values per index — without copy-pasting four `.to()` calls. Below: Modal's two eyes blink with slightly different durations (110ms vs 130ms) and recovery delays — feels hand-animated, not symmetrical.
From david-umoru-claude-mascot-extracted.js — line 49
// Each of 4 spikes gets a different rotation + scaleY .to(e, { rotation: (i) => [-7, -8, -8, -9][i], scaleY: (i) => [1.35, 1.3, 1.2, 1.15][i], duration: 0.4, ease: "power2.out" }, "<")
svgOrigin for proper SVG transform pivots.
CSS transform-origin uses percentages; SVG transforms ignore that. gsap.set(el, { svgOrigin: "x y" }) sets the rotation pivot in SVG coordinates. Below: Modal's head rotates around the bottom-center of his body band (svgOrigin 8 13) — feels like a head-tilt, not a free-floating spin.
From david-umoru — line 29-32
// Set pivot at SVG coordinates (16.5, 86) gsap.set(r.current, { svgOrigin: "16.5 86" }); gsap.set(x.current, { svgOrigin: "37.5 86" }); // Then rotate — pivots around that point gsap.to(l.current, { rotation: -3, x: -3, y: -5, svgOrigin: "53 65", duration: 0.4, ease: "power2.out" })
Labels for synchronized cluster sequencing.
Instead of chaining `.to().to().to()` and praying the timing works, David adds named labels (.addLabel("walk")) and anchors animations to them with offset syntax: "walk", "walk+=0.1", "walk+=0.2". Lets him fire parallel clusters at exact moments — the horizontal walk + body sway + alternating limb pulses all start at "walk" but tick at offset beats. Below: Modal's "fired" reaction — squash, jump, body-band pulse, and ★ emote all anchor to a single label.
From david-umoru — line 113
.addLabel("walk") .to(d.current, { x: j, duration: 2.2, ease: "linear" }, "walk") .to([r,c], { scaleY: 0.45, duration: 0.1 }, "walk") .to([r,c], { scaleY: 1, duration: 0.1 }, "walk+=0.1") .to([x,s], { scaleY: 0.45, duration: 0.1 }, "walk+=0.1") .to([x,s], { scaleY: 1, duration: 0.1 }, "walk+=0.2")
Alternating-pair scaleY rhythm — the walking beat.
David doesn't have legs to animate, just 4 spikes. He pairs them [r,c] + [x,s] and alternates scaleY: 0.45 ↔ 1 on a 100ms grid with the second pair offset by 50ms. The pairs trade off who's "compressed" — creating a 2/4 marching beat that reads as walking. Modal has no legs either, but the body band can be split into halves that pulse the same way.
Modal port — alternating-pair
// Two halves alternate squash on a 100ms grid const tl = gsap.timeline({ repeat: -1 }); tl.addLabel("step"); // Beat 1 — left squashes tl.to(bandL, { scaleY: 0.45, ease: "power2.out" }, "step"); tl.to(bandL, { scaleY: 1, ease: "power2.in" }, "step+=0.1"); // Beat 2 — right squashes (overlaps left's recovery) tl.to(bandR, { scaleY: 0.45, ease: "power2.out" }, "step+=0.1"); tl.to(bandR, { scaleY: 1, ease: "power2.in" }, "step+=0.2");
David's ease mix: power2.out, power3.in, linear. No elastic.
David uses zero elastic, zero bounce. Just the power-of-2/3 family for action-recovery and linear for steady motion. The result is crisp and engineered, not slime-cute. We've been overusing elastic.out(1, 0.4) on Modal — see how it changes when we swap to David's mix.
The full walk cycle, ported to v2 silhouette.
All five patterns combined. Modal walks left to right with linear horizontal travel. Body band split into halves alternates scaleY 0.45 ↔ 1 on a 100ms grid (Pattern 4). Body itself rotates ±2° alternately using svgOrigin "8 13" at his feet (Pattern 2). Eyes blink with function-based values for slight asymmetry (Pattern 1). Whole sequence anchored to a label called "walk" (Pattern 3). Eases are power2.out / power2.in / linear, no elastic (Pattern 5). Click "Walk" to play.