const canvas = document.getElementById('pc');
const ctx = canvas.getContext('2d');
const nameEl = document.getElementById('pname');
const countEl = document.getElementById('pcount');
const cs = getComputedStyle(document.documentElement);
function col(name, fb) { const v = cs.getPropertyValue(name).trim(); return v || fb; }
const FG = col('--fg', '#15151a');
const MUTED = col('--muted', '#6b6b76');
const LINE = col('--line', '#e2e0d8');
const ACCENT = col('--accent', '#5b5bf7');
const ACCENT2 = col('--accent-2', '#f25c8a');
const ACCENT3 = col('--accent-3', '#18c29c');
let w = 0, h = 0;
function resize() {
const dpr = Math.min(devicePixelRatio || 1, 2);
w = innerWidth; h = innerHeight;
canvas.width = w * dpr; canvas.height = h * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
addEventListener('resize', resize);
resize();
function easeInOut(t) { return t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2; }
function easeOut(t) { return 1 - Math.pow(1 - t, 2); }
function R() { return h * 0.065; }
function ball(x, y, rx, ry, color, alpha) {
ctx.globalAlpha = alpha === undefined ? 1 : alpha;
ctx.beginPath();
ctx.ellipse(x, y, Math.max(rx, 1), Math.max(ry, 1), 0, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
ctx.globalAlpha = 1;
}
function seg(x1, y1, x2, y2, color, width, dash) {
ctx.strokeStyle = color; ctx.lineWidth = width || 1;
ctx.setLineDash(dash || []);
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
ctx.setLineDash([]);
}
const PHASES = [
{ name: 'Squash & Stretch', draw: function (t) {
const gy = h * 0.72;
seg(w * 0.1, gy, w * 0.9, gy, LINE, 1);
const b = Math.abs(Math.sin(t * Math.PI));
const y = gy - b * h * 0.4;
const sq = 1 - b;
ball(w * 0.5, y - R() * (1 - sq * 0.3), R() * (1 + sq * 0.5), R() * (1 - sq * 0.35), ACCENT);
} },
{ name: 'Anticipation', draw: function (t) {
const gy = h * 0.55;
seg(w * 0.1, gy, w * 0.9, gy, LINE, 1);
const x = t < 0.3
? w * 0.2 - easeOut(t / 0.3) * w * 0.06
: w * 0.14 + easeOut(Math.min((t - 0.3) / 0.55, 1)) * w * 0.62;
ball(x, gy, R(), R(), ACCENT);
} },
{ name: 'Staging', draw: function (t) {
const gy = h * 0.55;
ball(w * 0.22, gy, R() * 0.8, R() * 0.8, MUTED, 0.25);
ball(w * 0.78, gy, R() * 0.8, R() * 0.8, MUTED, 0.25);
const pulse = 1 + Math.sin(t * Math.PI * 2) * 0.1;
ball(w * 0.5, gy, R() * 1.3 * pulse, R() * 1.3 * pulse, ACCENT2);
} },
{ name: 'Straight Ahead vs Pose to Pose', draw: function (t) {
const y1 = h * 0.32, y2 = h * 0.68;
seg(w * 0.1, y1, w * 0.9, y1, LINE, 1, [2, 4]);
seg(w * 0.1, y2, w * 0.9, y2, LINE, 1, [2, 4]);
const x1 = w * 0.15 + t * w * 0.7;
ball(x1, y1 + Math.sin(t * 22) * h * 0.05, R() * 0.7, R() * 0.7, ACCENT);
const poses = [0.15, 0.4, 0.65, 0.85];
const idx = Math.min(Math.floor(t * poses.length), poses.length - 1);
ball(w * poses[idx], y2, R() * 0.85, R() * 0.85, ACCENT2);
} },
{ name: 'Follow Through & Overlap', draw: function (t) {
const gy = h * 0.55;
const x = w * 0.5 + Math.sin(t * Math.PI * 2) * w * 0.32;
ball(w * 0.5 + Math.sin((t - 0.09) * Math.PI * 2) * w * 0.32, gy, R() * 0.65, R() * 0.65, ACCENT, 0.35);
ball(w * 0.5 + Math.sin((t - 0.045) * Math.PI * 2) * w * 0.32, gy, R() * 0.82, R() * 0.82, ACCENT, 0.6);
ball(x, gy, R(), R(), ACCENT);
} },
{ name: 'Slow In & Slow Out', draw: function (t) {
const gy = h * 0.55;
for (let i = 0; i <= 8; i++) {
const px = w * 0.15 + easeInOut(i / 8) * w * 0.7;
ball(px, gy, R() * 0.16, R() * 0.16, MUTED, 0.5);
}
ball(w * 0.15 + easeInOut(t) * w * 0.7, gy, R() * 0.85, R() * 0.85, ACCENT3);
} },
{ name: 'Arcs', draw: function (t) {
const x0 = w * 0.15, x1 = w * 0.85, gy = h * 0.68, apex = h * 0.22;
seg(x0, gy, x1, gy, LINE, 1, [2, 4]);
ctx.beginPath();
for (let i = 0; i <= 30; i++) {
const tt = i / 30;
const px = x0 + (x1 - x0) * tt;
const py = gy - Math.sin(tt * Math.PI) * (gy - apex);
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.strokeStyle = LINE; ctx.lineWidth = 1; ctx.stroke();
const px = x0 + (x1 - x0) * t;
const py = gy - Math.sin(t * Math.PI) * (gy - apex);
ball(px, py, R() * 0.75, R() * 0.75, ACCENT);
} },
{ name: 'Secondary Action', draw: function (t) {
const gy = h * 0.55;
const x = w * 0.15 + t * w * 0.7;
ball(x, gy, R(), R(), ACCENT);
const ang = t * Math.PI * 10;
ball(x + Math.cos(ang) * R() * 1.7, gy + Math.sin(ang) * R() * 1.7, R() * 0.3, R() * 0.3, ACCENT2);
} },
{ name: 'Timing', draw: function (t) {
const y1 = h * 0.32, y2 = h * 0.68;
const x = w * 0.15 + t * w * 0.7;
ball(x, y1 - Math.abs(Math.sin(t * 3 * Math.PI)) * h * 0.15, R() * 0.5, R() * 0.5, ACCENT);
ball(x, y2 - Math.abs(Math.sin(t * Math.PI)) * h * 0.28, R() * 1.15, R() * 1.15, ACCENT2);
} },
{ name: 'Exaggeration', draw: function (t) {
const gy = h * 0.72;
seg(w * 0.1, gy, w * 0.9, gy, LINE, 1);
const b = Math.abs(Math.sin(t * Math.PI));
const sq = 1 - b;
const y = gy - b * h * 0.46;
ball(w * 0.35, gy - Math.abs(Math.sin(t * Math.PI)) * h * 0.25 - R() * 0.5, R() * 0.5, R() * 0.5, MUTED, 0.4);
ball(w * 0.65, y - R() * 1.15 * (1 - sq * 0.9), R() * 1.15 * (1 + sq * 1.5), R() * 1.15 * (1 - sq * 0.8), ACCENT2);
} },
{ name: 'Solid Drawing', draw: function (t) {
const cx = w * 0.5, cy = h * 0.55;
const rot = t * Math.PI * 2;
const rx = Math.max(Math.abs(Math.cos(rot)) * R() * 1.3, R() * 0.35);
ball(cx, cy, rx, R() * 1.3, ACCENT);
ball(cx + Math.sin(rot) * rx * 0.4, cy - R() * 0.5, R() * 0.22, R() * 0.32, '#ffffff', 0.5);
} },
{ name: 'Appeal', draw: function (t) {
const cx = w * 0.5, cy = h * 0.58;
const breathe = 1 + Math.sin(t * Math.PI * 2) * 0.04;
const rx = R() * 1.5 * breathe, ry = R() * 1.3 * breathe;
ball(cx, cy, rx, ry, ACCENT3);
const blinkFrac = t % 1;
const blink = blinkFrac > 0.92 ? 0.15 : 1;
ball(cx - rx * 0.32, cy - ry * 0.15, R() * 0.14, R() * 0.14 * blink, FG);
ball(cx + rx * 0.32, cy - ry * 0.15, R() * 0.14, R() * 0.14 * blink, FG);
ctx.strokeStyle = FG; ctx.lineWidth = 2;
ctx.beginPath();
ctx.arc(cx, cy + ry * 0.28, rx * 0.28, 0.15 * Math.PI, 0.85 * Math.PI);
ctx.stroke();
} },
];
let phaseIdx = -1;
const DUR = 1900;
const startT = performance.now();
function loop(now) {
const elapsed = Math.max(0, now - startT);
const idx = Math.floor(elapsed / DUR) % PHASES.length;
const t = (elapsed % DUR) / DUR;
if (idx !== phaseIdx) {
phaseIdx = idx;
nameEl.textContent = PHASES[idx].name;
countEl.textContent = (idx + 1) + ' / ' + PHASES.length;
}
ctx.clearRect(0, 0, w, h);
PHASES[idx].draw(t);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);