{"id":"pip-the-chick","title":"Pip","category":"Drawn film","prompt":"A 15 second animated film on warm cream paper: a speckled egg wobbles, cracks and a fluffy yellow chick pops out blinking. It waddles across snowy hills at dawn with pine trees, slips and slides down a slope on its belly, then the sky turns to night with green northern lights while it sits on a frozen pond watching. It ends with the handwritten title \"Pip\" writing itself on, the chick hopping in beside it, and a burst of snowflake confetti.","designedBy":{"provider":"google","model":"gemini-3.8-flash","inference":{"inputTokens":18575,"outputTokens":9919,"costUsd":0.1227}},"render":{"credits":8,"priceUsd":0.12,"format":"mp4","renderMs":32678,"frameCount":360,"bytes":2418102,"seed":1379261883,"width":1280,"height":720,"fps":30,"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=pip-the-chick), and it exports as MP4 or GIF, not as a still."},"renderRequest":{"drawContract":"const meta = { duration: 15, fps: 24, size: [1280, 720], seed: 104 };\n// ── design graph kit: linked by the server from what this film calls — do not edit ──\nvar kit = {};\n// pp:MotionBasics — linked from the design graph\nkit.clamp = function (v, a, b) { return v < a ? a : v > b ? b : v; };\n// Numbers, or COLOURS: models reach for lerp('#4a284e', '#321c38', u) to blend a sky, and got '#4a284eNaN'.\nkit.rgba = function (c) {\n  var s = String(c).trim(), m;\n  if ((m = /^#([0-9a-f]{3,8})$/i.exec(s))) { var h = m[1]; if (h.length <= 4) h = h.replace(/./g, '$&$&'); return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), h.length >= 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1]; }\n  if ((m = /^rgba?\\(([^)]+)\\)$/i.exec(s))) { var p = m[1].split(','); return [+p[0], +p[1], +p[2], p.length > 3 ? +p[3] : 1]; }\n  return null;\n};\nkit.lerp = function (a, b, u) {\n  if (typeof a === 'string' || typeof b === 'string') {\n    var ca = kit.rgba(a), cb = kit.rgba(b); if (!ca || !cb) return u < 0.5 ? a : b;\n    var k = function (i) { return ca[i] + (cb[i] - ca[i]) * u; };\n    return 'rgba(' + Math.round(k(0)) + ',' + Math.round(k(1)) + ',' + Math.round(k(2)) + ',' + Math.round(k(3) * 1000) / 1000 + ')';\n  }\n  return a + (b - a) * u;\n};\nkit.seg = function (t, a, b) { return b === a ? (t >= b ? 1 : 0) : kit.clamp((t - a) / (b - a), 0, 1); };\nkit.hash = function (n) { var s = Math.sin(n * 127.1 + 311.7) * 43758.5453; return s - Math.floor(s); };\nkit.ease = {\n  linear: function (u) { return u; },\n  \"in\": function (u) { return u * u * u; },\n  out: function (u) { return 1 - Math.pow(1 - u, 3); },\n  inOut: function (u) { return u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2; },\n  back: function (u) { var c = 1.7; return 1 + (c + 1) * Math.pow(u - 1, 3) + c * Math.pow(u - 1, 2); },\n  elastic: function (u) { return u === 0 || u === 1 ? u : Math.pow(2, -10 * u) * Math.sin((u * 10 - 0.75) * (Math.PI * 2 / 3)) + 1; }\n};\n// The film's own frame (meta.size), so a technique can size itself as a share of it — a composition rule, kept.\nkit.frame = function () { var m = typeof meta !== 'undefined' && meta && meta.size ? meta.size : [1280, 720]; return { w: Number(m[0]) || 1280, h: Number(m[1]) || 720 }; };\nkit.settle = function (u, freq, damp) { return Math.exp(-(damp || 5) * u) * Math.cos((freq || 3) * Math.PI * u); };\nkit.line = function (ctx, pts, w, col) {\n  ctx.beginPath();\n  for (var i = 0; i < pts.length; i++) { if (i) ctx.lineTo(pts[i][0], pts[i][1]); else ctx.moveTo(pts[i][0], pts[i][1]); }\n  ctx.lineWidth = w; ctx.strokeStyle = col; ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.stroke();\n};\n// pp:SquashOnImpact — linked from the design graph\nkit.squash = function (t, tImpact, o) {\n  o = o || {}; var amt = o.amount == null ? 0.4 : o.amount, dur = o.dur || 0.8;\n  if (t < tImpact) return { sx: 1, sy: 1 };\n  var w = kit.settle(kit.seg(t, tImpact, tImpact + dur), 3, 4.5) * amt;\n  return { sx: 1 + w, sy: 1 / (1 + w) };\n};\nkit.stretch = function (speed, amt) { var s = 1 + kit.clamp(speed, 0, 1) * (amt == null ? 0.3 : amt); return { sx: 1 / s, sy: s }; };\n// pp:PaperGround — linked from the design graph\nkit.paper = function (ctx, w, h, o) {\n  // overlay: true — fibre and vignette only, over a finished frame. Without it, paper is an opaque GROUND: a film that\n  // drew it last as \"paper_overlay\" covered its whole picture in cream (gemini-3.8-flash, 2026-10-04).\n  o = o || {}; var y0 = o.y || 0;\n  ctx.save(); if (!o.overlay) { ctx.fillStyle = o.color || '#f4ecd8'; ctx.fillRect(0, y0, w, h); }\n  var n = o.grain == null ? Math.round(w * h / 3500) : o.grain;\n  for (var i = 0; i < n; i++) { ctx.fillStyle = 'rgba(120,90,60,' + (0.05 + kit.hash(i + 77) * 0.08).toFixed(3) + ')'; ctx.fillRect(kit.hash(i) * w, y0 + kit.hash(i + 999) * h, 1.5 + kit.hash(i + 5) * 2, 1); }\n  if (o.vignette !== false) { var g = ctx.createRadialGradient(w / 2, y0 + h / 2, Math.min(w, h) * 0.3, w / 2, y0 + h / 2, Math.max(w, h) * 0.75); g.addColorStop(0, 'rgba(0,0,0,0)'); g.addColorStop(1, 'rgba(90,60,30,' + (o.vignette == null ? 0.16 : o.vignette) + ')'); ctx.fillStyle = g; ctx.fillRect(0, y0, w, h); }\n  ctx.restore();\n};\nkit.sky = function (ctx, w, h, colors, y0) {\n  y0 = y0 || 0; var g = ctx.createLinearGradient(0, y0, 0, y0 + h);\n  for (var i = 0; i < colors.length; i++) g.addColorStop(colors.length === 1 ? 0 : i / (colors.length - 1), colors[i]);\n  ctx.fillStyle = g; ctx.fillRect(0, y0, w, h); return g;\n};\n// pp:InkedLayers — linked from the design graph\nkit.hills = function (ctx, o) {\n  var w = o.w, h = o.h, y = o.y, amp = o.amp == null ? 30 : o.amp, f = o.freq || 0.005, ph = o.phase || 0, sc = o.scroll || 0, oy = o.offsetY || 0;\n  function crest(x) { var wx = x + sc; return oy + y + amp * Math.sin(wx * f + ph) + amp * 0.35 * Math.sin(wx * f * 2.7 + ph * 1.3); }\n  var pts = [];\n  for (var x = -20; x <= w + 20; x += 16) pts.push([x, crest(x) + (kit.hash(Math.floor((x + sc) / 16)) - 0.5) * 1.6]);\n  ctx.save();\n  if (o.shadow !== false) { ctx.shadowColor = 'rgba(60,40,20,0.22)'; ctx.shadowBlur = 14; ctx.shadowOffsetY = -4; }\n  ctx.beginPath(); ctx.moveTo(-20, oy + h + 40); for (var i = 0; i < pts.length; i++) ctx.lineTo(pts[i][0], pts[i][1]); ctx.lineTo(w + 20, oy + h + 40); ctx.closePath();\n  ctx.fillStyle = o.fill || '#93c47d'; ctx.fill(); ctx.restore();\n  if (o.hatch) {\n    ctx.save(); ctx.strokeStyle = o.hatch; ctx.lineWidth = 2;\n    for (var k = 0; k < (o.hatchCount || 26); k++) {\n      var hx = ((kit.hash(k * 3 + ph) * w * 1.6 - sc) % (w + 80) + w + 80) % (w + 80) - 40, hy = crest(hx) + 18 + kit.hash(k * 7 + ph) * 50;\n      ctx.beginPath(); ctx.moveTo(hx, hy); ctx.lineTo(hx + 10, hy - 7); ctx.stroke();\n    }\n    ctx.restore();\n  }\n  if (o.ink !== null) kit.line(ctx, pts, o.inkWidth || 2.6, o.ink || '#3a2c28');\n  return crest;\n};\n// pp:SkyElements — linked from the design graph\nkit.cloud = function (ctx, x, y, s, o) {\n  o = o || {}; var P = [[0, 0, 34], [32, -14, 40], [70, 0, 32], [36, 12, 30], [-26, 8, 24], [96, 10, 22]], i;\n  ctx.save();\n  // A faded cloud has no ink: outlined puffs cannot fade as one shape without their insides showing.\n  if (o.ink !== null && !(o.opacity != null && o.opacity < 1)) { ctx.lineWidth = 6; ctx.strokeStyle = o.ink || '#3a2c28'; for (i = 0; i < P.length; i++) { ctx.beginPath(); ctx.arc(x + P[i][0] * s, y + P[i][1] * s, P[i][2] * s, 0, Math.PI * 2); ctx.stroke(); } }\n  // ONE path for every puff, filled once: a translucent fill stays one even shape instead of showing every overlap.\n  ctx.beginPath(); for (i = 0; i < P.length; i++) { ctx.moveTo(x + (P[i][0] + P[i][2]) * s, y + P[i][1] * s); ctx.arc(x + P[i][0] * s, y + P[i][1] * s, P[i][2] * s, 0, Math.PI * 2); }\n  // The fill is drawn OPAQUE over the outline strokes, or every inner puff edge shows through it; a translucent\n  // rgba() fill keeps its colour and loses its alpha. Use opacity for a soft cloud: it fades the whole cloud as one.\n  if (o.opacity != null) ctx.globalAlpha = o.opacity;\n  ctx.fillStyle = String(o.fill || '#ffffff').replace(/^rgba\\(([^,]+),([^,]+),([^,]+),[^)]*\\)$/, 'rgb($1,$2,$3)'); ctx.fill('nonzero');\n  ctx.restore();\n};\nkit.sun = function (ctx, x, y, r, t, o) {\n  o = o || {}; t = t || 0; ctx.save(); ctx.translate(x, y); ctx.rotate(t * 0.5);\n  for (var i = 0; i < 12; i++) { ctx.rotate(Math.PI / 6); kit.line(ctx, [[r * 1.25, 0], [r * 1.7 + r * 0.12 * Math.sin(t * 4 + i), 0]], Math.max(2, r * 0.08), o.ray || '#e8a317'); }\n  ctx.restore();\n  ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fillStyle = o.fill || '#ffcd4a'; ctx.fill();\n  if (o.ink !== null) { ctx.lineWidth = 3; ctx.strokeStyle = o.ink || '#3a2c28'; ctx.stroke(); }\n};\nkit.moon = function (ctx, x, y, r, o) {\n  o = o || {}; ctx.save();\n  var g = ctx.createRadialGradient(x, y, r * 0.5, x, y, r * 2.8); g.addColorStop(0, 'rgba(255,240,190,0.45)'); g.addColorStop(1, 'rgba(255,240,190,0)');\n  ctx.fillStyle = g; ctx.fillRect(x - r * 3, y - r * 3, r * 6, r * 6);\n  ctx.beginPath(); ctx.rect(x - r * 2, y - r * 2, r * 4, r * 4); ctx.arc(x + r * 0.48, y - r * 0.26, r * 0.9, 0, Math.PI * 2, true); ctx.clip('evenodd');\n  ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fillStyle = o.fill || '#fff3c4'; ctx.fill();\n  ctx.lineWidth = 3; ctx.strokeStyle = o.edge || '#e8d38a'; ctx.stroke(); ctx.restore();\n};\nkit.stars = function (ctx, w, h, t, o) {\n  o = o || {}; var n = o.count || 140, seed = o.seed || 500, y0 = o.y || 0;\n  for (var i = 0; i < n; i++) {\n    var x = kit.hash(i + seed) * w, y = y0 + kit.hash(i + seed + 400) * h, tw = 0.5 + 0.5 * Math.sin(t * (2 + kit.hash(i) * 4) + i), r = 0.8 + kit.hash(i + 1) * 1.8;\n    ctx.fillStyle = 'rgba(255,248,220,' + (0.35 + 0.65 * tw).toFixed(3) + ')'; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();\n    if (i % 17 === 0) { var c = 'rgba(255,248,220,' + tw.toFixed(3) + ')'; kit.line(ctx, [[x - 8 * tw, y], [x + 8 * tw, y]], 1.5, c); kit.line(ctx, [[x, y - 8 * tw], [x, y + 8 * tw]], 1.5, c); }\n  }\n};\nkit.rain = function (ctx, w, h, t, o) {\n  o = o || {}; var n = o.count || 120, sp = o.speed || 900, y0 = o.y || 0, slant = o.slant == null ? 0.2 : o.slant;\n  ctx.save(); ctx.lineCap = 'round'; ctx.lineWidth = o.width || 2;\n  for (var i = 0; i < n; i++) {\n    var x = kit.hash(i + 200) * (w + 200) - 100, len = 30 + kit.hash(i + 300) * 50;\n    var y = y0 + ((kit.hash(i + 400) * (h + 200) + t * sp * (0.8 + kit.hash(i) * 0.6)) % (h + 200)) - 100;\n    ctx.strokeStyle = o.color || ('rgba(255,255,255,' + (0.25 + kit.hash(i + 50) * 0.4).toFixed(3) + ')');\n    ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x - len * slant, y + len); ctx.stroke();\n  }\n  ctx.restore();\n};\n// pp:FaceOnAnything — linked from the design graph\n// THE HERO OF ANY SHAPE, at hero size. body(ctx, px) draws the body in a UNIT box (x and y from -1 to 1, feet at y = 1;\n// px = one screen pixel in those units, for line widths). The graph sizes it as a share of the frame (never a dot),\n// squashes it about its feet, puts a contact shadow under it and a face on it.\nkit.hero = function (ctx, x, feetY, o, body) {\n  o = o || {}; var H = kit.frame().h, size = Math.max(o.far ? 0 : 0.22, o.size || 0.28), R = size * H / 2, sx = o.sx || 1, sy = o.sy || 1;\n  ctx.save(); ctx.translate(x, feetY);\n  if (o.shadow !== false) { ctx.save(); ctx.scale(sx, 0.2); ctx.beginPath(); ctx.arc(0, 0, R * 0.9, 0, Math.PI * 2); ctx.fillStyle = 'rgba(60,30,20,0.18)'; ctx.fill(); ctx.restore(); }\n  ctx.scale(sx, sy); ctx.translate(0, -R); if (o.rot) ctx.rotate(o.rot); ctx.scale(R, R);\n  ctx.lineJoin = 'round'; ctx.lineCap = 'round';\n  if (typeof body === 'function') body(ctx, 1 / R);\n  ctx.restore();\n  if (o.face !== false) {\n    var f = o.faceAt || [0, -0.1], fx = x + f[0] * R * sx, fy = feetY - R * sy + f[1] * R * sy;\n    kit.face(ctx, fx, fy, { size: (o.faceSize || 0.5) * size, eyes: o.eyes, look: o.look, blink: o.blink, mouth: o.mouth, blush: o.blush, ink: o.ink });\n  }\n};\nkit.face = function (ctx, cx, cy, o) {\n  o = o || {}; var W = (o.size || 0.12) * kit.frame().h, ink = o.ink || '#3a2c28', eyes = o.eyes == null ? 1 : o.eyes, look = o.look || [0, 0], er = W * 0.2 * eyes;\n  ctx.save(); ctx.translate(cx, cy);\n  if (er > 0.5) {\n    for (var side = -1; side <= 1; side += 2) {\n      ctx.save(); ctx.translate(side * W * 0.27, -W * 0.08); ctx.scale(1, o.blink ? 0.12 : 1);\n      ctx.beginPath(); ctx.arc(0, 0, er, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill(); ctx.lineWidth = Math.max(2, W * 0.03); ctx.strokeStyle = ink; ctx.stroke();\n      ctx.beginPath(); ctx.arc(look[0] * er * 0.4, look[1] * er * 0.4, er * 0.5, 0, Math.PI * 2); ctx.fillStyle = ink; ctx.fill();\n      ctx.beginPath(); ctx.arc(look[0] * er * 0.4 - er * 0.16, look[1] * er * 0.4 - er * 0.18, er * 0.15, 0, Math.PI * 2); ctx.fillStyle = '#fff'; ctx.fill();\n      ctx.restore();\n    }\n  }\n  if (o.blush) { ctx.fillStyle = 'rgba(255,120,120,0.35)'; ctx.beginPath(); ctx.ellipse(-W * 0.42, W * 0.14, W * 0.1, W * 0.06, 0, 0, Math.PI * 2); ctx.ellipse(W * 0.42, W * 0.14, W * 0.1, W * 0.06, 0, 0, Math.PI * 2); ctx.fill(); }\n  ctx.lineWidth = Math.max(2, W * 0.035); ctx.strokeStyle = ink; ctx.lineCap = 'round';\n  if (o.mouth === 'smile') { ctx.beginPath(); ctx.arc(0, W * 0.12, W * 0.16, 0.15 * Math.PI, 0.85 * Math.PI); ctx.stroke(); }\n  else if (o.mouth === 'o') { ctx.beginPath(); ctx.ellipse(0, W * 0.22, W * 0.06, W * 0.08, 0, 0, Math.PI * 2); ctx.fillStyle = ink; ctx.fill(); }\n  else if (o.mouth === 'flat') { ctx.beginPath(); ctx.moveTo(-W * 0.1, W * 0.22); ctx.lineTo(W * 0.1, W * 0.22); ctx.stroke(); }\n  ctx.restore();\n};\n// pp:WriteOnTitle — linked from the design graph\nkit.writeOn = function (ctx, text, x, y, o) {\n  o = o || {}; ctx.save(); ctx.textBaseline = 'alphabetic';\n  if (o.width) { ctx.font = '100px ' + (o.family || 'cursive'); var px = 100 * o.width * kit.frame().w / Math.max(1, ctx.measureText(text).width); ctx.font = (o.weight ? o.weight + ' ' : '') + px.toFixed(1) + 'px ' + (o.family || 'cursive'); }\n  else ctx.font = o.font || '120px cursive';\n  var w = ctx.measureText(text).width, x0 = x - w / 2, u = kit.clamp(o.u == null ? 1 : o.u, 0, 1), edge = x0 - 10 + u * (w + 30);\n  ctx.save(); ctx.beginPath(); ctx.rect(x0 - 40, y - w - 400, edge - x0 + 40, w + 800); ctx.clip();\n  ctx.fillStyle = o.color || '#3a2c28'; ctx.fillText(text, x0, y); ctx.restore();\n  if (o.nib !== false && u > 0 && u < 1) { ctx.beginPath(); ctx.arc(edge, y - parseFloat(ctx.font.replace(/^[^0-9]*/, '')) * 0.3, 5, 0, Math.PI * 2); ctx.fillStyle = o.nibColor || '#ec5b3c'; ctx.fill(); }\n  ctx.restore(); return w;\n};\n// pp:CelebrationHit — linked from the design graph\nkit.confetti = function (ctx, x, y, age, o) {\n  if (age < 0) return; o = o || {}; var n = o.count || 90, cols = o.colors || ['#ec5b3c', '#ffcd4a', '#5aa9a0', '#b48cff', '#3a2c28'], spread = o.spread || 1, grav = o.gravity == null ? 140 : o.gravity;\n  for (var i = 0; i < n; i++) {\n    var a = kit.hash(i + 10) * Math.PI * 2, sp = (260 + kit.hash(i + 20) * 520) * spread, k = age * Math.exp(-age * 0.9);\n    var px = x + Math.cos(a) * sp * k, py = y + Math.sin(a) * sp * k + grav * age * age, fade = kit.clamp(1.6 - age * 0.55, 0, 1);\n    if (fade <= 0) continue;\n    ctx.save(); ctx.translate(px, py); ctx.rotate(age * (4 + kit.hash(i) * 8) + i); ctx.globalAlpha = fade;\n    ctx.fillStyle = cols[i % cols.length]; ctx.fillRect(-5, -2.5, 10 + kit.hash(i + 3) * 6, 5); ctx.restore();\n  }\n};\nkit.burst = function (ctx, x, y, age, o) {\n  if (age < 0 || age > 0.4) return; o = o || {}; var n = o.rays || 10, len = (o.len || 60) * (1 - age / 0.4), r0 = (o.r || 40) + age * 260;\n  ctx.save(); ctx.globalAlpha = 1 - age / 0.4;\n  for (var i = 0; i < n; i++) { var a = i / n * Math.PI * 2 + 0.3; kit.line(ctx, [[x + Math.cos(a) * r0, y + Math.sin(a) * r0], [x + Math.cos(a) * (r0 + len), y + Math.sin(a) * (r0 + len)]], o.width || 4, o.color || '#3a2c28'); }\n  ctx.restore();\n};\n// ── end of design graph kit ──\n\n// Shared state & helper positioning across entities\nconst SCENE = {\n  hillY: (x, scroll = 0) => 530 + Math.sin((x + scroll) * 0.005) * 45 + Math.cos((x + scroll) * 0.012) * 18,\n  pondY: 535,\n};\n\n// Procedural pine tree helper\nfunction drawPine(ctx, x, y, s, dark = '#233d32', snow = '#f0f5f5') {\n  ctx.save();\n  ctx.translate(x, y);\n  ctx.scale(s, s);\n\n  // Trunk\n  ctx.fillStyle = '#4a3328';\n  ctx.fillRect(-7, 0, 14, 28);\n\n  // Foliage tiers (bottom to top)\n  const tiers = [\n    { w: 75, h: 50, y: 0 },\n    { w: 60, h: 44, y: -30 },\n    { w: 42, h: 38, y: -58 },\n  ];\n\n  for (const t of tiers) {\n    ctx.beginPath();\n    ctx.moveTo(0, t.y - t.h);\n    ctx.lineTo(t.w / 2, t.y);\n    ctx.lineTo(-t.w / 2, t.y);\n    ctx.closePath();\n    ctx.fillStyle = dark;\n    ctx.fill();\n    ctx.lineWidth = 3;\n    ctx.strokeStyle = '#182921';\n    ctx.stroke();\n\n    // Snow cap on tier\n    ctx.beginPath();\n    ctx.moveTo(-t.w / 2 + 5, t.y - 2);\n    ctx.quadraticCurveTo(0, t.y + 7, t.w / 2 - 5, t.y - 2);\n    ctx.lineTo(t.w / 2 - 10, t.y - 10);\n    ctx.quadraticCurveTo(0, t.y - 4, -t.w / 2 + 10, t.y - 10);\n    ctx.closePath();\n    ctx.fillStyle = snow;\n    ctx.fill();\n  }\n  ctx.restore();\n}\n\n// Custom Fluffy Chick body renderer for kit.hero\nfunction drawChickBody(ctx, px, opts = {}) {\n  const { wingAngle = 0, hasShell = false, bellySlide = false } = opts;\n\n  ctx.save();\n  if (bellySlide) {\n    ctx.rotate(Math.PI * 0.45);\n    ctx.scale(1.15, 0.85);\n  }\n\n  // Little tail tuft\n  ctx.beginPath();\n  ctx.moveTo(-0.65, 0.2);\n  ctx.quadraticCurveTo(-0.95, 0.05, -0.85, -0.15);\n  ctx.quadraticCurveTo(-0.7, 0.0, -0.55, 0.05);\n  ctx.fillStyle = '#f5c518';\n  ctx.fill();\n  ctx.lineWidth = 3 * px;\n  ctx.strokeStyle = '#2b2118';\n  ctx.stroke();\n\n  // Round fluffy body\n  ctx.beginPath();\n  ctx.arc(0, 0.1, 0.65, 0, Math.PI * 2);\n  ctx.fillStyle = '#fed636';\n  ctx.fill();\n  ctx.lineWidth = 4 * px;\n  ctx.strokeStyle = '#2b2118';\n  ctx.stroke();\n\n  // Head tuft\n  ctx.beginPath();\n  ctx.moveTo(-0.1, -0.55);\n  ctx.quadraticCurveTo(-0.15, -0.9, 0.05, -0.85);\n  ctx.quadraticCurveTo(0.15, -0.92, 0.18, -0.6);\n  ctx.fillStyle = '#fed636';\n  ctx.fill();\n  ctx.stroke();\n\n  // Fluffy tummy highlight\n  ctx.beginPath();\n  ctx.arc(0.08, 0.22, 0.42, 0, Math.PI * 2);\n  ctx.fillStyle = '#fff089';\n  ctx.fill();\n\n  // Wing\n  ctx.save();\n  ctx.translate(-0.15, 0.12);\n  ctx.rotate(wingAngle);\n  ctx.beginPath();\n  ctx.ellipse(0, 0, 0.35, 0.22, 0.3, 0, Math.PI * 2);\n  ctx.fillStyle = '#f3be11';\n  ctx.fill();\n  ctx.lineWidth = 3.5 * px;\n  ctx.strokeStyle = '#2b2118';\n  ctx.stroke();\n  ctx.restore();\n\n  // Beak\n  ctx.beginPath();\n  ctx.moveTo(0.35, -0.06);\n  ctx.lineTo(0.72, 0.02);\n  ctx.lineTo(0.35, 0.14);\n  ctx.closePath();\n  ctx.fillStyle = '#f87219';\n  ctx.fill();\n  ctx.lineWidth = 3 * px;\n  ctx.strokeStyle = '#2b2118';\n  ctx.stroke();\n\n  // Egg shell cap if recently hatched\n  if (hasShell) {\n    ctx.beginPath();\n    ctx.arc(0, -0.45, 0.38, Math.PI, 0, false);\n    ctx.lineTo(0.35, -0.35);\n    ctx.lineTo(0.18, -0.45);\n    ctx.lineTo(0.0, -0.32);\n    ctx.lineTo(-0.18, -0.46);\n    ctx.lineTo(-0.35, -0.36);\n    ctx.closePath();\n    ctx.fillStyle = '#f7eedd';\n    ctx.fill();\n    ctx.lineWidth = 3 * px;\n    ctx.strokeStyle = '#2b2118';\n    ctx.stroke();\n\n    // Speckles on shell\n    ctx.fillStyle = '#a68065';\n    for (const [sx, sy] of [[-0.15, -0.55], [0.1, -0.62], [0.22, -0.48]]) {\n      ctx.beginPath();\n      ctx.arc(sx, sy, 0.03, 0, Math.PI * 2);\n      ctx.fill();\n    }\n  }\n\n  // Little orange feet (if not sliding)\n  if (!bellySlide) {\n    ctx.fillStyle = '#f87219';\n    for (const fx of [-0.2, 0.18]) {\n      ctx.beginPath();\n      ctx.ellipse(fx, 0.72, 0.12, 0.05, 0, 0, Math.PI * 2);\n      ctx.fill();\n      ctx.lineWidth = 2.5 * px;\n      ctx.strokeStyle = '#2b2118';\n      ctx.stroke();\n    }\n  }\n  ctx.restore();\n}\n\nconst entities = [\n  // 1. PAPER TEXTURE CANVAS BASE\n  {\n    id: 'paper_bg',\n    kind: 'background',\n    from: 0,\n    to: 15,\n    params: { color: '#f7f1e3' },\n    draw(ctx, t, p) {\n      kit.paper(ctx, 1280, 720, { color: p.color, vignette: 0.18 });\n    },\n  },\n\n  // 2. ACT 1: NEST & HATCHING EGG (0.0s - 3.8s)\n  {\n    id: 'act1_hatch_scene',\n    kind: 'prop',\n    from: 0,\n    to: 3.8,\n    params: { x: 640, y: 480 },\n    draw(ctx, t, p, rng) {\n      const alpha = 1 - kit.ease.in(kit.seg(t, 3.4, 3.8));\n      ctx.globalAlpha = Math.max(0, alpha);\n\n      // Warm nursery glow background\n      const grad = ctx.createRadialGradient(p.x, p.y - 40, 20, p.x, p.y - 40, 420);\n      grad.addColorStop(0, 'rgba(255, 243, 214, 0.9)');\n      grad.addColorStop(1, 'rgba(247, 235, 210, 0)');\n      ctx.fillStyle = grad;\n      ctx.fillRect(0, 0, 1280, 720);\n\n      // Twig nest\n      ctx.save();\n      ctx.translate(p.x, p.y + 40);\n      ctx.lineWidth = 5;\n      ctx.strokeStyle = '#6a472c';\n      for (let i = 0; i < 28; i++) {\n        const ang = (i / 28) * Math.PI + 0.1;\n        const r1 = 120 + ((i * 19) % 25);\n        const r2 = 140 + ((i * 37) % 30);\n        ctx.beginPath();\n        ctx.moveTo(Math.cos(ang) * -r1, Math.sin(ang) * 45);\n        ctx.quadraticCurveTo(0, 75 + (i % 5) * 4, Math.cos(ang) * r2, Math.sin(ang) * 45);\n        ctx.strokeStyle = i % 2 === 0 ? '#543621' : '#7c5535';\n        ctx.stroke();\n      }\n      ctx.restore();\n\n      // Egg or Hatching fragments\n      const crackProg = kit.seg(t, 1.2, 1.8);\n      const wobble = t < 1.3 ? Math.sin(t * 18) * (t < 0.8 ? 5 : 16) * Math.min(1, t * 1.5) : 0;\n\n      if (crackProg < 0.8) {\n        ctx.save();\n        ctx.translate(p.x, p.y);\n        ctx.rotate((wobble * Math.PI) / 180);\n\n        // Intact / cracking egg\n        ctx.beginPath();\n        ctx.ellipse(0, -30, 75, 105, 0, 0, Math.PI * 2);\n        ctx.fillStyle = '#f7eedd';\n        ctx.fill();\n        ctx.lineWidth = 5;\n        ctx.strokeStyle = '#2b2118';\n        ctx.stroke();\n\n        // Speckles\n        ctx.fillStyle = '#a68065';\n        const spots = [[-25, -60], [15, -75], [30, -30], [-35, -10], [5, 10], [-10, -40]];\n        for (const [sx, sy] of spots) {\n          ctx.beginPath();\n          ctx.arc(sx, sy, 5, 0, Math.PI * 2);\n          ctx.fill();\n        }\n\n        // Cracks appearing\n        if (t > 0.9) {\n          const cLen = kit.seg(t, 0.9, 1.3);\n          ctx.beginPath();\n          ctx.moveTo(-50 * cLen, -25);\n          ctx.lineTo(-20 * cLen, -35);\n          ctx.lineTo(10 * cLen, -20);\n          ctx.lineTo(35 * cLen, -40);\n          ctx.lineTo(55 * cLen, -28);\n          ctx.lineWidth = 4;\n          ctx.strokeStyle = '#2b2118';\n          ctx.stroke();\n        }\n        ctx.restore();\n      } else {\n        // Bottom shell cup\n        ctx.save();\n        ctx.translate(p.x, p.y);\n        ctx.beginPath();\n        ctx.arc(0, 10, 75, 0.1, Math.PI - 0.1, false);\n        ctx.lineTo(-70, 0);\n        ctx.lineTo(-45, 15);\n        ctx.lineTo(-20, -5);\n        ctx.lineTo(10, 18);\n        ctx.lineTo(40, -2);\n        ctx.lineTo(70, 12);\n        ctx.closePath();\n        ctx.fillStyle = '#f7eedd';\n        ctx.fill();\n        ctx.lineWidth = 4.5;\n        ctx.strokeStyle = '#2b2118';\n        ctx.stroke();\n        ctx.restore();\n      }\n    },\n  },\n\n  // 3. CHICK IN NEST POPPING OUT & BLINKING (1.4s - 3.8s)\n  {\n    id: 'chick_hatch',\n    kind: 'character',\n    from: 1.4,\n    to: 3.8,\n    params: { x: 640, y: 440, size: 0.32 },\n    draw(ctx, t, p) {\n      const alpha = 1 - kit.ease.in(kit.seg(t, 3.4, 3.8));\n      ctx.globalAlpha = Math.max(0, alpha);\n\n      const popU = kit.ease.back(kit.seg(t, 1.4, 1.95));\n      const chickY = p.y + (1 - popU) * 60;\n      const s = kit.squash(t, 1.85, { amount: 0.35, dur: 0.6 });\n\n      const eyesOpen = kit.ease.out(kit.seg(t, 1.95, 2.5));\n      const lookX = Math.sin(t * 3) * 0.4;\n      const hasShell = t < 2.9;\n\n      kit.hero(\n        ctx,\n        p.x,\n        chickY,\n        {\n          size: p.size,\n          sx: s.sx,\n          sy: s.sy,\n          eyes: eyesOpen,\n          look: [lookX, -0.1],\n          mouth: t > 2.2 ? 'smile' : 'o',\n          blush: true,\n          faceAt: [0.1, -0.05],\n        },\n        (c, px) => {\n          drawChickBody(c, px, { wingAngle: Math.sin(t * 12) * 0.25, hasShell });\n        }\n      );\n\n      // Hatching spark lines\n      if (t >= 1.6 && t <= 2.2) {\n        kit.burst(ctx, p.x, p.y - 70, t - 1.6, { rays: 8 });\n      }\n    },\n  },\n\n  // 4. ACT 2 & 3: SNOWY DAWN TO FROZEN SLOPE (3.4s - 10.5s)\n  {\n    id: 'snow_hills_dawn',\n    kind: 'background',\n    from: 3.4,\n    to: 10.5,\n    params: {},\n    draw(ctx, t, p) {\n      const enterAlpha = kit.ease.out(kit.seg(t, 3.4, 4.0));\n      const exitAlpha = 1 - kit.ease.in(kit.seg(t, 9.8, 10.5));\n      ctx.globalAlpha = enterAlpha * exitAlpha;\n\n      // Dawn sky gradient: peach to lavender to soft cream\n      const skyGrad = ctx.createLinearGradient(0, 0, 0, 500);\n      skyGrad.addColorStop(0, '#ebd4c9');\n      skyGrad.addColorStop(0.5, '#f7dbb6');\n      skyGrad.addColorStop(1, '#fffaeb');\n      ctx.fillStyle = skyGrad;\n      ctx.fillRect(0, 0, 1280, 720);\n\n      // Soft rising sun\n      kit.sun(ctx, 960, 220, 55, t);\n\n      // Distant mountain ridge\n      ctx.beginPath();\n      ctx.moveTo(0, 420);\n      ctx.lineTo(240, 310);\n      ctx.lineTo(520, 380);\n      ctx.lineTo(840, 290);\n      ctx.lineTo(1120, 360);\n      ctx.lineTo(1280, 320);\n      ctx.lineTo(1280, 720);\n      ctx.lineTo(0, 720);\n      ctx.closePath();\n      ctx.fillStyle = '#dfd6de';\n      ctx.fill();\n\n      // Distant small pine trees\n      drawPine(ctx, 220, 380, 0.45, '#3a5448', '#e8edf0');\n      drawPine(ctx, 260, 395, 0.38, '#3a5448', '#e8edf0');\n      drawPine(ctx, 780, 365, 0.5, '#3a5448', '#e8edf0');\n      drawPine(ctx, 830, 380, 0.42, '#3a5448', '#e8edf0');\n\n      // Midground rolling snowy hill with parallax scroll\n      const scroll1 = (t - 3.4) * 85;\n      kit.hills(ctx, {\n        w: 1280,\n        h: 720,\n        y: 520,\n        amp: 42,\n        freq: 0.0035,\n        phase: 1.5,\n        scroll: scroll1 * 0.4,\n        fill: '#e3ebf0',\n        ink: '#27333d',\n        hatch: 'rgba(165, 185, 200, 0.4)',\n      });\n\n      // Pine trees along midground ridge\n      const treePos = [140, 480, 890, 1220];\n      for (const tx of treePos) {\n        const sx = ((tx - scroll1 * 0.4) % 1360) - 40;\n        const sy = 480 + Math.sin((sx + scroll1 * 0.4) * 0.0035 + 1.5) * 42;\n        drawPine(ctx, sx, sy, 0.8, '#264235', '#f8fbff');\n      }\n\n      // Foreground main snowy slope (steeper during slide 7.5 - 10.2)\n      const slideSteep = kit.ease.inOut(kit.seg(t, 7.2, 8.2)) * 60;\n      kit.hills(ctx, {\n        w: 1280,\n        h: 720,\n        y: 570 + slideSteep,\n        amp: 36,\n        freq: 0.0045,\n        phase: 2.8,\n        scroll: scroll1 * (t > 7.4 ? 1.8 : 0.9),\n        fill: '#fbfdfe',\n        ink: '#27333d',\n        hatch: 'rgba(180, 205, 220, 0.3)',\n      });\n    },\n  },\n\n  // 5. CHICK WADDLING THEN SLIPPING & BELLY SLIDING (3.4s - 10.4s)\n  {\n    id: 'chick_waddle_and_slide',\n    kind: 'character',\n    from: 3.4,\n    to: 10.4,\n    params: { size: 0.26 },\n    draw(ctx, t, p) {\n      const enterAlpha = kit.ease.out(kit.seg(t, 3.4, 3.8));\n      const exitAlpha = 1 - kit.ease.in(kit.seg(t, 9.8, 10.4));\n      ctx.globalAlpha = enterAlpha * exitAlpha;\n\n      const isSlide = t >= 7.3;\n\n      let x, y, rot = 0, sx = 1, sy = 1, mouth = 'smile', wing = 0;\n\n      if (!isSlide) {\n        // Waddle phase across snowy hills\n        const u = kit.seg(t, 3.4, 7.3);\n        x = 180 + u * 450;\n        const bob = Math.abs(Math.sin((t - 3.4) * 8));\n        y = 555 - bob * 16;\n        rot = Math.sin((t - 3.4) * 8) * 0.14;\n        wing = Math.sin((t - 3.4) * 14) * 0.3;\n        mouth = 'smile';\n      } else {\n        // Slip (anticipation wobble 7.2 - 7.6) and joyful belly slide (7.6 - 10.2)\n        const slipU = kit.seg(t, 7.3, 7.65);\n        if (slipU < 1) {\n          x = 630 + slipU * 30;\n          y = 555 - Math.sin(slipU * Math.PI) * 22;\n          rot = -slipU * 0.5; // tips back in surprise\n          mouth = 'o';\n          wing = 0.6;\n        } else {\n          // Slide down icy slope\n          const slideU = kit.ease.in(kit.seg(t, 7.65, 10.0));\n          x = 660 + slideU * 520;\n          y = 570 + slideU * 70 + Math.sin(slideU * 16) * 5;\n          rot = 0.28;\n          mouth = 'o';\n          wing = -0.5;\n\n          // Snow spray particles trailing behind belly\n          ctx.save();\n          ctx.fillStyle = '#ffffff';\n          ctx.strokeStyle = '#c9dde8';\n          ctx.lineWidth = 1.5;\n          for (let sp = 0; sp < 12; sp++) {\n            const px = x - 40 - sp * 14 - ((t * 80 + sp * 23) % 40);\n            const py = y + 15 + Math.sin(t * 15 + sp) * 12;\n            const pr = 3 + (sp % 4);\n            ctx.beginPath();\n            ctx.arc(px, py, pr, 0, Math.PI * 2);\n            ctx.fill();\n            ctx.stroke();\n          }\n          ctx.restore();\n        }\n      }\n\n      kit.hero(\n        ctx,\n        x,\n        y,\n        {\n          size: p.size,\n          rot,\n          sx,\n          sy,\n          eyes: isSlide && t < 7.7 ? 1.0 : 0.9,\n          look: [0.6, 0.1],\n          mouth,\n          blush: true,\n          faceAt: [0.12, -0.08],\n        },\n        (c, px) => {\n          drawChickBody(c, px, {\n            wingAngle: wing,\n            bellySlide: isSlide && t >= 7.6,\n          });\n        }\n      );\n    },\n  },\n\n  // 6. ACT 4: NORTHERN LIGHTS OVER FROZEN POND (10.0s - 15.0s)\n  {\n    id: 'aurora_night_pond',\n    kind: 'background',\n    from: 10.0,\n    to: 15.0,\n    params: {},\n    draw(ctx, t, p) {\n      const enterAlpha = kit.ease.inOut(kit.seg(t, 10.0, 10.8));\n      ctx.globalAlpha = enterAlpha;\n\n      // Deep arctic twilight night sky\n      const nightGrad = ctx.createLinearGradient(0, 0, 0, 520);\n      nightGrad.addColorStop(0, '#0a1626');\n      nightGrad.addColorStop(0.5, '#122d3e');\n      nightGrad.addColorStop(1, '#1b3b4b');\n      ctx.fillStyle = nightGrad;\n      ctx.fillRect(0, 0, 1280, 720);\n\n      // Twinkling stars\n      kit.stars(ctx, 1280, 360, t, { count: 48, y: 15 });\n\n      // Crescent Moon\n      kit.moon(ctx, 1080, 110, 32);\n\n      // Green & Teal Northern Lights (Aurora Borealis) curtains\n      ctx.save();\n      ctx.globalCompositeOperation = 'screen';\n      for (let layer = 0; layer < 3; layer++) {\n        const color = layer === 1 ? 'rgba(74, 236, 178, 0.42)' : 'rgba(46, 218, 142, 0.32)';\n        ctx.fillStyle = color;\n\n        ctx.beginPath();\n        const baseAy = 120 + layer * 35;\n        ctx.moveTo(0, baseAy);\n\n        for (let x = 0; x <= 1280; x += 40) {\n          const wave = Math.sin((x * 0.005) + (t * 1.4) + layer) * 55 +\n                       Math.cos((x * 0.009) - (t * 0.9)) * 30;\n          ctx.lineTo(x, baseAy + wave);\n        }\n        ctx.lineTo(1280, baseAy + 160);\n        for (let x = 1280; x >= 0; x -= 40) {\n          const wave = Math.sin((x * 0.005) + (t * 1.4) + layer) * 55;\n          ctx.lineTo(x, baseAy + wave + 140);\n        }\n        ctx.closePath();\n        ctx.fill();\n      }\n      ctx.restore();\n\n      // Silhouetted snowy pines on horizon\n      drawPine(ctx, 120, 500, 0.95, '#0d1d18', '#8ba89f');\n      drawPine(ctx, 180, 515, 0.75, '#0d1d18', '#8ba89f');\n      drawPine(ctx, 230, 525, 0.6, '#0d1d18', '#8ba89f');\n      drawPine(ctx, 1080, 505, 0.9, '#0d1d18', '#8ba89f');\n      drawPine(ctx, 1140, 520, 0.7, '#0d1d18', '#8ba89f');\n\n      // Frozen Ice Pond\n      const iceGrad = ctx.createLinearGradient(0, 510, 0, 720);\n      iceGrad.addColorStop(0, '#6fa8b6');\n      iceGrad.addColorStop(0.3, '#4d8396');\n      iceGrad.addColorStop(1, '#2c596d');\n\n      ctx.beginPath();\n      ctx.ellipse(640, 610, 620, 115, 0, 0, Math.PI * 2);\n      ctx.fillStyle = iceGrad;\n      ctx.fill();\n      ctx.lineWidth = 4;\n      ctx.strokeStyle = '#203c4a';\n      ctx.stroke();\n\n      // Ice reflection & sheen lines\n      ctx.save();\n      ctx.strokeStyle = 'rgba(168, 230, 218, 0.45)';\n      ctx.lineWidth = 2.5;\n      for (const [ix, iy, len] of [[420, 580, 130], [670, 610, 190], [530, 640, 100], [820, 575, 140]]) {\n        ctx.beginPath();\n        ctx.moveTo(ix, iy);\n        ctx.lineTo(ix + len, iy);\n        ctx.stroke();\n      }\n      ctx.restore();\n    },\n  },\n\n  // 7. CHICK SITTING ON POND WATCHING AURORA THEN HOPPING FOR TITLE (10.4s - 15.0s)\n  {\n    id: 'chick_stargaze_and_hop',\n    kind: 'character',\n    from: 10.4,\n    to: 15.0,\n    params: { size: 0.28 },\n    draw(ctx, t, p) {\n      const enterAlpha = kit.ease.out(kit.seg(t, 10.4, 10.9));\n      ctx.globalAlpha = enterAlpha;\n\n      let x, y, sx = 1, sy = 1, eyes = 1, look = [0.2, -0.6], mouth = 'o', rot = 0;\n\n      if (t < 13.0) {\n        // Cozy sitting on ice looking up in wonder at aurora\n        x = 510;\n        y = 575;\n        rot = -0.1 + Math.sin(t * 1.5) * 0.05;\n        look = [0.1, -0.7]; // gazing up\n        mouth = 'o';\n      } else {\n        // Hop in beside title \"Pip\"\n        const hopU = kit.ease.back(kit.seg(t, 13.0, 13.7));\n        x = 510 + hopU * 270; // lands around x = 780\n        const hopArc = Math.sin(kit.seg(t, 13.0, 13.7) * Math.PI) * 70;\n        y = 575 - hopArc;\n\n        if (t >= 13.7) {\n          const s = kit.squash(t, 13.7, { amount: 0.35, dur: 0.6 });\n          sx = s.sx;\n          sy = s.sy;\n          rot = Math.sin(t * 12) * 0.08;\n          mouth = 'smile';\n          look = [-0.4, -0.1]; // smiling at the title\n        }\n      }\n\n      kit.hero(\n        ctx,\n        x,\n        y,\n        {\n          size: p.size,\n          sx,\n          sy,\n          rot,\n          eyes,\n          look,\n          mouth,\n          blush: true,\n          faceAt: [0.1, -0.06],\n        },\n        (c, px) => {\n          drawChickBody(c, px, { wingAngle: Math.sin(t * 8) * 0.2 });\n        }\n      );\n    },\n  },\n\n  // 8. TITLE \"Pip\" WRITES ON IN ELEGANT HANDWRITTEN SCRIPT (12.8s - 15.0s)\n  {\n    id: 'title_pip',\n    kind: 'title',\n    from: 12.8,\n    to: 15.0,\n    params: { text: 'Pip', color: '#fffaeb' },\n    draw(ctx, t, p) {\n      const writeU = kit.ease.inOut(kit.seg(t, 12.9, 14.1));\n\n      ctx.save();\n      // Drop shadow for title readability on dark aurora sky\n      ctx.shadowColor = 'rgba(10, 25, 35, 0.7)';\n      ctx.shadowBlur = 12;\n      ctx.shadowOffsetX = 3;\n      ctx.shadowOffsetY = 4;\n\n      kit.writeOn(ctx, p.text, 440, 390, {\n        width: 0.34,\n        family: '\"Caveat\", \"Great Vibes\", \"Brush Script MT\", cursive',\n        color: p.color,\n        u: writeU,\n      });\n      ctx.restore();\n    },\n  },\n\n  // 9. SNOWFLAKE CONFETTI CELEBRATION BURST (13.7s - 15.0s)\n  {\n    id: 'snowflake_burst',\n    kind: 'effect',\n    from: 13.7,\n    to: 15.0,\n    params: { x: 780, y: 500 },\n    draw(ctx, t, p) {\n      const dt = t - 13.7;\n      kit.confetti(ctx, p.x, p.y, dt, {\n        count: 75,\n        colors: ['#ffffff', '#d5f5ec', '#fed636', '#a1e3d8', '#fffaeb'],\n      });\n\n      // Extra hand-drawn sparkling snowflake stars\n      ctx.save();\n      ctx.strokeStyle = '#ffffff';\n      ctx.lineWidth = 2.5;\n      for (let i = 0; i < 8; i++) {\n        const ang = (i / 8) * Math.PI * 2 + dt * 0.5;\n        const dist = 30 + dt * 160 + (i % 3) * 20;\n        const fx = p.x + Math.cos(ang) * dist;\n        const fy = p.y + Math.sin(ang) * dist * 0.65;\n        const s = 7 + (i % 4);\n\n        ctx.beginPath();\n        ctx.moveTo(fx - s, fy);\n        ctx.lineTo(fx + s, fy);\n        ctx.moveTo(fx, fy - s);\n        ctx.lineTo(fx, fy + s);\n        ctx.stroke();\n      }\n      ctx.restore();\n    },\n  },\n\n  // 10. FINAL PAPER TEXTURE OVERLAY (subtle grain over whole piece)\n  {\n    id: 'paper_overlay',\n    kind: 'overlay',\n    from: 0,\n    to: 15,\n    params: {},\n    draw(ctx, t) {\n      kit.paper(ctx, 1280, 720, { overlay: true });\n    },\n  },\n];\n\n// Pure function drawing every entity in declaration order\nfunction draw(ctx, t, params, rng) {\n  for (const e of entities) {\n    if (t < e.from || t > e.to) continue;\n    ctx.save();\n    e.draw(ctx, t, e.params, rng.for(e.id));\n    ctx.restore();\n  }\n}\n\n// Complete Musical Score & SFX timing mapped to every beat\nfunction compose(params) {\n  const notes = [];\n\n  // Act 1: Egg wobble & crack pop (0 - 3.5s)\n  notes.push(\n    { inst: 'marimba', t: 0.2, midi: 72, dur: 0.3, vel: 0.4 },\n    { inst: 'marimba', t: 0.6, midi: 74, dur: 0.3, vel: 0.45 },\n    { inst: 'marimba', t: 0.9, midi: 76, dur: 0.3, vel: 0.5 },\n    { inst: 'pop',     t: 1.8, dur: 0.4, vel: 0.85 }, // Pop out of egg!\n    { inst: 'bell',    t: 1.85, midi: 84, dur: 1.2, vel: 0.75 },\n    { inst: 'harp',    t: 2.1, midi: 79, dur: 0.8, vel: 0.6 },\n    { inst: 'harp',    t: 2.3, midi: 81, dur: 0.8, vel: 0.65 },\n    { inst: 'harp',    t: 2.5, midi: 84, dur: 1.2, vel: 0.7 }\n  );\n\n  // Act 2: Playful waddling theme at dawn (3.5 - 7.2s)\n  const dawnChords = [\n    { t: 3.5, bass: 48, melody: [60, 64, 67] }, // C\n    { t: 4.8, bass: 53, melody: [65, 69, 72] }, // F\n    { t: 6.0, bass: 55, melody: [67, 71, 74] }, // G\n  ];\n\n  for (const c of dawnChords) {\n    notes.push(\n      { inst: 'pad', t: c.t, midi: c.bass + 12, dur: 1.4, vel: 0.35 },\n      { inst: 'bass', t: c.t, midi: c.bass, dur: 1.0, vel: 0.5 }\n    );\n    c.melody.forEach((m, idx) => {\n      notes.push({ inst: 'marimba', t: c.t + idx * 0.35, midi: m, dur: 0.4, vel: 0.6 });\n    });\n  }\n\n  // Act 3: Whoosh slip & belly slide down hill (7.3 - 10.0s)\n  notes.push(\n    { inst: 'whoosh', t: 7.3, dur: 0.6, vel: 0.75 },\n    { inst: 'flute',  t: 7.4, midi: 84, dur: 0.4, vel: 0.6 },\n    { inst: 'flute',  t: 7.7, midi: 79, dur: 0.6, vel: 0.65 },\n    { inst: 'flute',  t: 8.1, midi: 72, dur: 0.8, vel: 0.7 },\n    { inst: 'wind',   t: 7.8, dur: 2.2, vel: 0.45 }\n  );\n\n  // Act 4: Ethereal Aurora Borealis chords (10.0 - 13.0s)\n  const aurora = [\n    { t: 10.2, root: 57 }, // Am\n    { t: 11.6, root: 53 }, // Fmaj7\n  ];\n  for (const a of aurora) {\n    notes.push(\n      { inst: 'strings', t: a.t, midi: a.root + 12, dur: 1.6, vel: 0.45 },\n      { inst: 'pad',     t: a.t, midi: a.root + 19, dur: 1.6, vel: 0.4 },\n      { inst: 'bell',    t: a.t + 0.4, midi: a.root + 24, dur: 1.5, vel: 0.6 }\n    );\n  }\n\n  // Act 5: Title flourish, chick hop & confetti hit (12.9 - 15.0s)\n  notes.push(\n    { inst: 'harp',  t: 12.9, midi: 60, dur: 0.3, vel: 0.6 },\n    { inst: 'harp',  t: 13.1, midi: 64, dur: 0.3, vel: 0.65 },\n    { inst: 'harp',  t: 13.3, midi: 67, dur: 0.4, vel: 0.7 },\n    { inst: 'harp',  t: 13.5, midi: 72, dur: 0.5, vel: 0.75 },\n    { inst: 'kick',  t: 13.7, dur: 0.4, vel: 0.85 }, // Landing hit!\n    { inst: 'bell',  t: 13.7, midi: 84, dur: 2.0, vel: 0.9 },\n    { inst: 'pluck', t: 13.8, midi: 96, dur: 1.0, vel: 0.75 }\n  );\n\n  return notes;\n}\n","exportProfile":{"format":"mp4","resolution":"720p","fps":30,"durationSeconds":15},"source":{"prompt":"A 15 second animated film on warm cream paper: a speckled egg wobbles, cracks and a fluffy yellow chick pops out blinking. It waddles across snowy hills at dawn with pine trees, slips and slides down a slope on its belly, then the sky turns to night with green northern lights while it sits on a frozen pond watching. It ends with the handwritten title \"Pip\" writing itself on, the chick hopping in beside it, and a burst of snowflake confetti.","provider":"google","model":"gemini-3.8-flash","app":"gallery-reproduction"}},"license":"CC0-1.0"}