{"id":"the-last-light-drawn","title":"The Last Light (drawn)","category":"Drawn film","prompt":"A 45-second quiet illustrated story: The Last Light. Cast the lighthouse as pp:Lighthouse variant ivory-lighthouse and the boat as pp:Sailboat variant amber-sailboat. The generic default variants are a house and car and must not be used. At dusk a lighthouse on a rocky island has gone dark; a small amber boat approaches the rocks. Establish the danger, show the unlit lantern close up, then the lantern ignites and its beam reaches the boat. The boat changes course away from the rocks and reaches sheltered water. End on the same lighthouse framing as the opening, now shining, with the boat safe. A consistent editorial papercut treatment: midnight blue sea, warm ivory lighthouse with one amber lantern and a dark slate roof, layered angular rocks, amber boat with a single ivory sail. Reuse exactly these silhouettes and colours across shots. Alternate wide views and meaningful detail shots. Use cuts for cause and effect and a dissolve only for elapsed time. Deliberate holds allow the viewer to understand the rescue. Keep text to the opening title THE LAST LIGHT and the closing line HOME, SAFELY, placed in clear empty sky with strong contrast. No narration, invented statistics, decorative particles or unrelated characters. The boat and lighthouse must remain recognizable throughout. Show the events visually, rather than explaining them with captions.","designedBy":{"provider":"google","model":"gemini-3.7-flash","inference":{"inputTokens":7313,"outputTokens":11604,"costUsd":0.1176}},"render":{"credits":18,"priceUsd":0.27,"format":"mp4","renderMs":207000,"frameCount":1080,"bytes":3047763,"seed":7,"width":1280,"height":720,"fps":24,"durationSeconds":45},"howToReproduce":{"renderApi":"POST /v1/renders with `renderRequest` below as the body (Authorization: Bearer <your pp_sa_… key>).","chat":"Paste the prompt into /chat: an illustrated film brief is answered with a program like this one, and plays at once.","note":"This is a draw contract — a Canvas 2D program. It has no editor ops; it opens in the Cloud Studio as a film (/studio?gallery-film=the-last-light-drawn), and it exports as MP4 or GIF, not as a still."},"renderRequest":{"drawContract":"const meta = { duration: 45, fps: 24, size: [1280, 720], seed: 7 };\n\nfunction draw(ctx, t, params, rng) {\n  const W = 1280;\n  const H = 720;\n\n  // -------------------------------------------------------------\n  // PALETTE & STYLES (Papercut Editorial)\n  // -------------------------------------------------------------\n  const C = {\n    skyTopDusk: \"#091224\",\n    skyMidDusk: \"#162847\",\n    skyLowDusk: \"#2C3D55\",\n    skyHorizonDusk: \"#4A4650\",\n    skyTopNight: \"#040914\",\n    skyMidNight: \"#09152B\",\n    skyLowNight: \"#0E203C\",\n    seaDeep: \"#071322\",\n    seaMid: \"#0B1D36\",\n    seaLayer1: \"#0F2646\",\n    seaLayer2: \"#14325C\",\n    seaLayer3: \"#1C4378\",\n    seaFoam: \"#4772A3\",\n    ivoryLight: \"#FDFBF7\",\n    ivoryShade: \"#E8DFC8\",\n    ivoryDeep: \"#D1C2A5\",\n    slateRoof: \"#1E293B\",\n    slateDark: \"#0F172A\",\n    amberGlow: \"#FFB020\",\n    amberCore: \"#FFF3D1\",\n    amberBoat: \"#C25E00\",\n    amberBoatHighlight: \"#E07A16\",\n    amberBoatDark: \"#873B00\",\n    sailIvory: \"#FAF6EE\",\n    sailShade: \"#DFD5C2\",\n    rock1: \"#151F2E\",\n    rock2: \"#1F2E42\",\n    rock3: \"#2B3D55\",\n    rockLight: \"#3B506B\"\n  };\n\n  // Helper: easing\n  const clamp = (v, min = 0, max = 1) => Math.max(min, Math.min(max, v));\n  const easeInOut = (u) => u * u * (3 - 2 * u);\n  const easeOut = (u) => 1 - Math.pow(1 - u, 3);\n  const easeIn = (u) => u * u * u;\n\n  // Draw background sky gradient\n  function drawSky(topCol, midCol, horizonCol) {\n    const grad = ctx.createLinearGradient(0, 0, 0, H * 0.75);\n    grad.addColorStop(0, topCol);\n    grad.addColorStop(0.55, midCol);\n    grad.addColorStop(1, horizonCol);\n    ctx.fillStyle = grad;\n    ctx.fillRect(0, 0, W, H);\n  }\n\n  // Draw papercut stars\n  function drawStars(alpha = 1) {\n    if (alpha <= 0) return;\n    ctx.save();\n    ctx.fillStyle = `rgba(255, 250, 230, ${alpha * 0.75})`;\n    const starCoords = [\n      [120, 60, 1.5], [240, 90, 1], [380, 45, 2], [510, 110, 1.2],\n      [680, 50, 1.8], [820, 75, 1], [940, 40, 2.2], [1050, 95, 1.3],\n      [1170, 60, 1.7], [180, 160, 1.2], [320, 190, 0.8], [890, 150, 1.4],\n      [1120, 180, 1], [60, 130, 0.9], [740, 130, 1.6], [980, 120, 0.9]\n    ];\n    for (let i = 0; i < starCoords.length; i++) {\n      const [sx, sy, sr] = starCoords[i];\n      const twinkle = 0.8 + 0.2 * Math.sin(t * 1.5 + i * 1.7);\n      ctx.beginPath();\n      ctx.arc(sx, sy, sr * twinkle, 0, Math.PI * 2);\n      ctx.fill();\n    }\n    ctx.restore();\n  }\n\n  // Helper: paper shadow\n  function applyShadow(blur = 12, offY = 6, alpha = 0.45) {\n    ctx.shadowColor = `rgba(3, 7, 18, ${alpha})`;\n    ctx.shadowBlur = blur;\n    ctx.shadowOffsetX = 0;\n    ctx.shadowOffsetY = offY;\n  }\n  function clearShadow() {\n    ctx.shadowColor = \"transparent\";\n    ctx.shadowBlur = 0;\n    ctx.shadowOffsetX = 0;\n    ctx.shadowOffsetY = 0;\n  }\n\n  // Helper: Sailboat (amber boat with ivory sail)\n  function drawSailboat(x, y, scale = 1, pitchDeg = 0, facingLeft = true) {\n    ctx.save();\n    ctx.translate(x, y);\n    ctx.rotate((pitchDeg * Math.PI) / 180);\n    if (!facingLeft) ctx.scale(-1, 1);\n    ctx.scale(scale, scale);\n\n    // Subtle water reflection\n    ctx.save();\n    ctx.globalAlpha = 0.25;\n    ctx.fillStyle = C.amberBoatDark;\n    ctx.beginPath();\n    ctx.ellipse(0, 18, 48, 6, 0, 0, Math.PI * 2);\n    ctx.fill();\n    ctx.restore();\n\n    // Hull shadow & body\n    applyShadow(8, 4, 0.35);\n\n    // Amber Hull\n    ctx.fillStyle = C.amberBoat;\n    ctx.beginPath();\n    ctx.moveTo(-52, -2);\n    ctx.bezierCurveTo(-46, 14, -20, 20, 0, 20);\n    ctx.bezierCurveTo(24, 20, 48, 14, 56, -4);\n    ctx.lineTo(44, -4);\n    ctx.bezierCurveTo(20, 12, -24, 12, -44, -2);\n    ctx.closePath();\n    ctx.fill();\n\n    // Hull inner facet / gunwale\n    ctx.fillStyle = C.amberBoatHighlight;\n    ctx.beginPath();\n    ctx.moveTo(-52, -2);\n    ctx.lineTo(56, -4);\n    ctx.lineTo(50, -8);\n    ctx.lineTo(-44, -6);\n    ctx.closePath();\n    ctx.fill();\n\n    // Dark trim\n    ctx.fillStyle = C.amberBoatDark;\n    ctx.beginPath();\n    ctx.moveTo(-50, 0);\n    ctx.lineTo(52, -2);\n    ctx.lineTo(52, 2);\n    ctx.lineTo(-48, 4);\n    ctx.closePath();\n    ctx.fill();\n\n    clearShadow();\n\n    // Mast\n    ctx.fillStyle = C.slateDark;\n    ctx.fillRect(-2, -78, 4, 76);\n\n    // Rigging line\n    ctx.strokeStyle = \"rgba(255,255,255,0.25)\";\n    ctx.lineWidth = 1.2;\n    ctx.beginPath();\n    ctx.moveTo(0, -74);\n    ctx.lineTo(-42, -4);\n    ctx.moveTo(0, -74);\n    ctx.lineTo(48, -4);\n    ctx.stroke();\n\n    // Ivory Mainsail\n    applyShadow(6, 3, 0.25);\n    ctx.fillStyle = C.sailIvory;\n    ctx.beginPath();\n    ctx.moveTo(2, -72);\n    ctx.bezierCurveTo(18, -46, 26, -20, 42, -8);\n    ctx.lineTo(2, -8);\n    ctx.closePath();\n    ctx.fill();\n\n    // Sail shadow / seam facet\n    ctx.fillStyle = C.sailShade;\n    ctx.beginPath();\n    ctx.moveTo(2, -72);\n    ctx.lineTo(14, -36);\n    ctx.lineTo(2, -8);\n    ctx.closePath();\n    ctx.fill();\n\n    // Jib Sail (front)\n    ctx.fillStyle = C.sailIvory;\n    ctx.beginPath();\n    ctx.moveTo(-2, -66);\n    ctx.lineTo(-2, -8);\n    ctx.bezierCurveTo(-18, -12, -32, -8, -38, -6);\n    ctx.closePath();\n    ctx.fill();\n\n    ctx.fillStyle = C.sailShade;\n    ctx.beginPath();\n    ctx.moveTo(-2, -66);\n    ctx.lineTo(-12, -32);\n    ctx.lineTo(-2, -8);\n    ctx.closePath();\n    ctx.fill();\n\n    clearShadow();\n    ctx.restore();\n  }\n\n  // Helper: Lighthouse (warm ivory, dark slate roof, amber lantern)\n  function drawLighthouse(x, y, scale = 1, beamAngle = null, glowIntensity = 0) {\n    ctx.save();\n    ctx.translate(x, y);\n    ctx.scale(scale, scale);\n\n    // Shadow for the entire tower\n    applyShadow(14, 8, 0.4);\n\n    // 1. Base / foundation (slate & ivory)\n    ctx.fillStyle = C.slateDark;\n    ctx.beginPath();\n    ctx.moveTo(-44, 0);\n    ctx.lineTo(44, 0);\n    ctx.lineTo(40, -18);\n    ctx.lineTo(-40, -18);\n    ctx.closePath();\n    ctx.fill();\n\n    // 2. Ivory Tapered Tower\n    ctx.fillStyle = C.ivoryLight;\n    ctx.beginPath();\n    ctx.moveTo(-38, -18);\n    ctx.lineTo(38, -18);\n    ctx.lineTo(24, -180);\n    ctx.lineTo(-24, -180);\n    ctx.closePath();\n    ctx.fill();\n\n    // Ivory tower shading (editorial papercut facet)\n    ctx.fillStyle = C.ivoryShade;\n    ctx.beginPath();\n    ctx.moveTo(0, -18);\n    ctx.lineTo(38, -18);\n    ctx.lineTo(24, -180);\n    ctx.lineTo(0, -180);\n    ctx.closePath();\n    ctx.fill();\n\n    // Darker rim edge on left\n    ctx.fillStyle = C.ivoryDeep;\n    ctx.beginPath();\n    ctx.moveTo(-38, -18);\n    ctx.lineTo(-28, -18);\n    ctx.lineTo(-18, -180);\n    ctx.lineTo(-24, -180);\n    ctx.closePath();\n    ctx.fill();\n\n    // Architectural tower bands (slate trim)\n    ctx.fillStyle = C.slateDark;\n    ctx.fillRect(-34, -70, 68, 6);\n    ctx.fillRect(-28, -130, 56, 6);\n\n    // Tower windows\n    ctx.fillStyle = C.slateDark;\n    ctx.beginPath();\n    ctx.roundRect(-4, -45, 8, 14, [4, 4, 0, 0]);\n    ctx.roundRect(-4, -105, 8, 14, [4, 4, 0, 0]);\n    ctx.fill();\n\n    // 3. Gallery & Balcony Platform\n    ctx.fillStyle = C.slateRoof;\n    ctx.fillRect(-30, -188, 60, 8);\n\n    // Gallery railing\n    ctx.strokeStyle = C.slateDark;\n    ctx.lineWidth = 2.5;\n    ctx.beginPath();\n    ctx.moveTo(-32, -198);\n    ctx.lineTo(32, -198);\n    for (let rx = -30; rx <= 30; rx += 10) {\n      ctx.moveTo(rx, -188);\n      ctx.lineTo(rx, -198);\n    }\n    ctx.stroke();\n\n    clearShadow();\n\n    // 4. Lantern Room (Glass & amber core)\n    const lanternY = -215;\n    ctx.fillStyle = \"rgba(15, 23, 42, 0.85)\";\n    ctx.fillRect(-20, -222, 40, 24);\n\n    if (glowIntensity > 0) {\n      // Warm Amber Glow inside lantern\n      const lanternGlow = ctx.createRadialGradient(0, lanternY, 2, 0, lanternY, 32);\n      lanternGlow.addColorStop(0, `rgba(255, 255, 230, ${clamp(glowIntensity, 0, 1)})`);\n      lanternGlow.addColorStop(0.35, `rgba(255, 176, 32, ${clamp(glowIntensity * 0.9, 0, 1)})`);\n      lanternGlow.addColorStop(0.8, `rgba(255, 140, 0, ${clamp(glowIntensity * 0.4, 0, 1)})`);\n      lanternGlow.addColorStop(1, \"rgba(255, 140, 0, 0)\");\n      ctx.fillStyle = lanternGlow;\n      ctx.beginPath();\n      ctx.arc(0, lanternY, 32, 0, Math.PI * 2);\n      ctx.fill();\n\n      // Core lamp element\n      ctx.fillStyle = C.amberCore;\n      ctx.beginPath();\n      ctx.arc(0, lanternY, 5 * glowIntensity, 0, Math.PI * 2);\n      ctx.fill();\n    }\n\n    // Glass struts\n    ctx.strokeStyle = C.slateDark;\n    ctx.lineWidth = 2;\n    ctx.beginPath();\n    ctx.strokeRect(-20, -222, 40, 24);\n    ctx.moveTo(-7, -222); ctx.lineTo(-7, -198);\n    ctx.moveTo(7, -222); ctx.lineTo(7, -198);\n    ctx.stroke();\n\n    // 5. Dark Slate Conical Roof & Finial\n    applyShadow(8, 4, 0.35);\n    ctx.fillStyle = C.slateRoof;\n    ctx.beginPath();\n    ctx.moveTo(-24, -222);\n    ctx.lineTo(24, -222);\n    ctx.lineTo(0, -252);\n    ctx.closePath();\n    ctx.fill();\n\n    // Roof shadow facet\n    ctx.fillStyle = C.slateDark;\n    ctx.beginPath();\n    ctx.moveTo(0, -222);\n    ctx.lineTo(24, -222);\n    ctx.lineTo(0, -252);\n    ctx.closePath();\n    ctx.fill();\n\n    // Finial ball & spike\n    ctx.fillStyle = C.slateDark;\n    ctx.beginPath();\n    ctx.arc(0, -255, 3.5, 0, Math.PI * 2);\n    ctx.fill();\n    ctx.fillRect(-1, -264, 2, 9);\n    clearShadow();\n\n    // 6. Light Beam (if active)\n    if (beamAngle !== null && glowIntensity > 0.1) {\n      ctx.save();\n      ctx.translate(0, lanternY);\n      ctx.rotate((beamAngle * Math.PI) / 180);\n\n      // Main projecting beam cone\n      const beamLength = 1100;\n      const beamSpread = 160;\n\n      const beamGrad = ctx.createLinearGradient(0, 0, beamLength, 0);\n      beamGrad.addColorStop(0, `rgba(255, 248, 220, ${0.9 * glowIntensity})`);\n      beamGrad.addColorStop(0.12, `rgba(255, 200, 64, ${0.75 * glowIntensity})`);\n      beamGrad.addColorStop(0.45, `rgba(255, 176, 32, ${0.35 * glowIntensity})`);\n      beamGrad.addColorStop(0.85, `rgba(255, 160, 0, ${0.08 * glowIntensity})`);\n      beamGrad.addColorStop(1, \"rgba(255, 140, 0, 0)\");\n\n      ctx.fillStyle = beamGrad;\n      ctx.beginPath();\n      ctx.moveTo(0, -6);\n      ctx.lineTo(beamLength, -beamSpread);\n      ctx.lineTo(beamLength, beamSpread);\n      ctx.lineTo(0, 6);\n      ctx.closePath();\n      ctx.fill();\n\n      // Opposite subtle back-scatter beam\n      const backGrad = ctx.createLinearGradient(0, 0, -beamLength * 0.4, 0);\n      backGrad.addColorStop(0, `rgba(255, 210, 90, ${0.4 * glowIntensity})`);\n      backGrad.addColorStop(0.5, `rgba(255, 160, 30, ${0.1 * glowIntensity})`);\n      backGrad.addColorStop(1, \"rgba(255, 140, 0, 0)\");\n\n      ctx.fillStyle = backGrad;\n      ctx.beginPath();\n      ctx.moveTo(0, -4);\n      ctx.lineTo(-beamLength * 0.4, -beamSpread * 0.35);\n      ctx.lineTo(-beamLength * 0.4, beamSpread * 0.35);\n      ctx.lineTo(0, 4);\n      ctx.closePath();\n      ctx.fill();\n\n      ctx.restore();\n    }\n\n    ctx.restore();\n  }\n\n  // Helper: Layered Angular Rocks\n  function drawRocks(originX, originY, width, height, color1 = C.rock1, color2 = C.rock2, color3 = C.rock3) {\n    ctx.save();\n    applyShadow(14, 8, 0.4);\n\n    // Deep base rock\n    ctx.fillStyle = color1;\n    ctx.beginPath();\n    ctx.moveTo(originX - width * 0.55, originY);\n    ctx.lineTo(originX - width * 0.42, originY - height * 0.45);\n    ctx.lineTo(originX - width * 0.2, originY - height * 0.75);\n    ctx.lineTo(originX, originY - height);\n    ctx.lineTo(originX + width * 0.28, originY - height * 0.82);\n    ctx.lineTo(originX + width * 0.52, originY - height * 0.35);\n    ctx.lineTo(originX + width * 0.6, originY);\n    ctx.closePath();\n    ctx.fill();\n\n    // Angular mid facet\n    ctx.fillStyle = color2;\n    ctx.beginPath();\n    ctx.moveTo(originX - width * 0.2, originY - height * 0.75);\n    ctx.lineTo(originX, originY - height);\n    ctx.lineTo(originX + width * 0.15, originY - height * 0.55);\n    ctx.lineTo(originX - width * 0.05, originY - height * 0.3);\n    ctx.closePath();\n    ctx.fill();\n\n    // Highlight facet\n    ctx.fillStyle = color3;\n    ctx.beginPath();\n    ctx.moveTo(originX, originY - height);\n    ctx.lineTo(originX + width * 0.28, originY - height * 0.82);\n    ctx.lineTo(originX + width * 0.22, originY - height * 0.45);\n    ctx.lineTo(originX + width * 0.15, originY - height * 0.55);\n    ctx.closePath();\n    ctx.fill();\n\n    clearShadow();\n    ctx.restore();\n  }\n\n  // Helper: Layered Papercut Sea Waves\n  function drawSea(baseY, wavePhase, darkness = 0) {\n    ctx.save();\n    const seaColors = [\n      C.seaLayer1,\n      C.seaLayer2,\n      C.seaLayer3,\n      C.seaMid,\n      C.seaDeep\n    ];\n\n    const layers = 5;\n    for (let i = 0; i < layers; i++) {\n      const ly = baseY + i * 42;\n      const freq = 0.007 + i * 0.002;\n      const amp = 10 + i * 4;\n      const speed = wavePhase * (1 + i * 0.35);\n\n      applyShadow(8, 4, 0.28);\n      ctx.fillStyle = seaColors[i];\n\n      ctx.beginPath();\n      ctx.moveTo(0, H);\n      ctx.lineTo(0, ly);\n\n      for (let x = 0; x <= W; x += 30) {\n        const y = ly + Math.sin(x * freq + speed) * amp + Math.cos(x * freq * 0.5 + speed * 0.8) * (amp * 0.35);\n        ctx.lineTo(x, y);\n      }\n      ctx.lineTo(W, ly);\n      ctx.lineTo(W, H);\n      ctx.closePath();\n      ctx.fill();\n\n      // Papercut sea crest highlight (foam edge)\n      if (i < 3) {\n        ctx.strokeStyle = `rgba(180, 215, 255, ${0.15 - i * 0.04})`;\n        ctx.lineWidth = 1.8;\n        ctx.stroke();\n      }\n    }\n    clearShadow();\n    ctx.restore();\n  }\n\n  // Helper: Dangerous reef breakers / jagged rocks\n  function drawReefBreakers(x, y, scale = 1, surge = 0) {\n    ctx.save();\n    ctx.translate(x, y);\n    ctx.scale(scale, scale);\n\n    // Jagged tooth rocks\n    applyShadow(12, 6, 0.45);\n    ctx.fillStyle = C.slateDark;\n    ctx.beginPath();\n    ctx.moveTo(-120, 40);\n    ctx.lineTo(-80, -50);\n    ctx.lineTo(-50, -10);\n    ctx.lineTo(-10, -75);\n    ctx.lineTo(30, -20);\n    ctx.lineTo(75, -95);\n    ctx.lineTo(110, -15);\n    ctx.lineTo(140, 40);\n    ctx.closePath();\n    ctx.fill();\n\n    // Facet\n    ctx.fillStyle = C.rock2;\n    ctx.beginPath();\n    ctx.moveTo(-10, -75);\n    ctx.lineTo(30, -20);\n    ctx.lineTo(0, 40);\n    ctx.lineTo(-30, 40);\n    ctx.closePath();\n    ctx.fill();\n\n    ctx.beginPath();\n    ctx.moveTo(75, -95);\n    ctx.lineTo(110, -15);\n    ctx.lineTo(80, 40);\n    ctx.closePath();\n    ctx.fill();\n    clearShadow();\n\n    // White water froth / spray around jagged rocks\n    ctx.fillStyle = `rgba(230, 245, 255, ${0.4 + 0.25 * Math.sin(surge)})`;\n    ctx.beginPath();\n    ctx.ellipse(-10, 15, 60, 14, 0, 0, Math.PI * 2);\n    ctx.ellipse(75, 10, 50, 12, 0, 0, Math.PI * 2);\n    ctx.fill();\n\n    ctx.restore();\n  }\n\n  // Typography Helper\n  function drawTitleText(text, yPos, alpha = 1, letterSpacing = 14) {\n    if (alpha <= 0.01) return;\n    ctx.save();\n    ctx.globalAlpha = alpha;\n    ctx.fillStyle = C.ivoryLight;\n    ctx.font = \"600 30px 'Cinzel', 'Trajan Pro', 'Georgia', serif\";\n    ctx.textAlign = \"center\";\n    ctx.textBaseline = \"middle\";\n\n    // Tracked out uppercase\n    const chars = text.split(\"\");\n    const charWidth = 24;\n    const totalWidth = (chars.length - 1) * letterSpacing;\n    const startX = W / 2 - totalWidth / 2;\n\n    for (let i = 0; i < chars.length; i++) {\n      ctx.fillText(chars[i], startX + i * letterSpacing, yPos);\n    }\n    ctx.restore();\n  }\n\n  // =============================================================\n  // TIMELINE & SHOT SCHEDULING (0 to 45s)\n  // =============================================================\n\n  // Shot 1: Wide establishing dusk shot (0.0s - 7.5s)\n  // Shot 2: Medium boat in perilous waters (7.5s - 14.0s)\n  // Shot 3: Close-up of unlit lantern room igniting (14.0s - 21.0s)\n  // Shot 4: Wide/Mid beam sweeps and strikes the boat (21.0s - 30.0s)\n  // Shot 5: Medium boat turning away from reef toward safe harbor (30.0s - 37.5s)\n  // Shot 6: Return to opening wide shot framing, now shining and safe (37.5s - 45.0s)\n\n  if (t < 7.5) {\n    // -------------------------------------------------------------\n    // SHOT 1: WIDE DUSK — THE LAST LIGHT\n    // -------------------------------------------------------------\n    const shotT = t; // 0 .. 7.5\n    drawSky(C.skyTopDusk, C.skyMidDusk, C.skyHorizonDusk);\n    drawStars(0.35);\n\n    // Distant dark island silhouette & rocks\n    drawRocks(320, 480, 520, 220, C.rock1, C.rock2, C.rock3);\n    drawRocks(180, 490, 220, 110, C.slateDark, C.rock1, C.rock2);\n\n    // Unlit ivory lighthouse on the high crag\n    drawLighthouse(320, 290, 1.05, null, 0);\n\n    // Sea waves\n    drawSea(460, shotT * 1.2);\n\n    // Distant amber sailboat approaching perilous waters far right\n    const boatX = 1040 - shotT * 12;\n    const boatY = 510 + Math.sin(shotT * 2.2) * 5;\n    const boatPitch = Math.sin(shotT * 2.5) * 4;\n    drawSailboat(boatX, boatY, 0.48, boatPitch, true);\n\n    // Breaker reef silhouette in mid-right\n    drawReefBreakers(880, 570, 0.65, shotT * 3);\n\n    // Opening Title: \"THE LAST LIGHT\"\n    const titleFadeIn = clamp((shotT - 1.0) / 1.5);\n    const titleFadeOut = clamp((6.5 - shotT) / 1.0);\n    const titleAlpha = titleFadeIn * titleFadeOut;\n    drawTitleText(\"THE LAST LIGHT\", 130, titleAlpha, 22);\n\n  } else if (t < 14.0) {\n    // -------------------------------------------------------------\n    // SHOT 2: PERILOUS APPROACH (MEDIUM ON AMBER BOAT)\n    // -------------------------------------------------------------\n    const shotT = t - 7.5; // 0 .. 6.5\n    drawSky(C.skyTopNight, C.skyMidNight, C.skyLowNight);\n    drawStars(0.5);\n\n    // Ominous restless sea in foreground & midground\n    drawSea(400, shotT * 1.8);\n\n    // Approaching looming jagged rocks on left\n    const rocksX = 260 - shotT * 15;\n    drawReefBreakers(rocksX, 510, 1.35, shotT * 3.5);\n\n    // Amber boat pitching heavily in the dark swells toward the reef\n    const boatX = 860 - shotT * 32;\n    const boatY = 460 + Math.sin(shotT * 3.0) * 16;\n    const boatPitch = -12 + Math.sin(shotT * 3.0 + 0.5) * 14;\n    drawSailboat(boatX, boatY, 1.6, boatPitch, true);\n\n    // Distant unlit lighthouse silhouette on the far horizon\n    drawRocks(1150, 360, 220, 80, C.slateDark, C.rock1, C.rock2);\n    drawLighthouse(1150, 300, 0.35, null, 0);\n\n  } else if (t < 21.0) {\n    // -------------------------------------------------------------\n    // SHOT 3: CLOSE-UP LANTERN ROOM — THE IGNITION\n    // -------------------------------------------------------------\n    const shotT = t - 14.0; // 0 .. 7.0\n    drawSky(C.skyTopNight, C.skyMidNight, C.skyLowNight);\n    drawStars(0.65);\n\n    // Dramatic close-up framing of the lantern room\n    const cx = W / 2;\n    const cy = H / 2 + 130;\n\n    // Ignition event starts at shotT = 3.5s (t = 17.5s)\n    let igniteProgress = 0;\n    if (shotT > 3.5) {\n      const u = clamp((shotT - 3.5) / 1.8);\n      igniteProgress = easeOut(u);\n    }\n\n    // Spark flicker right before ignition\n    let preFlicker = 0;\n    if (shotT >= 2.8 && shotT <= 3.5) {\n      preFlicker = Math.sin((shotT - 2.8) * 25) > 0 ? 0.35 : 0.05;\n    }\n    const currentGlow = Math.max(igniteProgress, preFlicker);\n\n    // Close-up tower geometry\n    applyShadow(16, 10, 0.5);\n\n    // Upper tower shaft\n    ctx.fillStyle = C.ivoryLight;\n    ctx.beginPath();\n    ctx.moveTo(cx - 160, cy + 260);\n    ctx.lineTo(cx + 160, cy + 260);\n    ctx.lineTo(cx + 130, cy + 50);\n    ctx.lineTo(cx - 130, cy + 50);\n    ctx.closePath();\n    ctx.fill();\n\n    ctx.fillStyle = C.ivoryShade;\n    ctx.beginPath();\n    ctx.moveTo(cx, cy + 260);\n    ctx.lineTo(cx + 160, cy + 260);\n    ctx.lineTo(cx + 130, cy + 50);\n    ctx.lineTo(cx, cy + 50);\n    ctx.closePath();\n    ctx.fill();\n\n    // Gallery balcony\n    ctx.fillStyle = C.slateRoof;\n    ctx.fillRect(cx - 190, cy + 20, 380, 32);\n\n    // Railing posts\n    ctx.strokeStyle = C.slateDark;\n    ctx.lineWidth = 5;\n    ctx.beginPath();\n    ctx.moveTo(cx - 200, cy - 25);\n    ctx.lineTo(cx + 200, cy - 25);\n    for (let rx = cx - 180; rx <= cx + 180; rx += 45) {\n      ctx.moveTo(rx, cy + 20);\n      ctx.lineTo(rx, cy - 25);\n    }\n    ctx.stroke();\n\n    clearShadow();\n\n    // Large glass lantern enclosure\n    ctx.fillStyle = \"rgba(10, 18, 32, 0.85)\";\n    ctx.fillRect(cx - 120, cy - 140, 240, 160);\n\n    // Brass lamp mechanism inside\n    ctx.fillStyle = \"#6E4514\";\n    ctx.fillRect(cx - 30, cy - 20, 60, 40);\n    ctx.fillStyle = \"#96601D\";\n    ctx.beginPath();\n    ctx.arc(cx, cy - 25, 28, 0, Math.PI * 2);\n    ctx.fill();\n\n    // Glowing mantel / flame\n    if (currentGlow > 0) {\n      // Massive warm ambient burst\n      const burstGrad = ctx.createRadialGradient(cx, cy - 35, 10, cx, cy - 35, 380);\n      burstGrad.addColorStop(0, `rgba(255, 255, 240, ${clamp(currentGlow, 0, 1)})`);\n      burstGrad.addColorStop(0.2, `rgba(255, 190, 40, ${clamp(currentGlow * 0.85, 0, 1)})`);\n      burstGrad.addColorStop(0.55, `rgba(255, 140, 10, ${clamp(currentGlow * 0.45, 0, 1)})`);\n      burstGrad.addColorStop(1, \"rgba(255, 120, 0, 0)\");\n      ctx.fillStyle = burstGrad;\n      ctx.beginPath();\n      ctx.arc(cx, cy - 35, 380, 0, Math.PI * 2);\n      ctx.fill();\n\n      // Flame core\n      ctx.fillStyle = C.amberCore;\n      ctx.beginPath();\n      ctx.arc(cx, cy - 35, 22 * currentGlow, 0, Math.PI * 2);\n      ctx.fill();\n    }\n\n    // Heavy slate lantern framing / glass struts\n    ctx.strokeStyle = C.slateDark;\n    ctx.lineWidth = 8;\n    ctx.strokeRect(cx - 120, cy - 140, 240, 160);\n    ctx.beginPath();\n    ctx.moveTo(cx - 40, cy - 140); ctx.lineTo(cx - 40, cy + 20);\n    ctx.moveTo(cx + 40, cy - 140); ctx.lineTo(cx + 40, cy + 20);\n    ctx.stroke();\n\n    // Dark slate conical cap\n    applyShadow(16, 8, 0.45);\n    ctx.fillStyle = C.slateRoof;\n    ctx.beginPath();\n    ctx.moveTo(cx - 145, cy - 140);\n    ctx.lineTo(cx + 145, cy - 140);\n    ctx.lineTo(cx, cy - 320);\n    ctx.closePath();\n    ctx.fill();\n\n    // Roof shadow facet\n    ctx.fillStyle = C.slateDark;\n    ctx.beginPath();\n    ctx.moveTo(cx, cy - 140);\n    ctx.lineTo(cx + 145, cy - 140);\n    ctx.lineTo(cx, cy - 320);\n    ctx.closePath();\n    ctx.fill();\n\n    // Spire & ball\n    ctx.fillStyle = C.slateDark;\n    ctx.beginPath();\n    ctx.arc(cx, cy - 330, 14, 0, Math.PI * 2);\n    ctx.fill();\n    ctx.fillRect(cx - 4, cy - 370, 8, 42);\n    clearShadow();\n\n  } else if (t < 30.0) {\n    // -------------------------------------------------------------\n    // SHOT 4: WIDE/MID — THE BEAM SWEEPS & STRIKES THE BOAT\n    // -------------------------------------------------------------\n    const shotT = t - 21.0; // 0 .. 9.0\n    drawSky(C.skyTopNight, C.skyMidNight, C.skyLowNight);\n    drawStars(0.8);\n\n    // Lighthouse island on left\n    drawRocks(260, 480, 480, 210, C.rock1, C.rock2, C.rock3);\n\n    // Beam sweep angle: sweeps clockwise from left (-60 deg) across the sea to the boat (+28 deg)\n    // Smooth ease toward the boat around shotT = 4.0s (t = 25s)\n    let beamDeg = -50 + shotT * 10;\n    if (shotT > 3.0) {\n      const u = clamp((shotT - 3.0) / 4.0);\n      beamDeg = -20 + easeInOut(u) * 48;\n    }\n\n    // Lighthouse with active beam\n    drawLighthouse(260, 300, 0.95, beamDeg, 1.0);\n\n    // Layered sea waves\n    drawSea(460, shotT * 1.4);\n\n    // Dangerous reef in center-right\n    drawReefBreakers(720, 560, 0.9, shotT * 2.8);\n\n    // Sailboat heading near the rocks\n    const boatX = 890 - shotT * 12;\n    const boatY = 510 + Math.sin(shotT * 2.4) * 8;\n    const boatPitch = -6 + Math.sin(shotT * 2.6) * 6;\n\n    // Check if beam is currently illuminating the boat (around shotT >= 4.5s)\n    const isIlluminated = shotT >= 4.5;\n    const boatScale = 0.85;\n\n    drawSailboat(boatX, boatY, boatScale, boatPitch, true);\n\n    if (isIlluminated) {\n      // Golden spotlight glow on the boat\n      const spotAlpha = clamp((shotT - 4.5) / 1.2) * 0.45;\n      ctx.save();\n      const spotGrad = ctx.createRadialGradient(boatX, boatY - 20, 10, boatX, boatY - 20, 120);\n      spotGrad.addColorStop(0, `rgba(255, 230, 140, ${spotAlpha})`);\n      spotGrad.addColorStop(0.6, `rgba(255, 180, 40, ${spotAlpha * 0.5})`);\n      spotGrad.addColorStop(1, \"rgba(255, 160, 0, 0)\");\n      ctx.fillStyle = spotGrad;\n      ctx.beginPath();\n      ctx.arc(boatX, boatY - 20, 120, 0, Math.PI * 2);\n      ctx.fill();\n      ctx.restore();\n    }\n\n  } else if (t < 37.5) {\n    // -------------------------------------------------------------\n    // SHOT 5: RESCUE / TURNING COURSE AWAY FROM DANGER\n    // -------------------------------------------------------------\n    const shotT = t - 30.0; // 0 .. 7.5\n    drawSky(C.skyTopNight, C.skyMidNight, C.skyLowNight);\n    drawStars(0.85);\n\n    // Passing sea\n    drawSea(420, shotT * 1.5);\n\n    // Jagged reef breaker slipping past into the dark background\n    const reefRecedeX = 300 - shotT * 28;\n    drawReefBreakers(reefRecedeX, 480, 1.1, shotT * 3.2);\n\n    // Medium shot of the amber sailboat turning its course\n    // Turn sequence: eases from left-facing (toward reef) to right-facing (into safe channel)\n    const turnProgress = clamp(shotT / 4.5);\n    const easedTurn = easeInOut(turnProgress);\n\n    const boatX = 640 + Math.sin(shotT * 1.2) * 20;\n    const boatY = 470 + Math.sin(shotT * 2.2) * 6;\n    const boatPitch = (1 - easedTurn) * (-10) + easedTurn * 4 + Math.sin(shotT * 2.5) * 3;\n\n    // Sailboat turns around smoothly\n    const facingLeft = turnedDirection(easedTurn);\n    function turnedDirection(prog) {\n      return prog < 0.5;\n    }\n\n    // Warm reassuring beam light bathing the scene\n    ctx.save();\n    const rescueLight = ctx.createLinearGradient(0, 0, W, H);\n    rescueLight.addColorStop(0, \"rgba(255, 210, 80, 0.28)\");\n    rescueLight.addColorStop(0.5, \"rgba(255, 180, 40, 0.12)\");\n    rescueLight.addColorStop(1, \"rgba(255, 140, 0, 0)\");\n    ctx.fillStyle = rescueLight;\n    ctx.fillRect(0, 0, W, H);\n    ctx.restore();\n\n    drawSailboat(boatX, boatY, 1.5, boatPitch, facingLeft);\n\n    // Distant lighthouse guiding beam sweeping overhead\n    ctx.save();\n    const sweepAngle = -35 + shotT * 4;\n    ctx.translate(140, 220);\n    ctx.rotate((sweepAngle * Math.PI) / 180);\n    const bgBeam = ctx.createLinearGradient(0, 0, 1200, 0);\n    bgBeam.addColorStop(0, \"rgba(255, 240, 180, 0.45)\");\n    bgBeam.addColorStop(0.5, \"rgba(255, 190, 50, 0.15)\");\n    bgBeam.addColorStop(1, \"rgba(255, 160, 0, 0)\");\n    ctx.fillStyle = bgBeam;\n    ctx.beginPath();\n    ctx.moveTo(0, -4);\n    ctx.lineTo(1200, -120);\n    ctx.lineTo(1200, 120);\n    ctx.lineTo(0, 4);\n    ctx.closePath();\n    ctx.fill();\n    ctx.restore();\n\n  } else {\n    // -------------------------------------------------------------\n    // SHOT 6: RETURN TO OPENING FRAMING — SAFE IN HARBOR\n    // -------------------------------------------------------------\n    const shotT = t - 37.5; // 0 .. 7.5 (Total duration 45.0s)\n\n    // Dissolve transition for elapsed time at entry (37.5s - 38.5s)\n    const dissolveAlpha = clamp(shotT / 1.0);\n\n    drawSky(C.skyTopNight, C.skyMidNight, C.skyLowNight);\n    drawStars(0.95);\n\n    // Majestic island rocks (exact framing of Shot 1)\n    drawRocks(320, 480, 520, 220, C.rock1, C.rock2, C.rock3);\n    drawRocks(180, 490, 220, 110, C.slateDark, C.rock1, C.rock2);\n\n    // Sheltered cove rocks on right providing harbor embrace\n    drawRocks(880, 530, 360, 140, C.slateDark, C.rock1, C.rock2);\n\n    // Lighthouse now shining brightly, beam sweeping rhythmically\n    const finalBeamAngle = -15 + Math.sin(shotT * 0.8) * 35;\n    drawLighthouse(320, 290, 1.05, finalBeamAngle, 1.0);\n\n    // Calm, tranquil sheltered sea\n    drawSea(470, shotT * 0.8);\n\n    // Amber sailboat resting safely in the calm harbor waters near the lighthouse\n    const safeBoatX = 460 + Math.sin(shotT * 0.7) * 4;\n    const safeBoatY = 530 + Math.sin(shotT * 1.2) * 3;\n    const safeBoatPitch = Math.sin(shotT * 1.4) * 1.8;\n    drawSailboat(safeBoatX, safeBoatY, 0.55, safeBoatPitch, false);\n\n    // Reassuring warm harbor water reflection from lighthouse\n    ctx.save();\n    const harborGlow = ctx.createRadialGradient(380, 490, 10, 380, 490, 180);\n    harborGlow.addColorStop(0, \"rgba(255, 210, 90, 0.22)\");\n    harborGlow.addColorStop(0.6, \"rgba(255, 170, 30, 0.08)\");\n    harborGlow.addColorStop(1, \"rgba(255, 140, 0, 0)\");\n    ctx.fillStyle = harborGlow;\n    ctx.beginPath();\n    ctx.arc(380, 490, 180, 0, Math.PI * 2);\n    ctx.fill();\n    ctx.restore();\n\n    // Closing Title: \"HOME, SAFELY\"\n    const textFadeIn = clamp((shotT - 1.2) / 1.5);\n    const textFadeOut = clamp((7.2 - shotT) / 1.0);\n    const textAlpha = textFadeIn * textFadeOut;\n    drawTitleText(\"HOME, SAFELY\", 130, textAlpha, 22);\n\n    // Dissolve overlay if in transition window\n    if (dissolveAlpha < 1.0) {\n      ctx.save();\n      ctx.fillStyle = `rgba(4, 9, 20, ${1 - dissolveAlpha})`;\n      ctx.fillRect(0, 0, W, H);\n      ctx.restore();\n    }\n  }\n\n  // Subtle paper texture vignette overlay across all shots\n  ctx.save();\n  const vignette = ctx.createRadialGradient(W / 2, H / 2, H * 0.45, W / 2, H / 2, W * 0.7);\n  vignette.addColorStop(0, \"rgba(0,0,0,0)\");\n  vignette.addColorStop(1, \"rgba(3, 7, 16, 0.4)\");\n  ctx.fillStyle = vignette;\n  ctx.fillRect(0, 0, W, H);\n  ctx.restore();\n}\n","exportProfile":{"format":"mp4","resolution":"720p","fps":24,"durationSeconds":45},"source":{"prompt":"A 45-second quiet illustrated story: The Last Light. Cast the lighthouse as pp:Lighthouse variant ivory-lighthouse and the boat as pp:Sailboat variant amber-sailboat. The generic default variants are a house and car and must not be used. At dusk a lighthouse on a rocky island has gone dark; a small amber boat approaches the rocks. Establish the danger, show the unlit lantern close up, then the lantern ignites and its beam reaches the boat. The boat changes course away from the rocks and reaches sheltered water. End on the same lighthouse framing as the opening, now shining, with the boat safe. A consistent editorial papercut treatment: midnight blue sea, warm ivory lighthouse with one amber lantern and a dark slate roof, layered angular rocks, amber boat with a single ivory sail. Reuse exactly these silhouettes and colours across shots. Alternate wide views and meaningful detail shots. Use cuts for cause and effect and a dissolve only for elapsed time. Deliberate holds allow the viewer to understand the rescue. Keep text to the opening title THE LAST LIGHT and the closing line HOME, SAFELY, placed in clear empty sky with strong contrast. No narration, invented statistics, decorative particles or unrelated characters. The boat and lighthouse must remain recognizable throughout. Show the events visually, rather than explaining them with captions.","provider":"google","model":"gemini-3.7-flash","app":"gallery-reproduction"}},"license":"CC0-1.0"}