{"id":"tide-pool","title":"Tide Pool","category":"Drawn film","prompt":"A 24-second illustrated film: dawn over a rocky tide pool. Light creeps across the water, anemones open, a small crab sidles between stones, and a single wave washes in, fills the pool and draws back leaving everything glistening. Closing title: TIDE POOL. 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":6142,"outputTokens":13464,"costUsd":0.3017}},"render":{"credits":10,"priceUsd":0.15,"format":"mp4","frameCount":576,"bytes":3224288,"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=tide-pool), 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  // ----------------------------------------------------\n  // UTILITIES & EASING\n  // ----------------------------------------------------\n  const clamp = (v, min, max) => Math.max(min, Math.min(max, v));\n  const smoothstep = (edge0, edge1, x) => {\n    const p = clamp((x - edge0) / (edge1 - edge0), 0, 1);\n    return p * p * (3 - 2 * p);\n  };\n  const lerp = (a, b, u) => a + (b - a) * u;\n\n  // ----------------------------------------------------\n  // STORY PHASES & TIMING\n  // ----------------------------------------------------\n  // Dawn progression: 0 (deep pre-dawn) -> 1 (bright golden morning)\n  const dawn = smoothstep(1.5, 11.0, t);\n  const goldenHour = smoothstep(8.0, 16.0, t);\n\n  // Organic Wave Lifecycle\n  // 11.5s: distant ocean swell rises\n  // 13.2s - 15.5s: wave crests over rocks, cascading in translucent sweeps\n  // 15.5s - 19.5s: gentle ebb draining out, leaving lace foam and glistening film\n  const waveIn = smoothstep(11.5, 14.8, t);\n  const waveOut = smoothstep(15.2, 19.8, t);\n  const waveActive = smoothstep(11.8, 14.2, t) * (1 - smoothstep(18.8, 20.6, t));\n  const waveHeightFactor = Math.sin(smoothstep(11.8, 19.0, t) * Math.PI);\n  const wetness = smoothstep(13.2, 16.0, t) * (1 - smoothstep(19.0, 24.0, t) * 0.2);\n\n  // Anemone expansion with dawn light\n  const anemoneOpen = smoothstep(3.0, 9.5, t);\n\n  // Crab timeline: creeps out at t=5.5s, tucks away before wave, emerges afterward\n  let crabX = 390;\n  let crabY = 475;\n  let crabVisible = 1;\n  let crabClawRaise = 0;\n  if (t < 5.5) {\n    crabVisible = 0;\n  } else if (t >= 5.5 && t < 12.2) {\n    const walkU = smoothstep(5.5, 11.5, t);\n    crabX = lerp(330, 485, walkU);\n    crabY = 485 - Math.sin(walkU * Math.PI * 4) * 6;\n    crabClawRaise = Math.sin(t * 3.5) * 0.5 + 0.5;\n  } else if (t >= 12.2 && t < 17.5) {\n    const tuckU = smoothstep(12.2, 13.2, t);\n    crabX = lerp(485, 470, tuckU);\n    crabY = lerp(485, 510, tuckU);\n    crabVisible = 1 - smoothstep(13.0, 14.0, t);\n  } else {\n    const peekU = smoothstep(17.8, 20.8, t);\n    crabX = lerp(470, 508, peekU);\n    crabY = lerp(510, 482, peekU);\n    crabClawRaise = Math.sin(t * 2.0) * 0.4 + 0.6;\n  }\n\n  // ----------------------------------------------------\n  // 1. SKY & DISTANT HORIZON\n  // ----------------------------------------------------\n  const skyGrad = ctx.createLinearGradient(0, 0, 0, 360);\n  const skyTop = `rgb(${lerp(18, 55, dawn)}, ${lerp(24, 75, dawn)}, ${lerp(58, 128, dawn)})`;\n  const skyMid = `rgb(${lerp(35, 180, dawn)}, ${lerp(42, 135, dawn)}, ${lerp(88, 145, dawn)})`;\n  const skyLow = `rgb(${lerp(75, 255, dawn)}, ${lerp(65, 185, dawn)}, ${lerp(95, 130, dawn)})`;\n  skyGrad.addColorStop(0, skyTop);\n  skyGrad.addColorStop(0.65, skyMid);\n  skyGrad.addColorStop(1.0, skyLow);\n  ctx.fillStyle = skyGrad;\n  ctx.fillRect(0, 0, W, H);\n\n  // Soft Stars fading away\n  if (dawn < 0.85) {\n    ctx.save();\n    ctx.fillStyle = `rgba(255, 250, 230, ${0.8 * (1 - dawn / 0.85)})`;\n    const starCoords = [\n      [140, 60, 1.2], [280, 40, 1.5], [430, 85, 0.9], [620, 50, 1.4],\n      [840, 70, 1.1], [1050, 45, 1.6], [1180, 90, 1.0], [320, 120, 0.8],\n      [960, 110, 1.3], [740, 30, 1.2], [160, 140, 0.7]\n    ];\n    for (const [sx, sy, sr] of starCoords) {\n      const twinkle = Math.sin(t * 3.0 + sx) * 0.3 + 0.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  // Rising Sun Atmospheric Glow\n  const sunX = 760;\n  const sunY = lerp(270, 205, smoothstep(2.0, 16.0, t));\n  const sunRadius = lerp(20, 42, dawn);\n\n  const sunAtmosphere = ctx.createRadialGradient(sunX, sunY, 10, sunX, sunY, 480);\n  sunAtmosphere.addColorStop(0, `rgba(255, 220, 150, ${0.45 * dawn})`);\n  sunAtmosphere.addColorStop(0.3, `rgba(255, 160, 110, ${0.28 * dawn})`);\n  sunAtmosphere.addColorStop(0.7, `rgba(255, 120, 100, ${0.12 * dawn})`);\n  sunAtmosphere.addColorStop(1.0, 'rgba(255, 100, 80, 0)');\n  ctx.fillStyle = sunAtmosphere;\n  ctx.fillRect(0, 0, W, 400);\n\n  if (dawn > 0.1) {\n    const sunCore = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, sunRadius * 2.5);\n    sunCore.addColorStop(0, `rgba(255, 255, 245, ${0.95 * dawn})`);\n    sunCore.addColorStop(0.25, `rgba(255, 235, 180, ${0.8 * dawn})`);\n    sunCore.addColorStop(0.6, `rgba(255, 180, 120, ${0.35 * dawn})`);\n    sunCore.addColorStop(1.0, 'rgba(255, 160, 100, 0)');\n    ctx.fillStyle = sunCore;\n    ctx.beginPath();\n    ctx.arc(sunX, sunY, sunRadius * 2.5, 0, Math.PI * 2);\n    ctx.fill();\n  }\n\n  // ----------------------------------------------------\n  // 2. OPEN SEA HORIZON & DISTANT SWELLS\n  // ----------------------------------------------------\n  const seaY = 245;\n  const seaGrad = ctx.createLinearGradient(0, seaY, 0, 380);\n  seaGrad.addColorStop(0, `rgb(${lerp(28, 90, dawn)}, ${lerp(45, 125, dawn)}, ${lerp(80, 150, dawn)})`);\n  seaGrad.addColorStop(0.6, `rgb(${lerp(18, 55, dawn)}, ${lerp(42, 105, dawn)}, ${lerp(70, 130, dawn)})`);\n  seaGrad.addColorStop(1, `rgb(${lerp(12, 35, dawn)}, ${lerp(30, 80, dawn)}, ${lerp(55, 105, dawn)})`);\n  ctx.fillStyle = seaGrad;\n  ctx.fillRect(0, seaY, W, 140);\n\n  ctx.save();\n  for (let i = 0; i < 5; i++) {\n    const ly = seaY + 12 + i * 18;\n    const wavePhase = t * 1.2 + i * 1.5;\n    ctx.beginPath();\n    ctx.moveTo(0, ly);\n    for (let x = 0; x <= W; x += 40) {\n      const wy = ly + Math.sin(x * 0.015 + wavePhase) * (2.0 + i * 0.8);\n      ctx.lineTo(x, wy);\n    }\n    ctx.lineTo(W, ly + 25);\n    ctx.lineTo(0, ly + 25);\n    ctx.closePath();\n    ctx.fillStyle = `rgba(${lerp(200, 255, dawn)}, ${lerp(160, 230, dawn)}, ${lerp(170, 210, dawn)}, ${0.08 + i * 0.03 + dawn * 0.06})`;\n    ctx.fill();\n  }\n  ctx.restore();\n\n  // Distant Sea Stacks / Headlands\n  const drawHeadland = (points, color) => {\n    ctx.fillStyle = color;\n    ctx.beginPath();\n    ctx.moveTo(points[0][0], points[0][1]);\n    for (let i = 1; i < points.length; i++) {\n      ctx.lineTo(points[i][0], points[i][1]);\n    }\n    ctx.closePath();\n    ctx.fill();\n  };\n\n  const distRockCol = `rgb(${lerp(20, 65, dawn)}, ${lerp(28, 70, dawn)}, ${lerp(50, 95, dawn)})`;\n  drawHeadland([[0, 245], [90, 225], [170, 232], [230, 215], [310, 245]], distRockCol);\n  drawHeadland([[980, 245], [1040, 218], [1110, 205], [1160, 218], [1220, 210], [1280, 245]], distRockCol);\n\n  // ----------------------------------------------------\n  // 3. MIDGROUND REEF SHELF & SEAWARD ROCK LEDGES\n  // ----------------------------------------------------\n  const midRockGrad = ctx.createLinearGradient(0, 230, 0, 420);\n  midRockGrad.addColorStop(0, `rgb(${lerp(22, 58, dawn)}, ${lerp(30, 68, dawn)}, ${lerp(48, 80, dawn)})`);\n  midRockGrad.addColorStop(1, `rgb(${lerp(14, 38, dawn)}, ${lerp(20, 48, dawn)}, ${lerp(32, 60, dawn)})`);\n  \n  ctx.fillStyle = midRockGrad;\n  ctx.beginPath();\n  ctx.moveTo(0, 310);\n  ctx.bezierCurveTo(160, 300, 260, 270, 440, 280);\n  ctx.bezierCurveTo(580, 288, 700, 265, 860, 275);\n  ctx.bezierCurveTo(1020, 285, 1150, 295, 1280, 305);\n  ctx.lineTo(1280, 420);\n  ctx.lineTo(0, 420);\n  ctx.closePath();\n  ctx.fill();\n\n  ctx.strokeStyle = `rgba(${lerp(180, 255, dawn)}, ${lerp(120, 200, dawn)}, ${lerp(110, 160, dawn)}, ${0.15 + dawn * 0.35})`;\n  ctx.lineWidth = 2.5;\n  ctx.stroke();\n\n  // ----------------------------------------------------\n  // 4. THE BASIN / POOL FLOOR (Submerged Layer)\n  // ----------------------------------------------------\n  const poolBedGrad = ctx.createLinearGradient(0, 340, 0, 680);\n  poolBedGrad.addColorStop(0, `rgb(${lerp(8, 18, dawn)}, ${lerp(20, 45, dawn)}, ${lerp(30, 60, dawn)})`);\n  poolBedGrad.addColorStop(0.5, `rgb(${lerp(6, 14, dawn)}, ${lerp(28, 55, dawn)}, ${lerp(36, 70, dawn)})`);\n  poolBedGrad.addColorStop(1, `rgb(${lerp(4, 10, dawn)}, ${lerp(18, 38, dawn)}, ${lerp(24, 48, dawn)})`);\n\n  ctx.fillStyle = poolBedGrad;\n  ctx.beginPath();\n  ctx.moveTo(120, 340);\n  ctx.bezierCurveTo(240, 320, 1040, 320, 1160, 340);\n  ctx.bezierCurveTo(1240, 450, 1220, 640, 1080, 690);\n  ctx.bezierCurveTo(800, 720, 440, 720, 180, 690);\n  ctx.bezierCurveTo(60, 620, 50, 430, 120, 340);\n  ctx.closePath();\n  ctx.fill();\n\n  // Submerged Pebbles\n  const pebbles = [\n    { x: 310, y: 560, rx: 18, ry: 11, rot: 0.3, col: [140, 110, 95] },\n    { x: 360, y: 610, rx: 24, ry: 15, rot: -0.4, col: [95, 115, 125] },\n    { x: 440, y: 580, rx: 14, ry: 9, rot: 0.8, col: [170, 130, 110] },\n    { x: 740, y: 610, rx: 22, ry: 13, rot: 0.2, col: [110, 130, 135] },\n    { x: 820, y: 570, rx: 16, ry: 10, rot: -0.5, col: [150, 105, 95] },\n    { x: 890, y: 620, rx: 26, ry: 16, rot: 0.6, col: [85, 105, 110] },\n    { x: 620, y: 640, rx: 32, ry: 18, rot: 0.1, col: [120, 105, 100] },\n    { x: 530, y: 625, rx: 15, ry: 10, rot: -0.2, col: [135, 140, 120] }\n  ];\n\n  for (const p of pebbles) {\n    ctx.save();\n    ctx.translate(p.x, p.y);\n    ctx.rotate(p.rot);\n    ctx.beginPath();\n    ctx.ellipse(0, 0, p.rx, p.ry, 0, 0, Math.PI * 2);\n    ctx.fillStyle = `rgb(${lerp(p.col[0] * 0.3, p.col[0], dawn * 0.8)}, ${lerp(p.col[1] * 0.3, p.col[1], dawn * 0.8)}, ${lerp(p.col[2] * 0.3, p.col[2], dawn * 0.8)})`;\n    ctx.fill();\n    ctx.strokeStyle = 'rgba(0,0,0,0.35)';\n    ctx.lineWidth = 1.5;\n    ctx.stroke();\n    ctx.restore();\n  }\n\n  // Sea Star\n  ctx.save();\n  ctx.translate(340, 620);\n  ctx.rotate(0.35);\n  ctx.fillStyle = `rgb(${lerp(120, 225, dawn)}, ${lerp(60, 110, dawn)}, ${lerp(70, 95, dawn)})`;\n  ctx.beginPath();\n  for (let i = 0; i < 5; i++) {\n    const armAngle = (i * Math.PI * 2) / 5 - Math.PI / 2;\n    const innerAngle = armAngle + Math.PI / 5;\n    const rOuter = 26;\n    const rInner = 10;\n    if (i === 0) ctx.moveTo(Math.cos(armAngle) * rOuter, Math.sin(armAngle) * rOuter);\n    else ctx.lineTo(Math.cos(armAngle) * rOuter, Math.sin(armAngle) * rOuter);\n    ctx.lineTo(Math.cos(innerAngle) * rInner, Math.sin(innerAngle) * rInner);\n  }\n  ctx.closePath();\n  ctx.fill();\n  ctx.restore();\n\n  // ----------------------------------------------------\n  // 5. SUBMERGED MARINE LIFE (Kelp & Blooming Anemones)\n  // ----------------------------------------------------\n  const drawKelpFrond = (baseX, baseY, length, swaySpeed, phase, leafColor) => {\n    ctx.save();\n    ctx.beginPath();\n    ctx.moveTo(baseX, baseY);\n    const segments = 10;\n    const segLen = length / segments;\n    let currX = baseX;\n    let currY = baseY;\n    \n    const waveDistort = waveHeightFactor * Math.sin(t * 7 + phase) * 22;\n\n    for (let s = 1; s <= segments; s++) {\n      const sway = Math.sin(t * swaySpeed + phase + s * 0.4) * (s * 3.2) + waveDistort * (s / segments);\n      const nextX = baseX + sway;\n      const nextY = baseY - s * segLen;\n      const cpX = (currX + nextX) / 2 + sway * 0.4;\n      const cpY = (currY + nextY) / 2;\n      ctx.quadraticCurveTo(cpX, cpY, nextX, nextY);\n      currX = nextX;\n      currY = nextY;\n    }\n\n    ctx.strokeStyle = leafColor;\n    ctx.lineWidth = 9;\n    ctx.lineCap = 'round';\n    ctx.stroke();\n\n    ctx.strokeStyle = `rgba(180, 220, 120, ${0.15 + dawn * 0.25})`;\n    ctx.lineWidth = 2.5;\n    ctx.stroke();\n    ctx.restore();\n  };\n\n  const kelpCol1 = `rgb(${lerp(15, 38, dawn)}, ${lerp(40, 85, dawn)}, ${lerp(30, 50, dawn)})`;\n  const kelpCol2 = `rgb(${lerp(20, 60, dawn)}, ${lerp(48, 110, dawn)}, ${lerp(25, 55, dawn)})`;\n  const kelpCol3 = `rgb(${lerp(25, 75, dawn)}, ${lerp(35, 70, dawn)}, ${lerp(45, 80, dawn)})`;\n\n  drawKelpFrond(240, 620, 180, 1.4, 0.0, kelpCol1);\n  drawKelpFrond(270, 635, 140, 1.7, 1.2, kelpCol2);\n  drawKelpFrond(940, 640, 210, 1.3, 2.5, kelpCol2);\n  drawKelpFrond(975, 650, 160, 1.6, 0.7, kelpCol1);\n  drawKelpFrond(1010, 630, 130, 1.8, 3.1, kelpCol3);\n\n  // Blooming Giant Sea Anemones\n  const drawAnemone = (cx, cy, baseRadius, tentacleCount, colCore, colTip, openFactor) => {\n    ctx.save();\n    ctx.translate(cx, cy);\n\n    ctx.beginPath();\n    ctx.ellipse(0, 5, baseRadius * 0.9, baseRadius * 0.55, 0, 0, Math.PI * 2);\n    ctx.fillStyle = colCore;\n    ctx.fill();\n\n    const effectiveOpen = clamp(openFactor, 0.18, 1.0);\n    const waveSurgeJitter = waveHeightFactor * Math.sin(t * 6.0) * 0.15;\n\n    for (let i = 0; i < tentacleCount; i++) {\n      const angle = (i / tentacleCount) * Math.PI * 2;\n      const tentLen = baseRadius * lerp(0.5, 2.4, effectiveOpen);\n      const curl = Math.sin(t * 1.8 + i * 0.9) * 0.25 + (1 - effectiveOpen) * 0.8 + waveSurgeJitter;\n      \n      const reachX = Math.cos(angle) * tentLen * (1 - curl * 0.3);\n      const reachY = Math.sin(angle) * tentLen * 0.65 - Math.sin(effectiveOpen * Math.PI * 0.5) * 8;\n      \n      const cpX = Math.cos(angle + curl * 0.6) * (tentLen * 0.5);\n      const cpY = Math.sin(angle + curl * 0.6) * (tentLen * 0.4);\n\n      ctx.beginPath();\n      ctx.moveTo(Math.cos(angle) * (baseRadius * 0.3), Math.sin(angle) * (baseRadius * 0.2));\n      ctx.quadraticCurveTo(cpX, cpY, reachX, reachY);\n\n      ctx.strokeStyle = colTip;\n      ctx.lineWidth = lerp(2.0, 4.2, effectiveOpen);\n      ctx.lineCap = 'round';\n      ctx.stroke();\n\n      ctx.beginPath();\n      ctx.arc(reachX, reachY, lerp(1.2, 3.2, effectiveOpen), 0, Math.PI * 2);\n      ctx.fillStyle = `rgba(255, 240, 210, ${0.4 + effectiveOpen * 0.5})`;\n      ctx.fill();\n    }\n\n    const centerGlow = ctx.createRadialGradient(0, 0, 0, 0, 0, baseRadius * 0.45);\n    centerGlow.addColorStop(0, `rgba(255, 255, 200, ${0.3 + effectiveOpen * 0.6})`);\n    centerGlow.addColorStop(0.7, colCore);\n    centerGlow.addColorStop(1, 'rgba(0,0,0,0)');\n    ctx.fillStyle = centerGlow;\n    ctx.beginPath();\n    ctx.arc(0, 0, baseRadius * 0.45, 0, Math.PI * 2);\n    ctx.fill();\n\n    ctx.restore();\n  };\n\n  // Anemone 1 (Emerald/Electric Mint)\n  drawAnemone(\n    480, 560, 24, 26,\n    `rgb(${lerp(18, 35, dawn)}, ${lerp(45, 110, dawn)}, ${lerp(40, 95, dawn)})`,\n    `rgb(${lerp(40, 90, dawn)}, ${lerp(95, 235, dawn)}, ${lerp(85, 190, dawn)})`,\n    anemoneOpen\n  );\n\n  // Anemone 2 (Rose/Peach Coral)\n  drawAnemone(\n    760, 545, 28, 30,\n    `rgb(${lerp(45, 115, dawn)}, ${lerp(20, 50, dawn)}, ${lerp(35, 75, dawn)})`,\n    `rgb(${lerp(110, 255, dawn)}, ${lerp(55, 140, dawn)}, ${lerp(80, 160, dawn)})`,\n    smoothstep(2.5, 8.5, t)\n  );\n\n  // Anemone 3 (Violet Satellite)\n  drawAnemone(\n    835, 520, 16, 18,\n    `rgb(${lerp(30, 80, dawn)}, ${lerp(20, 45, dawn)}, ${lerp(50, 120, dawn)})`,\n    `rgb(${lerp(75, 190, dawn)}, ${lerp(50, 125, dawn)}, ${lerp(120, 240, dawn)})`,\n    smoothstep(4.0, 10.5, t)\n  );\n\n  // ----------------------------------------------------\n  // 6. POOL WATER BODY & CAUSTICS\n  // ----------------------------------------------------\n  const baseWaterLevel = 450;\n  const poolWaterRise = waveHeightFactor * 28;\n  const waterSurfaceY = baseWaterLevel - poolWaterRise;\n\n  // Pool Water Gradient Tint\n  const poolWaterGrad = ctx.createLinearGradient(0, waterSurfaceY, 0, 700);\n  const waveBrighten = waveActive * 0.25;\n  poolWaterGrad.addColorStop(0, `rgba(${lerp(30, 95, dawn + waveBrighten)}, ${lerp(90, 205, dawn + waveBrighten)}, ${lerp(110, 215, dawn + waveBrighten)}, 0.42)`);\n  poolWaterGrad.addColorStop(0.5, `rgba(${lerp(15, 35, dawn)}, ${lerp(65, 140, dawn)}, ${lerp(85, 155, dawn)}, 0.58)`);\n  poolWaterGrad.addColorStop(1, `rgba(${lerp(8, 15, dawn)}, ${lerp(40, 80, dawn)}, ${lerp(55, 100, dawn)}, 0.75)`);\n\n  ctx.fillStyle = poolWaterGrad;\n  ctx.beginPath();\n  ctx.moveTo(135, waterSurfaceY);\n  ctx.bezierCurveTo(380, waterSurfaceY - 4, 880, waterSurfaceY - 4, 1145, waterSurfaceY);\n  ctx.bezierCurveTo(1230, 480, 1210, 630, 1070, 685);\n  ctx.bezierCurveTo(800, 715, 450, 715, 190, 685);\n  ctx.bezierCurveTo(85, 620, 75, 480, 135, waterSurfaceY);\n  ctx.closePath();\n  ctx.fill();\n\n  // Sunlight Caustics\n  if (dawn > 0.25) {\n    ctx.save();\n    ctx.globalCompositeOperation = 'screen';\n    const causticAlpha = (dawn * 0.22) * (1 + waveActive * 0.6);\n    ctx.strokeStyle = `rgba(255, 245, 200, ${causticAlpha})`;\n    ctx.lineWidth = 2.2;\n\n    for (let c = 0; c < 7; c++) {\n      const cx = 350 + c * 90;\n      const cy = 520 + Math.sin(c + t * 1.5) * 20;\n      ctx.beginPath();\n      for (let a = 0; a <= Math.PI * 2; a += 0.6) {\n        const rad = 25 + Math.sin(a * 3 + t * 2.2 + c) * 10;\n        const px = cx + Math.cos(a) * rad * 1.6;\n        const py = cy + Math.sin(a) * rad * 0.8;\n        if (a === 0) ctx.moveTo(px, py);\n        else ctx.lineTo(px, py);\n      }\n      ctx.closePath();\n      ctx.stroke();\n    }\n    ctx.restore();\n  }\n\n  // ----------------------------------------------------\n  // 7. SURROUNDING ROCK FORMATIONS & PAPER-CUT LEDGES\n  // ----------------------------------------------------\n  const drawRockFace = (pathFn, baseCol, rimCol, isWet) => {\n    ctx.save();\n    ctx.shadowColor = 'rgba(2, 6, 12, 0.55)';\n    ctx.shadowBlur = 18;\n    ctx.shadowOffsetX = 3;\n    ctx.shadowOffsetY = 8;\n    ctx.fillStyle = baseCol;\n    ctx.beginPath();\n    pathFn(ctx);\n    ctx.fill();\n    ctx.restore();\n\n    ctx.save();\n    ctx.strokeStyle = rimCol;\n    ctx.lineWidth = 2.0;\n    ctx.beginPath();\n    pathFn(ctx);\n    ctx.stroke();\n    ctx.restore();\n\n    if (isWet > 0.05) {\n      ctx.save();\n      ctx.globalCompositeOperation = 'screen';\n      ctx.strokeStyle = `rgba(255, 245, 225, ${isWet * 0.45})`;\n      ctx.lineWidth = 3.5;\n      ctx.beginPath();\n      pathFn(ctx);\n      ctx.stroke();\n      ctx.restore();\n    }\n  };\n\n  const rockBaseCol = `rgb(${lerp(18, 48, dawn)}, ${lerp(24, 56, dawn)}, ${lerp(35, 68, dawn)})`;\n  const rockRimCol = `rgba(${lerp(160, 255, dawn)}, ${lerp(120, 210, dawn)}, ${lerp(120, 180, dawn)}, ${0.2 + dawn * 0.4})`;\n\n  // Left Foreground Rock Bluff\n  drawRockFace(\n    (c) => {\n      c.moveTo(0, 260);\n      c.bezierCurveTo(90, 280, 160, 330, 210, 390);\n      c.bezierCurveTo(240, 440, 220, 490, 290, 520);\n      c.bezierCurveTo(340, 545, 310, 590, 260, 640);\n      c.bezierCurveTo(210, 690, 90, 720, 0, 720);\n      c.closePath();\n    },\n    rockBaseCol,\n    rockRimCol,\n    wetness\n  );\n\n  // Right Foreground Tiered Boulder\n  drawRockFace(\n    (c) => {\n      c.moveTo(W, 280);\n      c.bezierCurveTo(1160, 300, 1100, 360, 1050, 420);\n      c.bezierCurveTo(980, 480, 1020, 530, 960, 580);\n      c.bezierCurveTo(910, 620, 940, 680, 1020, 720);\n      c.lineTo(W, 720);\n      c.closePath();\n    },\n    rockBaseCol,\n    rockRimCol,\n    wetness\n  );\n\n  // Central Shelf Stone\n  drawRockFace(\n    (c) => {\n      c.moveTo(270, 480);\n      c.bezierCurveTo(350, 465, 450, 460, 540, 485);\n      c.bezierCurveTo(570, 505, 550, 535, 490, 545);\n      c.bezierCurveTo(410, 555, 330, 535, 270, 480);\n      c.closePath();\n    },\n    `rgb(${lerp(14, 38, dawn)}, ${lerp(20, 46, dawn)}, ${lerp(30, 58, dawn)})`,\n    rockRimCol,\n    wetness\n  );\n\n  // Barnacles\n  const barnacles = [\n    [295, 482], [315, 478], [330, 484], [420, 472], [435, 476],\n    [520, 490], [535, 498], [1030, 440], [1045, 452], [1035, 465]\n  ];\n  ctx.fillStyle = `rgb(${lerp(100, 185, dawn)}, ${lerp(110, 195, dawn)}, ${lerp(120, 190, dawn)})`;\n  for (const [bx, by] of barnacles) {\n    ctx.beginPath();\n    ctx.arc(bx, by, 3.2, 0, Math.PI * 2);\n    ctx.fill();\n    ctx.fillStyle = 'rgba(20, 20, 30, 0.6)';\n    ctx.beginPath();\n    ctx.arc(bx, by, 1.2, 0, Math.PI * 2);\n    ctx.fill();\n  }\n\n  // ----------------------------------------------------\n  // 8. THE LITTLE SHORE CRAB\n  // ----------------------------------------------------\n  if (crabVisible > 0.05) {\n    ctx.save();\n    ctx.translate(crabX, crabY);\n    ctx.scale(0.85, 0.85);\n    ctx.globalAlpha = crabVisible;\n\n    const crabCarapaceCol = `rgb(${lerp(130, 230, dawn)}, ${lerp(50, 95, dawn)}, ${lerp(40, 60, dawn)})`;\n    const crabLegCol = `rgb(${lerp(110, 205, dawn)}, ${lerp(40, 80, dawn)}, ${lerp(30, 50, dawn)})`;\n\n    ctx.beginPath();\n    ctx.ellipse(0, 12, 18, 6, 0, 0, Math.PI * 2);\n    ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';\n    ctx.fill();\n\n    const stepCycle = t * 14;\n    for (let side = -1; side <= 1; side += 2) {\n      for (let leg = 0; leg < 3; leg++) {\n        const legPhase = stepCycle + leg * 1.8 + (side > 0 ? Math.PI : 0);\n        const legLift = Math.max(0, Math.sin(legPhase)) * 4;\n        const legSpread = (leg - 1) * 7;\n\n        ctx.strokeStyle = crabLegCol;\n        ctx.lineWidth = 2.4;\n        ctx.lineCap = 'round';\n        ctx.beginPath();\n        ctx.moveTo(side * 8, 2 + legSpread * 0.3);\n        const jointX = side * 18 + Math.cos(legPhase) * 2;\n        const jointY = -4 + legSpread * 0.4 - legLift;\n        const footX = side * 24 + legSpread;\n        const footY = 8 - legLift * 0.3;\n        ctx.quadraticCurveTo(jointX, jointY, footX, footY);\n        ctx.stroke();\n      }\n    }\n\n    ctx.fillStyle = crabCarapaceCol;\n    ctx.beginPath();\n    ctx.ellipse(0, 0, 14, 10, 0, 0, Math.PI * 2);\n    ctx.fill();\n    ctx.strokeStyle = `rgba(255, 230, 180, ${0.3 + dawn * 0.4})`;\n    ctx.lineWidth = 1.2;\n    ctx.stroke();\n\n    for (let e = -1; e <= 1; e += 2) {\n      ctx.strokeStyle = crabCarapaceCol;\n      ctx.lineWidth = 2;\n      ctx.beginPath();\n      ctx.moveTo(e * 4, -6);\n      ctx.lineTo(e * 6, -12);\n      ctx.stroke();\n\n      ctx.fillStyle = '#111';\n      ctx.beginPath();\n      ctx.arc(e * 6, -13, 2.2, 0, Math.PI * 2);\n      ctx.fill();\n\n      ctx.fillStyle = '#fff';\n      ctx.beginPath();\n      ctx.arc(e * 6 + 0.6, -13.6, 0.8, 0, Math.PI * 2);\n      ctx.fill();\n    }\n\n    for (let c = -1; c <= 1; c += 2) {\n      const clawYOffset = -crabClawRaise * 6;\n      ctx.strokeStyle = crabCarapaceCol;\n      ctx.lineWidth = 3.2;\n      ctx.beginPath();\n      ctx.moveTo(c * 10, -2);\n      ctx.quadraticCurveTo(c * 20, -10 + clawYOffset, c * 16, -16 + clawYOffset);\n      ctx.stroke();\n\n      ctx.fillStyle = `rgb(${lerp(150, 255, dawn)}, ${lerp(60, 120, dawn)}, ${lerp(40, 70, dawn)})`;\n      ctx.beginPath();\n      ctx.ellipse(c * 16, -16 + clawYOffset, 5, 3.5, c * 0.4, 0, Math.PI * 2);\n      ctx.fill();\n    }\n\n    ctx.restore();\n  }\n\n  // ----------------------------------------------------\n  // 9. THE TRANSLUCENT INCOMING SWELL & ORGANIC FOAM LACE\n  // ----------------------------------------------------\n  if (t > 11.2 && t < 20.8) {\n    ctx.save();\n\n    // Natural fluid envelope: advance -> crest & pour -> ebb\n    const surgeAdv = smoothstep(11.4, 15.0, t);\n    const surgeRec = smoothstep(15.2, 20.2, t);\n    const swellAmplitude = Math.sin(smoothstep(11.4, 19.8, t) * Math.PI);\n    const peakHeight = swellAmplitude * 64;\n\n    // A. Translucent Oceanic Swell Body (Rolling gently over midground reef)\n    if (swellAmplitude > 0.01) {\n      ctx.save();\n      ctx.globalCompositeOperation = 'source-over';\n\n      const crestBaseY = 275 - peakHeight * 0.85;\n      const swellGrad = ctx.createLinearGradient(0, crestBaseY - 15, 0, 480);\n      swellGrad.addColorStop(0, `rgba(235, 255, 250, ${0.85 * (1 - surgeRec * 0.6)})`);\n      swellGrad.addColorStop(0.18, `rgba(130, 235, 220, ${0.62 * (1 - surgeRec * 0.5)})`);\n      swellGrad.addColorStop(0.5, `rgba(35, 145, 160, ${0.42 * (1 - surgeRec * 0.7)})`);\n      swellGrad.addColorStop(0.85, `rgba(12, 70, 95, ${0.2 * (1 - surgeRec)})`);\n      swellGrad.addColorStop(1.0, 'rgba(10, 50, 70, 0)');\n\n      ctx.fillStyle = swellGrad;\n      ctx.beginPath();\n      ctx.moveTo(140, 310);\n      \n      // Undulating, curved crest line following natural wave physics\n      const cp1X = lerp(320, 380, surgeAdv);\n      const cp1Y = crestBaseY - Math.sin(t * 4.5) * 6;\n      const cp2X = lerp(680, 760, surgeAdv);\n      const cp2Y = crestBaseY - 14 + Math.cos(t * 3.8) * 8;\n      const endX = 1110;\n      const endY = 305;\n\n      ctx.bezierCurveTo(cp1X, cp1Y, cp2X, cp2Y, endX, endY);\n      // Soft lower underbelly blending into the pool bowl\n      ctx.bezierCurveTo(920, 440, 360, 450, 140, 310);\n      ctx.closePath();\n      ctx.fill();\n      ctx.restore();\n\n      // B. Sunlit Translucent Highlights on the Wave Shoulder\n      ctx.save();\n      ctx.globalCompositeOperation = 'screen';\n      const sunGlintGrad = ctx.createLinearGradient(sunX - 180, 0, sunX + 180, 0);\n      sunGlintGrad.addColorStop(0, 'rgba(255, 240, 200, 0)');\n      sunGlintGrad.addColorStop(0.5, `rgba(255, 248, 220, ${0.45 * swellAmplitude * dawn})`);\n      sunGlintGrad.addColorStop(1, 'rgba(255, 240, 200, 0)');\n\n      ctx.strokeStyle = sunGlintGrad;\n      ctx.lineWidth = 6;\n      ctx.beginPath();\n      ctx.moveTo(180, 305);\n      ctx.bezierCurveTo(cp1X, cp1Y + 3, cp2X, cp2Y + 3, endX - 40, endY);\n      ctx.stroke();\n      ctx.restore();\n    }\n\n    // C. Curving Cascades / Water veils pouring into the pool\n    if (t >= 13.0 && t <= 17.6) {\n      const cascadeLife = Math.sin(smoothstep(13.0, 17.6, t) * Math.PI);\n      \n      const drawWaterVeil = (xRoot, width, len, phaseShift) => {\n        ctx.save();\n        ctx.globalCompositeOperation = 'screen';\n        const veilGrad = ctx.createLinearGradient(xRoot, 280, xRoot + 15, 280 + len);\n        veilGrad.addColorStop(0, `rgba(255, 255, 255, ${0.8 * cascadeLife})`);\n        veilGrad.addColorStop(0.3, `rgba(180, 245, 235, ${0.55 * cascadeLife})`);\n        veilGrad.addColorStop(0.8, `rgba(100, 210, 215, ${0.3 * cascadeLife})`);\n        veilGrad.addColorStop(1.0, 'rgba(255, 255, 255, 0)');\n\n        ctx.fillStyle = veilGrad;\n        ctx.beginPath();\n        const leftX = xRoot - width / 2;\n        const rightX = xRoot + width / 2;\n        ctx.moveTo(leftX, 285);\n        ctx.bezierCurveTo(\n          leftX - 8 + Math.sin(t * 8 + phaseShift) * 4, 340,\n          leftX + 4, 400,\n          leftX + 12, 285 + len\n        );\n        ctx.bezierCurveTo(\n          rightX + 6, 285 + len + 4,\n          rightX + 10, 360,\n          rightX, 285\n        );\n        ctx.closePath();\n        ctx.fill();\n\n        // Spray and effervescence at base\n        const splashGlow = ctx.createRadialGradient(xRoot + 6, 285 + len, 2, xRoot + 6, 285 + len, width * 1.4);\n        splashGlow.addColorStop(0, `rgba(255, 255, 255, ${0.7 * cascadeLife})`);\n        splashGlow.addColorStop(0.6, `rgba(190, 245, 240, ${0.35 * cascadeLife})`);\n        splashGlow.addColorStop(1.0, 'rgba(255, 255, 255, 0)');\n        ctx.fillStyle = splashGlow;\n        ctx.beginPath();\n        ctx.ellipse(xRoot + 6, 285 + len, width * 1.4, width * 0.5, 0, 0, Math.PI * 2);\n        ctx.fill();\n        ctx.restore();\n      };\n\n      // Left chute and Center-Right plunge\n      drawWaterVeil(380, 34, 185, 0.5);\n      drawWaterVeil(415, 22, 160, 1.8);\n      drawWaterVeil(690, 48, 205, 2.2);\n      drawWaterVeil(730, 30, 175, 3.6);\n    }\n\n    // D. Soft Foamy Leading Edge & Trailing Filigree Rings\n    const foamAlpha = Math.sin(smoothstep(12.8, 19.8, t) * Math.PI);\n    if (foamAlpha > 0.01) {\n      ctx.save();\n      ctx.globalCompositeOperation = 'screen';\n\n      // 1. Organic scalloped crest lace\n      const crestY = 275 - peakHeight * 0.85;\n      for (let layer = 0; layer < 3; layer++) {\n        const lyrAlpha = foamAlpha * (0.65 - layer * 0.16);\n        ctx.strokeStyle = `rgba(255, 255, 255, ${lyrAlpha})`;\n        ctx.lineWidth = 3.2 - layer * 0.8;\n        ctx.lineCap = 'round';\n        ctx.beginPath();\n\n        const xStart = 190 + layer * 15;\n        const xEnd = 1060 - layer * 15;\n        ctx.moveTo(xStart, crestY + layer * 6);\n\n        for (let fx = xStart; fx <= xEnd; fx += 28) {\n          const waveScallop = Math.sin(fx * 0.06 + t * 7 + layer * 1.4) * (4.5 - layer * 1.0);\n          const py = crestY + layer * 6 + waveScallop;\n          const cpX = fx + 14;\n          const cpY = py - 3.5;\n          ctx.quadraticCurveTo(cpX, cpY, fx + 28, py);\n        }\n        ctx.stroke();\n      }\n\n      // 2. Swirling pool swash lines / foam webs expanding across the basin\n      const ringProg = smoothstep(13.2, 18.5, t);\n      for (let r = 0; r < 5; r++) {\n        const ringPhase = ringProg * 2.2 - r * 0.35;\n        if (ringPhase > 0 && ringPhase < 1.4) {\n          const ringScale = Math.sin(clamp(ringPhase, 0, 1) * Math.PI * 0.5);\n          const ringFade = Math.sin(clamp(ringPhase / 1.4, 0, 1) * Math.PI) * foamAlpha;\n\n          ctx.strokeStyle = `rgba(240, 255, 250, ${ringFade * 0.55})`;\n          ctx.lineWidth = 1.8;\n          ctx.beginPath();\n\n          const rCenterY = 470 + r * 30 + Math.sin(t * 2 + r) * 6;\n          const rRadiusX = 220 + ringScale * 290;\n          const rRadiusY = 32 + ringScale * 45;\n\n          for (let a = 0; a <= Math.PI * 2; a += 0.25) {\n            const pert = Math.sin(a * 7 + t * 4 + r) * 6 * ringScale;\n            const px = 640 + Math.cos(a) * (rRadiusX + pert);\n            const py = rCenterY + Math.sin(a) * (rRadiusY + pert * 0.4);\n            if (a === 0) ctx.moveTo(px, py);\n            else ctx.lineTo(px, py);\n          }\n          ctx.closePath();\n          ctx.stroke();\n        }\n      }\n\n      // 3. Delicate effervescent clusters\n      ctx.fillStyle = `rgba(245, 255, 255, ${foamAlpha * 0.75})`;\n      for (let b = 0; b < 24; b++) {\n        const bPhase = (b * 47 + t * 35) % 680;\n        const bx = 300 + bPhase;\n        const by = 450 + ((b * 29 + Math.sin(b + t * 2.5) * 16) % 180);\n        const br = 1.4 + (b % 3) * 0.8;\n        ctx.beginPath();\n        ctx.arc(bx, by, br, 0, Math.PI * 2);\n        ctx.fill();\n      }\n\n      ctx.restore();\n    }\n\n    ctx.restore();\n  }\n\n  // ----------------------------------------------------\n  // 10. POST-WAVE GLISTEN & SPECULAR TWINKLE PASS\n  // ----------------------------------------------------\n  if (wetness > 0.1) {\n    ctx.save();\n    ctx.globalCompositeOperation = 'screen';\n    \n    const sparkles = [\n      [240, 360, 0.4], [310, 420, 0.9], [460, 480, 1.4], [520, 485, 0.7],\n      [280, 520, 1.2], [980, 430, 1.6], [1050, 490, 0.8], [930, 560, 1.1],\n      [180, 330, 0.5], [640, 320, 1.3], [810, 310, 0.9], [410, 540, 1.5],\n      [360, 470, 2.1], [720, 480, 1.8], [610, 510, 0.3], [880, 490, 2.5]\n    ];\n\n    for (const [sx, sy, sPhase] of sparkles) {\n      const sparkleIntensity = Math.pow(Math.sin(t * 4.0 + sPhase) * 0.5 + 0.5, 3.0) * wetness;\n      if (sparkleIntensity > 0.12) {\n        const sSize = 4.0 + sparkleIntensity * 6.5;\n        const spkGrad = ctx.createRadialGradient(sx, sy, 0, sx, sy, sSize);\n        spkGrad.addColorStop(0, `rgba(255, 255, 255, ${sparkleIntensity * 0.95})`);\n        spkGrad.addColorStop(0.4, `rgba(255, 242, 205, ${sparkleIntensity * 0.55})`);\n        spkGrad.addColorStop(1, 'rgba(255, 220, 180, 0)');\n        \n        ctx.fillStyle = spkGrad;\n        ctx.beginPath();\n        ctx.arc(sx, sy, sSize, 0, Math.PI * 2);\n        ctx.fill();\n\n        if (sparkleIntensity > 0.55) {\n          ctx.strokeStyle = `rgba(255, 255, 255, ${sparkleIntensity * 0.75})`;\n          ctx.lineWidth = 1.0;\n          ctx.beginPath();\n          ctx.moveTo(sx - sSize * 1.6, sy);\n          ctx.lineTo(sx + sSize * 1.6, sy);\n          ctx.moveTo(sx, sy - sSize * 1.6);\n          ctx.lineTo(sx, sy + sSize * 1.6);\n          ctx.stroke();\n        }\n      }\n    }\n    ctx.restore();\n  }\n\n  // ----------------------------------------------------\n  // 11. MORNING SUNBEAMS & ATMOSPHERIC GOD RAYS\n  // ----------------------------------------------------\n  if (dawn > 0.3) {\n    ctx.save();\n    ctx.globalCompositeOperation = 'screen';\n    const rayAlpha = (dawn * 0.12) * (1 + Math.sin(t * 0.8) * 0.15);\n    \n    ctx.fillStyle = `rgba(255, 235, 175, ${rayAlpha})`;\n    const rayOrigins = [\n      { angle: 0.38, width: 95 },\n      { angle: 0.52, width: 140 },\n      { angle: 0.68, width: 110 },\n      { angle: 0.82, width: 85 }\n    ];\n\n    for (const ray of rayOrigins) {\n      ctx.beginPath();\n      ctx.moveTo(sunX, sunY);\n      const endX1 = sunX - Math.cos(ray.angle) * 1100;\n      const endY1 = sunY + Math.sin(ray.angle) * 1100;\n      const endX2 = endX1 - ray.width;\n      const endY2 = endY1 + ray.width * 0.6;\n      ctx.lineTo(endX1, endY1);\n      ctx.lineTo(endX2, endY2);\n      ctx.closePath();\n      ctx.fill();\n    }\n    ctx.restore();\n  }\n\n  // ----------------------------------------------------\n  // 12. CINEMATIC VIGNETTE\n  // ----------------------------------------------------\n  const vigGrad = ctx.createRadialGradient(W / 2, H / 2, 380, W / 2, H / 2, 720);\n  vigGrad.addColorStop(0, 'rgba(0,0,0,0)');\n  vigGrad.addColorStop(0.8, 'rgba(8, 12, 24, 0.22)');\n  vigGrad.addColorStop(1.0, 'rgba(4, 6, 15, 0.58)');\n  ctx.fillStyle = vigGrad;\n  ctx.fillRect(0, 0, W, H);\n\n  // ----------------------------------------------------\n  // 13. CLOSING TITLE: \"TIDE POOL\"\n  // ----------------------------------------------------\n  if (t > 19.0) {\n    const titleAlpha = smoothstep(19.5, 22.0, t);\n    ctx.save();\n    ctx.textAlign = 'center';\n    ctx.textBaseline = 'middle';\n    \n    ctx.font = '300 28px -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Cinzel\", \"Didot\", serif';\n    ctx.letterSpacing = '14px';\n    \n    ctx.shadowColor = 'rgba(0, 0, 0, 0.4)';\n    ctx.shadowBlur = 12;\n    ctx.shadowOffsetX = 0;\n    ctx.shadowOffsetY = 2;\n\n    ctx.fillStyle = `rgba(255, 250, 240, ${titleAlpha * 0.95})`;\n    ctx.fillText('T I D E   P O O L', W / 2, 95);\n\n    const lineLen = 140 * smoothstep(20.0, 22.5, t);\n    ctx.strokeStyle = `rgba(255, 225, 180, ${titleAlpha * 0.6})`;\n    ctx.lineWidth = 1.0;\n    ctx.beginPath();\n    ctx.moveTo(W / 2 - lineLen / 2, 118);\n    ctx.lineTo(W / 2 + lineLen / 2, 118);\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: dawn over a rocky tide pool. Light creeps across the water, anemones open, a small crab sidles between stones, and a single wave washes in, fills the pool and draws back leaving everything glistening. Closing title: TIDE POOL. 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"}