{"id":"paper-boat","title":"Paper Boat","category":"Drawn film","prompt":"A 24-second illustrated film: a folded paper boat drifts down a rain-filled street gutter at dusk, passes beneath a streetlamp whose glow ripples on the water, and sails out into a wide puddle that reflects the first stars. Closing title: PAPER BOAT. Paint it, do not assemble it: graded skies, soft radial glows built from gradients (never a flat translucent disc), layered depth with atmospheric fade, paper-cut shapes with subtle shadows, gentle continuous motion, and one clear moment of change. No text except a small closing title in clear space. Every frame should stand as a composed picture.","designedBy":{"provider":"google","model":"gemini-3.7-flash · 1 revision","inference":{"inputTokens":6138,"outputTokens":10942,"costUsd":0.2457}},"render":{"credits":10,"priceUsd":0.15,"format":"mp4","renderMs":115559,"frameCount":576,"bytes":4695475,"seed":42,"width":1280,"height":720,"fps":24,"durationSeconds":24},"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=paper-boat), and it exports as MP4 or GIF, not as a still."},"renderRequest":{"drawContract":"const meta = { duration: 24, fps: 24, size: [1280, 720], seed: 42 };\n\nfunction draw(ctx, t, params, rng) {\n  const w = 1280;\n  const h = 720;\n\n  // --- Easing & Interpolation Helpers ---\n  const clamp = (v, min, max) => Math.max(min, Math.min(max, v));\n  const smoothstep = (a, b, x) => {\n    const u = clamp((x - a) / (b - a), 0, 1);\n    return u * u * (3 - 2 * u);\n  };\n  const lerp = (a, b, u) => a + (b - a) * u;\n\n  // --- Story Arcs ---\n  // 0-7s: Rain in twilight gutter, boat enters\n  // 7-15s: Drifting into warm streetlamp pool, glowing ripples & caustics\n  // 15-20s: Gutter opens into wide puddle, dusk deepens into starry night mirror\n  // 20-24s: Boat rests in starry reflection, title emerges\n  const stage = t / 24;\n  const nightfall = smoothstep(6.5, 19.5, t);\n  const puddleOpen = smoothstep(13.0, 19.0, t);\n\n  // Smooth cinematic camera pan\n  const camX = lerp(0, 320, stage);\n  const camY = lerp(0, -20, smoothstep(12, 22, t));\n\n  ctx.save();\n  ctx.translate(-camX * 0.15, camY * 0.15);\n\n  // ==========================================\n  // 1. SKY & DISTANT URBAN TWILIGHT\n  // ==========================================\n  const skyGrad = ctx.createLinearGradient(0, 0, 0, h * 0.52);\n  const topSky = nightfall < 0.5\n    ? `rgb(${lerp(18, 7, nightfall * 2)}, ${lerp(22, 10, nightfall * 2)}, ${lerp(44, 24, nightfall * 2)})`\n    : `rgb(${lerp(7, 3, (nightfall - 0.5) * 2)}, ${lerp(10, 5, (nightfall - 0.5) * 2)}, ${lerp(24, 15, (nightfall - 0.5) * 2)})`;\n  const midSky = nightfall < 0.5\n    ? `rgb(${lerp(48, 18, nightfall * 2)}, ${lerp(38, 18, nightfall * 2)}, ${lerp(68, 38, nightfall * 2)})`\n    : `rgb(${lerp(18, 8, (nightfall - 0.5) * 2)}, ${lerp(18, 10, (nightfall - 0.5) * 2)}, ${lerp(38, 24, (nightfall - 0.5) * 2)})`;\n  const lowSky = nightfall < 0.5\n    ? `rgb(${lerp(90, 26, nightfall * 2)}, ${lerp(56, 24, nightfall * 2)}, ${lerp(75, 42, nightfall * 2)})`\n    : `rgb(${lerp(26, 10, (nightfall - 0.5) * 2)}, ${lerp(24, 12, (nightfall - 0.5) * 2)}, ${lerp(42, 26, (nightfall - 0.5) * 2)})`;\n\n  skyGrad.addColorStop(0.0, topSky);\n  skyGrad.addColorStop(0.55, midSky);\n  skyGrad.addColorStop(1.0, lowSky);\n  ctx.fillStyle = skyGrad;\n  ctx.fillRect(0, 0, w * 1.35, h);\n\n  // Rooftop Silhouettes\n  ctx.fillStyle = \"rgba(10, 12, 20, 0.85)\";\n  ctx.beginPath();\n  ctx.moveTo(0, h * 0.40);\n  const skyline = [\n    [0, 260], [90, 250], [140, 210], [210, 210], [220, 245],\n    [320, 230], [390, 195], [440, 195], [460, 255], [580, 240],\n    [640, 205], [720, 205], [740, 260], [860, 235], [920, 185],\n    [980, 185], [1010, 240], [1150, 225], [1260, 200], [1320, 200],\n    [1380, 270], [1480, 270]\n  ];\n  for (let i = 0; i < skyline.length; i++) {\n    ctx.lineTo(skyline[i][0], skyline[i][1] + 15);\n  }\n  ctx.lineTo(w * 1.35, h * 0.45);\n  ctx.lineTo(w * 1.35, h * 0.52);\n  ctx.lineTo(0, h * 0.52);\n  ctx.closePath();\n  ctx.fill();\n\n  // Distant warm window lights\n  const winAlpha = (0.2 + 0.5 * nightfall) * 0.5;\n  ctx.fillStyle = `rgba(255, 185, 95, ${winAlpha})`;\n  const windows = [\n    [160, 220, 8, 12], [180, 225, 7, 10], [350, 238, 6, 8],\n    [410, 210, 10, 14], [670, 220, 8, 12], [690, 230, 8, 10],\n    [945, 200, 9, 14], [960, 215, 7, 10], [1200, 235, 8, 12]\n  ];\n  windows.forEach(([wx, wy, ww, wh]) => {\n    ctx.fillRect(wx, wy + 15, ww, wh);\n  });\n\n  // Atmospheric twilight haze\n  const hazeGrad = ctx.createLinearGradient(0, 160, 0, h * 0.52);\n  hazeGrad.addColorStop(0, \"rgba(25, 20, 35, 0)\");\n  hazeGrad.addColorStop(1, `rgba(18, 22, 36, ${0.4 + nightfall * 0.4})`);\n  ctx.fillStyle = hazeGrad;\n  ctx.fillRect(0, 160, w * 1.35, h * 0.52 - 160);\n\n  // Stars in the Sky\n  if (nightfall > 0.1) {\n    const starAlpha = smoothstep(0.1, 0.85, nightfall);\n    for (let i = 0; i < 70; i++) {\n      const sx = ((i * 137.5 + 45) % (w * 1.25));\n      const sy = 20 + ((i * 93.3 + 12) % (h * 0.30));\n      const twinkle = 0.65 + 0.35 * Math.sin(t * 2.8 + i * 1.9);\n      const sRadius = (0.8 + (i % 3) * 0.5) * starAlpha;\n\n      ctx.save();\n      ctx.globalAlpha = starAlpha * twinkle * (0.35 + 0.65 * ((i % 5) / 5));\n      ctx.fillStyle = i % 7 === 0 ? \"#ffd9aa\" : (i % 4 === 0 ? \"#bde3ff\" : \"#ffffff\");\n      ctx.beginPath();\n      ctx.arc(sx, sy, sRadius, 0, Math.PI * 2);\n      ctx.fill();\n      ctx.restore();\n    }\n  }\n\n  // ==========================================\n  // 2. SIDEWALK, CURB & STREET\n  // ==========================================\n  const curbTopY = h * 0.42;\n  const curbEdgeY = h * 0.48;\n  const gutterWaterTopY = h * 0.51;\n\n  // Sidewalk slab surface\n  const sidewalkGrad = ctx.createLinearGradient(0, curbTopY - 30, 0, curbEdgeY);\n  sidewalkGrad.addColorStop(0, \"rgb(25, 27, 34)\");\n  sidewalkGrad.addColorStop(1, \"rgb(35, 38, 46)\");\n  ctx.fillStyle = sidewalkGrad;\n  ctx.fillRect(0, curbTopY, w * 1.35, curbEdgeY - curbTopY);\n\n  // Pavement joint lines\n  ctx.strokeStyle = \"rgba(15, 17, 22, 0.55)\";\n  ctx.lineWidth = 1.5;\n  for (let sx = 30; sx < w * 1.35; sx += 140) {\n    ctx.beginPath();\n    ctx.moveTo(sx, curbTopY);\n    ctx.lineTo(sx - 35, curbEdgeY);\n    ctx.stroke();\n  }\n\n  // Curbstone bevel (vertical drop)\n  const curbFaceGrad = ctx.createLinearGradient(0, curbEdgeY, 0, gutterWaterTopY);\n  curbFaceGrad.addColorStop(0, \"rgb(52, 56, 66)\");\n  curbFaceGrad.addColorStop(0.35, \"rgb(30, 33, 40)\");\n  curbFaceGrad.addColorStop(1, \"rgb(15, 17, 22)\");\n  ctx.fillStyle = curbFaceGrad;\n  ctx.fillRect(0, curbEdgeY, w * 1.35, gutterWaterTopY - curbEdgeY + 6);\n\n  // Wet curb bevel highlight\n  ctx.strokeStyle = \"rgba(180, 200, 230, 0.22)\";\n  ctx.lineWidth = 1.5;\n  ctx.beginPath();\n  ctx.moveTo(0, curbEdgeY);\n  ctx.lineTo(w * 1.35, curbEdgeY);\n  ctx.stroke();\n\n  // Road asphalt base\n  const asphaltGrad = ctx.createLinearGradient(0, gutterWaterTopY, 0, h);\n  asphaltGrad.addColorStop(0, \"rgb(14, 16, 22)\");\n  asphaltGrad.addColorStop(0.5, \"rgb(9, 11, 16)\");\n  asphaltGrad.addColorStop(1, \"rgb(5, 7, 10)\");\n  ctx.fillStyle = asphaltGrad;\n  ctx.fillRect(0, gutterWaterTopY, w * 1.35, h - gutterWaterTopY);\n\n  // ==========================================\n  // 3. WATER SURFACE (GUTTER -> WIDE PUDDLE)\n  // ==========================================\n  const puddleLeft = lerp(0, -120, puddleOpen);\n  const puddleTopY = lerp(gutterWaterTopY, gutterWaterTopY - 14, puddleOpen);\n\n  const waterBaseGrad = ctx.createLinearGradient(0, puddleTopY, 0, h);\n  waterBaseGrad.addColorStop(0.0, \"rgba(16, 23, 38, 0.95)\");\n  waterBaseGrad.addColorStop(0.4, \"rgba(10, 16, 28, 0.97)\");\n  waterBaseGrad.addColorStop(1.0, \"rgba(5, 8, 15, 0.99)\");\n  ctx.fillStyle = waterBaseGrad;\n  ctx.fillRect(puddleLeft, puddleTopY, w * 1.35, h - puddleTopY);\n\n  // Reflected sky & stars on puddle mirror\n  if (puddleOpen > 0.05) {\n    ctx.save();\n    const mirrorAlpha = smoothstep(0.05, 0.9, puddleOpen) * (0.35 + 0.4 * nightfall);\n    ctx.globalAlpha = mirrorAlpha;\n\n    const refSkyGrad = ctx.createLinearGradient(0, puddleTopY, 0, h);\n    refSkyGrad.addColorStop(0, \"rgba(42, 28, 55, 0.4)\");\n    refSkyGrad.addColorStop(0.4, \"rgba(16, 22, 42, 0.65)\");\n    refSkyGrad.addColorStop(1, \"rgba(6, 11, 24, 0.85)\");\n    ctx.fillStyle = refSkyGrad;\n    ctx.fillRect(puddleLeft, puddleTopY, w * 1.35, h - puddleTopY);\n\n    // Reflected stars in puddle\n    const starAlpha = smoothstep(0.15, 0.85, nightfall);\n    if (starAlpha > 0.08) {\n      for (let i = 0; i < 50; i++) {\n        const rsx = ((i * 147.2 + 80) % (w * 1.2));\n        const rsy = puddleTopY + 35 + ((i * 81.7 + 25) % (h * 0.44));\n        const waterJitter = Math.sin(t * 2.6 + i * 1.7) * 2.2;\n        const rTwinkle = (0.5 + 0.5 * Math.sin(t * 3.0 + i * 2.2)) * starAlpha;\n\n        ctx.fillStyle = i % 5 === 0 ? \"#ffd4a0\" : \"#d8eeff\";\n        ctx.globalAlpha = mirrorAlpha * rTwinkle * 0.8;\n        ctx.beginPath();\n        ctx.ellipse(rsx + waterJitter, rsy, 1.8, 0.9, 0, 0, Math.PI * 2);\n        ctx.fill();\n\n        if (i % 7 === 0) {\n          ctx.globalAlpha = mirrorAlpha * rTwinkle * 0.25;\n          ctx.beginPath();\n          ctx.ellipse(rsx + waterJitter, rsy, 5.5, 1.6, 0, 0, Math.PI * 2);\n          ctx.fill();\n        }\n      }\n    }\n    ctx.restore();\n  }\n\n  // ==========================================\n  // 4. STREETLAMP (ORNATE POST + SOFT GLOW CONE)\n  // ==========================================\n  const lampX = 660;\n  const lampHeadY = 110;\n  const lampBaseY = curbTopY + 14;\n\n  // Lamp Post Silhouette\n  ctx.save();\n  ctx.strokeStyle = \"#111318\";\n  ctx.fillStyle = \"#15181f\";\n\n  // Post pole & ornate base\n  ctx.lineWidth = 11;\n  ctx.beginPath();\n  ctx.moveTo(lampX, lampBaseY);\n  ctx.lineTo(lampX, lampHeadY + 45);\n  ctx.stroke();\n\n  // Decorative post bracket\n  ctx.lineWidth = 4;\n  ctx.beginPath();\n  ctx.moveTo(lampX, lampHeadY + 80);\n  ctx.bezierCurveTo(lampX - 28, lampHeadY + 50, lampX - 38, lampHeadY + 18, lampX - 10, lampHeadY + 6);\n  ctx.lineTo(lampX + 35, lampHeadY + 6);\n  ctx.stroke();\n\n  // Lantern Housing\n  ctx.fillStyle = \"#1c2028\";\n  ctx.beginPath();\n  ctx.moveTo(lampX - 24, lampHeadY + 12);\n  ctx.lineTo(lampX + 24, lampHeadY + 12);\n  ctx.lineTo(lampX + 16, lampHeadY + 46);\n  ctx.lineTo(lampX - 16, lampHeadY + 46);\n  ctx.closePath();\n  ctx.fill();\n  ctx.stroke();\n\n  // Lantern Cap\n  ctx.beginPath();\n  ctx.moveTo(lampX - 30, lampHeadY + 12);\n  ctx.lineTo(lampX, lampHeadY - 9);\n  ctx.lineTo(lampX + 30, lampHeadY + 12);\n  ctx.closePath();\n  ctx.fill();\n\n  // --- Warm Multi-layered Halo around Lamp Head ---\n  const haloRad1 = ctx.createRadialGradient(lampX, lampHeadY + 28, 2, lampX, lampHeadY + 28, 55);\n  haloRad1.addColorStop(0, \"rgba(255, 245, 210, 0.95)\");\n  haloRad1.addColorStop(0.3, \"rgba(255, 205, 110, 0.65)\");\n  haloRad1.addColorStop(0.7, \"rgba(255, 160, 60, 0.2)\");\n  haloRad1.addColorStop(1, \"rgba(255, 140, 40, 0)\");\n  ctx.fillStyle = haloRad1;\n  ctx.beginPath();\n  ctx.arc(lampX, lampHeadY + 28, 55, 0, Math.PI * 2);\n  ctx.fill();\n\n  const haloRad2 = ctx.createRadialGradient(lampX, lampHeadY + 28, 20, lampX, lampHeadY + 28, 190);\n  haloRad2.addColorStop(0, \"rgba(255, 210, 110, 0.28)\");\n  haloRad2.addColorStop(0.5, \"rgba(255, 165, 60, 0.10)\");\n  haloRad2.addColorStop(1, \"rgba(240, 130, 40, 0)\");\n  ctx.fillStyle = haloRad2;\n  ctx.beginPath();\n  ctx.arc(lampX, lampHeadY + 28, 190, 0, Math.PI * 2);\n  ctx.fill();\n\n  // --- Soft Luminous Light Cone (Graded, feathered, realistic volumetric haze) ---\n  // Multiple soft radial & linear passes to avoid harsh polygon edges\n  for (let c = 0; c < 3; c++) {\n    const spread = 280 + c * 80;\n    const coneGrad = ctx.createRadialGradient(\n      lampX, lampHeadY + 35, 10,\n      lampX, lampHeadY + 360, spread\n    );\n    coneGrad.addColorStop(0, `rgba(255, 230, 160, ${0.28 - c * 0.07})`);\n    coneGrad.addColorStop(0.35, `rgba(255, 195, 105, ${0.16 - c * 0.04})`);\n    coneGrad.addColorStop(0.7, `rgba(240, 150, 60, ${0.06 - c * 0.015})`);\n    coneGrad.addColorStop(1, \"rgba(220, 130, 40, 0)\");\n\n    ctx.fillStyle = coneGrad;\n    ctx.beginPath();\n    ctx.moveTo(lampX - 18, lampHeadY + 45);\n    ctx.quadraticCurveTo(lampX - 160 - c * 60, h * 0.7, lampX - 360 - c * 80, h);\n    ctx.lineTo(lampX + 360 + c * 80, h);\n    ctx.quadraticCurveTo(lampX + 160 + c * 60, h * 0.7, lampX + 18, lampHeadY + 45);\n    ctx.closePath();\n    ctx.fill();\n  }\n\n  // Broad soft pool on the ground\n  const groundPool = ctx.createRadialGradient(lampX - 10, h * 0.72, 30, lampX - 10, h * 0.72, 340);\n  groundPool.addColorStop(0, \"rgba(255, 215, 125, 0.28)\");\n  groundPool.addColorStop(0.4, \"rgba(255, 175, 80, 0.14)\");\n  groundPool.addColorStop(0.8, \"rgba(220, 130, 45, 0.04)\");\n  groundPool.addColorStop(1, \"rgba(200, 100, 30, 0)\");\n  ctx.fillStyle = groundPool;\n  ctx.beginPath();\n  ctx.ellipse(lampX - 10, h * 0.72, 340, 170, 0, 0, Math.PI * 2);\n  ctx.fill();\n\n  // Shimmering horizontal water ripples under streetlamp\n  const lampReflectX = lampX - 15;\n  for (let ry = gutterWaterTopY + 12; ry < h - 25; ry += 6) {\n    const depthRatio = (ry - gutterWaterTopY) / (h - gutterWaterTopY);\n    const waveDistort = Math.sin(ry * 0.16 - t * 3.4) * 26 + Math.cos(ry * 0.09 + t * 2.2) * 16;\n    const bandWidth = (160 + depthRatio * 200) * Math.max(0.1, Math.cos((ry - 510) * 0.015));\n    const bandAlpha = (0.22 + 0.25 * Math.sin(t * 3.6 + ry * 0.28)) * (1 - Math.abs(depthRatio - 0.5) * 1.3);\n\n    if (bandAlpha > 0.02) {\n      const glintGrad = ctx.createLinearGradient(\n        lampReflectX - bandWidth + waveDistort, ry,\n        lampReflectX + bandWidth + waveDistort, ry\n      );\n      glintGrad.addColorStop(0, \"rgba(255, 195, 100, 0)\");\n      glintGrad.addColorStop(0.3, `rgba(255, 225, 140, ${bandAlpha * 0.65})`);\n      glintGrad.addColorStop(0.5, `rgba(255, 250, 195, ${bandAlpha})`);\n      glintGrad.addColorStop(0.7, `rgba(255, 210, 115, ${bandAlpha * 0.65})`);\n      glintGrad.addColorStop(1, \"rgba(255, 165, 75, 0)\");\n\n      ctx.strokeStyle = glintGrad;\n      ctx.lineWidth = lerp(1.4, 3.4, depthRatio);\n      ctx.beginPath();\n      ctx.moveTo(lampReflectX - bandWidth + waveDistort, ry);\n      ctx.lineTo(lampReflectX + bandWidth + waveDistort, ry);\n      ctx.stroke();\n    }\n  }\n  ctx.restore();\n\n  // ==========================================\n  // 5. EXPANDING WATER RIPPLES (RAINDROP IMPACTS)\n  // ==========================================\n  // Clear concentric expanding elliptical rings\n  const rippleCount = 28;\n  ctx.save();\n  for (let i = 0; i < rippleCount; i++) {\n    const period = 2.8;\n    const phaseOffset = (i * 0.47) * period;\n    const cycle = ((t + phaseOffset) % period) / period; // 0..1\n\n    // Seeded distribution across gutter and puddle\n    const rx = ((i * 187.3 + 40) % (w * 1.2));\n    const ry = gutterWaterTopY + 15 + ((i * 83.1 + 20) % (h * 0.44));\n\n    const maxRadius = 38 + (i % 5) * 16;\n    const rX = cycle * maxRadius;\n    const rY = rX * 0.32; // Perspective compression on water surface\n\n    // Fade in quickly, then gently fade out as ring expands\n    const ringAlpha = Math.sin(cycle * Math.PI) * (1 - cycle * 0.6) * 0.42;\n\n    if (ringAlpha > 0.01) {\n      const distLamp = Math.abs(rx - lampX);\n      const isUnderLamp = distLamp < 280;\n      \n      const strokeColor = isUnderLamp\n        ? `rgba(255, 220, 145, ${ringAlpha * (1 - distLamp / 360)})`\n        : `rgba(185, 215, 250, ${ringAlpha * 0.65})`;\n\n      // Outer expanding ring\n      ctx.strokeStyle = strokeColor;\n      ctx.lineWidth = lerp(1.6, 0.7, cycle);\n      ctx.beginPath();\n      ctx.ellipse(rx, ry, rX, rY, 0, 0, Math.PI * 2);\n      ctx.stroke();\n\n      // Inner secondary concentric echo ring\n      if (cycle > 0.2) {\n        const innerCycle = cycle - 0.18;\n        const innerRX = innerCycle * maxRadius;\n        const innerRY = innerRX * 0.32;\n        ctx.strokeStyle = isUnderLamp\n          ? `rgba(255, 235, 175, ${ringAlpha * 0.45})`\n          : `rgba(195, 225, 255, ${ringAlpha * 0.35})`;\n        ctx.lineWidth = lerp(1.2, 0.5, innerCycle);\n        ctx.beginPath();\n        ctx.ellipse(rx, ry, innerRX, innerRY, 0, 0, Math.PI * 2);\n        ctx.stroke();\n      }\n    }\n  }\n  ctx.restore();\n\n  // Floating autumn leaves drifting along\n  const leaves = [\n    { seedX: 110, seedY: gutterWaterTopY + 45, scale: 1.2, rot: 0.3, col: \"#8a3c20\" },\n    { seedX: 360, seedY: gutterWaterTopY + 110, scale: 1.4, rot: -0.6, col: \"#a5541e\" },\n    { seedX: 720, seedY: gutterWaterTopY + 70, scale: 1.1, rot: 1.1, col: \"#6c2a1a\" },\n    { seedX: 980, seedY: gutterWaterTopY + 160, scale: 1.3, rot: -0.4, col: \"#92461e\" },\n    { seedX: 1200, seedY: gutterWaterTopY + 130, scale: 1.2, rot: 0.7, col: \"#7a3518\" }\n  ];\n\n  leaves.forEach((lf, idx) => {\n    const lx = (lf.seedX + t * 20 + Math.sin(t * 0.8 + idx) * 14) % (w * 1.35);\n    const ly = lf.seedY + Math.sin(t * 1.2 + idx * 2) * 5;\n    const lRot = lf.rot + Math.sin(t * 0.9 + idx) * 0.15;\n\n    ctx.save();\n    ctx.translate(lx, ly);\n    ctx.rotate(lRot);\n    ctx.scale(lf.scale, lf.scale * 0.5);\n\n    ctx.fillStyle = \"rgba(0, 0, 0, 0.45)\";\n    ctx.beginPath();\n    ctx.ellipse(2, 6, 14, 7, 0, 0, Math.PI * 2);\n    ctx.fill();\n\n    ctx.fillStyle = lf.col;\n    ctx.beginPath();\n    ctx.moveTo(-14, 0);\n    ctx.quadraticCurveTo(0, -10, 14, 0);\n    ctx.quadraticCurveTo(0, 10, -14, 0);\n    ctx.fill();\n    ctx.restore();\n  });\n\n  // ==========================================\n  // 6. THE PROMINENT PAPER BOAT & WATER REFLECTION\n  // ==========================================\n  // Large, heroic origami subject (~3x larger than initial draft)\n  const boatProgress = stage;\n  const boatX = lerp(-120, 840, Math.pow(boatProgress, 0.92));\n  const boatBaseY = lerp(gutterWaterTopY + 80, gutterWaterTopY + 185, smoothstep(0.35, 0.92, stage));\n\n  // Natural water bobbing\n  const bobHeave = Math.sin(t * 3.0) * 4.5 + Math.cos(t * 4.5) * 2.0;\n  const boatPitch = Math.sin(t * 2.6 + 0.3) * 0.048 + Math.cos(t * 1.8) * 0.024;\n  const boatY = boatBaseY + bobHeave;\n\n  // Lamp proximity factor for dynamic warm/cool lighting on boat facets\n  const boatLampProx = Math.exp(-Math.pow((boatX - lampX) / 220, 2));\n\n  // Color tuning: cool dusk paper transitioning into warm golden streetlamp\n  const pr = lerp(210, 255, boatLampProx * 0.7);\n  const pg = lerp(220, 238, boatLampProx * 0.6);\n  const pb = lerp(235, 180, boatLampProx * 0.75);\n\n  ctx.save();\n  ctx.translate(boatX, boatY);\n\n  // --- Water Shadow & Wake ---\n  const shadowGrad = ctx.createRadialGradient(0, 16, 10, 0, 16, 150);\n  shadowGrad.addColorStop(0, \"rgba(2, 4, 10, 0.85)\");\n  shadowGrad.addColorStop(0.5, \"rgba(5, 10, 20, 0.45)\");\n  shadowGrad.addColorStop(1, \"rgba(10, 18, 30, 0)\");\n  ctx.fillStyle = shadowGrad;\n  ctx.beginPath();\n  ctx.ellipse(0, 20, 140, 36, 0, 0, Math.PI * 2);\n  ctx.fill();\n\n  // Expanding wake rings around the boat\n  for (let wIdx = 1; wIdx <= 3; wIdx++) {\n    const wakeAge = (t * 1.8 + wIdx * 0.9) % 2.7;\n    const wakeX = -50 - wakeAge * 45;\n    const wakeW = 35 + wakeAge * 55;\n    const wakeH = 12 + wakeAge * 16;\n    const wakeAlpha = (1 - wakeAge / 2.7) * 0.28;\n\n    ctx.strokeStyle = boatLampProx > 0.3 \n      ? `rgba(255, 225, 160, ${wakeAlpha * boatLampProx})` \n      : `rgba(180, 215, 255, ${wakeAlpha})`;\n    ctx.lineWidth = 1.6;\n    ctx.beginPath();\n    ctx.ellipse(wakeX, 22, wakeW, wakeH, -0.04, 0, Math.PI * 2);\n    ctx.stroke();\n  }\n\n  // --- INVERTED WATER REFLECTION BENEATH BOAT ---\n  ctx.save();\n  ctx.translate(0, 24); // Surface waterline contact\n  ctx.scale(1, -0.75);   // Inverted and compressed vertically\n  ctx.globalAlpha = 0.42 + boatLampProx * 0.25;\n\n  // Wavy horizontal distortion for reflection\n  const refWave = Math.sin(t * 3.5) * 6;\n  ctx.transform(1, 0, Math.sin(t * 4.0) * 0.08, 1, refWave, 0);\n\n  // Reflected Main Sail\n  const refSailGrad = ctx.createLinearGradient(0, 0, 0, -110);\n  refSailGrad.addColorStop(0, `rgb(${pr * 0.8}, ${pg * 0.8}, ${pb * 0.8})`);\n  refSailGrad.addColorStop(1, \"rgba(10, 18, 32, 0)\");\n  ctx.fillStyle = refSailGrad;\n  ctx.beginPath();\n  ctx.moveTo(0, -115);\n  ctx.lineTo(-65, 0);\n  ctx.lineTo(65, 0);\n  ctx.closePath();\n  ctx.fill();\n\n  // Reflected Hull\n  const refHullGrad = ctx.createLinearGradient(0, 0, 0, 35);\n  refHullGrad.addColorStop(0, `rgb(${pr * 0.75}, ${pg * 0.75}, ${pb * 0.75})`);\n  refHullGrad.addColorStop(1, \"rgba(5, 10, 20, 0)\");\n  ctx.fillStyle = refHullGrad;\n  ctx.beginPath();\n  ctx.moveTo(-145, -5);\n  ctx.lineTo(145, -5);\n  ctx.lineTo(40, 35);\n  ctx.lineTo(-40, 35);\n  ctx.closePath();\n  ctx.fill();\n\n  // Horizontal ripple ripples slicing through reflection\n  ctx.strokeStyle = \"rgba(10, 18, 30, 0.6)\";\n  ctx.lineWidth = 3;\n  for (let sly = -80; sly < 30; sly += 12) {\n    ctx.beginPath();\n    ctx.moveTo(-120, sly + Math.sin(sly + t * 4) * 4);\n    ctx.lineTo(120, sly + Math.sin(sly + t * 4) * 4);\n    ctx.stroke();\n  }\n  ctx.restore();\n\n  // --- THE FOLDED PAPER BOAT (ORIGAMI CRAFT) ---\n  ctx.rotate(boatPitch);\n\n  // 1. Back/Port Sail Triangle (in shade)\n  const backSailGrad = ctx.createLinearGradient(-15, -115, -70, 0);\n  backSailGrad.addColorStop(0, `rgb(${pr * 0.72}, ${pg * 0.73}, ${pb * 0.77})`);\n  backSailGrad.addColorStop(1, `rgb(${pr * 0.58}, ${pg * 0.60}, ${pb * 0.65})`);\n  ctx.fillStyle = backSailGrad;\n  ctx.beginPath();\n  ctx.moveTo(0, -115); // Mast peak\n  ctx.lineTo(-65, 0);   // Port deck fold\n  ctx.lineTo(0, 10);    // Center spine fold\n  ctx.closePath();\n  ctx.fill();\n  ctx.strokeStyle = \"rgba(0, 0, 0, 0.2)\";\n  ctx.lineWidth = 1.2;\n  ctx.stroke();\n\n  // 2. Front/Starboard Sail Triangle (catching key light)\n  const frontSailGrad = ctx.createLinearGradient(0, -115, 65, 10);\n  frontSailGrad.addColorStop(0, `rgb(${pr * 1.05}, ${pg * 1.04}, ${pb * 1.02})`);\n  frontSailGrad.addColorStop(0.6, `rgb(${pr * 0.92}, ${pg * 0.90}, ${pb * 0.88})`);\n  frontSailGrad.addColorStop(1, `rgb(${pr * 0.82}, ${pg * 0.80}, ${pb * 0.78})`);\n  ctx.fillStyle = frontSailGrad;\n  ctx.beginPath();\n  ctx.moveTo(0, -115); // Mast peak\n  ctx.lineTo(0, 10);    // Center spine fold\n  ctx.lineTo(65, 0);    // Starboard deck fold\n  ctx.closePath();\n  ctx.fill();\n  ctx.stroke();\n\n  // 3. Left Hull Bow Flap\n  const leftHullGrad = ctx.createLinearGradient(-145, -6, 0, 32);\n  leftHullGrad.addColorStop(0, `rgb(${pr * 0.82}, ${pg * 0.83}, ${pb * 0.86})`);\n  leftHullGrad.addColorStop(1, `rgb(${pr * 0.64}, ${pg * 0.66}, ${pb * 0.70})`);\n  ctx.fillStyle = leftHullGrad;\n  ctx.beginPath();\n  ctx.moveTo(-145, -6); // Bow point\n  ctx.lineTo(-38, 32);  // Port keel fold\n  ctx.lineTo(0, 35);    // Keel center\n  ctx.lineTo(-65, 0);   // Inner deck fold\n  ctx.closePath();\n  ctx.fill();\n  ctx.stroke();\n\n  // 4. Right Hull Stern Flap (facing light)\n  const rightHullGrad = ctx.createLinearGradient(0, 0, 145, 32);\n  rightHullGrad.addColorStop(0, `rgb(${pr * 0.98}, ${pg * 0.96}, ${pb * 0.92})`);\n  rightHullGrad.addColorStop(0.6, `rgb(${pr * 0.86}, ${pg * 0.84}, ${pb * 0.80})`);\n  rightHullGrad.addColorStop(1, `rgb(${pr * 0.74}, ${pg * 0.72}, ${pb * 0.68})`);\n  ctx.fillStyle = rightHullGrad;\n  ctx.beginPath();\n  ctx.moveTo(0, 35);    // Keel center\n  ctx.lineTo(38, 32);   // Starboard keel fold\n  ctx.lineTo(145, -6);  // Stern point\n  ctx.lineTo(65, 0);    // Inner deck fold\n  ctx.closePath();\n  ctx.fill();\n  ctx.stroke();\n\n  // 5. Center Bottom Keel Flap (Origami classic tuck)\n  const keelGrad = ctx.createLinearGradient(0, 10, 0, 42);\n  keelGrad.addColorStop(0, `rgb(${pr * 0.55}, ${pg * 0.57}, ${pb * 0.62})`);\n  keelGrad.addColorStop(1, `rgb(${pr * 0.42}, ${pg * 0.44}, ${pb * 0.48})`);\n  ctx.fillStyle = keelGrad;\n  ctx.beginPath();\n  ctx.moveTo(-38, 32);\n  ctx.lineTo(0, 42);\n  ctx.lineTo(38, 32);\n  ctx.lineTo(0, 10);\n  ctx.closePath();\n  ctx.fill();\n  ctx.stroke();\n\n  // --- Distinct Origami Crease & Fold Lines ---\n  ctx.strokeStyle = `rgba(255, 255, 255, ${0.45 + boatLampProx * 0.45})`;\n  ctx.lineWidth = 1.6;\n  ctx.beginPath();\n  // Mast ridge\n  ctx.moveTo(0, -115);\n  ctx.lineTo(0, 10);\n  // Outer deck fold line\n  ctx.moveTo(-145, -6);\n  ctx.lineTo(145, -6);\n  ctx.stroke();\n\n  // Inner shadow along central deck crease\n  ctx.strokeStyle = \"rgba(0, 0, 0, 0.28)\";\n  ctx.lineWidth = 1.5;\n  ctx.beginPath();\n  ctx.moveTo(-65, 0);\n  ctx.lineTo(0, 10);\n  ctx.lineTo(65, 0);\n  ctx.stroke();\n\n  // Waterline contact specular highlight\n  ctx.strokeStyle = `rgba(225, 245, 255, ${0.4 + boatLampProx * 0.45})`;\n  ctx.lineWidth = 2.4;\n  ctx.beginPath();\n  ctx.moveTo(-130, 8);\n  ctx.quadraticCurveTo(0, 40, 130, 8);\n  ctx.stroke();\n\n  ctx.restore(); // Exit boat local coords\n\n  // ==========================================\n  // 7. FINE SLANTED RAIN STREAKS\n  // ==========================================\n  // Rain calms slightly as the sky opens into starry night\n  const rainStrength = lerp(0.9, 0.4, puddleOpen);\n  const dropTotal = Math.floor(130 * rainStrength);\n\n  ctx.save();\n  ctx.lineWidth = 1.3;\n  ctx.lineCap = \"round\";\n\n  for (let i = 0; i < dropTotal; i++) {\n    const dropSpeed = 780 + (i % 6) * 90;\n    const slantX = 65; // Slanted by breeze\n    const dropY = ((t * dropSpeed + i * 41.7) % (h + 120)) - 60;\n    const dropX = ((i * 31.9 + t * slantX) % (w * 1.35)) - 40;\n    const dropLen = 18 + (i % 5) * 8;\n\n    // Rain brightness increases dramatically when passing through streetlamp cone\n    const distToLamp = Math.abs(dropX - lampX);\n    const inLampGlow = distToLamp < 220 && dropY > lampHeadY;\n    const dropAlpha = (inLampGlow ? 0.65 : 0.22) * rainStrength;\n\n    ctx.strokeStyle = inLampGlow\n      ? `rgba(255, 235, 175, ${dropAlpha})`\n      : `rgba(195, 225, 255, ${dropAlpha})`;\n\n    ctx.beginPath();\n    ctx.moveTo(dropX, dropY);\n    ctx.lineTo(dropX + 4.5, dropY + dropLen);\n    ctx.stroke();\n  }\n  ctx.restore();\n\n  // ==========================================\n  // 8. VIGNETTE & ATMOSPHERE\n  // ==========================================\n  ctx.restore(); // Restore global camera pan\n\n  // Painterly edge vignette\n  const vignette = ctx.createRadialGradient(w * 0.5, h * 0.5, h * 0.35, w * 0.5, h * 0.5, w * 0.72);\n  vignette.addColorStop(0, \"rgba(0, 0, 0, 0)\");\n  vignette.addColorStop(0.7, \"rgba(3, 5, 12, 0.22)\");\n  vignette.addColorStop(1, \"rgba(1, 2, 6, 0.75)\");\n  ctx.fillStyle = vignette;\n  ctx.fillRect(0, 0, w, h);\n\n  // ==========================================\n  // 9. CLOSING TITLE: \"PAPER BOAT\"\n  // ==========================================\n  // Appears gracefully in clear upper sky space (20s to 24s)\n  if (t > 19.5) {\n    const titleAlpha = smoothstep(19.5, 21.8, t);\n    const titleFloat = lerp(10, 0, smoothstep(19.5, 22.5, t));\n\n    ctx.save();\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n\n    const titleX = w * 0.5;\n    const titleY = h * 0.20 + titleFloat;\n\n    // Soft luminous radial backdrop halo behind title\n    const titleGlow = ctx.createRadialGradient(titleX, titleY, 10, titleX, titleY, 180);\n    titleGlow.addColorStop(0, `rgba(255, 240, 215, ${titleAlpha * 0.16})`);\n    titleGlow.addColorStop(1, \"rgba(255, 240, 215, 0)\");\n    ctx.fillStyle = titleGlow;\n    ctx.beginPath();\n    ctx.arc(titleX, titleY, 180, 0, Math.PI * 2);\n    ctx.fill();\n\n    // Elegant spaced serif title\n    ctx.font = \"300 28px Georgia, 'Times New Roman', serif\";\n    ctx.letterSpacing = \"0.42em\";\n\n    // Shadow\n    ctx.fillStyle = `rgba(3, 6, 14, ${titleAlpha * 0.95})`;\n    ctx.fillText(\"P A P E R   B O A T\", titleX + 1, titleY + 2);\n\n    // Warm ivory / starlight text\n    ctx.fillStyle = `rgba(250, 246, 238, ${titleAlpha * 0.96})`;\n    ctx.fillText(\"P A P E R   B O A T\", titleX, titleY);\n\n    // Subtle underline rule\n    const ruleW = 46 * titleAlpha;\n    ctx.strokeStyle = `rgba(215, 228, 248, ${titleAlpha * 0.45})`;\n    ctx.lineWidth = 1.0;\n    ctx.beginPath();\n    ctx.moveTo(titleX - ruleW, titleY + 24);\n    ctx.lineTo(titleX + ruleW, titleY + 24);\n    ctx.stroke();\n\n    ctx.restore();\n  }\n}\n","exportProfile":{"format":"mp4","resolution":"720p","fps":24,"durationSeconds":24},"source":{"prompt":"A 24-second illustrated film: a folded paper boat drifts down a rain-filled street gutter at dusk, passes beneath a streetlamp whose glow ripples on the water, and sails out into a wide puddle that reflects the first stars. Closing title: PAPER BOAT. Paint it, do not assemble it: graded skies, soft radial glows built from gradients (never a flat translucent disc), layered depth with atmospheric fade, paper-cut shapes with subtle shadows, gentle continuous motion, and one clear moment of change. No text except a small closing title in clear space. Every frame should stand as a composed picture.","provider":"google","model":"gemini-3.7-flash · 1 revision","app":"gallery-reproduction"}},"license":"CC0-1.0"}