| name | canvas-animation |
| description | Build interactive canvas-based animations with pixel-art typography, spring physics, mouse/touch interaction, and idle animations. Use when asked to create animated backgrounds, letter grids, particle effects, or any interactive canvas component for React/Next.js sites. |
| argument-hint | ["description of the animation"] |
Interactive Canvas Animation Builder
Build production-quality, interactive canvas animations for React/Next.js projects. $ARGUMENTS
Architecture
Every canvas animation component follows this structure:
1. Configuration constants (physics, sizing, colors)
2. Data definitions (fonts, shapes, patterns)
3. Pre-computation / caching layer
4. Tile/sprite baking (offscreen canvases at retina resolution)
5. React component with useEffect lifecycle:
a. Layout computation (responsive sizing, DPR handling)
b. State initialization (springs, positions, modes)
c. Animation loop (requestAnimationFrame)
d. Event listeners (pointer, touch, resize, theme)
e. Cleanup
Core Techniques
Retina Canvas
Always render at device pixel ratio for crisp output:
const dpr = window.devicePixelRatio || 1;
canvas.width = Math.round(rect.width * dpr);
canvas.height = Math.round(rect.height * dpr);
ctx.scale(dpr, dpr);
Spring Physics
Use damped springs for natural, organic motion. Frame-relative dt for consistent behavior across refresh rates:
const dt = Math.min((now - lastTime) / (1000 / 60), 3);
function springStep(pos, vel, target, tension, friction, dt) {
const accel = tension * (target - pos) - friction * vel;
vel += accel * dt;
pos += vel * dt;
return [pos, vel];
}
Good starting values:
- Snappy: tension 0.22, friction 0.45
- Smooth: tension 0.15, friction 0.4
- Lazy: tension 0.08, friction 0.35
Tile Baking
Pre-render repeated shapes to offscreen canvases, then use drawImage for each instance. Much faster than drawing paths per frame:
function bakeTile(size, color, drawFn) {
const c = document.createElement("canvas");
c.width = size; c.height = size;
const ctx = c.getContext("2d")!;
drawFn(ctx, size, color);
return c;
}
Mouse/Touch Interaction
Use window-level pointermove for mouse (works through layered content), parent-level touch events for mobile:
window.addEventListener("pointermove", (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const inBounds = x >= 0 && x <= rect.width && y >= 0 && y <= rect.height;
if (inBounds) { }
else if (hovering) { }
});
section.addEventListener("touchstart", handler, { passive: true });
section.addEventListener("touchmove", handler, { passive: true });
section.addEventListener("touchend", handler);
Idle Animation (Lissajous)
When not interacting, animate the focus point in a figure-8 pattern:
const angle = (elapsed / PERIOD) * Math.PI * 2;
const idleX = (Math.sin(angle - Math.PI / 2) + 1) / 2;
const idleY = ((Math.sin(2 * angle) + 1) / 2) * (rows - 1);
2D Gaussian Height/Intensity Distribution
Drive visual intensity based on distance from a focus point:
const sx = (itemX - focusX) / SIGMA_X;
const sy = (itemY - focusY) / SIGMA_Y;
const gaussian = Math.exp(-0.5 * (sx * sx + sy * sy));
const target = MIN + (MAX - MIN) * gaussian * blend;
Pixel-Art Font System
Define letters as stretchable bitmaps with fixed caps and repeatable body rows:
interface LetterDef {
topCap: string[];
body: string;
bottomCap: string[];
midBar?: string[];
upperBody?: string;
}
Generate shadow cells adjacent to body cells at offsets [+1,0], [0,+1], [+1,+1].
Theme Awareness
Watch for theme changes and rebake tiles:
const observer = new MutationObserver(() => {
color = getComputedStyle(el).getPropertyValue("--your-color").trim();
});
observer.observe(document.documentElement, {
attributes: true, attributeFilter: ["data-theme"]
});
Blend & Mode System
Manage interaction intensity with a blend value (0 = idle, 1 = active):
- Warmup: Ramp tension gradually when hovering starts (prevents jarring snap)
- Idle fade-in: Use quadratic or quartic easing for gentle ambient effect
- Overshoot: Allow blend slightly >1 (e.g. max 1.35) for bouncy feel
Performance Checklist
Canvas Positioning
For background animations behind content:
<section className="relative">
<canvas className="absolute inset-0 w-full h-full pointer-events-none opacity-30"
aria-hidden="true" tabIndex={-1} />
{}
</section>
Reference Implementation
See src/components/letter-grid.tsx in the yannickwenger.de project for a complete example with all techniques above.