{"id":"pinepaper-in-a-minute-v1","title":"PinePaper in a Minute (v1)","category":"Drawn film","prompt":"PinePaper in a minute — a 60-second landing demo written directly as a draw contract (not through the chat): describe it, relate it, edit it by hand, ship it; obsidian ground, warm paper, violet and cyan ink; a scored C-major bed with a hit on every event.","designedBy":{"provider":"anthropic","model":"claude-opus-5-5 (written directly)","inference":{"measured":false,"inputTokens":2470,"outputTokens":7120,"costUsd":0.3807,"inPerMTokUsd":10,"outPerMTokUsd":50,"estimated":true,"basis":"estimated as one chat turn — the draw-contract prompt and brief in, the 26,341-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":60,"priceUsd":0.9,"format":"mp4","renderMs":326925,"frameCount":1800,"bytes":5078474,"seed":11,"width":1920,"height":1080,"fps":30,"durationSeconds":60},"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=pinepaper-in-a-minute-v1), and it exports as MP4 or GIF, not as a still."},"renderRequest":{"drawContract":"const meta = { duration: 60, fps: 30, size: [1920, 1080], seed: 11 };\n\n// ─────────────────────────────────────────────────────────────────────────────────────────────\n// PINEPAPER IN A MINUTE — describe it, relate it, edit it by hand, ship it.\n// Obsidian ground, warm paper, violet and cyan ink. Six acts on one clock; the score in\n// compose() sits on the same beats (BEATS) the picture uses.\n// ─────────────────────────────────────────────────────────────────────────────────────────────\n\nconst W = 1920, H = 1080;\nconst INK = { ground: '#07090f', ground2: '#111633', paper: '#f4efe4', paperEdge: '#d9cfbb', ink: '#161a28',\n  violet: '#7c6cff', violetSoft: '#a99dff', cyan: '#38bdf8', amber: '#fbbf24', ember: '#f59e0b', moon: '#cbd5e1',\n  pink: '#f472b6', mint: '#34d399', muted: '#8b93a7', dim: '#5b6378' };\n\nconst BEATS = { title: 0, describe: 7.5, typeStart: 8.2, typeEnd: 12.2, enter: 12.6, relate: 17, orbit: 21,\n  touch: 29, grab: 30.4, release: 33.6, path: 34.4, ship: 40, film: 40.8, print: 42.6, embed: 44.4, end: 50 };\n\nconst clamp = (v, a = 0, b = 1) => Math.max(a, Math.min(b, v));\nconst ease = (u) => { u = clamp(u); return u * u * (3 - 2 * u); };\nconst easeOut = (u) => 1 - Math.pow(1 - clamp(u), 3);\nconst lerp = (a, b, u) => a + (b - a) * u;\nconst win = (t, from, to, fin = 0.6, fout = 0.6) => Math.min(ease((t - from) / fin), ease((to - t) / fout));\nconst FONT = (w, px) => `${w} ${px}px Roboto, \"Helvetica Neue\", Arial, sans-serif`;\nconst MONO = (px) => `500 ${px}px \"Roboto Mono\", Menlo, \"DejaVu Sans Mono\", monospace`;\n\nfunction roundRect(ctx, x, y, w, h, r) {\n  ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r);\n  ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath();\n}\n\n// The pine sprig, inked needle by needle. prog 0..1 reveals it.\nfunction drawSprig(ctx, x, y, s, prog, color) {\n  ctx.save(); ctx.translate(x, y); ctx.scale(s, s);\n  ctx.strokeStyle = color; ctx.lineCap = 'round'; ctx.lineJoin = 'round';\n  const stem = clamp(prog * 1.4);\n  ctx.lineWidth = 7; ctx.beginPath(); ctx.moveTo(0, 0);\n  const steps = 24;\n  for (let i = 1; i <= steps * stem; i++) { const u = i / steps; ctx.lineTo(Math.sin(u * 1.4) * 18, -u * 230); }\n  ctx.stroke();\n  const n = 13;\n  for (let i = 0; i < n; i++) {\n    const u = (i + 1) / (n + 1), grow = clamp((prog - 0.12 - i * 0.055) * 5);\n    if (grow <= 0) continue;\n    const bx = Math.sin(u * 1.4) * 18, by = -u * 230, len = (78 - u * 46) * grow;\n    ctx.lineWidth = 4.5;\n    for (const side of [-1, 1]) { ctx.beginPath(); ctx.moveTo(bx, by); ctx.lineTo(bx + side * len * 0.86, by - len * 0.5); ctx.stroke(); }\n  }\n  ctx.restore();\n}\n\n// Letters of a word set one by one with a small rise; returns the width.\nfunction setWord(ctx, word, cx, cy, font, color, prog, spacing) {\n  ctx.font = font; ctx.textBaseline = 'alphabetic';\n  const widths = [...word].map((c) => ctx.measureText(c).width + spacing);\n  const total = widths.reduce((a, b) => a + b, 0) - spacing;\n  let x = cx - total / 2;\n  [...word].forEach((c, i) => {\n    const u = ease((prog * (word.length + 3) - i) / 3);\n    if (u > 0) { ctx.globalAlpha = u; ctx.fillStyle = color; ctx.fillText(c, x, cy + (1 - u) * 26); }\n    x += widths[i];\n  });\n  ctx.globalAlpha = 1;\n  return total;\n}\n\n// ── The solar system is ONE shared state, so dragging the sun drags everything related to it. ──\nconst SYSTEM = { sunHomeX: 820, sunHomeY: 560, dragToX: 1060, dragToY: 520, planetR: 220, moonR: 64, planetSpeed: 0.62, moonSpeed: 2.3 };\nfunction sunAt(t, p) {\n  const u = ease((t - BEATS.grab - 0.25) / (BEATS.release - BEATS.grab - 0.25));\n  return { x: lerp(p.sunHomeX, p.dragToX, u), y: lerp(p.sunHomeY, p.dragToY, u) };\n}\nfunction planetAt(t, p) {\n  const s = sunAt(t, p), a = Math.max(0, t - BEATS.orbit) * p.planetSpeed - 0.35;\n  return { x: s.x + Math.cos(a) * p.planetR, y: s.y + Math.sin(a) * p.planetR, a };\n}\nfunction moonAt(t, p) {\n  const pl = planetAt(t, p), a = Math.max(0, t - BEATS.orbit) * p.moonSpeed - 1.2;\n  return { x: pl.x + Math.cos(a) * p.moonR, y: pl.y + Math.sin(a) * p.moonR };\n}\nconst SHEET = { x: 960, y: 560, w: 1400, h: 790 };\nconst sheetOn = (t) => win(t, 6.6, BEATS.ship + 0.9, 1.0, 0.8);\n\nfunction miniOrbit(ctx, cx, cy, s, a) {\n  ctx.save(); ctx.translate(cx, cy); ctx.scale(s, s);\n  ctx.strokeStyle = 'rgba(124,108,255,0.45)'; ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(0, 0, 60, 0, Math.PI * 2); ctx.stroke();\n  ctx.fillStyle = INK.amber; ctx.beginPath(); ctx.arc(0, 0, 18, 0, Math.PI * 2); ctx.fill();\n  ctx.fillStyle = INK.cyan; ctx.beginPath(); ctx.arc(Math.cos(a) * 60, Math.sin(a) * 60, 8, 0, Math.PI * 2); ctx.fill();\n  ctx.restore();\n}\n\nconst entities = [\n  { id: 'ground', kind: 'background', from: 0, to: 60,\n    params: { top: INK.ground2, bottom: INK.ground, grid: 'rgba(148,163,184,0.07)', flecks: 46 },\n    draw(ctx, t, p, rng) {\n      const g = ctx.createRadialGradient(W / 2, H * 0.42, 60, W / 2, H * 0.5, W * 0.75);\n      g.addColorStop(0, p.top); g.addColorStop(1, p.bottom); ctx.fillStyle = g; ctx.fillRect(0, 0, W, H);\n      ctx.fillStyle = p.grid;\n      for (let x = 40; x < W; x += 48) for (let y = 40; y < H; y += 48) ctx.fillRect(x, y, 2, 2);\n      for (let i = 0; i < p.flecks; i++) {\n        const bx = rng() * W, by = rng() * H, sz = 2 + rng() * 5, ph = rng() * 6.28, sp = 0.15 + rng() * 0.25;\n        ctx.globalAlpha = 0.12 + 0.1 * Math.sin(t * sp * 3 + ph);\n        ctx.fillStyle = i % 5 === 0 ? INK.violetSoft : INK.paper;\n        ctx.save(); ctx.translate(bx + Math.sin(t * sp + ph) * 22, (by - t * 9 * sp + H) % H); ctx.rotate(ph + t * sp);\n        ctx.fillRect(-sz, -sz * 0.6, sz * 2, sz * 1.2); ctx.restore();\n      }\n      ctx.globalAlpha = 1;\n    } },\n\n  // ── ACT 1 · TITLE ──────────────────────────────────────────────────────────────────────────\n  { id: 'title-sprig', kind: 'prop', from: 0.4, to: 7.4, params: { x: 520, y: 620, scale: 1.25, color: INK.paper },\n    draw(ctx, t, p) { ctx.globalAlpha = win(t, 0.4, 7.4, 0.4, 0.9); drawSprig(ctx, p.x, p.y, p.scale, (t - 0.6) / 2.8, p.color); },\n    bounds(t, p) { return [p.x - 110 * p.scale, p.y - 240 * p.scale, 220 * p.scale, 250 * p.scale]; } },\n  { id: 'title-wordmark', kind: 'title', from: 1.4, to: 7.4, params: { x: 1040, y: 590, color: INK.paper, accent: INK.violetSoft },\n    draw(ctx, t, p) {\n      ctx.globalAlpha = win(t, 1.4, 7.4, 0.3, 0.9);\n      setWord(ctx, 'PinePaper', p.x, p.y, FONT(700, 150), p.color, (t - 1.5) / 2.2, 4);\n      ctx.globalAlpha = win(t, 3.6, 7.4, 0.8, 0.9);\n      ctx.font = MONO(30); ctx.fillStyle = p.accent; ctx.textAlign = 'center';\n      ctx.fillText('a minute with the design tool that moves by relations', p.x - 40, p.y + 78); ctx.textAlign = 'left';\n    },\n    bounds(t, p) { return [p.x - 380, p.y - 130, 760, 230]; } },\n\n  // ── THE STAGE: a sheet of paper that the next three acts happen on ─────────────────────────\n  { id: 'sheet', kind: 'prop', from: 6.6, to: 41, params: SHEET,\n    draw(ctx, t, p) {\n      const on = sheetOn(t); if (on <= 0) return;\n      const enter = easeOut((t - 6.6) / 1.1), leave = ease((t - BEATS.ship) / 0.9);\n      ctx.globalAlpha = on;\n      ctx.translate(p.x, p.y + (1 - enter) * 80); ctx.rotate((-1.2 + enter * 0.6) * Math.PI / 180); ctx.scale(0.94 + 0.06 * enter - 0.25 * leave, 0.94 + 0.06 * enter - 0.25 * leave);\n      ctx.shadowColor = 'rgba(0,0,0,0.55)'; ctx.shadowBlur = 50; ctx.shadowOffsetY = 18;\n      ctx.fillStyle = INK.paper; roundRect(ctx, -p.w / 2, -p.h / 2, p.w, p.h, 14); ctx.fill();\n      ctx.shadowColor = 'transparent';\n      ctx.strokeStyle = INK.paperEdge; ctx.lineWidth = 2; ctx.stroke();\n      ctx.fillStyle = 'rgba(22,26,40,0.05)';\n      for (let x = -p.w / 2 + 40; x < p.w / 2; x += 40) for (let y = -p.h / 2 + 40; y < p.h / 2; y += 40) ctx.fillRect(x, y, 2, 2);\n    },\n    bounds(t, p) { return [p.x - p.w / 2, p.y - p.h / 2, p.w, p.h]; } },\n\n  { id: 'chapter', kind: 'title', from: 7.2, to: 40.4, params: { x: 330, y: 250, color: INK.ink, accent: INK.violet },\n    draw(ctx, t, p) {\n      const acts = [['01', 'Describe it.', BEATS.describe, BEATS.relate], ['02', 'Relate it.', BEATS.relate, BEATS.touch], ['03', 'Edit it by hand.', BEATS.touch, BEATS.ship + 0.4]];\n      for (const [n, label, a, b] of acts) {\n        const o = win(t, a, b, 0.5, 0.45) * sheetOn(t); if (o <= 0) continue;\n        ctx.globalAlpha = o;\n        ctx.font = MONO(26); ctx.fillStyle = p.accent; ctx.fillText(n, p.x, p.y);\n        ctx.font = FONT(700, 54); ctx.fillStyle = p.color; ctx.fillText(label, p.x + 58, p.y + 6);\n      }\n    },\n    bounds(t, p) { return [p.x, p.y - 50, 560, 70]; } },\n\n  // ── ACT 2 · DESCRIBE ───────────────────────────────────────────────────────────────────────\n  { id: 'prompt', kind: 'ui', from: 7.6, to: 17.6, params: { x: 960, y: 872, w: 1100, text: 'a sun, a planet and a moon — make them orbit', color: INK.ink, accent: INK.violet },\n    draw(ctx, t, p) {\n      const o = win(t, 7.6, 17.6, 0.5, 0.7) * sheetOn(t); if (o <= 0) return;\n      ctx.globalAlpha = o;\n      const flash = Math.max(0, 1 - (t - BEATS.enter) / 0.5) * (t >= BEATS.enter ? 1 : 0);\n      ctx.fillStyle = flash > 0 ? `rgba(124,108,255,${0.12 + 0.3 * flash})` : '#ffffff';\n      roundRect(ctx, p.x - p.w / 2, p.y - 36, p.w, 72, 36); ctx.fill();\n      ctx.strokeStyle = p.accent; ctx.lineWidth = 2.5; ctx.stroke();\n      const n = Math.floor(clamp((t - BEATS.typeStart) / (BEATS.typeEnd - BEATS.typeStart)) * p.text.length);\n      ctx.font = FONT(500, 32); ctx.fillStyle = p.color; ctx.fillText(p.text.slice(0, n), p.x - p.w / 2 + 44, p.y + 11);\n      if (t < BEATS.enter && Math.floor(t * 2.4) % 2 === 0) { const cw = ctx.measureText(p.text.slice(0, n)).width; ctx.fillStyle = p.accent; ctx.fillRect(p.x - p.w / 2 + 48 + cw, p.y - 18, 3, 36); }\n      ctx.fillStyle = p.accent; roundRect(ctx, p.x + p.w / 2 - 64, p.y - 24, 48, 48, 24); ctx.fill();\n      ctx.strokeStyle = '#fff'; ctx.lineWidth = 3.5; ctx.beginPath(); ctx.moveTo(p.x + p.w / 2 - 49, p.y); ctx.lineTo(p.x + p.w / 2 - 31, p.y); ctx.moveTo(p.x + p.w / 2 - 38, p.y - 7); ctx.lineTo(p.x + p.w / 2 - 31, p.y); ctx.lineTo(p.x + p.w / 2 - 38, p.y + 7); ctx.stroke();\n    },\n    bounds(t, p) { return [p.x - p.w / 2, p.y - 36, p.w, 72]; } },\n\n  // ── ACTS 2–4 · THE SYSTEM: three nodes, two relations, one shared state ──────────────────\n  { id: 'relations', kind: 'graph', from: 17, to: 40.4, params: SYSTEM,\n    draw(ctx, t, p) {\n      const o = win(t, 17, 40.4, 0.4, 0.45) * sheetOn(t); if (o <= 0) return;\n      const s = sunAt(t, p), pl = planetAt(t, p), m = moonAt(t, p);\n      const grow = (at) => ease((t - at) / 1.3);\n      // orbit rings once the relation is live\n      const live = ease((t - BEATS.orbit) / 1.2);\n      ctx.globalAlpha = o * live * 0.8; ctx.setLineDash([6, 10]); ctx.strokeStyle = INK.violet; ctx.lineWidth = 2;\n      ctx.beginPath(); ctx.arc(s.x, s.y, p.planetR, 0, Math.PI * 2); ctx.stroke();\n      ctx.beginPath(); ctx.arc(pl.x, pl.y, p.moonR, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]);\n      // the edges, drawn as a graph first\n      const edges = [[pl, s, 17.4], [m, pl, 18.6]];\n      for (const [a, b, at] of edges) {\n        const g = grow(at) * (1 - live); if (g <= 0.01) continue;\n        ctx.globalAlpha = o; ctx.strokeStyle = INK.violet; ctx.lineWidth = 4;\n        ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(lerp(a.x, b.x, g), lerp(a.y, b.y, g)); ctx.stroke();\n        const mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2 - 20;\n        ctx.globalAlpha = o * ease((t - at - 0.7) / 0.5) * (1 - live);\n        ctx.fillStyle = INK.violet; roundRect(ctx, mx - 58, my - 24, 116, 40, 20); ctx.fill();\n        ctx.font = MONO(22); ctx.fillStyle = '#fff'; ctx.textAlign = 'center'; ctx.fillText('orbits', mx, my + 4); ctx.textAlign = 'left';\n      }\n      // planet trail\n      if (live > 0) {\n        ctx.globalAlpha = o * live;\n        const a0 = pl.a;\n        for (let i = 0; i < 18; i++) { const a = a0 - i * 0.05; ctx.fillStyle = `rgba(56,189,248,${0.35 * (1 - i / 18)})`; ctx.beginPath(); ctx.arc(s.x + Math.cos(a) * p.planetR, s.y + Math.sin(a) * p.planetR, 12 - i * 0.5, 0, Math.PI * 2); ctx.fill(); }\n      }\n    } },\n  { id: 'sun', kind: 'character', from: 12.9, to: 40.4, params: SYSTEM,\n    draw(ctx, t, p) {\n      const o = win(t, 12.9, 40.4, 0.35, 0.45) * sheetOn(t); if (o <= 0) return;\n      const s = sunAt(t, p), pop = easeOut((t - 12.9) / 0.45), held = t > BEATS.grab && t < BEATS.release + 0.2;\n      ctx.globalAlpha = o;\n      const glow = ctx.createRadialGradient(s.x, s.y, 10, s.x, s.y, 120); glow.addColorStop(0, 'rgba(251,191,36,0.45)'); glow.addColorStop(1, 'rgba(251,191,36,0)');\n      ctx.fillStyle = glow; ctx.beginPath(); ctx.arc(s.x, s.y, 120, 0, Math.PI * 2); ctx.fill();\n      ctx.fillStyle = INK.amber; ctx.beginPath(); ctx.arc(s.x, s.y, 58 * pop, 0, Math.PI * 2); ctx.fill();\n      if (held) { ctx.strokeStyle = INK.violet; ctx.lineWidth = 4; ctx.setLineDash([10, 8]); ctx.beginPath(); ctx.arc(s.x, s.y, 76, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]); }\n      ctx.font = MONO(22); ctx.fillStyle = INK.ink; ctx.textAlign = 'center'; ctx.globalAlpha = o * win(t, 13.2, BEATS.orbit + 1.2, 0.4, 0.8); ctx.fillText('sun', s.x, s.y + 96); ctx.textAlign = 'left';\n    },\n    bounds(t, p) { const s = sunAt(t, p); return [s.x - 60, s.y - 60, 120, 120]; } },\n  { id: 'planet', kind: 'character', from: 13.5, to: 40.4, params: SYSTEM,\n    draw(ctx, t, p) {\n      const o = win(t, 13.5, 40.4, 0.35, 0.45) * sheetOn(t); if (o <= 0) return;\n      const pl = planetAt(t, p), pop = easeOut((t - 13.5) / 0.45);\n      ctx.globalAlpha = o; ctx.fillStyle = INK.cyan; ctx.beginPath(); ctx.arc(pl.x, pl.y, 24 * pop, 0, Math.PI * 2); ctx.fill();\n      ctx.fillStyle = 'rgba(255,255,255,0.5)'; ctx.beginPath(); ctx.arc(pl.x - 7, pl.y - 7, 7 * pop, 0, Math.PI * 2); ctx.fill();\n      ctx.font = MONO(22); ctx.fillStyle = INK.ink; ctx.textAlign = 'center'; ctx.globalAlpha = o * win(t, 13.8, BEATS.orbit + 1.2, 0.4, 0.8); ctx.fillText('planet', pl.x, pl.y + 56); ctx.textAlign = 'left';\n    },\n    bounds(t, p) { const pl = planetAt(t, p); return [pl.x - 26, pl.y - 26, 52, 52]; } },\n  { id: 'moon', kind: 'character', from: 14.1, to: 40.4, params: SYSTEM,\n    draw(ctx, t, p) {\n      const o = win(t, 14.1, 40.4, 0.35, 0.45) * sheetOn(t); if (o <= 0) return;\n      const m = moonAt(t, p), pop = easeOut((t - 14.1) / 0.45);\n      ctx.globalAlpha = o; ctx.fillStyle = INK.moon; ctx.beginPath(); ctx.arc(m.x, m.y, 12 * pop, 0, Math.PI * 2); ctx.fill();\n      ctx.strokeStyle = INK.dim; ctx.lineWidth = 1.5; ctx.stroke();\n      ctx.font = MONO(20); ctx.fillStyle = INK.ink; ctx.textAlign = 'center'; ctx.globalAlpha = o * win(t, 14.4, BEATS.orbit + 1.2, 0.4, 0.8); ctx.fillText('moon', m.x, m.y - 26); ctx.textAlign = 'left';\n    },\n    bounds(t, p) { const m = moonAt(t, p); return [m.x - 14, m.y - 14, 28, 28]; } },\n\n  // ── ACT 4 · EDIT IT BY HAND ────────────────────────────────────────────────────────────────\n  { id: 'pointer', kind: 'ui', from: BEATS.touch + 0.3, to: BEATS.release + 1.2, params: { startX: 1320, startY: 330, color: INK.ink },\n    draw(ctx, t, p) {\n      const o = win(t, BEATS.touch + 0.3, BEATS.release + 1.2, 0.4, 0.5) * sheetOn(t); if (o <= 0) return;\n      const s = sunAt(t, SYSTEM), reach = ease((t - BEATS.touch - 0.3) / (BEATS.grab - BEATS.touch - 0.3));\n      const x = t < BEATS.grab ? lerp(p.startX, s.x + 20, reach) : s.x + 20, y = t < BEATS.grab ? lerp(p.startY, s.y + 18, reach) : s.y + 18;\n      const press = t > BEATS.grab && t < BEATS.release ? 0.9 : 1;\n      ctx.globalAlpha = o; ctx.translate(x, y); ctx.scale(press * 1.6, press * 1.6);\n      ctx.fillStyle = '#ffffff'; ctx.strokeStyle = p.color; ctx.lineWidth = 1.6;\n      ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(0, 26); ctx.lineTo(7, 20); ctx.lineTo(12, 31); ctx.lineTo(16, 29); ctx.lineTo(11, 18); ctx.lineTo(20, 18); ctx.closePath(); ctx.fill(); ctx.stroke();\n    } },\n  { id: 'caption-drag', kind: 'title', from: BEATS.grab, to: BEATS.path + 0.6, params: { x: 1180, y: 860, color: INK.dim },\n    draw(ctx, t, p) {\n      ctx.globalAlpha = win(t, BEATS.grab + 0.3, BEATS.path + 0.6, 0.5, 0.5) * sheetOn(t);\n      ctx.font = MONO(26); ctx.fillStyle = p.color; ctx.textAlign = 'center'; ctx.fillText('move the sun — the relations hold', p.x, p.y); ctx.textAlign = 'left';\n    } },\n  { id: 'path-demo', kind: 'prop', from: BEATS.path, to: 40.4, params: { x: 360, y: 640, w: 330, h: 190, a: INK.violet, b: INK.pink, speed: 0.19 },\n    draw(ctx, t, p) {\n      const o = win(t, BEATS.path, 40.4, 0.5, 0.45) * sheetOn(t); if (o <= 0) return;\n      const draw = ease((t - BEATS.path) / 1.1), per = 2 * (p.w + p.h);\n      ctx.globalAlpha = o; ctx.setLineDash([8, 10]); ctx.strokeStyle = INK.dim; ctx.lineWidth = 2.5;\n      ctx.beginPath(); ctx.rect(p.x, p.y, p.w * draw, p.h * draw); ctx.stroke(); ctx.setLineDash([]);\n      const along = (u) => { let d = ((u % 1) + 1) % 1 * per; if (d < p.w) return [p.x + d, p.y]; d -= p.w; if (d < p.h) return [p.x + p.w, p.y + d]; d -= p.h; if (d < p.w) return [p.x + p.w - d, p.y + p.h]; d -= p.w; return [p.x, p.y + p.h - d]; };\n      const u = Math.max(0, t - BEATS.path - 0.9) * p.speed;\n      for (const [off, col] of [[0, p.a], [0.5, p.b]]) {\n        const [x, y] = along(u + off); ctx.globalAlpha = o * ease((t - BEATS.path - 0.6) / 0.5);\n        ctx.fillStyle = col; ctx.save(); ctx.translate(x, y); ctx.rotate(u * 6.28); ctx.fillRect(-22, -22, 44, 44); ctx.restore();\n      }\n      ctx.globalAlpha = o * ease((t - BEATS.path - 1) / 0.6); ctx.font = MONO(22); ctx.fillStyle = INK.dim;\n      ctx.fillText('moves_along_path · phase 0 and 0.5', p.x, p.y + p.h + 44);\n    },\n    bounds(t, p) { return [p.x - 24, p.y - 24, p.w + 48, p.h + 48]; } },\n\n  // ── ACT 5 · SHIP IT ────────────────────────────────────────────────────────────────────────\n  { id: 'ship-title', kind: 'title', from: BEATS.ship + 0.3, to: BEATS.end + 0.3, params: { x: 960, y: 200, color: INK.paper, accent: INK.violetSoft },\n    draw(ctx, t, p) {\n      ctx.globalAlpha = win(t, BEATS.ship + 0.3, BEATS.end + 0.3, 0.6, 0.6); ctx.textAlign = 'center';\n      ctx.font = MONO(26); ctx.fillStyle = p.accent; ctx.fillText('04', p.x, p.y - 58);\n      ctx.font = FONT(700, 64); ctx.fillStyle = p.color; ctx.fillText('Ship it — one design, every format.', p.x, p.y + 10); ctx.textAlign = 'left';\n    } },\n  { id: 'out-film', kind: 'prop', from: BEATS.film, to: BEATS.end + 0.3, params: { x: 470, y: 560, color: INK.paper },\n    draw(ctx, t, p) {\n      const o = win(t, BEATS.film, BEATS.end + 0.3, 0.3, 0.6), up = easeOut((t - BEATS.film) / 0.7); if (o <= 0) return;\n      ctx.globalAlpha = o; ctx.translate(p.x, p.y + (1 - up) * 300);\n      ctx.fillStyle = '#0d1020'; roundRect(ctx, -200, -130, 400, 260, 16); ctx.fill(); ctx.strokeStyle = '#2a3150'; ctx.lineWidth = 2; ctx.stroke();\n      for (let i = 0; i < 9; i++) { ctx.fillStyle = '#1f2540'; roundRect(ctx, -186 + i * 42, -120, 24, 16, 4); ctx.fill(); roundRect(ctx, -186 + i * 42, 104, 24, 16, 4); ctx.fill(); }\n      for (let k = 0; k < 3; k++) { ctx.fillStyle = '#161b33'; ctx.fillRect(-180 + k * 124, -88, 112, 176); miniOrbit(ctx, -124 + k * 124, 0, 0.72, t * 0.9 + k * 1.1); }\n      ctx.font = FONT(600, 30); ctx.fillStyle = p.color; ctx.textAlign = 'center'; ctx.fillText('MP4 · with its score', 0, 196); ctx.textAlign = 'left';\n    },\n    bounds(t, p) { return [p.x - 200, p.y - 130, 400, 260]; } },\n  { id: 'out-print', kind: 'prop', from: BEATS.print, to: BEATS.end + 0.3, params: { x: 960, y: 560, color: INK.paper },\n    draw(ctx, t, p) {\n      const o = win(t, BEATS.print, BEATS.end + 0.3, 0.3, 0.6), up = easeOut((t - BEATS.print) / 0.7); if (o <= 0) return;\n      ctx.globalAlpha = o; ctx.translate(p.x, p.y + (1 - up) * 300);\n      ctx.shadowColor = 'rgba(0,0,0,0.5)'; ctx.shadowBlur = 30; ctx.fillStyle = '#ffffff'; ctx.fillRect(-120, -170, 240, 340); ctx.shadowColor = 'transparent';\n      ctx.fillStyle = INK.ink; ctx.fillRect(-96, -146, 192, 16);\n      for (let i = 0; i < 5; i++) { ctx.fillStyle = '#d7dbe6'; ctx.fillRect(-96, -110 + i * 20, 192 - (i % 2) * 50, 7); }\n      miniOrbit(ctx, 0, 70, 0.95, 0.6);\n      ctx.font = FONT(600, 30); ctx.fillStyle = p.color; ctx.textAlign = 'center'; ctx.fillText('A4 · print-true PDF', 0, 226); ctx.textAlign = 'left';\n    },\n    bounds(t, p) { return [p.x - 120, p.y - 170, 240, 340]; } },\n  { id: 'out-embed', kind: 'prop', from: BEATS.embed, to: BEATS.end + 0.3, params: { x: 1450, y: 560, color: INK.paper },\n    draw(ctx, t, p) {\n      const o = win(t, BEATS.embed, BEATS.end + 0.3, 0.3, 0.6), up = easeOut((t - BEATS.embed) / 0.7); if (o <= 0) return;\n      ctx.globalAlpha = o; ctx.translate(p.x, p.y + (1 - up) * 300);\n      ctx.fillStyle = '#f8fafc'; roundRect(ctx, -200, -130, 400, 260, 14); ctx.fill();\n      ctx.fillStyle = '#e2e8f0'; roundRect(ctx, -200, -130, 400, 40, 14); ctx.fill(); ctx.fillRect(-200, -104, 400, 14);\n      for (const [i, c] of [[0, '#f87171'], [1, '#fbbf24'], [2, '#34d399']]) { ctx.fillStyle = c; ctx.beginPath(); ctx.arc(-176 + i * 22, -110, 6, 0, Math.PI * 2); ctx.fill(); }\n      ctx.fillStyle = '#0f172a'; ctx.fillRect(-180, -76, 360, 186); miniOrbit(ctx, 0, 17, 1.05, t * 1.4);\n      ctx.fillStyle = INK.violet; roundRect(ctx, 104, 66, 64, 34, 8); ctx.fill(); ctx.font = MONO(20); ctx.fillStyle = '#fff'; ctx.fillText('</>', 116, 90);\n      ctx.font = FONT(600, 30); ctx.fillStyle = p.color; ctx.textAlign = 'center'; ctx.fillText('a live, clickable embed', 0, 196); ctx.textAlign = 'left';\n    },\n    bounds(t, p) { return [p.x - 200, p.y - 130, 400, 260]; } },\n\n  // ── ACT 6 · END CARD ───────────────────────────────────────────────────────────────────────\n  { id: 'end-sprig', kind: 'prop', from: BEATS.end, to: 60, params: { x: 560, y: 600, scale: 1.05, color: INK.paper },\n    draw(ctx, t, p) {\n      ctx.globalAlpha = win(t, BEATS.end, 60.5, 0.6, 0.5); drawSprig(ctx, p.x, p.y, p.scale, (t - BEATS.end) / 2.2, p.color);\n      const a = (t - BEATS.end) * 1.3; ctx.fillStyle = INK.cyan; ctx.beginPath(); ctx.arc(p.x + 8 + Math.cos(a) * 150, p.y - 120 + Math.sin(a) * 150, 9, 0, Math.PI * 2); ctx.fill();\n    },\n    bounds(t, p) { return [p.x - 150, p.y - 270, 300, 300]; } },\n  { id: 'end-wordmark', kind: 'title', from: BEATS.end + 0.4, to: 60, params: { x: 1070, y: 560, color: INK.paper, accent: INK.violetSoft, url: 'cloud.pinepaper.studio' },\n    draw(ctx, t, p) {\n      ctx.globalAlpha = win(t, BEATS.end + 0.4, 60.5, 0.3, 0.5);\n      setWord(ctx, 'PinePaper', p.x, p.y, FONT(700, 140), p.color, (t - BEATS.end - 0.5) / 2, 4);\n      ctx.textAlign = 'center';\n      ctx.globalAlpha = win(t, BEATS.end + 2.2, 60.5, 0.8, 0.5); ctx.font = FONT(500, 38); ctx.fillStyle = p.accent;\n      ctx.fillText('Describe it. Relate it. Edit it by hand. Ship it.', p.x - 40, p.y + 84);\n      ctx.globalAlpha = win(t, BEATS.end + 3.4, 60.5, 0.8, 0.5); ctx.font = MONO(30); ctx.fillStyle = INK.muted; ctx.fillText(p.url, p.x - 40, p.y + 150);\n      ctx.textAlign = 'left';\n    },\n    bounds(t, p) { return [p.x - 400, p.y - 130, 800, 300]; } },\n];\n\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\n// ── THE SCORE: a warm C-major bed that moves with the acts, and a hit on every event. ──────\nfunction compose(params) {\n  const B = BEATS, n = [];\n  const chord = (t, dur, notes, vel) => { for (const m of notes) n.push({ inst: 'pad', t, midi: m, dur, vel }); };\n  chord(0.2, 7.3, [48, 55, 64], 0.22);            // C   — title\n  chord(7.5, 9.5, [45, 52, 60], 0.2);             // Am  — describe\n  chord(17, 6, [41, 48, 57], 0.2);                // F   — relate\n  chord(23, 6, [43, 50, 59], 0.2);                // G   — the system comes alive\n  chord(29, 11, [48, 55, 64], 0.2);               // C   — touch\n  chord(40, 5, [41, 48, 57], 0.2);                // F   — ship\n  chord(45, 5, [43, 50, 59], 0.2);                // G\n  chord(50, 9.8, [36, 48, 55, 64], 0.24);         // C   — resolve\n  // the sprig inks itself, needle by needle (pentatonic, rising)\n  const penta = [72, 74, 76, 79, 81, 84, 86];\n  for (let i = 0; i < 9; i++) n.push({ inst: 'pluck', t: 0.9 + i * 0.3, midi: penta[i % 7], dur: 0.35, vel: 0.28 });\n  // the wordmark lands\n  n.push({ inst: 'bell', t: 3.6, midi: 76, dur: 1.4, vel: 0.3 }, { inst: 'bell', t: 3.6, midi: 84, dur: 1.4, vel: 0.2 });\n  // typing: one soft tick per character\n  const chars = 44;\n  for (let i = 0; i < chars; i += 2) n.push({ inst: 'hat', t: B.typeStart + (B.typeEnd - B.typeStart) * (i / chars), dur: 0.05, vel: 0.1 });\n  n.push({ inst: 'pop', t: B.enter, dur: 0.12, vel: 0.5 });\n  // three nodes arrive\n  [[12.9, 67], [13.5, 71], [14.1, 74]].forEach(([t, m]) => { n.push({ inst: 'pop', t, dur: 0.1, vel: 0.45 }, { inst: 'bell', t: t + 0.02, midi: m, dur: 0.9, vel: 0.28 }); });\n  // two relations are drawn\n  n.push({ inst: 'pluck', t: 17.4, midi: 60, dur: 0.6, vel: 0.4 }, { inst: 'pluck', t: 17.9, midi: 67, dur: 0.6, vel: 0.35 });\n  n.push({ inst: 'pluck', t: 18.6, midi: 64, dur: 0.6, vel: 0.4 }, { inst: 'pluck', t: 19.1, midi: 71, dur: 0.6, vel: 0.35 });\n  // the system comes alive: a whoosh, then a quiet orbiting arpeggio\n  n.push({ inst: 'whoosh', t: B.orbit - 0.3, dur: 1.0, vel: 0.35 });\n  const arp = [60, 64, 67, 72, 67, 64];\n  for (let t = B.orbit + 0.5, i = 0; t < B.touch; t += 0.5, i++) n.push({ inst: 'pluck', t, midi: arp[i % arp.length], dur: 0.3, vel: 0.2 });\n  // grab and release the sun\n  n.push({ inst: 'pop', t: B.grab, dur: 0.1, vel: 0.5 }, { inst: 'tom', t: B.release, dur: 0.3, vel: 0.5 });\n  for (let t = B.grab + 0.4, i = 0; t < B.release; t += 0.4, i++) n.push({ inst: 'pluck', t, midi: 64 + (i % 3) * 3, dur: 0.25, vel: 0.2 });\n  // the path demo: a steady pulse\n  for (let t = B.path + 1, i = 0; t < B.ship; t += 0.6, i++) n.push({ inst: i % 2 ? 'hat' : 'tom', t, dur: 0.15, vel: 0.18 });\n  // three outputs slide in\n  for (const t of [B.film, B.print, B.embed]) n.push({ inst: 'whoosh', t: t - 0.15, dur: 0.6, vel: 0.4 }, { inst: 'kick', t: t + 0.55, dur: 0.3, vel: 0.55 });\n  // the end card: a bright resolving chord, then one last bell\n  for (const m of [60, 64, 67, 72]) n.push({ inst: 'bell', t: B.end + 0.4, midi: m, dur: 2.4, vel: 0.26 });\n  n.push({ inst: 'bell', t: 57.2, midi: 79, dur: 2.2, vel: 0.2 });\n  return n;\n}\n","exportProfile":{"format":"mp4","resolution":"720p","fps":30,"durationSeconds":60},"source":{"prompt":"PinePaper in a minute — a 60-second landing demo written directly as a draw contract (not through the chat): describe it, relate it, edit it by hand, ship it; obsidian ground, warm paper, violet and cyan ink; a scored C-major bed with a hit on every event.","provider":"anthropic","model":"claude-opus-5-5 (written directly)","app":"gallery-reproduction"}},"license":"CC0-1.0"}