{"id":"first-snow","title":"First Snow","category":"Drawn film","prompt":"A 26-second illustrated film: a red fox crosses a pine forest clearing as the first snow of winter begins. Flakes thicken, the ground whitens, and the fox's tracks slowly fill behind it. It pauses, looks back, and trots on into blue dusk. Closing title: FIRST SNOW. 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","inference":{"inputTokens":6148,"outputTokens":10760,"costUsd":0.1079}},"render":{"credits":11,"priceUsd":0.165,"format":"mp4","frameCount":624,"bytes":4030127,"seed":42,"width":1280,"height":720,"fps":24,"durationSeconds":26},"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=first-snow), and it exports as MP4 or GIF, not as a still."},"renderRequest":{"drawContract":"const meta = { duration: 26, 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  // Helper math & easing functions\n  // ----------------------------------------------------\n  const clamp = (v, min, max) => Math.max(min, Math.min(max, v));\n  const smoothstep = (edge0, edge1, x) => {\n    const u = clamp((x - edge0) / (edge1 - edge0), 0, 1);\n    return u * u * (3 - 2 * u);\n  };\n  const lerp = (a, b, u) => a + (b - a) * u;\n  const pseudo = (n) => {\n    const s = Math.sin(n * 127.1 + 311.7) * 43758.5453123;\n    return s - Math.floor(s);\n  };\n\n  // Progression of winter evening / snow intensity (0 to 1)\n  const duskProgress = smoothstep(2, 22, t);\n  const snowIntensity = smoothstep(1, 16, t);\n\n  // Clearing ground profile function\n  function groundY(x) {\n    return (\n      515 +\n      Math.sin(x * 0.0028 + 0.4) * 26 +\n      Math.cos(x * 0.0065) * 10 +\n      Math.sin(x * 0.014) * 3\n    );\n  }\n\n  // ----------------------------------------------------\n  // 1. Sky & Atmospheric Twilight Gradient\n  // ----------------------------------------------------\n  const skyGrad = ctx.createLinearGradient(0, 0, 0, H * 0.85);\n  // Transitions from early dusk gold-rose-indigo to deep winter sapphire\n  const skyTop = duskProgress < 0.6\n    ? `rgb(${lerp(18, 10, duskProgress / 0.6)}, ${lerp(28, 16, duskProgress / 0.6)}, ${lerp(58, 38, duskProgress / 0.6)})`\n    : `rgb(${lerp(10, 6, (duskProgress - 0.6) / 0.4)}, ${lerp(16, 10, (duskProgress - 0.6) / 0.4)}, ${lerp(38, 22, (duskProgress - 0.6) / 0.4)})`;\n\n  const skyMid = duskProgress < 0.5\n    ? `rgb(${lerp(65, 34, duskProgress / 0.5)}, ${lerp(60, 36, duskProgress / 0.5)}, ${lerp(92, 68, duskProgress / 0.5)})`\n    : `rgb(${lerp(34, 18, (duskProgress - 0.5) / 0.5)}, ${lerp(36, 22, (duskProgress - 0.5) / 0.5)}, ${lerp(68, 44, (duskProgress - 0.5) / 0.5)})`;\n\n  const skyLow = duskProgress < 0.5\n    ? `rgb(${lerp(168, 62, duskProgress / 0.5)}, ${lerp(118, 54, duskProgress / 0.5)}, ${lerp(112, 78, duskProgress / 0.5)})`\n    : `rgb(${lerp(62, 32, (duskProgress - 0.5) / 0.5)}, ${lerp(54, 34, (duskProgress - 0.5) / 0.5)}, ${lerp(78, 56, (duskProgress - 0.5) / 0.5)})`;\n\n  skyGrad.addColorStop(0, skyTop);\n  skyGrad.addColorStop(0.55, skyMid);\n  skyGrad.addColorStop(1, skyLow);\n\n  ctx.fillStyle = skyGrad;\n  ctx.fillRect(0, 0, W, H);\n\n  // Soft lingering horizon glow behind the pines\n  const horizonGlow = ctx.createRadialGradient(\n    W * 0.48,\n    420,\n    30,\n    W * 0.48,\n    420,\n    580\n  );\n  const glowAlpha = lerp(0.38, 0.12, duskProgress);\n  horizonGlow.addColorStop(0, `rgba(240, 160, 110, ${glowAlpha})`);\n  horizonGlow.addColorStop(0.35, `rgba(180, 110, 125, ${glowAlpha * 0.65})`);\n  horizonGlow.addColorStop(0.7, `rgba(70, 65, 110, ${glowAlpha * 0.25})`);\n  horizonGlow.addColorStop(1, 'rgba(20, 25, 45, 0)');\n  ctx.fillStyle = horizonGlow;\n  ctx.fillRect(0, 0, W, H);\n\n  // ----------------------------------------------------\n  // 2. Distant Mountain Silhouettes & Deep Ridge\n  // ----------------------------------------------------\n  function drawRidge(pts, color) {\n    ctx.beginPath();\n    ctx.moveTo(pts[0][0], pts[0][1]);\n    for (let i = 1; i < pts.length; i++) {\n      ctx.lineTo(pts[i][0], pts[i][1]);\n    }\n    ctx.lineTo(W + 50, H);\n    ctx.lineTo(-50, H);\n    ctx.closePath();\n    ctx.fillStyle = color;\n    ctx.fill();\n  }\n\n  // Distant mountain\n  const distColor = lerpColor(\n    [48, 52, 78],\n    [22, 26, 44],\n    duskProgress\n  );\n  drawRidge(\n    [\n      [-40, 360],\n      [180, 330],\n      [340, 350],\n      [520, 310],\n      [680, 345],\n      [890, 320],\n      [1080, 355],\n      [1320, 330],\n    ],\n    `rgb(${distColor[0]}, ${distColor[1]}, ${distColor[2]})`\n  );\n\n  // Atmospheric mist over distant mountains\n  const mist1 = ctx.createLinearGradient(0, 300, 0, 480);\n  mist1.addColorStop(0, 'rgba(180, 190, 215, 0)');\n  mist1.addColorStop(0.5, `rgba(140, 150, 185, ${lerp(0.18, 0.08, duskProgress)})`);\n  mist1.addColorStop(1, 'rgba(120, 130, 160, 0)');\n  ctx.fillStyle = mist1;\n  ctx.fillRect(0, 280, W, 200);\n\n  // ----------------------------------------------------\n  // 3. Procedural Pine Forest Silhouette Layers\n  // ----------------------------------------------------\n  function drawPineTree(x, baseY, height, width, branchColor, snowAmount, seedVal) {\n    ctx.save();\n    ctx.translate(x, baseY);\n\n    // Trunk\n    ctx.fillStyle = branchColor;\n    ctx.beginPath();\n    ctx.rect(-width * 0.08, -height, width * 0.16, height);\n    ctx.fill();\n\n    // Boughs in tiers\n    const tiers = 7;\n    for (let i = 0; i < tiers; i++) {\n      const u = i / (tiers - 1);\n      const tierY = -height * (0.2 + u * 0.78);\n      const tierW = width * (1.05 - u * 0.75);\n      const tierH = height * (0.28 - u * 0.1);\n      const sag = Math.sin(seedVal + i * 1.7) * 4;\n\n      ctx.beginPath();\n      ctx.moveTo(0, tierY - tierH);\n      ctx.quadraticCurveTo(\n        tierW * 0.55,\n        tierY - tierH * 0.2,\n        tierW * 0.5,\n        tierY + sag\n      );\n      ctx.quadraticCurveTo(0, tierY - tierH * 0.25, -tierW * 0.5, tierY + sag);\n      ctx.quadraticCurveTo(\n        -tierW * 0.55,\n        tierY - tierH * 0.2,\n        0,\n        tierY - tierH\n      );\n      ctx.closePath();\n      ctx.fillStyle = branchColor;\n      ctx.fill();\n\n      // Snow on branches (soft paper-cut edge)\n      if (snowAmount > 0.02) {\n        ctx.beginPath();\n        ctx.moveTo(0, tierY - tierH);\n        ctx.quadraticCurveTo(\n          tierW * 0.5,\n          tierY - tierH * 0.25,\n          tierW * 0.46,\n          tierY - 2 + sag\n        );\n        ctx.quadraticCurveTo(\n          tierW * 0.25,\n          tierY - tierH * 0.15,\n          0,\n          tierY - tierH * 0.6\n        );\n        ctx.quadraticCurveTo(\n          -tierW * 0.25,\n          tierY - tierH * 0.15,\n          -tierW * 0.46,\n          tierY - 2 + sag\n        );\n        ctx.quadraticCurveTo(\n          -tierW * 0.5,\n          tierY - tierH * 0.25,\n          0,\n          tierY - tierH\n        );\n        ctx.closePath();\n\n        const snowCol = lerpColor([200, 208, 224], [225, 232, 248], duskProgress);\n        ctx.fillStyle = `rgba(${snowCol[0]}, ${snowCol[1]}, ${snowCol[2]}, ${snowAmount * 0.88})`;\n        ctx.fill();\n      }\n    }\n    ctx.restore();\n  }\n\n  // Layer 1: Distant Pines\n  const treeCountFar = 32;\n  for (let i = 0; i <= treeCountFar; i++) {\n    const tx = (i / treeCountFar) * (W + 240) - 120;\n    // Keep clearing somewhat open in center\n    const centerDist = Math.abs(tx - W * 0.46);\n    if (centerDist < 140 && i % 3 !== 0) continue;\n    const p = pseudo(i * 7.13);\n    const ty = 460 + Math.sin(tx * 0.003) * 15 + p * 18;\n    const th = 110 + p * 55;\n    const tw = 40 + p * 20;\n    const farCol = `rgb(${lerp(38, 16, duskProgress)}, ${lerp(46, 22, duskProgress)}, ${lerp(66, 36, duskProgress)})`;\n    drawPineTree(tx, ty, th, tw, farCol, snowIntensity * 0.5, i);\n  }\n\n  // Midground haze\n  const mist2 = ctx.createLinearGradient(0, 420, 0, 520);\n  mist2.addColorStop(0, 'rgba(150, 165, 195, 0)');\n  mist2.addColorStop(\n    0.6,\n    `rgba(130, 145, 180, ${lerp(0.22, 0.12, duskProgress)})`\n  );\n  mist2.addColorStop(1, 'rgba(100, 115, 150, 0)');\n  ctx.fillStyle = mist2;\n  ctx.fillRect(0, 400, W, 140);\n\n  // Layer 2: Mid-distance Pines (Framing clearing)\n  const midTrees = [\n    { x: -30, h: 260, w: 90, s: 1 },\n    { x: 40, h: 290, w: 95, s: 2 },\n    { x: 110, h: 230, w: 80, s: 3 },\n    { x: 190, h: 270, w: 85, s: 4 },\n    { x: 260, h: 210, w: 75, s: 5 },\n    { x: 860, h: 220, w: 75, s: 6 },\n    { x: 940, h: 260, w: 85, s: 7 },\n    { x: 1020, h: 290, w: 95, s: 8 },\n    { x: 1110, h: 320, w: 100, s: 9 },\n    { x: 1200, h: 275, w: 90, s: 10 },\n    { x: 1290, h: 310, w: 105, s: 11 },\n  ];\n\n  for (const tInfo of midTrees) {\n    const gy = groundY(tInfo.x) - 8;\n    const midCol = `rgb(${lerp(24, 10, duskProgress)}, ${lerp(30, 14, duskProgress)}, ${lerp(46, 24, duskProgress)})`;\n    drawPineTree(\n      tInfo.x,\n      gy,\n      tInfo.h,\n      tInfo.w,\n      midCol,\n      snowIntensity * 0.75,\n      tInfo.s\n    );\n  }\n\n  // ----------------------------------------------------\n  // 4. Forest Clearing Ground & Snow Accumulation\n  // ----------------------------------------------------\n  // Ground base: Autumn earth transitioning into fresh crisp white/periwinkle snow mantle\n  ctx.beginPath();\n  ctx.moveTo(-50, H);\n  ctx.lineTo(-50, groundY(-50));\n  for (let x = -40; x <= W + 50; x += 15) {\n    ctx.lineTo(x, groundY(x));\n  }\n  ctx.lineTo(W + 50, H);\n  ctx.closePath();\n\n  const groundGrad = ctx.createLinearGradient(0, 480, 0, H);\n  const snowR = lerp(110, 195, snowIntensity);\n  const snowG = lerp(105, 206, snowIntensity);\n  const snowB = lerp(120, 226, snowIntensity);\n\n  const deepSnowR = lerp(45, 115, snowIntensity);\n  const deepSnowG = lerp(48, 125, snowIntensity);\n  const deepSnowB = lerp(68, 160, snowIntensity);\n\n  groundGrad.addColorStop(0, `rgb(${snowR}, ${snowG}, ${snowB})`);\n  groundGrad.addColorStop(0.35, `rgb(${snowR * 0.88}, ${snowG * 0.88}, ${snowB * 0.92})`);\n  groundGrad.addColorStop(1, `rgb(${deepSnowR}, ${deepSnowG}, ${deepSnowB})`);\n  ctx.fillStyle = groundGrad;\n  ctx.fill();\n\n  // Subtle paper-cut terrain shadow contour\n  ctx.beginPath();\n  ctx.moveTo(-50, groundY(-50) + 18);\n  for (let x = -40; x <= W + 50; x += 25) {\n    ctx.lineTo(x, groundY(x) + 18 + Math.sin(x * 0.009) * 6);\n  }\n  ctx.lineTo(W + 50, H);\n  ctx.lineTo(-50, H);\n  ctx.closePath();\n  ctx.fillStyle = `rgba(${deepSnowR * 0.6}, ${deepSnowG * 0.6}, ${deepSnowB * 0.75}, 0.22)`;\n  ctx.fill();\n\n  // Gentle grass/frost blades peeking through early on, slowly buried by snow\n  if (snowIntensity < 0.95) {\n    const grassAlpha = (1 - snowIntensity) * 0.45;\n    ctx.strokeStyle = `rgba(50, 45, 38, ${grassAlpha})`;\n    ctx.lineWidth = 1.4;\n    for (let i = 0; i < 45; i++) {\n      const gx = 60 + pseudo(i * 13.7) * (W - 120);\n      const gy = groundY(gx) + pseudo(i * 5.2) * 12;\n      const gh = 6 + pseudo(i * 9.1) * 8;\n      ctx.beginPath();\n      ctx.moveTo(gx, gy);\n      ctx.quadraticCurveTo(\n        gx + (pseudo(i * 3.3) - 0.5) * 8,\n        gy - gh * 0.6,\n        gx + (pseudo(i * 3.3) - 0.5) * 12,\n        gy - gh\n      );\n      ctx.stroke();\n    }\n  }\n\n  // ----------------------------------------------------\n  // 5. Fox Kinematics & Walking Path\n  // ----------------------------------------------------\n  // Fox timeline:\n  // t: 0.0 -> 8.0  : trots in from left (-100 -> 610)\n  // t: 8.0 -> 14.0 : stops in clearing, looks back, breathes gentle vapor\n  // t: 14.0 -> 21.5: trots on into right pine woods (610 -> 1380)\n  // t: 21.5 -> 26.0: camera / woods in quiet snowy twilight, title appears\n\n  let foxX = -120;\n  let foxMoving = false;\n  let foxSpeed = 0;\n  let lookBackFactor = 0; // 0 = forward, 1 = looking back over shoulder\n\n  const pauseX = 615;\n\n  if (t < 8.0) {\n    const u = t / 8.0;\n    foxX = lerp(-120, pauseX, smoothstep(0, 1, u));\n    foxMoving = t < 7.7;\n    foxSpeed = 1.0;\n  } else if (t < 14.0) {\n    foxX = pauseX;\n    foxMoving = false;\n    foxSpeed = 0;\n\n    // Head turn animation (turns back at t=9.5, holds till 12.3, turns forward)\n    if (t >= 9.2 && t < 12.8) {\n      lookBackFactor = smoothstep(9.2, 10.2, t) * (1 - smoothstep(11.8, 12.8, t));\n    }\n  } else if (t <= 22.0) {\n    const u = (t - 14.0) / 7.5;\n    foxX = lerp(pauseX, 1380, smoothstep(0, 1, u));\n    foxMoving = t > 14.2 && t < 21.2;\n    foxSpeed = 1.0;\n  } else {\n    foxX = 1420;\n    foxMoving = false;\n  }\n\n  const foxGroundY = groundY(foxX);\n\n  // ----------------------------------------------------\n  // 6. Fox Footprints / Tracks (Filling with snow behind it)\n  // ----------------------------------------------------\n  // Pre-calculate step events along fox timeline\n  const stepRate = 0.28; // seconds per footprint\n  const maxSteps = 100;\n\n  for (let s = 0; s < maxSteps; s++) {\n    // Determine time of this step\n    let stepT = 0;\n    if (s < 28) {\n      stepT = s * stepRate;\n      if (stepT > 7.7) continue;\n    } else {\n      stepT = 14.2 + (s - 28) * stepRate;\n      if (stepT > 21.0) continue;\n    }\n\n    if (t > stepT) {\n      // Position of this step\n      let stX = 0;\n      if (stepT < 8.0) {\n        stX = lerp(-120, pauseX, smoothstep(0, 1, stepT / 8.0));\n      } else {\n        stX = lerp(pauseX, 1380, smoothstep(0, 1, (stepT - 14.0) / 7.5));\n      }\n\n      const stY = groundY(stX);\n      const isRightPaw = s % 2 === 0;\n      const pawOffset = isRightPaw ? 3.5 : -3.5;\n\n      // Track fills as snow falls over time!\n      const trackAge = t - stepT;\n      // Tracks fill faster as snow thickens\n      const fillProgress = clamp(trackAge * 0.08 * (0.8 + snowIntensity * 1.2), 0, 1);\n      const trackAlpha = (1 - fillProgress) * lerp(0.6, 0.75, snowIntensity);\n\n      if (trackAlpha > 0.02 && stX > -20 && stX < W + 20) {\n        ctx.save();\n        ctx.translate(stX, stY + pawOffset * 0.4);\n\n        // Track indentation shadow (crisp, soft interior)\n        const trackColor = lerpColor([90, 85, 95], [130, 142, 175], duskProgress);\n        ctx.fillStyle = `rgba(${trackColor[0]}, ${trackColor[1]}, ${trackColor[2]}, ${trackAlpha})`;\n\n        // Main paw pad\n        ctx.beginPath();\n        ctx.ellipse(0, 0, 3.8 * (1 - fillProgress * 0.4), 2.4 * (1 - fillProgress * 0.5), 0.15, 0, Math.PI * 2);\n        ctx.fill();\n\n        // Claw / toe marks\n        if (fillProgress < 0.6) {\n          ctx.beginPath();\n          ctx.arc(3.5, -1.5, 1.2, 0, Math.PI * 2);\n          ctx.arc(4.2, 0.5, 1.2, 0, Math.PI * 2);\n          ctx.arc(3.2, 2.2, 1.2, 0, Math.PI * 2);\n          ctx.fill();\n        }\n\n        // Soft freshly fallen drift dust inside older tracks\n        if (fillProgress > 0.15) {\n          ctx.fillStyle = `rgba(235, 240, 252, ${fillProgress * trackAlpha * 0.7})`;\n          ctx.beginPath();\n          ctx.ellipse(0.5, -0.4, 2.6, 1.6, 0, 0, Math.PI * 2);\n          ctx.fill();\n        }\n\n        ctx.restore();\n      }\n    }\n  }\n\n  // ----------------------------------------------------\n  // 7. Render The Fox (Illustrated Paper-Cut & Painterly)\n  // ----------------------------------------------------\n  if (foxX > -80 && foxX < W + 80) {\n    ctx.save();\n    ctx.translate(foxX, foxGroundY - 4);\n\n    // Subtle breathing / trotting bob\n    const walkPhase = t * 13.5;\n    const verticalBob = foxMoving ? Math.sin(walkPhase * 2) * 2.2 : Math.sin(t * 2.2) * 0.8;\n\n    // Ground shadow beneath fox\n    const shadowGrad = ctx.createRadialGradient(0, 3, 2, 0, 3, 38);\n    shadowGrad.addColorStop(0, 'rgba(40, 48, 70, 0.45)');\n    shadowGrad.addColorStop(0.6, 'rgba(50, 60, 85, 0.18)');\n    shadowGrad.addColorStop(1, 'rgba(70, 80, 110, 0)');\n    ctx.fillStyle = shadowGrad;\n    ctx.beginPath();\n    ctx.ellipse(0, 2, 34, 9, 0, 0, Math.PI * 2);\n    ctx.fill();\n\n    ctx.translate(0, verticalBob);\n\n    // Fox Colors\n    const furOrange = '#cf531d';\n    const furDeepOrange = '#a93a0d';\n    const furShadow = '#782607';\n    const furCream = '#f8ede2';\n    const furDarkPaw = '#221915';\n\n    // Tail motion: elegant swaying curve\n    const tailPhase = foxMoving ? Math.sin(walkPhase - 0.8) * 0.28 : Math.sin(t * 1.8) * 0.12;\n    const tailBaseX = -28;\n    const tailBaseY = -22;\n\n    ctx.save();\n    ctx.translate(tailBaseX, tailBaseY);\n    ctx.rotate(0.25 + tailPhase);\n\n    // Tail body (full lush brush tail)\n    ctx.beginPath();\n    ctx.moveTo(0, 0);\n    ctx.bezierCurveTo(-22, -6, -42, 8, -55, 2);\n    ctx.bezierCurveTo(-65, -3, -58, -20, -38, -18);\n    ctx.bezierCurveTo(-18, -16, -6, -8, 0, 0);\n    ctx.closePath();\n    ctx.fillStyle = furOrange;\n    ctx.fill();\n\n    // White tail brush tip\n    ctx.beginPath();\n    ctx.moveTo(-45, 0);\n    ctx.bezierCurveTo(-53, 5, -60, 2, -65, -3);\n    ctx.bezierCurveTo(-58, -18, -48, -16, -42, -10);\n    ctx.quadraticCurveTo(-45, -5, -45, 0);\n    ctx.closePath();\n    ctx.fillStyle = furCream;\n    ctx.fill();\n    ctx.restore();\n\n    // Legs: Back-left and Front-left (distant legs, drawn in shadow)\n    const legSwing1 = foxMoving ? Math.sin(walkPhase) : 0;\n    const legSwing2 = foxMoving ? Math.sin(walkPhase + Math.PI) : 0;\n\n    function drawLeg(startX, startY, swing, isBack, isFar) {\n      ctx.save();\n      ctx.translate(startX, startY);\n      const angle = swing * 0.35 + (isBack ? 0.08 : -0.05);\n      ctx.rotate(angle);\n\n      const legH = isBack ? 26 : 24;\n      ctx.beginPath();\n      ctx.moveTo(-3.5, 0);\n      ctx.lineTo(-2, legH * 0.55);\n      ctx.lineTo(-1.5, legH);\n      ctx.lineTo(3.5, legH);\n      ctx.lineTo(2.5, legH * 0.5);\n      ctx.lineTo(3.5, 0);\n      ctx.closePath();\n      ctx.fillStyle = isFar ? furShadow : furOrange;\n      ctx.fill();\n\n      // Dark stocking / paw\n      ctx.beginPath();\n      ctx.moveTo(-2, legH * 0.58);\n      ctx.lineTo(-1.5, legH);\n      ctx.lineTo(3.8, legH);\n      ctx.lineTo(2.6, legH * 0.58);\n      ctx.closePath();\n      ctx.fillStyle = furDarkPaw;\n      ctx.fill();\n      ctx.restore();\n    }\n\n    // Far legs\n    drawLeg(-18, -14, legSwing2, true, true);\n    drawLeg(18, -16, legSwing1, false, true);\n\n    // Fox Torso\n    ctx.beginPath();\n    ctx.moveTo(-28, -22);\n    ctx.bezierCurveTo(-26, -34, 6, -35, 22, -26); // back spine\n    ctx.bezierCurveTo(28, -20, 26, -10, 16, -6);  // chest\n    ctx.bezierCurveTo(2, -4, -14, -6, -26, -14);  // belly\n    ctx.closePath();\n    ctx.fillStyle = furOrange;\n    ctx.fill();\n\n    // Fox Cream Belly / Underbelly fluff\n    ctx.beginPath();\n    ctx.moveTo(-16, -10);\n    ctx.bezierCurveTo(-4, -6, 12, -7, 20, -16);\n    ctx.bezierCurveTo(24, -12, 18, -6, 10, -5);\n    ctx.bezierCurveTo(-4, -4, -12, -6, -16, -10);\n    ctx.closePath();\n    ctx.fillStyle = furCream;\n    ctx.fill();\n\n    // Near legs\n    drawLeg(-16, -14, legSwing1, true, false);\n    drawLeg(20, -16, legSwing2, false, false);\n\n    // Fox Chest Fluff & Neck\n    ctx.beginPath();\n    ctx.moveTo(12, -28);\n    ctx.quadraticCurveTo(24, -32, 28, -36);\n    ctx.quadraticCurveTo(34, -22, 22, -14);\n    ctx.quadraticCurveTo(14, -18, 12, -28);\n    ctx.closePath();\n    ctx.fillStyle = furCream;\n    ctx.fill();\n\n    // Head & Muzzle (animated rotation when looking back)\n    ctx.save();\n    const headPivotX = 26;\n    const headPivotY = -34;\n    ctx.translate(headPivotX, headPivotY);\n\n    // Smooth head turn calculation (-135 degrees when fully looking back)\n    const headAngle = -lookBackFactor * 2.35 + (foxMoving ? Math.sin(walkPhase * 2) * 0.05 : 0);\n    ctx.rotate(headAngle);\n\n    // Head base wedge\n    ctx.beginPath();\n    ctx.moveTo(-8, 5);\n    ctx.lineTo(16, 2);   // snout tip\n    ctx.lineTo(7, -10);  // forehead\n    ctx.lineTo(-6, -8);  // crown\n    ctx.closePath();\n    ctx.fillStyle = furOrange;\n    ctx.fill();\n\n    // Cream cheek / jaw patch\n    ctx.beginPath();\n    ctx.moveTo(1, 4);\n    ctx.lineTo(15, 2);\n    ctx.lineTo(7, -1);\n    ctx.quadraticCurveTo(2, -1, -4, 4);\n    ctx.closePath();\n    ctx.fillStyle = furCream;\n    ctx.fill();\n\n    // Nose\n    ctx.beginPath();\n    ctx.arc(15.5, 1.8, 1.6, 0, Math.PI * 2);\n    ctx.fillStyle = furDarkPaw;\n    ctx.fill();\n\n    // Keen dark eye\n    ctx.beginPath();\n    ctx.ellipse(6, -3.2, 1.6, 1.1, 0.2, 0, Math.PI * 2);\n    ctx.fillStyle = '#1c120c';\n    ctx.fill();\n\n    // Catchlight in eye\n    ctx.beginPath();\n    ctx.arc(6.3, -3.6, 0.45, 0, Math.PI * 2);\n    ctx.fillStyle = '#ffffff';\n    ctx.fill();\n\n    // Ears (Alert triangular ears)\n    // Left / Near Ear\n    ctx.beginPath();\n    ctx.moveTo(-3, -7);\n    ctx.lineTo(1, -20);\n    ctx.lineTo(7, -9);\n    ctx.closePath();\n    ctx.fillStyle = furOrange;\n    ctx.fill();\n\n    // Ear inner fluff\n    ctx.beginPath();\n    ctx.moveTo(-1, -7);\n    ctx.lineTo(1.5, -17);\n    ctx.lineTo(5.5, -9);\n    ctx.closePath();\n    ctx.fillStyle = furCream;\n    ctx.fill();\n\n    // Ear dark back edge\n    ctx.beginPath();\n    ctx.moveTo(1, -20);\n    ctx.lineTo(7, -9);\n    ctx.lineTo(5, -9);\n    ctx.lineTo(0.5, -18);\n    ctx.closePath();\n    ctx.fillStyle = furDarkPaw;\n    ctx.fill();\n\n    // Breath vapor puffs when paused in the cold air\n    if (lookBackFactor > 0.4) {\n      for (let p = 0; p < 3; p++) {\n        const puffAge = ((t * 1.4 + p * 0.9) % 2.5);\n        const puffScale = 1 + puffAge * 3.5;\n        const puffAlpha = (1 - puffAge / 2.5) * 0.35 * lookBackFactor;\n        const pX = 18 + puffAge * 14;\n        const pY = 1 - puffAge * 8;\n\n        ctx.beginPath();\n        ctx.arc(pX, pY, puffScale * 2.2, 0, Math.PI * 2);\n        ctx.fillStyle = `rgba(240, 245, 255, ${puffAlpha})`;\n        ctx.fill();\n      }\n    }\n\n    ctx.restore(); // Head\n    ctx.restore(); // Fox\n  }\n\n  // ----------------------------------------------------\n  // 8. Foreground Birch & Pine Silhouettes (Framing)\n  // ----------------------------------------------------\n  function drawBirchTrunk(bx, by, bHeight, bWidth, lean) {\n    ctx.save();\n    ctx.translate(bx, by);\n    ctx.rotate(lean);\n\n    // Trunk\n    const bGrad = ctx.createLinearGradient(-bWidth / 2, 0, bWidth / 2, 0);\n    bGrad.addColorStop(0, 'rgb(35, 42, 55)');\n    bGrad.addColorStop(0.3, 'rgb(180, 190, 205)');\n    bGrad.addColorStop(0.7, 'rgb(215, 222, 235)');\n    bGrad.addColorStop(1, 'rgb(25, 30, 42)');\n\n    ctx.fillStyle = bGrad;\n    ctx.beginPath();\n    ctx.rect(-bWidth / 2, -bHeight, bWidth, bHeight);\n    ctx.fill();\n\n    // Birch dark lenticels / notches\n    ctx.fillStyle = 'rgba(20, 24, 35, 0.75)';\n    for (let i = 0; i < 18; i++) {\n      const ny = -bHeight * (0.05 + (i / 18) * 0.9);\n      const nw = bWidth * (0.3 + pseudo(i * 4.1) * 0.45);\n      const nx = (pseudo(i * 8.2) - 0.5) * (bWidth - nw);\n      ctx.beginPath();\n      ctx.ellipse(nx, ny, nw * 0.5, 2.2, (pseudo(i) - 0.5) * 0.2, 0, Math.PI * 2);\n      ctx.fill();\n    }\n    ctx.restore();\n  }\n\n  // Foreground trunks at frame edges\n  drawBirchTrunk(-25, H + 20, 780, 42, 0.04);\n  drawBirchTrunk(W + 15, H + 20, 780, 36, -0.05);\n\n  // Delicate foreground winter pine branch dipping from top-right\n  ctx.save();\n  ctx.translate(W - 40, -10);\n  ctx.rotate(0.32);\n  drawPineTree(0, 0, 320, 110, `rgb(16, 20, 30)`, snowIntensity * 0.85, 99);\n  ctx.restore();\n\n  // ----------------------------------------------------\n  // 9. Falling Snow Particle System (Multi-Layer Depth)\n  // ----------------------------------------------------\n  // Deterministic snowflake computation across depth planes\n  const totalFlakes = 260;\n  const activeFlakes = Math.floor(totalFlakes * lerp(0.15, 1.0, snowIntensity));\n\n  for (let i = 0; i < activeFlakes; i++) {\n    const pSeed = i * 19.341;\n    const depth = pseudo(pSeed + 1.2); // 0 (far) to 1 (near foreground)\n    const fallSpeed = lerp(45, 130, depth) * (0.85 + pseudo(pSeed + 3.1) * 0.3);\n    const driftAmp = lerp(12, 38, depth);\n    const driftFreq = lerp(0.8, 1.6, depth);\n\n    // Initial random offsets\n    const x0 = pseudo(pSeed + 2.5) * (W + 200) - 100;\n    const y0 = pseudo(pSeed + 5.7) * (H + 100) - 50;\n\n    // Time integrated position (pure function of t)\n    const curY = (y0 + t * fallSpeed) % (H + 80) - 40;\n    const curX =\n      x0 +\n      Math.sin(t * driftFreq + pSeed) * driftAmp +\n      (t * lerp(8, 24, depth)) % (W + 100) -\n      50;\n\n    const flakeRadius = lerp(1.0, 3.8, depth);\n    const flakeAlpha = lerp(0.25, 0.85, depth) * lerp(0.4, 1.0, snowIntensity);\n\n    ctx.beginPath();\n    ctx.arc(curX, curY, flakeRadius, 0, Math.PI * 2);\n\n    if (depth > 0.82) {\n      // Large soft bokeh snowflake close to camera\n      const flakeGrad = ctx.createRadialGradient(\n        curX,\n        curY,\n        0,\n        curX,\n        curY,\n        flakeRadius * 2.2\n      );\n      flakeGrad.addColorStop(0, `rgba(255, 255, 255, ${flakeAlpha})`);\n      flakeGrad.addColorStop(0.5, `rgba(230, 240, 255, ${flakeAlpha * 0.5})`);\n      flakeGrad.addColorStop(1, 'rgba(210, 225, 255, 0)');\n      ctx.fillStyle = flakeGrad;\n      ctx.arc(curX, curY, flakeRadius * 2.2, 0, Math.PI * 2);\n      ctx.fill();\n    } else {\n      ctx.fillStyle = `rgba(240, 246, 255, ${flakeAlpha})`;\n      ctx.fill();\n    }\n  }\n\n  // ----------------------------------------------------\n  // 10. Cinematic Vignette & Atmospheric Color Wash\n  // ----------------------------------------------------\n  const vignette = ctx.createRadialGradient(\n    W * 0.5,\n    H * 0.48,\n    W * 0.35,\n    W * 0.5,\n    H * 0.48,\n    W * 0.72\n  );\n  vignette.addColorStop(0, 'rgba(0, 0, 0, 0)');\n  vignette.addColorStop(0.7, 'rgba(8, 12, 24, 0.2)');\n  vignette.addColorStop(1, 'rgba(4, 6, 16, 0.55)');\n  ctx.fillStyle = vignette;\n  ctx.fillRect(0, 0, W, H);\n\n  // ----------------------------------------------------\n  // 11. Closing Title: FIRST SNOW (t = 21.0 to 26.0)\n  // ----------------------------------------------------\n  if (t > 20.8) {\n    const titleFade = smoothstep(21.0, 22.8, t) * (1 - smoothstep(25.0, 26.0, 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.34;\n\n    // Soft warm ethereal glow behind title\n    const textGlow = ctx.createRadialGradient(\n      titleX,\n      titleY,\n      10,\n      titleX,\n      titleY,\n      220\n    );\n    textGlow.addColorStop(0, `rgba(180, 205, 245, ${titleFade * 0.22})`);\n    textGlow.addColorStop(1, 'rgba(20, 30, 60, 0)');\n    ctx.fillStyle = textGlow;\n    ctx.fillRect(titleX - 250, titleY - 100, 500, 200);\n\n    // Title text: FIRST SNOW\n    ctx.font = '300 32px \"Cinzel\", \"Georgia\", \"Playfair Display\", serif';\n    ctx.letterSpacing = '14px';\n\n    // Drop shadow\n    ctx.fillStyle = `rgba(8, 14, 28, ${titleFade * 0.8})`;\n    ctx.fillText('FIRST SNOW', titleX + 1, titleY + 2);\n\n    // Main crisp white letters\n    ctx.fillStyle = `rgba(244, 248, 255, ${titleFade * 0.95})`;\n    ctx.fillText('FIRST SNOW', titleX, titleY);\n\n    // Delicate minimalist underline accent\n    const lineW = 70 * titleFade;\n    ctx.strokeStyle = `rgba(215, 230, 255, ${titleFade * 0.45})`;\n    ctx.lineWidth = 1.0;\n    ctx.beginPath();\n    ctx.moveTo(titleX - lineW, titleY + 28);\n    ctx.lineTo(titleX + lineW, titleY + 28);\n    ctx.stroke();\n\n    ctx.restore();\n  }\n\n  // Helper color interpolation\n  function lerpColor(c1, c2, u) {\n    return [\n      Math.round(lerp(c1[0], c2[0], u)),\n      Math.round(lerp(c1[1], c2[1], u)),\n      Math.round(lerp(c1[2], c2[2], u)),\n    ];\n  }\n}\n","exportProfile":{"format":"mp4","resolution":"720p","fps":24,"durationSeconds":26},"source":{"prompt":"A 26-second illustrated film: a red fox crosses a pine forest clearing as the first snow of winter begins. Flakes thicken, the ground whitens, and the fox's tracks slowly fill behind it. It pauses, looks back, and trots on into blue dusk. Closing title: FIRST SNOW. 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","app":"gallery-reproduction"}},"license":"CC0-1.0"}