{"id":"valley-to-wasteland","title":"Valley to Wasteland","category":"Drawn film","prompt":"An 8-second clip: a peaceful valley that turns into a data-centre wasteland","designedBy":{"provider":"google","model":"gemini-3.7-flash","inference":{"inputTokens":16515,"outputTokens":9271,"costUsd":0.1132}},"render":{"credits":8,"priceUsd":0.12,"format":"mp4","renderMs":32046,"frameCount":192,"bytes":3111704,"seed":647039304,"width":1920,"height":1080,"fps":30,"durationSeconds":8},"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=valley-to-wasteland), and it exports as MP4 or GIF, not as a still."},"renderRequest":{"drawContract":"const meta = { duration: 8, fps: 24, size: [1280, 720], seed: 42 };\n// ── design graph kit: linked by the server from what this film calls — do not edit ──\nvar kit = {};\n// pp:MotionBasics — linked from the design graph\nkit.clamp = function (v, a, b) { return v < a ? a : v > b ? b : v; };\n// Numbers, or COLOURS: models reach for lerp('#4a284e', '#321c38', u) to blend a sky, and got '#4a284eNaN'.\nkit.rgba = function (c) {\n  var s = String(c).trim(), m;\n  if ((m = /^#([0-9a-f]{3,8})$/i.exec(s))) { var h = m[1]; if (h.length <= 4) h = h.replace(/./g, '$&$&'); return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), h.length >= 8 ? parseInt(h.slice(6, 8), 16) / 255 : 1]; }\n  if ((m = /^rgba?\\(([^)]+)\\)$/i.exec(s))) { var p = m[1].split(','); return [+p[0], +p[1], +p[2], p.length > 3 ? +p[3] : 1]; }\n  return null;\n};\nkit.lerp = function (a, b, u) {\n  if (typeof a === 'string' || typeof b === 'string') {\n    var ca = kit.rgba(a), cb = kit.rgba(b); if (!ca || !cb) return u < 0.5 ? a : b;\n    var k = function (i) { return ca[i] + (cb[i] - ca[i]) * u; };\n    return 'rgba(' + Math.round(k(0)) + ',' + Math.round(k(1)) + ',' + Math.round(k(2)) + ',' + Math.round(k(3) * 1000) / 1000 + ')';\n  }\n  return a + (b - a) * u;\n};\nkit.seg = function (t, a, b) { return b === a ? (t >= b ? 1 : 0) : kit.clamp((t - a) / (b - a), 0, 1); };\nkit.hash = function (n) { var s = Math.sin(n * 127.1 + 311.7) * 43758.5453; return s - Math.floor(s); };\nkit.ease = {\n  linear: function (u) { return u; },\n  \"in\": function (u) { return u * u * u; },\n  out: function (u) { return 1 - Math.pow(1 - u, 3); },\n  inOut: function (u) { return u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2; },\n  back: function (u) { var c = 1.7; return 1 + (c + 1) * Math.pow(u - 1, 3) + c * Math.pow(u - 1, 2); },\n  elastic: function (u) { return u === 0 || u === 1 ? u : Math.pow(2, -10 * u) * Math.sin((u * 10 - 0.75) * (Math.PI * 2 / 3)) + 1; }\n};\n// The film's own frame (meta.size), so a technique can size itself as a share of it — a composition rule, kept.\nkit.frame = function () { var m = typeof meta !== 'undefined' && meta && meta.size ? meta.size : [1280, 720]; return { w: Number(m[0]) || 1280, h: Number(m[1]) || 720 }; };\nkit.settle = function (u, freq, damp) { return Math.exp(-(damp || 5) * u) * Math.cos((freq || 3) * Math.PI * u); };\nkit.line = function (ctx, pts, w, col) {\n  ctx.beginPath();\n  for (var i = 0; i < pts.length; i++) { if (i) ctx.lineTo(pts[i][0], pts[i][1]); else ctx.moveTo(pts[i][0], pts[i][1]); }\n  ctx.lineWidth = w; ctx.strokeStyle = col; ctx.lineJoin = 'round'; ctx.lineCap = 'round'; ctx.stroke();\n};\n// pp:SkyElements — linked from the design graph\nkit.cloud = function (ctx, x, y, s, o) {\n  o = o || {}; var P = [[0, 0, 34], [32, -14, 40], [70, 0, 32], [36, 12, 30], [-26, 8, 24], [96, 10, 22]], i;\n  ctx.save();\n  // A faded cloud has no ink: outlined puffs cannot fade as one shape without their insides showing.\n  if (o.ink !== null && !(o.opacity != null && o.opacity < 1)) { ctx.lineWidth = 6; ctx.strokeStyle = o.ink || '#3a2c28'; for (i = 0; i < P.length; i++) { ctx.beginPath(); ctx.arc(x + P[i][0] * s, y + P[i][1] * s, P[i][2] * s, 0, Math.PI * 2); ctx.stroke(); } }\n  // ONE path for every puff, filled once: a translucent fill stays one even shape instead of showing every overlap.\n  ctx.beginPath(); for (i = 0; i < P.length; i++) { ctx.moveTo(x + (P[i][0] + P[i][2]) * s, y + P[i][1] * s); ctx.arc(x + P[i][0] * s, y + P[i][1] * s, P[i][2] * s, 0, Math.PI * 2); }\n  // The fill is drawn OPAQUE over the outline strokes, or every inner puff edge shows through it; a translucent\n  // rgba() fill keeps its colour and loses its alpha. Use opacity for a soft cloud: it fades the whole cloud as one.\n  if (o.opacity != null) ctx.globalAlpha = o.opacity;\n  ctx.fillStyle = String(o.fill || '#ffffff').replace(/^rgba\\(([^,]+),([^,]+),([^,]+),[^)]*\\)$/, 'rgb($1,$2,$3)'); ctx.fill('nonzero');\n  ctx.restore();\n};\nkit.sun = function (ctx, x, y, r, t, o) {\n  o = o || {}; t = t || 0; ctx.save(); ctx.translate(x, y); ctx.rotate(t * 0.5);\n  for (var i = 0; i < 12; i++) { ctx.rotate(Math.PI / 6); kit.line(ctx, [[r * 1.25, 0], [r * 1.7 + r * 0.12 * Math.sin(t * 4 + i), 0]], Math.max(2, r * 0.08), o.ray || '#e8a317'); }\n  ctx.restore();\n  ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fillStyle = o.fill || '#ffcd4a'; ctx.fill();\n  if (o.ink !== null) { ctx.lineWidth = 3; ctx.strokeStyle = o.ink || '#3a2c28'; ctx.stroke(); }\n};\nkit.moon = function (ctx, x, y, r, o) {\n  o = o || {}; ctx.save();\n  var g = ctx.createRadialGradient(x, y, r * 0.5, x, y, r * 2.8); g.addColorStop(0, 'rgba(255,240,190,0.45)'); g.addColorStop(1, 'rgba(255,240,190,0)');\n  ctx.fillStyle = g; ctx.fillRect(x - r * 3, y - r * 3, r * 6, r * 6);\n  ctx.beginPath(); ctx.rect(x - r * 2, y - r * 2, r * 4, r * 4); ctx.arc(x + r * 0.48, y - r * 0.26, r * 0.9, 0, Math.PI * 2, true); ctx.clip('evenodd');\n  ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fillStyle = o.fill || '#fff3c4'; ctx.fill();\n  ctx.lineWidth = 3; ctx.strokeStyle = o.edge || '#e8d38a'; ctx.stroke(); ctx.restore();\n};\nkit.stars = function (ctx, w, h, t, o) {\n  o = o || {}; var n = o.count || 140, seed = o.seed || 500, y0 = o.y || 0;\n  for (var i = 0; i < n; i++) {\n    var x = kit.hash(i + seed) * w, y = y0 + kit.hash(i + seed + 400) * h, tw = 0.5 + 0.5 * Math.sin(t * (2 + kit.hash(i) * 4) + i), r = 0.8 + kit.hash(i + 1) * 1.8;\n    ctx.fillStyle = 'rgba(255,248,220,' + (0.35 + 0.65 * tw).toFixed(3) + ')'; ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();\n    if (i % 17 === 0) { var c = 'rgba(255,248,220,' + tw.toFixed(3) + ')'; kit.line(ctx, [[x - 8 * tw, y], [x + 8 * tw, y]], 1.5, c); kit.line(ctx, [[x, y - 8 * tw], [x, y + 8 * tw]], 1.5, c); }\n  }\n};\nkit.rain = function (ctx, w, h, t, o) {\n  o = o || {}; var n = o.count || 120, sp = o.speed || 900, y0 = o.y || 0, slant = o.slant == null ? 0.2 : o.slant;\n  ctx.save(); ctx.lineCap = 'round'; ctx.lineWidth = o.width || 2;\n  for (var i = 0; i < n; i++) {\n    var x = kit.hash(i + 200) * (w + 200) - 100, len = 30 + kit.hash(i + 300) * 50;\n    var y = y0 + ((kit.hash(i + 400) * (h + 200) + t * sp * (0.8 + kit.hash(i) * 0.6)) % (h + 200)) - 100;\n    ctx.strokeStyle = o.color || ('rgba(255,255,255,' + (0.25 + kit.hash(i + 50) * 0.4).toFixed(3) + ')');\n    ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x - len * slant, y + len); ctx.stroke();\n  }\n  ctx.restore();\n};\n// ── end of design graph kit ──\n\nfunction drawTree(ctx, x, y, scale, health, t, idSeed) {\n  ctx.save();\n  ctx.translate(x, y);\n  ctx.scale(scale, scale);\n\n  const sway = Math.sin(t * 2 + idSeed) * 2 * health;\n  ctx.rotate((sway * Math.PI) / 180);\n\n  // Trunk\n  ctx.fillStyle = kit.lerp('#4a3319', '#1f1a14', 1 - health);\n  ctx.beginPath();\n  ctx.moveTo(-5, 0);\n  ctx.lineTo(-3, -35);\n  ctx.lineTo(3, -35);\n  ctx.lineTo(5, 0);\n  ctx.fill();\n\n  if (health > 0.05) {\n    // Foliage foliage layers (fade and shrink as health drops)\n    const foliageColor = kit.lerp('#2e6b27', '#6e5428', 1 - health);\n    ctx.fillStyle = foliageColor;\n    ctx.globalAlpha = health;\n\n    // Conifer / Pine layers\n    for (let i = 0; i < 3; i++) {\n      const ly = -25 - i * 18;\n      const lw = 22 - i * 4;\n      ctx.beginPath();\n      ctx.moveTo(-lw, ly);\n      ctx.lineTo(0, ly - 22);\n      ctx.lineTo(lw, ly);\n      ctx.closePath();\n      ctx.fill();\n    }\n  }\n\n  // Bare dead branches if dead\n  if (health < 0.8) {\n    ctx.strokeStyle = '#1a1612';\n    ctx.lineWidth = 1.5;\n    ctx.globalAlpha = (1 - health) * 0.9;\n    ctx.beginPath();\n    ctx.moveTo(0, -25);\n    ctx.lineTo(-12, -35);\n    ctx.moveTo(0, -32);\n    ctx.lineTo(10, -42);\n    ctx.moveTo(0, -20);\n    ctx.lineTo(8, -28);\n    ctx.stroke();\n  }\n\n  ctx.restore();\n}\n\nfunction drawServerBuilding(ctx, x, y, w, h, riseProgress, t, seed) {\n  if (riseProgress <= 0) return;\n  const currH = h * kit.ease.out(riseProgress);\n\n  ctx.save();\n  ctx.translate(x, y);\n\n  // Shadow\n  ctx.fillStyle = 'rgba(0, 0, 0, 0.4)';\n  ctx.fillRect(-w / 2 - 8, -currH + 6, w + 16, currH);\n\n  // Building Body (Dark Brutalist Metal / Concrete)\n  const grad = ctx.createLinearGradient(-w / 2, 0, w / 2, 0);\n  grad.addColorStop(0, '#1c1f26');\n  grad.addColorStop(0.5, '#282d37');\n  grad.addColorStop(1, '#15171d');\n  ctx.fillStyle = grad;\n  ctx.fillRect(-w / 2, -currH, w, currH);\n\n  // Structural edge / bevel\n  ctx.strokeStyle = '#3e4554';\n  ctx.lineWidth = 2;\n  ctx.strokeRect(-w / 2, -currH, w, currH);\n\n  // Rooftop AC / HVAC Units & Industrial vents\n  if (currH > 40) {\n    ctx.fillStyle = '#111317';\n    ctx.fillRect(-w / 2 + 10, -currH - 12, w * 0.4, 12);\n    ctx.fillRect(5, -currH - 16, w * 0.35, 16);\n\n    // Hazard Warning Beacon (blinking red)\n    const beaconBlink = Math.sin(t * 6 + seed) > 0 ? 1 : 0.2;\n    ctx.fillStyle = `rgba(255, 30, 30, ${beaconBlink})`;\n    ctx.shadowColor = '#ff2222';\n    ctx.shadowBlur = beaconBlink > 0.5 ? 12 : 0;\n    ctx.beginPath();\n    ctx.arc(-w / 2 + 15, -currH - 15, 3.5, 0, Math.PI * 2);\n    ctx.fill();\n    ctx.shadowBlur = 0;\n  }\n\n  // Server rack window slits / glowing LED arrays inside\n  const rows = Math.floor(currH / 14);\n  const cols = Math.floor((w - 20) / 16);\n\n  for (let r = 1; r < rows; r++) {\n    const rowY = -currH + r * 14;\n    for (let c = 0; c < cols; c++) {\n      const colX = -w / 2 + 12 + c * 16;\n      // Procedural server LED activity\n      const flicker = Math.sin(t * 8 + r * 3.7 + c * 5.1 + seed * 10);\n      const isBlue = (r + c + seed) % 3 !== 0;\n\n      if (flicker > -0.2) {\n        ctx.fillStyle = isBlue\n          ? (flicker > 0.4 ? '#4df0ff' : '#0077aa')\n          : (flicker > 0.3 ? '#39ff14' : '#1b5e20');\n        ctx.shadowColor = isBlue ? '#00e5ff' : '#39ff14';\n        ctx.shadowBlur = 4;\n        ctx.fillRect(colX, rowY, 9, 5);\n      } else {\n        ctx.fillStyle = '#0a0d12';\n        ctx.shadowBlur = 0;\n        ctx.fillRect(colX, rowY, 9, 5);\n      }\n    }\n  }\n\n  ctx.restore();\n}\n\nfunction drawPylon(ctx, x, y, scale, progress) {\n  if (progress <= 0) return;\n  ctx.save();\n  ctx.translate(x, y);\n  ctx.scale(scale * progress, scale * progress);\n\n  ctx.strokeStyle = '#23272e';\n  ctx.lineWidth = 2.5;\n\n  // Mast\n  ctx.beginPath();\n  ctx.moveTo(-18, 0);\n  ctx.lineTo(-4, -140);\n  ctx.lineTo(4, -140);\n  ctx.lineTo(18, 0);\n\n  // Crossarms\n  ctx.moveTo(-45, -110); ctx.lineTo(45, -110);\n  ctx.moveTo(-38, -80);  ctx.lineTo(38, -80);\n  ctx.moveTo(-50, -50);  ctx.lineTo(50, -50);\n\n  // Lattice cross bracing\n  ctx.moveTo(-14, -25); ctx.lineTo(14, -50);\n  ctx.moveTo(14, -25);  ctx.lineTo(-14, -50);\n  ctx.moveTo(-10, -55); ctx.lineTo(10, -80);\n  ctx.moveTo(10, -55);  ctx.lineTo(-10, -80);\n  ctx.moveTo(-6, -85);  ctx.lineTo(6, -110);\n  ctx.moveTo(6, -85);   ctx.lineTo(-6, -110);\n  ctx.stroke();\n\n  // Red beacon on top\n  ctx.fillStyle = '#ff2b2b';\n  ctx.beginPath();\n  ctx.arc(0, -144, 4, 0, Math.PI * 2);\n  ctx.fill();\n\n  ctx.restore();\n}\n\nconst entities = [\n  // 1. SKY & ATMOSPHERE\n  {\n    id: 'sky',\n    kind: 'background',\n    from: 0,\n    to: 8,\n    params: {},\n    draw(ctx, t, p) {\n      const W = 1280, H = 720;\n      const trans = kit.seg(t, 2.5, 5.5); // 0 (pastoral) -> 1 (wasteland)\n\n      // Gradient sky\n      const topColor = kit.lerp('#3a7bd5', '#0a0d14', trans);\n      const midColor = kit.lerp('#68d8d6', '#1a1f2c', trans);\n      const botColor = kit.lerp('#fef9e7', '#3d2621', trans);\n\n      const grad = ctx.createLinearGradient(0, 0, 0, H * 0.7);\n      grad.addColorStop(0, topColor);\n      grad.addColorStop(0.55, midColor);\n      grad.addColorStop(1, botColor);\n      ctx.fillStyle = grad;\n      ctx.fillRect(0, 0, W, H);\n\n      // Pastoral Sun vs Smoggy industrial haze\n      const sunX = 950;\n      const sunY = 160 + trans * 120;\n      const sunAlpha = 1 - trans * 0.9;\n\n      if (sunAlpha > 0.01) {\n        ctx.save();\n        ctx.globalAlpha = sunAlpha;\n        // Sun glow\n        const sunGrad = ctx.createRadialGradient(sunX, sunY, 10, sunX, sunY, 140);\n        sunGrad.addColorStop(0, '#fff4cc');\n        sunGrad.addColorStop(0.3, 'rgba(255, 215, 120, 0.6)');\n        sunGrad.addColorStop(1, 'rgba(255, 200, 100, 0)');\n        ctx.fillStyle = sunGrad;\n        ctx.beginPath();\n        ctx.arc(sunX, sunY, 140, 0, Math.PI * 2);\n        ctx.fill();\n\n        // Sun disc\n        ctx.fillStyle = '#ffffff';\n        ctx.beginPath();\n        ctx.arc(sunX, sunY, 28, 0, Math.PI * 2);\n        ctx.fill();\n        ctx.restore();\n      }\n\n      // Pastoral Clouds fading out\n      if (trans < 0.9) {\n        ctx.save();\n        ctx.globalAlpha = (1 - trans) * 0.85;\n        kit.cloud(ctx, 220 + t * 8, 120, 1.2, { fill: '#ffffff' });\n        kit.cloud(ctx, 680 + t * 6, 90, 0.9, { fill: '#ffffff' });\n        kit.cloud(ctx, 1100 + t * 7, 140, 1.4, { fill: '#ffffff' });\n        ctx.restore();\n      }\n\n      // Smog & industrial haze layer rising\n      if (trans > 0.1) {\n        ctx.save();\n        ctx.globalAlpha = trans * 0.75;\n        const smogGrad = ctx.createLinearGradient(0, H * 0.2, 0, H * 0.65);\n        smogGrad.addColorStop(0, 'rgba(30, 20, 25, 0)');\n        smogGrad.addColorStop(0.6, 'rgba(80, 45, 35, 0.45)');\n        smogGrad.addColorStop(1, 'rgba(35, 30, 40, 0.85)');\n        ctx.fillStyle = smogGrad;\n        ctx.fillRect(0, H * 0.15, W, H * 0.55);\n        ctx.restore();\n      }\n    }\n  },\n\n  // 2. DISTANT MOUNTAINS / INDUSTRIAL HORIZON\n  {\n    id: 'distant_mountains',\n    kind: 'scenery',\n    from: 0,\n    to: 8,\n    params: {},\n    draw(ctx, t, p) {\n      const W = 1280, H = 720;\n      const trans = kit.seg(t, 2.5, 5.5);\n\n      const mountainColor = kit.lerp('#508496', '#1b2029', trans);\n      ctx.fillStyle = mountainColor;\n\n      ctx.beginPath();\n      ctx.moveTo(0, H * 0.55);\n      // Jagged mountain spine\n      const peaks = [\n        [0, 360], [140, 280], [290, 340], [450, 250], [600, 320],\n        [760, 230], [920, 310], [1080, 240], [1200, 300], [1280, 270]\n      ];\n      ctx.moveTo(0, H);\n      ctx.lineTo(0, peaks[0][1]);\n      for (let i = 1; i < peaks.length; i++) {\n        ctx.lineTo(peaks[i][0], peaks[i][1]);\n      }\n      ctx.lineTo(W, H);\n      ctx.closePath();\n      ctx.fill();\n\n      // Distant smoke stacks / Cooling towers rising on horizon\n      if (trans > 0.2) {\n        const stackProgress = kit.seg(t, 3.0, 5.8);\n        ctx.save();\n        ctx.globalAlpha = stackProgress * 0.9;\n        ctx.fillStyle = '#14181f';\n\n        const stacks = [220, 380, 680, 840, 1020];\n        stacks.forEach((sx, idx) => {\n          const sh = 40 + (idx % 3) * 18;\n          const currSh = sh * kit.ease.out(stackProgress);\n          ctx.fillRect(sx - 10, 310 - currSh, 20, currSh);\n\n          // Steam plumes billowing from tops\n          const plumeAge = (t * 2.5 + idx) % 3;\n          for (let pIdx = 0; pIdx < 4; pIdx++) {\n            const py = 310 - currSh - pIdx * 14 - plumeAge * 4;\n            const px = sx + Math.sin(t * 2 + pIdx + idx) * 8 + pIdx * 6;\n            const pr = 8 + pIdx * 7;\n            const pAlpha = (1 - pIdx / 4) * 0.4 * stackProgress;\n            ctx.fillStyle = `rgba(180, 175, 170, ${pAlpha})`;\n            ctx.beginPath();\n            ctx.arc(px, py, pr, 0, Math.PI * 2);\n            ctx.fill();\n          }\n        });\n        ctx.restore();\n      }\n    }\n  },\n\n  // 3. MIDGROUND VALLEY & DATA COMPLEX WAREHOUSES\n  {\n    id: 'midground_valley',\n    kind: 'scenery',\n    from: 0,\n    to: 8,\n    params: {},\n    draw(ctx, t, p) {\n      const W = 1280, H = 720;\n      const trans = kit.seg(t, 2.5, 5.5);\n\n      // Midground rolling hills changing to concrete terrace\n      const midFill = kit.lerp('#5b9b42', '#22262c', trans);\n      ctx.fillStyle = midFill;\n\n      ctx.beginPath();\n      ctx.moveTo(0, H);\n      ctx.lineTo(0, 420);\n      ctx.bezierCurveTo(300, 380, 550, 450, 850, 400);\n      ctx.bezierCurveTo(1050, 370, 1200, 410, 1280, 390);\n      ctx.lineTo(W, H);\n      ctx.closePath();\n      ctx.fill();\n\n      // Concrete retaining walls / foundation cuts as wasteland sets in\n      if (trans > 0.15) {\n        ctx.save();\n        ctx.globalAlpha = trans;\n        ctx.fillStyle = '#17191e';\n        ctx.fillRect(180, 410, 360, 24);\n        ctx.fillRect(660, 395, 420, 28);\n        ctx.strokeStyle = '#323742';\n        ctx.lineWidth = 2;\n        ctx.strokeRect(180, 410, 360, 24);\n        ctx.strokeRect(660, 395, 420, 28);\n        ctx.restore();\n      }\n\n      // Midground Data Centre Blocks emerging\n      const build1 = kit.seg(t, 2.8, 4.8);\n      drawServerBuilding(ctx, 360, 410, 260, 120, build1, t, 1);\n\n      const build2 = kit.seg(t, 3.2, 5.2);\n      drawServerBuilding(ctx, 840, 395, 310, 140, build2, t, 2);\n\n      const build3 = kit.seg(t, 3.6, 5.5);\n      drawServerBuilding(ctx, 1140, 405, 180, 100, build3, t, 3);\n\n      // Midground trees transforming to dead poles\n      const treeHealth = 1 - trans;\n      const midTrees = [\n        [70, 425, 0.9], [120, 435, 1.1], [530, 435, 0.85],\n        [600, 440, 1.0], [1020, 405, 0.75], [1240, 415, 1.0]\n      ];\n      midTrees.forEach(([tx, ty, ts], i) => {\n        drawTree(ctx, tx, ty, ts, treeHealth, t, i * 2.3);\n      });\n    }\n  },\n\n  // 4. POWER GRID & HIGH-VOLTAGE PYLONS\n  {\n    id: 'power_grid',\n    kind: 'infrastructure',\n    from: 0,\n    to: 8,\n    params: {},\n    draw(ctx, t, p) {\n      const trans = kit.seg(t, 3.0, 5.6);\n      if (trans <= 0) return;\n\n      const pylon1 = kit.seg(t, 3.0, 4.6);\n      const pylon2 = kit.seg(t, 3.4, 5.0);\n      const pylon3 = kit.seg(t, 3.8, 5.4);\n\n      drawPylon(ctx, 180, 440, 0.85, pylon1);\n      drawPylon(ctx, 580, 450, 1.0, pylon2);\n      drawPylon(ctx, 990, 430, 0.9, pylon3);\n\n      // Power cables spanning across pylons\n      if (trans > 0.4) {\n        ctx.save();\n        ctx.strokeStyle = '#121418';\n        ctx.lineWidth = 1.8;\n        ctx.globalAlpha = kit.seg(t, 4.2, 5.8);\n\n        // Sagging catenary curves\n        const drawCable = (x1, y1, x2, y2, sag) => {\n          ctx.beginPath();\n          ctx.moveTo(x1, y1);\n          ctx.quadraticCurveTo((x1 + x2) / 2, (y1 + y2) / 2 + sag, x2, y2);\n          ctx.stroke();\n        };\n\n        // Wire sets\n        drawCable(0, 360, 180, 345, 18);\n        drawCable(180, 345, 580, 340, 32);\n        drawCable(580, 340, 990, 330, 35);\n        drawCable(990, 330, 1280, 350, 24);\n\n        drawCable(180, 370, 580, 370, 28);\n        drawCable(580, 370, 990, 360, 30);\n\n        // Electric pulse particles travelling on wires\n        const pulse = (t * 220) % 400;\n        ctx.fillStyle = '#64ffff';\n        ctx.shadowColor = '#00f0ff';\n        ctx.shadowBlur = 8;\n        ctx.beginPath();\n        const px = 180 + pulse;\n        const py = 345 + (pulse / 400) * -5 + Math.sin((pulse / 400) * Math.PI) * 32;\n        ctx.arc(px, py, 2.5, 0, Math.PI * 2);\n        ctx.fill();\n        ctx.restore();\n      }\n    }\n  },\n\n  // 5. FOREGROUND VALLEY, RIVER & MEGA-FACILITY ENTRANCE\n  {\n    id: 'foreground',\n    kind: 'foreground',\n    from: 0,\n    to: 8,\n    params: {},\n    draw(ctx, t, p) {\n      const W = 1280, H = 720;\n      const trans = kit.seg(t, 2.5, 5.5);\n\n      // Foreground terrain\n      const foreFill = kit.lerp('#488231', '#181b20', trans);\n      ctx.fillStyle = foreFill;\n\n      ctx.beginPath();\n      ctx.moveTo(0, H);\n      ctx.lineTo(0, 510);\n      ctx.bezierCurveTo(340, 520, 500, 560, 750, 515);\n      ctx.bezierCurveTo(950, 480, 1150, 530, 1280, 490);\n      ctx.lineTo(W, H);\n      ctx.closePath();\n      ctx.fill();\n\n      // RIVER / PIPELINE CONDUIT\n      // At t=0..3: Pristine turquoise winding stream\n      // At t=3..8: Polluted canal encased in industrial pipe channels\n      const riverWater = kit.lerp('#38b2ac', '#111e24', trans);\n      ctx.fillStyle = riverWater;\n\n      ctx.beginPath();\n      ctx.moveTo(420, 545);\n      ctx.bezierCurveTo(460, 590, 380, 640, 340, H);\n      ctx.lineTo(460, H);\n      ctx.bezierCurveTo(500, 640, 550, 590, 480, 545);\n      ctx.closePath();\n      ctx.fill();\n\n      // Sparkling river highlights in pastoral mode\n      if (trans < 0.6) {\n        ctx.save();\n        ctx.strokeStyle = '#e6fffa';\n        ctx.lineWidth = 2;\n        ctx.globalAlpha = (1 - trans * 1.6) * 0.8;\n        for (let r = 0; r < 5; r++) {\n          const ry = 565 + r * 28;\n          const rx = 410 + Math.sin(t * 3 + r) * 15 + r * 10;\n          ctx.beginPath();\n          ctx.moveTo(rx, ry);\n          ctx.lineTo(rx + 24, ry);\n          ctx.stroke();\n        }\n        ctx.restore();\n      }\n\n      // Industrial pipeline conduits clamping over the river\n      if (trans > 0.25) {\n        ctx.save();\n        const pipeProg = kit.seg(t, 3.2, 5.5);\n        ctx.globalAlpha = pipeProg;\n        ctx.fillStyle = '#262a33';\n        ctx.strokeStyle = '#414856';\n        ctx.lineWidth = 3;\n\n        // Giant dual cooling water pipe\n        ctx.beginPath();\n        ctx.rect(300, 640, 220, 45);\n        ctx.fill();\n        ctx.stroke();\n\n        // Pipe bolts and warning bands\n        ctx.fillStyle = '#eab308';\n        ctx.fillRect(360, 640, 16, 45);\n        ctx.fillRect(440, 640, 16, 45);\n        ctx.restore();\n      }\n\n      // Massive Foreground Server Complex Unit on the Right\n      const fgBuild = kit.seg(t, 3.4, 5.6);\n      drawServerBuilding(ctx, 1100, 720, 380, 320, fgBuild, t, 5);\n\n      // Foreground Cooling Fan exhaust on Left\n      if (trans > 0.3) {\n        const fanRise = kit.seg(t, 3.6, 5.8);\n        ctx.save();\n        ctx.globalAlpha = fanRise;\n        ctx.translate(140, 640);\n\n        // Housing\n        ctx.fillStyle = '#15171c';\n        ctx.fillRect(-80, -90, 160, 90);\n        ctx.strokeStyle = '#383e4c';\n        ctx.lineWidth = 3;\n        ctx.strokeRect(-80, -90, 160, 90);\n\n        // Circular fan vent\n        ctx.fillStyle = '#0a0b0e';\n        ctx.beginPath();\n        ctx.arc(0, -45, 34, 0, Math.PI * 2);\n        ctx.fill();\n        ctx.stroke();\n\n        // Spinning blades\n        ctx.save();\n        ctx.translate(0, -45);\n        ctx.rotate(t * 14);\n        ctx.strokeStyle = '#555f73';\n        ctx.lineWidth = 6;\n        for (let b = 0; b < 4; b++) {\n          ctx.rotate(Math.PI / 2);\n          ctx.beginPath();\n          ctx.moveTo(0, 0);\n          ctx.lineTo(26, 0);\n          ctx.stroke();\n        }\n        ctx.restore();\n\n        // Blue glow under exhaust\n        ctx.fillStyle = '#00f0ff';\n        ctx.shadowColor = '#00f0ff';\n        ctx.shadowBlur = 12;\n        ctx.beginPath();\n        ctx.arc(0, -45, 6, 0, Math.PI * 2);\n        ctx.fill();\n        ctx.shadowBlur = 0;\n\n        ctx.restore();\n      }\n\n      // Foreground Trees (lush green -> dead withered silhouettes)\n      const fgTreeHealth = 1 - trans;\n      const fgTrees = [\n        [40, 560, 1.8], [110, 540, 1.4], [250, 570, 1.5],\n        [680, 550, 1.6], [780, 530, 1.3]\n      ];\n      fgTrees.forEach(([tx, ty, ts], i) => {\n        drawTree(ctx, tx, ty, ts, fgTreeHealth, t, i * 4.1 + 10);\n      });\n\n      // Wildflowers in grass (fade out on transition)\n      if (fgTreeHealth > 0.05) {\n        ctx.save();\n        ctx.globalAlpha = fgTreeHealth * 0.9;\n        const flowerColors = ['#ff6b6b', '#feca57', '#ff9ff3', '#ffffff'];\n        for (let f = 0; f < 25; f++) {\n          const fx = 30 + (f * 47) % 700;\n          const fy = 520 + (f * 31) % 180;\n          ctx.fillStyle = flowerColors[f % flowerColors.length];\n          ctx.beginPath();\n          ctx.arc(fx, fy, 2.5, 0, Math.PI * 2);\n          ctx.fill();\n        }\n        ctx.restore();\n      }\n    }\n  },\n\n  // 6. CINEMATIC NARRATIVE TELEMETRY OVERLAYS\n  {\n    id: 'telemetry_overlay',\n    kind: 'hud',\n    from: 0,\n    to: 8,\n    params: {},\n    draw(ctx, t, p) {\n      const W = 1280, H = 720;\n\n      ctx.save();\n      // Phase 1: Natural state (0.0s - 2.8s)\n      if (t < 3.0) {\n        const u = kit.seg(t, 0.4, 1.0);\n        const fadeOut = 1 - kit.seg(t, 2.4, 2.9);\n        ctx.globalAlpha = kit.ease.out(u) * fadeOut;\n\n        ctx.font = '600 20px monospace';\n        ctx.fillStyle = '#1e3a2f';\n        ctx.fillText('LOC // GREEN RIVER VALLEY BASIN', 60, 70);\n\n        ctx.font = '400 14px monospace';\n        ctx.fillStyle = '#2d5a47';\n        ctx.fillText('BIOMASS: 98.4%  |  GRID DEMAND: 0.00 MW  |  AMBIENT NOISE: 24 dB', 60, 95);\n      }\n\n      // Phase 2: Industrial Transition (3.2s - 5.4s)\n      else if (t >= 3.0 && t < 5.6) {\n        const u = kit.seg(t, 3.1, 3.6);\n        const fadeOut = 1 - kit.seg(t, 5.0, 5.5);\n        ctx.globalAlpha = kit.ease.out(u) * fadeOut;\n\n        ctx.font = '700 20px monospace';\n        ctx.fillStyle = '#f59e0b';\n        ctx.fillText('⚡ HYPERSCALE SITE DEVELOPMENT // PHASE IV', 60, 70);\n\n        ctx.font = '400 14px monospace';\n        ctx.fillStyle = '#d97706';\n        ctx.fillText('CLEAR-CUT: 100%  |  TRANSIT LINE: ENERGIZED  |  SERVER CAPACITY: +120,000 U', 60, 95);\n      }\n\n      // Phase 3: Total Wasteland / Compute Complex (5.6s - 8.0s)\n      else {\n        const u = kit.seg(t, 5.6, 6.2);\n        ctx.globalAlpha = kit.ease.out(u);\n\n        ctx.font = '700 22px monospace';\n        ctx.fillStyle = '#00f0ff';\n        ctx.shadowColor = '#00f0ff';\n        ctx.shadowBlur = 10;\n        ctx.fillText('DATA COMPLEX 09 // HYPERSCALE HUB', 60, 70);\n        ctx.shadowBlur = 0;\n\n        ctx.font = '500 14px monospace';\n        ctx.fillStyle = '#94a3b8';\n        ctx.fillText('CONTINUOUS LOAD: 14.8 GW  |  COOLING: LIQUID EVAPORATIVE  |  AI CLUSTERS: ONLINE', 60, 96);\n\n        // Live blinking status tag\n        const blink = Math.sin(t * 8) > 0;\n        ctx.fillStyle = blink ? '#ef4444' : '#7f1d1d';\n        ctx.beginPath();\n        ctx.arc(65, 125, 5, 0, Math.PI * 2);\n        ctx.fill();\n\n        ctx.font = '600 13px monospace';\n        ctx.fillStyle = '#ef4444';\n        ctx.fillText('ZERO BIOSPHERE RECOVERY', 80, 129);\n      }\n\n      // Subtle letterbox vignette\n      const vig = ctx.createRadialGradient(W / 2, H / 2, H * 0.4, W / 2, H / 2, W * 0.7);\n      vig.addColorStop(0, 'rgba(0,0,0,0)');\n      vig.addColorStop(1, 'rgba(0,0,0,0.5)');\n      ctx.fillStyle = vig;\n      ctx.fillRect(0, 0, W, H);\n\n      ctx.restore();\n    }\n  }\n];\n\nfunction draw(ctx, t, params, rng) {\n  for (const e of entities) {\n    if (t < e.from || t > e.to) continue;\n    ctx.save();\n    e.draw(ctx, t, e.params, rng.for(e.id));\n    ctx.restore();\n  }\n}\n\nfunction compose(params) {\n  const notes = [];\n\n  // 1. Pastoral Opening (0.0s - 2.8s) - Gentle organic harp and pad\n  const pastoralChords = [\n    { t: 0.0, root: 65 }, // F major\n    { t: 1.2, root: 62 }, // D minor\n    { t: 2.2, root: 60 }, // C major\n  ];\n\n  pastoralChords.forEach(ch => {\n    notes.push({ inst: 'pad', t: ch.t, midi: ch.root, dur: 1.8, vel: 0.4 });\n    notes.push({ inst: 'pad', t: ch.t, midi: ch.root + 7, dur: 1.8, vel: 0.35 });\n    notes.push({ inst: 'harp', t: ch.t + 0.1, midi: ch.root + 12, dur: 0.8, vel: 0.5 });\n    notes.push({ inst: 'harp', t: ch.t + 0.4, midi: ch.root + 16, dur: 0.8, vel: 0.45 });\n  });\n\n  // Birds / Wind in nature\n  notes.push({ inst: 'wind', t: 0.0, dur: 2.5, vel: 0.3 });\n  notes.push({ inst: 'flute', t: 0.8, midi: 77, dur: 0.6, vel: 0.4 });\n  notes.push({ inst: 'flute', t: 1.6, midi: 81, dur: 0.8, vel: 0.45 });\n\n  // 2. Transition Shock & Construction Incline (2.8s - 5.5s)\n  notes.push({ inst: 'whoosh', t: 2.6, dur: 1.5, vel: 0.7 });\n  notes.push({ inst: 'noise', t: 3.0, dur: 2.2, vel: 0.5 });\n  notes.push({ inst: 'zap', t: 3.5, dur: 0.8, vel: 0.6 });\n\n  // Rhythmic mechanical hits starting up\n  for (let tHit = 3.0; tHit < 5.4; tHit += 0.4) {\n    notes.push({ inst: 'tom', t: tHit, dur: 0.3, vel: 0.55 });\n    notes.push({ inst: 'hat', t: tHit + 0.2, dur: 0.1, vel: 0.4 });\n  }\n\n  // 3. Dystopian Data Complex Drone & Cold Compute Pulses (5.4s - 8.0s)\n  // Deep industrial sub bass\n  notes.push({ inst: 'bass', t: 5.4, midi: 36, dur: 2.6, vel: 0.85 }); // Low C\n  notes.push({ inst: 'bass', t: 6.8, midi: 34, dur: 1.2, vel: 0.8 });  // Low Bb\n\n  // Heavy cyber kicks\n  [5.4, 6.0, 6.6, 7.2].forEach(kTime => {\n    notes.push({ inst: 'kick', t: kTime, dur: 0.4, vel: 0.85 });\n    notes.push({ inst: 'snare', t: kTime + 0.3, dur: 0.2, vel: 0.5 });\n  });\n\n  // Fast compute server arpeggios (digital LED server chatter)\n  const arpNotes = [72, 75, 79, 84, 82, 79, 87, 84];\n  for (let i = 0; i < 16; i++) {\n    const at = 5.4 + i * 0.15;\n    if (at < 8.0) {\n      notes.push({\n        inst: 'bell',\n        t: at,\n        midi: arpNotes[i % arpNotes.length],\n        dur: 0.18,\n        vel: 0.4\n      });\n    }\n  }\n\n  return notes;\n}\n","exportProfile":{"format":"mp4","resolution":"720p","fps":30,"durationSeconds":8},"source":{"prompt":"An 8-second clip: a peaceful valley that turns into a data-centre wasteland","provider":"google","model":"gemini-3.7-flash","app":"gallery-reproduction"}},"license":"CC0-1.0"}