{"id":"the-pounce-drawn","title":"The Pounce (drawn)","category":"Drawn film","prompt":"A tiger hunting a small red devil, as a fifteen-second drawn film: stalk, coil, launch, full extension, the strike, the landing — and the devil gets away. Every thing on screen is declared by name, so it can be selected, recoloured, hidden or moved without rewriting the program.","designedBy":{"provider":"anthropic","model":"claude-fable-5-1","inference":{"measured":false,"inputTokens":2048,"outputTokens":5482,"costUsd":0.2946,"inPerMTokUsd":10,"outPerMTokUsd":50,"estimated":true,"basis":"estimated as one chat turn — the draw-contract prompt and brief in, the 20,283-character program out, at ~3.7 characters per token; the model is not in the rate catalogue, so it is priced at the catalogue ceiling"}},"render":{"credits":6,"priceUsd":0.09,"format":"mp4","renderMs":20997,"frameCount":360,"bytes":1215722,"seed":3880854923,"width":1280,"height":720,"fps":24,"durationSeconds":15},"howToReproduce":{"renderApi":"POST /v1/renders with `renderRequest` below as the body (Authorization: Bearer <your pp_sa_… key>).","chat":"Paste the prompt into /chat: an illustrated film brief is answered with a program like this one, and plays at once.","note":"This is a draw contract — a Canvas 2D program. It has no editor ops; it opens in the Cloud Studio as a film (/studio?gallery-film=the-pounce-drawn), and it exports as MP4 or GIF, not as a still."},"renderRequest":{"drawContract":"const meta = { duration: 15, fps: 24, size: [1280, 720], seed: 11 };\n\n// ---- helpers ---------------------------------------------------------------------------------\nconst W = 1280, H = 720, G = 590;                       // G: the ground line\nconst clamp = (v, a, b) => Math.max(a, Math.min(b, v));\nconst ease = (u) => { u = clamp(u, 0, 1); return u * u * (3 - 2 * u); };\nconst span = (t, a, b) => ease((t - a) / (b - a));      // 0 → 1 across [a, b], eased\nconst lerp = (a, b, u) => a + (b - a) * u;\nconst hash = (i) => { const s = Math.sin(i * 127.1 + 311.7) * 43758.5453; return s - Math.floor(s); };  // stable per index, not per frame\nconst fade = (t, from, to, d) => clamp(Math.min((t - from) / d, (to - t) / d), 0, 1);\n\n// A tapered limb segment with round ends.\nfunction limb(ctx, x1, y1, x2, y2, w1, w2, color) {\n  const a = Math.atan2(y2 - y1, x2 - x1), nx = Math.sin(a), ny = -Math.cos(a);\n  ctx.fillStyle = color; ctx.beginPath();\n  ctx.moveTo(x1 + nx * w1, y1 + ny * w1); ctx.lineTo(x2 + nx * w2, y2 + ny * w2);\n  ctx.arc(x2, y2, w2, a - Math.PI / 2, a + Math.PI / 2);\n  ctx.lineTo(x1 - nx * w1, y1 - ny * w1); ctx.arc(x1, y1, w1, a + Math.PI / 2, a - Math.PI / 2);\n  ctx.closePath(); ctx.fill();\n}\n// Two-bone IK: where is the knee, given the hip, the foot and the bone lengths?\nfunction knee(hx, hy, fx, fy, l1, l2, bend) {\n  let dx = fx - hx, dy = fy - hy, d = Math.hypot(dx, dy); const max = l1 + l2 - 0.5;\n  if (d > max) { dx *= max / d; dy *= max / d; d = max; }\n  const a = Math.atan2(dy, dx), k = Math.acos(clamp((l1 * l1 + d * d - l2 * l2) / (2 * l1 * d), -1, 1));\n  return { kx: hx + Math.cos(a + bend * k) * l1, ky: hy + Math.sin(a + bend * k) * l1, fx: hx + dx, fy: hy + dy };\n}\n\n// ---- the hunt, as a function of time ------------------------------------------------------------\n// Every number the tiger's body needs at time t. The devil reads it too, to ride on the tiger's back.\nconst BEATS = { stalk: 0.8, coil: 5.8, launch: 7.3, fly: 7.7, land: 8.7, settle: 9.3, search: 10.2, ride: 12.5 };\nfunction hunt(t, p) {\n  const B = BEATS, x0 = p.x, L = 250 * p.scale;\n  const stalkU = clamp((t - B.stalk) / (B.coil - B.stalk), 0, 1);\n  let hx = x0 + 230 * stalkU, hy = G - 116 * p.scale, th = 0.05, head = 0.18, walk = t > B.stalk && t < B.coil ? 1 : 0;\n  let hind = [-14, 116], front = [12, 116], tailA = 0.5, tailB = 0.3, mouth = 0, ears = 0, eyeUp = 0;\n  const coil = span(t, B.coil, B.coil + 0.7) * (1 - span(t, B.launch, B.fly));\n  hy += 40 * coil * p.scale; th += 0.10 * coil; head -= 0.10 * coil; ears = coil * 0.6;\n  hx += coil * (t < B.launch ? Math.sin(t * 38) * 3.5 : 0) - 14 * coil;          // the wiggle before the spring\n  hind = [lerp(hind[0], 34, coil), lerp(hind[1], 76, coil)]; front = [lerp(front[0], 30, coil), lerp(front[1], 78, coil)];\n  tailA += 0.5 * coil + Math.sin(t * 9) * 0.10 * coil;\n  const push = span(t, B.launch, B.fly);\n  if (t >= B.launch && t < B.land) {\n    const u = clamp((t - B.launch) / (B.land - B.launch), 0, 1), arc = Math.sin(Math.PI * clamp((t - B.launch) / (B.land - B.launch + 0.05), 0, 1));\n    hx = lerp(x0 + 216, x0 + 640, ease(u)); hy = G - 80 * p.scale - 210 * arc * p.scale;\n    th = lerp(-0.55, 0.42, ease(u)); head = lerp(-0.25, 0.30, u); mouth = ease(u) * 0.9; ears = 1;\n    hind = [lerp(-70, 60, ease(clamp((u - 0.45) / 0.55, 0, 1))), lerp(88, 66, u)]; front = [lerp(40, 88, push), lerp(70, 74, u)];\n    tailA = lerp(1.2, -0.3, u); tailB = lerp(0.4, -0.5, u);\n  }\n  if (t >= B.land) {\n    const s = span(t, B.land, B.settle), skid = (1 - Math.pow(1 - clamp((t - B.land) / 1.4, 0, 1), 3)) * 56;\n    hx = x0 + 640 + skid; hy = lerp(G - 80 * p.scale, G - 116 * p.scale, s); th = lerp(0.42, 0.03, s);\n    head = lerp(0.30, 0.05, s); mouth = lerp(0.9, 0, span(t, B.land, B.land + 0.35)); ears = 1 - s;\n    hind = [lerp(60, -14, s), lerp(70, 116, s)]; front = [lerp(88, 12, s), lerp(78, 116, s)]; tailA = lerp(-0.3, 0.6, s); tailB = 0.3;\n  }\n  if (t >= B.search) {\n    const k = span(t, B.search, B.search + 0.4) * (1 - span(t, B.ride, B.ride + 0.3));\n    head += k * Math.sin((t - B.search) * 2.6) * 0.55; tailA += k * Math.sin(t * 3.2) * 0.35;\n  }\n  if (t >= B.ride) { const r = span(t, B.ride, B.ride + 0.5); ears = r; eyeUp = r; head -= 0.12 * r; tailA = lerp(tailA, 0.15, r); }\n  return { hx, hy, th, L, head, walk, hind, front, tailA, tailB, mouth, ears, eyeUp };\n}\n\nfunction drawTiger(ctx, t, p) {\n  const s = hunt(t, p), k = p.scale, c = Math.cos(s.th), sn = Math.sin(s.th);\n  const at = (lx, ly) => [s.hx + (lx * c - ly * sn), s.hy + (lx * sn + ly * c)];      // body frame → world\n  const dark = p.stripe, fur = p.color, belly = p.belly;\n  // legs: far pair first (darker), then body, then near pair\n  const legPair = (far) => {\n    const tone = far ? p.shade : fur, off = far ? 0.5 : 0;\n    [[\"h\", 18 * k, s.hind, 66 * k, 60 * k, 1], [\"f\", s.L - 26 * k, s.front, 58 * k, 58 * k, -1]].forEach(([id, ax, tgt, l1, l2, bend]) => {\n      const [hx, hy] = at(ax, 22 * k); let fx, fy;\n      if (s.walk) {                                                                  // planted stance, lifted swing\n        const ph = ((t * 0.9 + (id === \"h\" ? 0 : 0.5) + off) % 1 + 1) % 1, stride = 38 * k;\n        const rel = ph < 0.62 ? stride * (0.5 - ph / 0.62) : stride * (-0.5 + (ph - 0.62) / 0.38);\n        fx = hx + rel + (id === \"h\" ? -8 : 10) * k; fy = G - (ph < 0.62 ? 0 : Math.sin(Math.PI * (ph - 0.62) / 0.38) * 16 * k);\n      } else { const w = at(ax + tgt[0] * k, 22 * k + tgt[1] * k); fx = w[0]; fy = Math.min(w[1], G); }\n      const j = knee(hx, hy, fx, fy, l1, l2, bend);\n      limb(ctx, hx, hy, j.kx, j.ky, 17 * k, 11 * k, tone); limb(ctx, j.kx, j.ky, j.fx, j.fy - 5 * k, 11 * k, 8 * k, tone);\n      ctx.fillStyle = tone; ctx.beginPath(); ctx.ellipse(j.fx + 7 * k, j.fy - 5 * k, 15 * k, 7.5 * k, 0, 0, Math.PI * 2); ctx.fill();\n    });\n  };\n  legPair(true);\n  // tail: two tapered bones with dark bands\n  const [tx, ty] = at(-4 * k, 2 * k), a1 = s.th + Math.PI - s.tailA, a2 = a1 - s.tailB - Math.sin(t * 2.3) * 0.18;\n  const m1 = [tx + Math.cos(a1) * 78 * k, ty + Math.sin(a1) * 78 * k], m2 = [m1[0] + Math.cos(a2) * 70 * k, m1[1] + Math.sin(a2) * 70 * k];\n  limb(ctx, tx, ty, m1[0], m1[1], 10 * k, 7 * k, fur); limb(ctx, m1[0], m1[1], m2[0], m2[1], 7 * k, 5 * k, fur);\n  for (let i = 1; i <= 4; i++) { const u = i / 5, q = [lerp(m1[0], m2[0], u), lerp(m1[1], m2[1], u)], n = [lerp(m1[0], m2[0], u + 0.08), lerp(m1[1], m2[1], u + 0.08)]; limb(ctx, q[0], q[1], n[0], n[1], 6.4 * k, 6 * k, dark); }\n  // body: haunch, barrel, chest — one curve in the body frame\n  ctx.save(); ctx.translate(s.hx, s.hy); ctx.rotate(s.th);\n  ctx.fillStyle = fur; ctx.beginPath(); ctx.moveTo(-26 * k, -6 * k);\n  ctx.bezierCurveTo(-30 * k, -58 * k, 60 * k, -52 * k, s.L * 0.5, -38 * k);\n  ctx.bezierCurveTo(s.L * 0.8, -46 * k, s.L + 14 * k, -40 * k, s.L + 10 * k, 0);\n  ctx.bezierCurveTo(s.L + 6 * k, 40 * k, s.L * 0.7, 40 * k, s.L * 0.5, 30 * k);\n  ctx.bezierCurveTo(60 * k, 26 * k, -14 * k, 46 * k, -26 * k, -6 * k); ctx.fill();\n  ctx.fillStyle = belly; ctx.beginPath(); ctx.moveTo(30 * k, 24 * k); ctx.bezierCurveTo(90 * k, 34 * k, s.L * 0.7, 40 * k, s.L - 10 * k, 26 * k);\n  ctx.bezierCurveTo(s.L * 0.7, 28 * k, 90 * k, 22 * k, 30 * k, 24 * k); ctx.fill();\n  ctx.fillStyle = dark;                                                              // six stripes across the spine\n  for (let i = 0; i < 6; i++) { const x = (22 + i * 36) * k, top = (-50 + Math.abs(i - 2.2) * 3.2) * k, lean = (i % 2 ? 10 : -6) * k;\n    ctx.beginPath(); ctx.moveTo(x - 7 * k, top); ctx.quadraticCurveTo(x + lean, top + 30 * k, x + lean * 0.4, top + 58 * k); ctx.quadraticCurveTo(x + lean + 9 * k, top + 28 * k, x + 8 * k, top); ctx.closePath(); ctx.fill(); }\n  // head, on the end of the chest\n  ctx.translate(s.L + 8 * k, -22 * k); ctx.rotate(s.head);\n  ctx.fillStyle = fur; ctx.beginPath(); ctx.ellipse(28 * k, 0, 40 * k, 34 * k, 0, 0, Math.PI * 2); ctx.fill();\n  const ear = (ex, flip) => { ctx.save(); ctx.translate(ex, -26 * k); ctx.rotate(-0.2 * flip - s.ears * 1.0); ctx.fillStyle = fur; ctx.beginPath(); ctx.moveTo(-12 * k, 4 * k); ctx.quadraticCurveTo(-2 * k, -30 * k, 12 * k, 2 * k); ctx.closePath(); ctx.fill();\n    ctx.fillStyle = p.inner; ctx.beginPath(); ctx.moveTo(-6 * k, 2 * k); ctx.quadraticCurveTo(-1 * k, -18 * k, 7 * k, 1 * k); ctx.closePath(); ctx.fill(); ctx.restore(); };\n  ear(8 * k, 1); ear(34 * k, -1);\n  ctx.fillStyle = belly; ctx.beginPath(); ctx.ellipse(52 * k, 12 * k, 22 * k, 16 * k, 0, 0, Math.PI * 2); ctx.fill();   // muzzle\n  if (s.mouth > 0.02) { ctx.fillStyle = \"#3a0d12\"; ctx.beginPath(); ctx.ellipse(54 * k, 22 * k, 14 * k, 11 * k * s.mouth, 0, 0, Math.PI * 2); ctx.fill();\n    ctx.fillStyle = \"#fff8ec\"; [[46, 14], [61, 14]].forEach(([fx, fy]) => { ctx.beginPath(); ctx.moveTo((fx - 3) * k, fy * k); ctx.lineTo(fx * k, (fy + 9 * s.mouth) * k); ctx.lineTo((fx + 3) * k, fy * k); ctx.fill(); }); }\n  ctx.fillStyle = \"#2a1a14\"; ctx.beginPath(); ctx.ellipse(70 * k, 5 * k, 6 * k, 4.5 * k, 0, 0, Math.PI * 2); ctx.fill();   // nose\n  ctx.fillStyle = \"#f6e27a\"; ctx.beginPath(); ctx.ellipse(38 * k, -8 * k, 8 * k, 7 * k, 0, 0, Math.PI * 2); ctx.fill();    // eye\n  ctx.fillStyle = \"#15100c\"; ctx.beginPath(); ctx.ellipse((39 + 2 * (1 - s.eyeUp)) * k, (-8 - 4 * s.eyeUp) * k, 3 * k, 5.5 * k, 0, 0, Math.PI * 2); ctx.fill();\n  ctx.fillStyle = dark; [[-0.5, 14], [0.1, 24], [0.6, 34]].forEach(([r, ex]) => { ctx.save(); ctx.translate(ex * k, -26 * k); ctx.rotate(r * 0.3); ctx.fillRect(-2.5 * k, 0, 5 * k, 15 * k); ctx.restore(); });\n  ctx.strokeStyle = \"rgba(255,248,236,0.85)\"; ctx.lineWidth = 1.4 * k; [-1, 0, 1].forEach((w) => { ctx.beginPath(); ctx.moveTo(60 * k, 12 * k); ctx.lineTo(96 * k, (8 + w * 9) * k); ctx.stroke(); });\n  ctx.restore();\n  legPair(false);\n}\n\nfunction drawDevil(ctx, x, y, k, p, t, alarmed, grin) {\n  ctx.save(); ctx.translate(x, y); ctx.scale(k, k);\n  ctx.strokeStyle = p.color; ctx.lineWidth = 5; ctx.lineCap = \"round\"; ctx.beginPath(); ctx.moveTo(-12, -14);                // tail with a spade\n  ctx.bezierCurveTo(-46, -10, -40, -56 - Math.sin(t * 5) * 6, -58, -62); ctx.stroke();\n  ctx.fillStyle = p.color; ctx.beginPath(); ctx.moveTo(-58, -76); ctx.lineTo(-48, -60); ctx.lineTo(-68, -60); ctx.closePath(); ctx.fill();\n  limb(ctx, -9, -8, -12, 0, 6, 5, p.color); limb(ctx, 9, -8, 12, 0, 6, 5, p.color);                                          // feet\n  ctx.beginPath(); ctx.ellipse(0, -30, 22, 26, 0, 0, Math.PI * 2); ctx.fill();                                               // body\n  ctx.beginPath(); ctx.ellipse(0, -66, 24, 21, 0, 0, Math.PI * 2); ctx.fill();                                               // head\n  ctx.fillStyle = p.horn; [[-1, -14], [1, 14]].forEach(([d, hx]) => { ctx.beginPath(); ctx.moveTo(hx - 6, -82); ctx.quadraticCurveTo(hx + d * 10, -104, hx + d * 4, -112); ctx.quadraticCurveTo(hx + d * 2, -92, hx + 6, -82); ctx.closePath(); ctx.fill(); });\n  const eye = 5 + alarmed * 4; ctx.fillStyle = \"#fff8ec\"; [-9, 9].forEach((ex) => { ctx.beginPath(); ctx.ellipse(ex, -68, eye, eye + 1, 0, 0, Math.PI * 2); ctx.fill(); });\n  ctx.fillStyle = \"#15100c\"; [-9, 9].forEach((ex) => { ctx.beginPath(); ctx.arc(ex - 2 + grin * 2, -67, 2.6, 0, Math.PI * 2); ctx.fill(); });\n  ctx.strokeStyle = \"#3a0d12\"; ctx.lineWidth = 2.5; ctx.beginPath();\n  if (grin > 0.1) { ctx.arc(0, -60, 10, 0.15 * Math.PI, 0.85 * Math.PI); } else { ctx.arc(0, -52 + alarmed * 2, 5 + alarmed * 2, 1.1 * Math.PI, 1.9 * Math.PI); } ctx.stroke();\n  ctx.strokeStyle = p.color; ctx.lineWidth = 5; ctx.beginPath(); ctx.moveTo(18, -36); ctx.lineTo(34, -52 - grin * 16 - Math.sin(t * 8) * grin * 8); ctx.stroke();   // the waving arm\n  ctx.beginPath(); ctx.moveTo(-18, -36); ctx.lineTo(-30, -24); ctx.stroke();\n  ctx.restore();\n}\n\nfunction puff(ctx, t, from, x, y, n, spread, color, seedBase) {\n  const u = (t - from) / 1.1; if (u < 0 || u > 1) return;\n  for (let i = 0; i < n; i++) { const a = hash(seedBase + i) * Math.PI * 2, r = spread * (0.3 + hash(seedBase + i + 50)) * (1 - Math.pow(1 - u, 2)), sz = (10 + hash(seedBase + i + 90) * 18) * (1 - u * 0.5);\n    ctx.globalAlpha = (1 - u) * 0.75; ctx.fillStyle = color; ctx.beginPath(); ctx.arc(x + Math.cos(a) * r, y + Math.sin(a) * r * 0.55 - u * 26, sz, 0, Math.PI * 2); ctx.fill(); }\n  ctx.globalAlpha = 1;\n}\n\nfunction grass(ctx, t, p, rows, seedBase) {\n  for (let i = 0; i < rows; i++) { const x = (i / rows) * (W + 60) - 30 + hash(seedBase + i) * 22, h = p.height * (0.55 + hash(seedBase + i + 300) * 0.7), sway = Math.sin(t * 1.4 + i * 0.7) * 7 * p.wind;\n    ctx.fillStyle = hash(seedBase + i + 600) > 0.5 ? p.color : p.tip; ctx.beginPath(); ctx.moveTo(x - 5, p.base); ctx.quadraticCurveTo(x + sway * 0.4, p.base - h * 0.6, x + sway, p.base - h); ctx.quadraticCurveTo(x + sway * 0.4 + 4, p.base - h * 0.5, x + 5, p.base); ctx.closePath(); ctx.fill(); }\n}\n\n// ---- WHAT IS ON SCREEN. Every thing a person could point at, in paint order. ---------------------\nconst TIGER = { x: 150, scale: 1, color: \"#e8872b\", shade: \"#b9621a\", belly: \"#f7e3c4\", stripe: \"#2a1a14\", inner: \"#f2b8a0\" };\nconst DEVIL = { x: 938, scale: 1, color: \"#d8323c\", horn: \"#f7e3c4\" };\nconst entities = [\n  { id: \"sky\", kind: \"background\", from: 0, to: 15, params: { top: \"#1b1442\", mid: \"#6a2c5f\", low: \"#f08a4b\" },\n    draw(ctx, t, p) { const g = ctx.createLinearGradient(0, 0, 0, G); g.addColorStop(0, p.top); g.addColorStop(0.62, p.mid); g.addColorStop(1, p.low); ctx.fillStyle = g; ctx.fillRect(0, 0, W, H); } },\n  { id: \"stars\", kind: \"weather\", from: 0, to: 15, params: { count: 70, color: \"#fff6dd\" },\n    draw(ctx, t, p) { for (let i = 0; i < p.count; i++) { ctx.globalAlpha = 0.35 + 0.5 * Math.abs(Math.sin(t * (0.6 + hash(i + 40)) + i)); ctx.fillStyle = p.color; ctx.beginPath(); ctx.arc(hash(i) * W, hash(i + 900) * G * 0.55, 0.8 + hash(i + 70) * 1.6, 0, Math.PI * 2); ctx.fill(); } ctx.globalAlpha = 1; } },\n  { id: \"moon\", kind: \"prop\", from: 0, to: 15, params: { x: 1010, y: 190, r: 74, color: \"#fdf1c9\", glow: \"rgba(253,241,201,0.16)\" },\n    draw(ctx, t, p) { ctx.fillStyle = p.glow; ctx.beginPath(); ctx.arc(p.x, p.y, p.r * 1.9, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.arc(p.x, p.y, p.r * 1.4, 0, Math.PI * 2); ctx.fill();\n      ctx.fillStyle = p.color; ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = \"rgba(200,170,120,0.25)\"; [[-22, -14, 15], [20, 18, 10], [8, -30, 7]].forEach(([dx, dy, r]) => { ctx.beginPath(); ctx.arc(p.x + dx, p.y + dy, r, 0, Math.PI * 2); ctx.fill(); }); },\n    bounds(t, p) { return [p.x - p.r, p.y - p.r, p.r * 2, p.r * 2]; } },\n  { id: \"hills\", kind: \"scenery\", from: 0, to: 15, params: { far: \"#3a1f4d\", near: \"#241338\", tree: \"#160b26\" },\n    draw(ctx, t, p) { ctx.fillStyle = p.far; ctx.beginPath(); ctx.moveTo(0, G); ctx.lineTo(0, 470); ctx.bezierCurveTo(260, 400, 420, 500, 640, 455); ctx.bezierCurveTo(860, 410, 1060, 490, W, 440); ctx.lineTo(W, G); ctx.fill();\n      ctx.fillStyle = p.near; ctx.beginPath(); ctx.moveTo(0, G); ctx.lineTo(0, 530); ctx.bezierCurveTo(300, 480, 520, 560, 800, 520); ctx.bezierCurveTo(1000, 495, 1160, 545, W, 515); ctx.lineTo(W, G); ctx.fill();\n      ctx.fillStyle = p.tree; [[210, 508, 1], [1130, 520, 0.8]].forEach(([x, y, k]) => { ctx.fillRect(x - 4 * k, y - 70 * k, 8 * k, 74 * k); ctx.beginPath(); ctx.ellipse(x, y - 78 * k, 64 * k, 17 * k, 0, 0, Math.PI * 2); ctx.fill(); ctx.beginPath(); ctx.ellipse(x + 26 * k, y - 94 * k, 40 * k, 12 * k, 0, 0, Math.PI * 2); ctx.fill(); }); } },\n  { id: \"ground\", kind: \"scenery\", from: 0, to: 15, params: { color: \"#1a0f2b\", rim: \"#f08a4b\" },\n    draw(ctx, t, p) { ctx.fillStyle = p.color; ctx.fillRect(0, G - 4, W, H - G + 4); ctx.globalAlpha = 0.35; ctx.fillStyle = p.rim; ctx.fillRect(0, G - 5, W, 2); ctx.globalAlpha = 1; } },\n  { id: \"grass-back\", kind: \"scenery\", from: 0, to: 15, params: { base: G + 2, height: 46, wind: 1, color: \"#2b1a40\", tip: \"#3d2556\" },\n    draw(ctx, t, p) { grass(ctx, t, p, 70, 1000); } },\n  { id: \"devil\", kind: \"character\", from: 0, to: 15, params: DEVIL,\n    draw(ctx, t, p) { const B = BEATS, gone = t >= B.land - 0.15 && t < B.ride - 0.05; if (gone) return;\n      if (t < B.land) { const alarmed = span(t, B.launch - 0.1, B.launch + 0.2), hop = Math.abs(Math.sin(t * 3.1)) * 10 * (1 - alarmed), shrink = 1 - span(t, B.land - 0.4, B.land - 0.15);\n        drawDevil(ctx, p.x, G - hop - alarmed * 6, p.scale * shrink, p, t, alarmed, 0); }\n      else { const s = hunt(t, TIGER), c = Math.cos(s.th), sn = Math.sin(s.th), lx = s.L * 0.42, ly = -44 * TIGER.scale, pop = span(t, B.ride - 0.05, B.ride + 0.3);\n        drawDevil(ctx, s.hx + lx * c - ly * sn, s.hy + lx * sn + ly * c + 2, p.scale * 0.8 * pop, p, t, 0, pop); } },\n    bounds(t, p) { return [p.x - 40 * p.scale, G - 120 * p.scale, 80 * p.scale, 120 * p.scale]; } },\n  { id: \"tiger\", kind: \"character\", from: 0, to: 15, params: TIGER,\n    draw(ctx, t, p) { drawTiger(ctx, t, p); },\n    bounds(t, p) { const s = hunt(t, p); return [s.hx - 110 * p.scale, s.hy - 90 * p.scale, s.L + 230 * p.scale, 215 * p.scale]; } },\n  { id: \"smoke\", kind: \"effect\", from: 8.3, to: 13.8, params: { color: \"#f3c9d0\" },\n    draw(ctx, t, p) { puff(ctx, t, BEATS.land - 0.4, DEVIL.x, G - 50, 14, 70, p.color, 200); const s = hunt(Math.max(t, BEATS.ride), TIGER); puff(ctx, t, BEATS.ride - 0.1, s.hx + s.L * 0.42, s.hy - 70, 12, 56, p.color, 400); } },\n  { id: \"dust\", kind: \"effect\", from: 8.6, to: 10.4, params: { color: \"#c9a27a\" },\n    draw(ctx, t, p) { puff(ctx, t, BEATS.land, TIGER.x + 640 + 230, G - 8, 18, 120, p.color, 700); puff(ctx, t, BEATS.land + 0.12, TIGER.x + 640 + 40, G - 8, 12, 80, p.color, 800); } },\n  { id: \"grass-front\", kind: \"scenery\", from: 0, to: 15, params: { base: H + 6, height: 92, wind: 1.4, color: \"#0e0719\", tip: \"#1c1030\" },\n    draw(ctx, t, p) { grass(ctx, t, p, 46, 3000); } },\n  { id: \"title\", kind: \"title\", from: 0.2, to: 3.0, params: { text: \"The Pounce\", x: 640, y: 120, size: 64, color: \"#fdf1c9\" },\n    draw(ctx, t, p) { ctx.globalAlpha = fade(t, 0.2, 3.0, 0.6); ctx.fillStyle = p.color; ctx.font = \"italic 600 \" + p.size + \"px Georgia, serif\"; ctx.textAlign = \"center\"; ctx.fillText(p.text, p.x, p.y); ctx.globalAlpha = 1; },\n    bounds(t, p) { return [p.x - p.size * 3, p.y - p.size, p.size * 6, p.size * 1.3]; } },\n];\n\n// ---- THE SCORE, on the same beats the picture uses (BEATS). Mixed into the MP4 by the engine. ----\nfunction compose(params) {\n  const B = BEATS, notes = [];\n  // A low, uneasy bed for the stalk: a minor chord that holds until the coil.\n  for (const m of [45, 52, 57]) notes.push({ inst: \"pad\", t: 0.2, midi: m, dur: B.coil - 0.2, vel: 0.28 });\n  // Soft paw-step plucks while it creeps, falling into silence as it stops.\n  for (let t = B.stalk + 0.3, i = 0; t < B.coil; t += 0.55, i++) notes.push({ inst: \"pluck\", t, midi: [57, 60, 57, 55][i % 4], dur: 0.3, vel: 0.35 });\n  // The coil: a rising tension of short notes, faster and higher.\n  for (let i = 0; i < 8; i++) notes.push({ inst: \"pluck\", t: B.coil + 0.15 + i * 0.16, midi: 60 + i, dur: 0.14, vel: 0.3 + i * 0.05 });\n  notes.push({ inst: \"whoosh\", t: B.launch, dur: 0.9, vel: 0.8 });                 // the leap\n  notes.push({ inst: \"kick\", t: B.land, dur: 0.35, vel: 0.95 });                   // the landing\n  notes.push({ inst: \"tom\", t: B.land + 0.12, dur: 0.3, vel: 0.6 });\n  for (let i = 0; i < 3; i++) notes.push({ inst: \"pop\", t: B.land - 0.35 + i * 0.1, dur: 0.12, vel: 0.5 });   // the devil vanishes in smoke\n  // The search: two questioning notes, unresolved.\n  notes.push({ inst: \"bell\", t: B.search + 0.4, midi: 69, dur: 0.8, vel: 0.35 }, { inst: \"bell\", t: B.search + 1.3, midi: 70, dur: 0.9, vel: 0.35 });\n  // The reveal: the devil pops up on the tiger's back — a bright resolving chord.\n  notes.push({ inst: \"pop\", t: B.ride - 0.1, dur: 0.12, vel: 0.6 });\n  for (const m of [60, 64, 67, 72]) notes.push({ inst: \"bell\", t: B.ride + 0.1, midi: m, dur: 1.8, vel: 0.32 });\n  for (const m of [48, 55, 60]) notes.push({ inst: \"pad\", t: B.ride + 0.1, midi: m, dur: 15 - B.ride - 0.2, vel: 0.24 });\n  return notes;\n}\n\n// draw() IS the loop over the declaration — nothing is drawn outside an entity.\nfunction draw(ctx, t, params, rng) {\n  for (const e of entities) {\n    if (t < e.from || t > e.to) continue;\n    ctx.save(); e.draw(ctx, t, e.params, rng.for(e.id)); ctx.restore();\n  }\n}\n","exportProfile":{"format":"mp4","resolution":"720p","fps":24,"durationSeconds":15},"source":{"prompt":"A tiger hunting a small red devil, as a fifteen-second drawn film: stalk, coil, launch, full extension, the strike, the landing — and the devil gets away. Every thing on screen is declared by name, so it can be selected, recoloured, hidden or moved without rewriting the program.","provider":"anthropic","model":"claude-fable-5-1","app":"gallery-reproduction"}},"license":"CC0-1.0"}