{
  "id": "opus-showcase-08-shadow-theatre-fox",
  "css": "",
  "fps": 30,
  "html": "<div style=\"left:0px;top:0px;width:1080px;height:1920px;background:#000000;border-radius:0px\" data-jc-track=\"0\" data-jc-duration=\"24\" data-jc-start=\"0\" data-jc-name=\"Background / base colour\" data-jc-kind=\"shape\" data-jc-clip=\"\" id=\"shape-1\"></div><div style=\"left:0px;top:0px;width:1080px;height:1920px;overflow:hidden;box-sizing:border-box;background:transparent\" data-jc-track=\"1\" data-jc-duration=\"24\" data-jc-start=\"0\" data-jc-name=\"Shadow theatre / scrim light, cut-paper forest, fox, owl, lantern, moon, proscenium (own code)\" data-jc-kind=\"custom\" data-jc-clip=\"\" id=\"custom-1\"></div><div data-jc-animation=\"{&quot;tracks&quot;:[{&quot;id&quot;:&quot;kick-kf-opacity-track&quot;,&quot;property&quot;:&quot;opacity&quot;,&quot;keyframes&quot;:[{&quot;id&quot;:&quot;kick-kf-opacity-track-key-1&quot;,&quot;time&quot;:0,&quot;value&quot;:0,&quot;easing&quot;:&quot;ease-out&quot;},{&quot;id&quot;:&quot;kick-kf-opacity-track-key-2&quot;,&quot;time&quot;:0.35,&quot;value&quot;:1,&quot;easing&quot;:&quot;ease-in-out&quot;},{&quot;id&quot;:&quot;kick-kf-opacity-track-key-3&quot;,&quot;time&quot;:4,&quot;value&quot;:1,&quot;easing&quot;:&quot;ease-in&quot;},{&quot;id&quot;:&quot;kick-kf-opacity-track-key-4&quot;,&quot;time&quot;:4.3,&quot;value&quot;:0,&quot;easing&quot;:&quot;ease-in-out&quot;}]},{&quot;id&quot;:&quot;kick-kf-blur-track&quot;,&quot;property&quot;:&quot;blur&quot;,&quot;keyframes&quot;:[{&quot;id&quot;:&quot;kick-kf-blur-track-key-1&quot;,&quot;time&quot;:0,&quot;value&quot;:12,&quot;easing&quot;:&quot;ease-out&quot;},{&quot;id&quot;:&quot;kick-kf-blur-track-key-2&quot;,&quot;time&quot;:0.4,&quot;value&quot;:0,&quot;easing&quot;:&quot;ease-in-out&quot;},{&quot;id&quot;:&quot;kick-kf-blur-track-key-3&quot;,&quot;time&quot;:4,&quot;value&quot;:0,&quot;easing&quot;:&quot;ease-in&quot;},{&quot;id&quot;:&quot;kick-kf-blur-track-key-4&quot;,&quot;time&quot;:4.3,&quot;value&quot;:12,&quot;easing&quot;:&quot;ease-in-out&quot;}]},{&quot;id&quot;:&quot;kick-kf-position-track&quot;,&quot;property&quot;:&quot;position&quot;,&quot;keyframes&quot;:[{&quot;id&quot;:&quot;kick-kf-position-track-key-1&quot;,&quot;time&quot;:0,&quot;value&quot;:{&quot;x&quot;:0,&quot;y&quot;:1604},&quot;easing&quot;:&quot;ease-out&quot;},{&quot;id&quot;:&quot;kick-kf-position-track-key-2&quot;,&quot;time&quot;:0.55,&quot;value&quot;:{&quot;x&quot;:0,&quot;y&quot;:1588},&quot;easing&quot;:&quot;ease-in-out&quot;}]}]}\" style=\"left:0px;top:1588px;width:1080px;height:50px;color:#C98A4B;font-family:Inter;font-size:26px;font-weight:700;text-align:center;white-space:normal;background:transparent;display:grid;align-content:center\" data-jc-track=\"3\" data-jc-duration=\"4.3\" data-jc-start=\"0.9\" data-jc-name=\"Title kicker / A shadow play\" data-jc-kind=\"text\" data-jc-clip=\"\" id=\"text-1\">A SHADOW PLAY</div><div data-jc-animation=\"{&quot;tracks&quot;:[{&quot;id&quot;:&quot;title-kf-opacity-track&quot;,&quot;property&quot;:&quot;opacity&quot;,&quot;keyframes&quot;:[{&quot;id&quot;:&quot;title-kf-opacity-track-key-1&quot;,&quot;time&quot;:0,&quot;value&quot;:0,&quot;easing&quot;:&quot;ease-out&quot;},{&quot;id&quot;:&quot;title-kf-opacity-track-key-2&quot;,&quot;time&quot;:0.35,&quot;value&quot;:1,&quot;easing&quot;:&quot;ease-in-out&quot;},{&quot;id&quot;:&quot;title-kf-opacity-track-key-3&quot;,&quot;time&quot;:3.8,&quot;value&quot;:1,&quot;easing&quot;:&quot;ease-in&quot;},{&quot;id&quot;:&quot;title-kf-opacity-track-key-4&quot;,&quot;time&quot;:4.1,&quot;value&quot;:0,&quot;easing&quot;:&quot;ease-in-out&quot;}]},{&quot;id&quot;:&quot;title-kf-blur-track&quot;,&quot;property&quot;:&quot;blur&quot;,&quot;keyframes&quot;:[{&quot;id&quot;:&quot;title-kf-blur-track-key-1&quot;,&quot;time&quot;:0,&quot;value&quot;:12,&quot;easing&quot;:&quot;ease-out&quot;},{&quot;id&quot;:&quot;title-kf-blur-track-key-2&quot;,&quot;time&quot;:0.4,&quot;value&quot;:0,&quot;easing&quot;:&quot;ease-in-out&quot;},{&quot;id&quot;:&quot;title-kf-blur-track-key-3&quot;,&quot;time&quot;:3.8,&quot;value&quot;:0,&quot;easing&quot;:&quot;ease-in&quot;},{&quot;id&quot;:&quot;title-kf-blur-track-key-4&quot;,&quot;time&quot;:4.1,&quot;value&quot;:12,&quot;easing&quot;:&quot;ease-in-out&quot;}]},{&quot;id&quot;:&quot;title-kf-position-track&quot;,&quot;property&quot;:&quot;position&quot;,&quot;keyframes&quot;:[{&quot;id&quot;:&quot;title-kf-position-track-key-1&quot;,&quot;time&quot;:0,&quot;value&quot;:{&quot;x&quot;:0,&quot;y&quot;:1676},&quot;easing&quot;:&quot;ease-out&quot;},{&quot;id&quot;:&quot;title-kf-position-track-key-2&quot;,&quot;time&quot;:0.55,&quot;value&quot;:{&quot;x&quot;:0,&quot;y&quot;:1640},&quot;easing&quot;:&quot;ease-in-out&quot;}]}]}\" style=\"left:0px;top:1640px;width:1080px;height:110px;color:#F6D9A8;font-family:Fraunces;font-size:66px;font-weight:600;text-align:center;white-space:normal;background:transparent;display:grid;align-content:center\" data-jc-track=\"3\" data-jc-duration=\"4.1\" data-jc-start=\"1.1\" data-jc-name=\"Title / The Fox and the Lantern\" data-jc-kind=\"text\" data-jc-clip=\"\" id=\"text-2\">The Fox and the Lantern</div><div data-jc-animation=\"{&quot;tracks&quot;:[{&quot;id&quot;:&quot;act1-kf-opacity-track&quot;,&quot;property&quot;:&quot;opacity&quot;,&quot;keyframes&quot;:[{&quot;id&quot;:&quot;act1-kf-opacity-track-key-1&quot;,&quot;time&quot;:0,&quot;value&quot;:0,&quot;easing&quot;:&quot;ease-out&quot;},{&quot;id&quot;:&quot;act1-kf-opacity-track-key-2&quot;,&quot;time&quot;:0.35,&quot;value&quot;:1,&quot;easing&quot;:&quot;ease-in-out&quot;},{&quot;id&quot;:&quot;act1-kf-opacity-track-key-3&quot;,&quot;time&quot;:4.3,&quot;value&quot;:1,&quot;easing&quot;:&quot;ease-in&quot;},{&quot;id&quot;:&quot;act1-kf-opacity-track-key-4&quot;,&quot;time&quot;:4.6,&quot;value&quot;:0,&quot;easing&quot;:&quot;ease-in-out&quot;}]},{&quot;id&quot;:&quot;act1-kf-blur-track&quot;,&quot;property&quot;:&quot;blur&quot;,&quot;keyframes&quot;:[{&quot;id&quot;:&quot;act1-kf-blur-track-key-1&quot;,&quot;time&quot;:0,&quot;value&quot;:12,&quot;easing&quot;:&quot;ease-out&quot;},{&quot;id&quot;:&quot;act1-kf-blur-track-key-2&quot;,&quot;time&quot;:0.4,&quot;value&quot;:0,&quot;easing&quot;:&quot;ease-in-out&quot;},{&quot;id&quot;:&quot;act1-kf-blur-track-key-3&quot;,&quot;time&quot;:4.3,&quot;value&quot;:0,&quot;easing&quot;:&quot;ease-in&quot;},{&quot;id&quot;:&quot;act1-kf-blur-track-key-4&quot;,&quot;time&quot;:4.6,&quot;value&quot;:12,&quot;easing&quot;:&quot;ease-in-out&quot;}]},{&quot;id&quot;:&quot;act1-kf-position-track&quot;,&quot;property&quot;:&quot;position&quot;,&quot;keyframes&quot;:[{&quot;id&quot;:&quot;act1-kf-position-track-key-1&quot;,&quot;time&quot;:0,&quot;value&quot;:{&quot;x&quot;:0,&quot;y&quot;:1676},&quot;easing&quot;:&quot;ease-out&quot;},{&quot;id&quot;:&quot;act1-kf-position-track-key-2&quot;,&quot;time&quot;:0.55,&quot;value&quot;:{&quot;x&quot;:0,&quot;y&quot;:1640},&quot;easing&quot;:&quot;ease-in-out&quot;}]}]}\" style=\"left:0px;top:1640px;width:1080px;height:110px;color:#E9B97A;font-family:Fraunces;font-size:48px;font-weight:600;text-align:center;white-space:normal;background:transparent;display:grid;align-content:center\" data-jc-track=\"3\" data-jc-duration=\"4.6\" data-jc-start=\"7\" data-jc-name=\"Caption / Through the sleeping forest\" data-jc-kind=\"text\" data-jc-clip=\"\" id=\"text-3\">Through the sleeping forest</div><div data-jc-animation=\"{&quot;tracks&quot;:[{&quot;id&quot;:&quot;act2-kf-opacity-track&quot;,&quot;property&quot;:&quot;opacity&quot;,&quot;keyframes&quot;:[{&quot;id&quot;:&quot;act2-kf-opacity-track-key-1&quot;,&quot;time&quot;:0,&quot;value&quot;:0,&quot;easing&quot;:&quot;ease-out&quot;},{&quot;id&quot;:&quot;act2-kf-opacity-track-key-2&quot;,&quot;time&quot;:0.35,&quot;value&quot;:1,&quot;easing&quot;:&quot;ease-in-out&quot;},{&quot;id&quot;:&quot;act2-kf-opacity-track-key-3&quot;,&quot;time&quot;:4.3,&quot;value&quot;:1,&quot;easing&quot;:&quot;ease-in&quot;},{&quot;id&quot;:&quot;act2-kf-opacity-track-key-4&quot;,&quot;time&quot;:4.6,&quot;value&quot;:0,&quot;easing&quot;:&quot;ease-in-out&quot;}]},{&quot;id&quot;:&quot;act2-kf-blur-track&quot;,&quot;property&quot;:&quot;blur&quot;,&quot;keyframes&quot;:[{&quot;id&quot;:&quot;act2-kf-blur-track-key-1&quot;,&quot;time&quot;:0,&quot;value&quot;:12,&quot;easing&quot;:&quot;ease-out&quot;},{&quot;id&quot;:&quot;act2-kf-blur-track-key-2&quot;,&quot;time&quot;:0.4,&quot;value&quot;:0,&quot;easing&quot;:&quot;ease-in-out&quot;},{&quot;id&quot;:&quot;act2-kf-blur-track-key-3&quot;,&quot;time&quot;:4.3,&quot;value&quot;:0,&quot;easing&quot;:&quot;ease-in&quot;},{&quot;id&quot;:&quot;act2-kf-blur-track-key-4&quot;,&quot;time&quot;:4.6,&quot;value&quot;:12,&quot;easing&quot;:&quot;ease-in-out&quot;}]},{&quot;id&quot;:&quot;act2-kf-position-track&quot;,&quot;property&quot;:&quot;position&quot;,&quot;keyframes&quot;:[{&quot;id&quot;:&quot;act2-kf-position-track-key-1&quot;,&quot;time&quot;:0,&quot;value&quot;:{&quot;x&quot;:0,&quot;y&quot;:1676},&quot;easing&quot;:&quot;ease-out&quot;},{&quot;id&quot;:&quot;act2-kf-position-track-key-2&quot;,&quot;time&quot;:0.55,&quot;value&quot;:{&quot;x&quot;:0,&quot;y&quot;:1640},&quot;easing&quot;:&quot;ease-in-out&quot;}]}]}\" style=\"left:0px;top:1640px;width:1080px;height:110px;color:#E9B97A;font-family:Fraunces;font-size:48px;font-weight:600;text-align:center;white-space:normal;background:transparent;display:grid;align-content:center\" data-jc-track=\"3\" data-jc-duration=\"4.6\" data-jc-start=\"13\" data-jc-name=\"Caption / Up the last hill\" data-jc-kind=\"text\" data-jc-clip=\"\" id=\"text-4\">Up the last hill</div><div data-jc-animation=\"{&quot;tracks&quot;:[{&quot;id&quot;:&quot;end-kf-opacity-track&quot;,&quot;property&quot;:&quot;opacity&quot;,&quot;keyframes&quot;:[{&quot;id&quot;:&quot;end-kf-opacity-track-key-1&quot;,&quot;time&quot;:0,&quot;value&quot;:0,&quot;easing&quot;:&quot;ease-out&quot;},{&quot;id&quot;:&quot;end-kf-opacity-track-key-2&quot;,&quot;time&quot;:0.35,&quot;value&quot;:1,&quot;easing&quot;:&quot;ease-in-out&quot;}]},{&quot;id&quot;:&quot;end-kf-blur-track&quot;,&quot;property&quot;:&quot;blur&quot;,&quot;keyframes&quot;:[{&quot;id&quot;:&quot;end-kf-blur-track-key-1&quot;,&quot;time&quot;:0,&quot;value&quot;:12,&quot;easing&quot;:&quot;ease-out&quot;},{&quot;id&quot;:&quot;end-kf-blur-track-key-2&quot;,&quot;time&quot;:0.4,&quot;value&quot;:0,&quot;easing&quot;:&quot;ease-in-out&quot;}]},{&quot;id&quot;:&quot;end-kf-position-track&quot;,&quot;property&quot;:&quot;position&quot;,&quot;keyframes&quot;:[{&quot;id&quot;:&quot;end-kf-position-track-key-1&quot;,&quot;time&quot;:0,&quot;value&quot;:{&quot;x&quot;:0,&quot;y&quot;:1661},&quot;easing&quot;:&quot;ease-out&quot;},{&quot;id&quot;:&quot;end-kf-position-track-key-2&quot;,&quot;time&quot;:0.55,&quot;value&quot;:{&quot;x&quot;:0,&quot;y&quot;:1625},&quot;easing&quot;:&quot;ease-in-out&quot;}]}]}\" style=\"left:0px;top:1625px;width:1080px;height:140px;color:#F6D9A8;font-family:Fraunces;font-size:56px;font-weight:600;text-align:center;white-space:normal;background:transparent;display:grid;align-content:center\" data-jc-track=\"3\" data-jc-duration=\"4.2\" data-jc-start=\"19.8\" data-jc-name=\"Closing line / Some lights are worth following\" data-jc-kind=\"text\" data-jc-clip=\"\" id=\"text-5\">Some lights are worth following.</div>",
  "name": "The Fox and the Lantern · Shadow theatre",
  "width": 1080,
  "format": "jsoncut-html",
  "height": 1920,
  "values": {},
  "version": 1,
  "duration": 24,
  "safeArea": {
    "top": 0,
    "left": 0,
    "right": 0,
    "bottom": 0
  },
  "variables": [],
  "customBlocks": [
    {
      "inputs": {
        "seed": 8
      },
      "adapter": "canvas-2d",
      "layerId": "custom-1",
      "javascript": "// Shadow theatre: a cut-paper fox follows a glowing lantern through the forest up to the moon. Own code, no presets.\nconst ctx = context, W = width, H = height;\nconst SX0 = 70, SX1 = W - 70, SY0 = 150, SY1 = 1540;      // the lit paper scrim inside the proscenium\nconst INK = '#120805', GLOW = '#FFC46A';\nconst hash = n => { const q = Math.sin(n * 127.1 + 311.7) * 43758.5453; return q - Math.floor(q); };\nconst span = (t, a, b) => Math.min(1, Math.max(0, (t - a) / (b - a)));\nconst ease = x => x * x * (3 - 2 * x);\nconst lerp = (a, b, u) => a + (b - a) * u;\nconst mk = (w, h) => { const c = document.createElement('canvas'); c.width = w; c.height = h; return c; };\n\n// ---------- camera, ground and the hill at the end of the path\nfunction camAt(t) { // 230 px/s with soft start and stop\n  const v = 230, a = 2.5, b = 17, r1 = 1, r2 = 1.5;\n  if (t < a) return 0;\n  if (t < a + r1) return v * (t - a) ** 2 / (2 * r1);\n  if (t < b - r2) return v * (r1 / 2 + (t - a - r1));\n  if (t < b) { const d = t - (b - r2); return v * (r1 / 2 + (b - r2 - a - r1) + d - d * d / (2 * r2)); }\n  return v * (r1 / 2 + (b - r2 - a - r1) + r2 / 2);\n}\nconst HILL = 3560;\nconst groundY = wx => 1330 - 230 * Math.exp(-(((wx - HILL) / 520) ** 2)) - 18 * Math.sin(wx * .004);\nconst foxXAt = t => t < 2.8 ? lerp(-300, 330, span(t, .6, 2.8)) : t < 17 ? 330 : lerp(330, 470, Math.sin(span(t, 17, 19.2) * Math.PI / 2));\n\n// ---------- cut-paper sprites, built once\nfunction treeSprite(type, seed) {\n  const c = mk(560, 1300), s = c.getContext('2d'), cx = 280, r = k => hash(seed * 31 + k);\n  s.fillStyle = '#000'; s.strokeStyle = '#000'; s.lineCap = 'round';\n  const lean = (r(1) - .5) * 40;\n  s.beginPath(); s.moveTo(cx - 30, 1300); s.lineTo(cx - 12 + lean, 300); s.lineTo(cx + 12 + lean, 300); s.lineTo(cx + 30, 1300); s.fill();\n  if (type === 0) { // pine with jagged tiers\n    for (let i = 0; i < 8; i++) {\n      const y = 1080 - i * 112, hw = 240 - i * 27, x0 = cx + lean * (1 - y / 1300);\n      s.beginPath(); s.moveTo(x0 - hw, y);\n      for (let k = 0; k <= 12; k++) s.lineTo(x0 - hw + k * hw / 6, y + (k % 2 ? 18 : 0));\n      s.lineTo(x0 + hw, y); s.lineTo(x0, y - 200); s.closePath(); s.fill();\n    }\n    s.beginPath(); s.moveTo(cx + lean - 14, 190); s.lineTo(cx + lean, 120); s.lineTo(cx + lean + 14, 190); s.fill();\n  } else if (type === 1) { // round tree with a lace of cut leaves\n    for (let i = 0; i < 6; i++) { const a = -2.6 + i * .42, l = 150 + r(i + 4) * 70; s.lineWidth = 16 - i; s.beginPath(); s.moveTo(cx + lean * .6, 640 - i * 30); s.quadraticCurveTo(cx + Math.cos(a) * l * .5, 560 - i * 40, cx + Math.cos(a) * l, 470 + Math.sin(a) * l * .5); s.stroke(); }\n    for (let i = 0; i < 11; i++) { const a = i / 11 * 6.2832, rr = 120 + r(i + 20) * 60; s.beginPath(); s.arc(cx + Math.cos(a) * 140 + lean * .5, 400 + Math.sin(a) * 120, rr, 0, 6.2832); s.fill(); }\n    s.globalCompositeOperation = 'destination-out';\n    for (let i = 0; i < 90; i++) { s.save(); s.translate(cx - 260 + r(i + 40) * 520, 180 + r(i + 90) * 420); s.rotate(r(i + 7) * 6.2832); s.beginPath(); s.ellipse(0, 0, 11, 4.5, 0, 0, 6.2832); s.fill(); s.restore(); }\n    s.globalCompositeOperation = 'source-over';\n  } else { // bare tree with forked branches\n    const branch = (x, y, a, l, w, d) => { const x2 = x + Math.cos(a) * l, y2 = y + Math.sin(a) * l; s.lineWidth = w; s.beginPath(); s.moveTo(x, y); s.lineTo(x2, y2); s.stroke(); if (d > 0) { branch(x2, y2, a - .45 - r(d + x) * .2, l * .72, w * .66, d - 1); branch(x2, y2, a + .4 + r(d + y) * .2, l * .7, w * .66, d - 1); } };\n    branch(cx + lean, 320, -Math.PI / 2, 170, 20, 4);\n    branch(cx + lean * .7, 620, -Math.PI / 2 - .7, 160, 14, 3); branch(cx + lean * .7, 560, -Math.PI / 2 + .75, 150, 13, 3);\n  }\n  return c;\n}\nfunction soften(src, blur, color) {\n  const c = mk(src.width + 60, src.height + 60), s = c.getContext('2d');\n  s.filter = `blur(${blur}px)`; s.drawImage(src, 30, 30); s.filter = 'none';\n  s.globalCompositeOperation = 'source-in'; s.fillStyle = color; s.fillRect(0, 0, c.width, c.height);\n  return c;\n}\nfunction fernSprite(seed) {\n  const c = mk(700, 600), s = c.getContext('2d'); s.fillStyle = '#000'; s.strokeStyle = '#000'; s.lineCap = 'round';\n  for (let f = 0; f < 5; f++) {\n    let px = 0, py = 0;\n    const a0 = -2.4 + f * .32 + (hash(seed + f) - .5) * .2, l = 380 + hash(seed + f + 9) * 160;\n    for (let k = 0; k < 26; k++) {\n      const u = k / 26, a = a0 + u * .9, x = 350 + Math.cos(a0) * l * u + Math.sin(u * 3) * 30, y = 600 + Math.sin(a0) * l * u + u * u * 120;\n      s.lineWidth = 9 * (1 - u) + 2; if (k) { s.beginPath(); s.moveTo(px, py); s.lineTo(x, y); s.stroke(); }\n      const lw = 34 * (1 - u * .8); for (const sd of [-1, 1]) { s.save(); s.translate(x, y); s.rotate(a + sd * 1.25); s.beginPath(); s.ellipse(lw * .55, 0, lw * .55, 7 * (1 - u * .6), 0, 0, 6.2832); s.fill(); s.restore(); }\n      px = x; py = y;\n    }\n  }\n  return c;\n}\nconst TREES = [0, 1, 2, 0, 1, 0].map((type, i) => treeSprite(type, i + 1));\nconst FAR = [0, 1, 0, 2].map((type, i) => soften(treeSprite(type, i + 20), 6, 'rgba(74,32,12,.5)'));\nconst FERNS = [fernSprite(3), fernSprite(8)].map(f => soften(f, 7, '#0B0503'));\nconst PAPER = mk(SX1 - SX0, SY1 - SY0);\n{ const s = PAPER.getContext('2d'); s.fillStyle = '#FFFFFF'; s.fillRect(0, 0, PAPER.width, PAPER.height);\n  for (let i = 0; i < 9000; i++) { s.fillStyle = `rgba(120,80,40,${.05 + hash(i) * .1})`; s.fillRect(hash(i * 1.7) * PAPER.width, hash(i * 2.9) * PAPER.height, 1 + hash(i + 3) * 2, 1); }\n  s.lineWidth = 1; for (let i = 0; i < 260; i++) { const x = hash(i + 7) * PAPER.width, y = hash(i + 11) * PAPER.height, a = hash(i + 13) * 6.2832, l = 20 + hash(i + 17) * 60; s.strokeStyle = `rgba(110,70,30,${.08 + hash(i + 19) * .1})`; s.beginPath(); s.moveTo(x, y); s.quadraticCurveTo(x + Math.cos(a) * l * .5 + 6, y + Math.sin(a) * l * .5, x + Math.cos(a) * l, y + Math.sin(a) * l); s.stroke(); } }\n\n// ---------- puppets\nfunction limb(x, y, a1, a2, l1, l2, w) {\n  const kx = x + Math.sin(a1) * l1, ky = y + Math.cos(a1) * l1, fx = kx + Math.sin(a2) * l2, fy = ky + Math.cos(a2) * l2;\n  ctx.lineWidth = w; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(kx, ky); ctx.stroke();\n  ctx.lineWidth = w * .7; ctx.beginPath(); ctx.moveTo(kx, ky); ctx.lineTo(fx, fy); ctx.stroke();\n  ctx.beginPath(); ctx.ellipse(fx + 5, fy - 2, 10, 6, 0, 0, 6.2832); ctx.fill();\n}\nfunction fox(x, y, phase, amp, sit, slope, look, t) {\n  ctx.save(); ctx.translate(x, y); ctx.rotate(slope * (1 - sit)); ctx.scale(1.35, 1.35);\n  ctx.fillStyle = INK; ctx.strokeStyle = INK; ctx.lineCap = 'round'; ctx.lineJoin = 'round';\n  const bob = -Math.abs(Math.sin(phase)) * 5 * amp;\n  const legA = k => Math.sin(phase + k) * .5 * amp;\n  // hind legs fold when the fox sits down\n  const hipY = -80 + 46 * sit + bob;\n  for (const k of [0, Math.PI]) limb(-62, hipY, legA(k + 1.6) * (1 - sit) + sit * 1.35, (legA(k + 1.6) - .35 * Math.max(0, Math.cos(phase + k)) * amp) * (1 - sit) - sit * .96, 42, 44, 17);\n  ctx.save(); ctx.translate(-62, hipY - 4); ctx.rotate(-sit * .55); ctx.translate(62, 0);\n  for (const k of [Math.PI, 0]) limb(58, 4, legA(k) * (1 - sit) - sit * .55, (legA(k) - .4 * Math.max(0, Math.cos(phase + k)) * amp) * (1 - sit) - sit * .55, 40 + sit * 4, 44 + sit * 6, 15);\n  ctx.beginPath(); ctx.ellipse(0, -12, 106, 42, 0, 0, 6.2832); ctx.fill();\n  ctx.beginPath(); ctx.moveTo(48, -40); ctx.quadraticCurveTo(96, -64, 118, -60); ctx.lineTo(130, -20); ctx.quadraticCurveTo(96, 6, 56, 18); ctx.fill();\n  ctx.save(); ctx.translate(118, -56); ctx.rotate(look);\n  ctx.beginPath(); ctx.ellipse(8, -6, 34, 27, 0, 0, 6.2832); ctx.fill();\n  ctx.beginPath(); ctx.moveTo(26, -22); ctx.quadraticCurveTo(58, -14, 80, -2); ctx.lineTo(72, 6); ctx.quadraticCurveTo(46, 14, 22, 16); ctx.fill();\n  ctx.beginPath(); ctx.moveTo(-12, -22); ctx.lineTo(-6, -70); ctx.lineTo(14, -28); ctx.fill();\n  ctx.beginPath(); ctx.moveTo(6, -28); ctx.lineTo(20, -72); ctx.lineTo(32, -26); ctx.fill();\n  ctx.fillStyle = GLOW; ctx.beginPath(); ctx.ellipse(26, -10, 5, 3.4, -.2, 0, 6.2832); ctx.fill();\n  ctx.restore();\n  ctx.restore();\n  // bushy tail with cut notches\n  const sway = Math.sin(t * 2.3) * .18 * (1 - sit * .7);\n  ctx.save(); ctx.translate(-98, -96 + bob + sit * 44); ctx.rotate(sway - sit * .7);\n  ctx.beginPath(); ctx.moveTo(0, -16); ctx.bezierCurveTo(-60, -60, -150, -48, -190, -6); ctx.lineTo(-170, 4); ctx.lineTo(-184, 14);\n  ctx.bezierCurveTo(-130, 40, -50, 30, 0, 16); ctx.closePath(); ctx.fill();\n  ctx.restore();\n  ctx.restore();\n}\nfunction lantern(x, y, s, a, t) {\n  if (a <= 0) return;\n  ctx.save(); ctx.globalAlpha = a;\n  ctx.globalCompositeOperation = 'lighter';\n  const halo = ctx.createRadialGradient(x, y, 0, x, y, 300 * s); halo.addColorStop(0, 'rgba(255,190,90,.55)'); halo.addColorStop(1, 'rgba(255,150,60,0)');\n  ctx.fillStyle = halo; ctx.fillRect(x - 300 * s, y - 300 * s, 600 * s, 600 * s);\n  ctx.globalCompositeOperation = 'source-over';\n  ctx.translate(x, y); ctx.scale(s, s); ctx.rotate(Math.sin(t * 1.7) * .06);\n  const body = new Path2D(); body.moveTo(-38, -52); body.quadraticCurveTo(0, -66, 38, -52); body.lineTo(28, 46); body.quadraticCurveTo(0, 54, -28, 46); body.closePath();\n  const gl = ctx.createRadialGradient(0, 10, 4, 0, 0, 60); gl.addColorStop(0, '#FFF2C4'); gl.addColorStop(.6, '#FFB04A'); gl.addColorStop(1, '#E06A22');\n  ctx.fillStyle = gl; ctx.fill(body);\n  ctx.strokeStyle = INK; ctx.lineWidth = 5; ctx.stroke(body);\n  ctx.lineWidth = 2.5; ctx.beginPath(); ctx.moveTo(-12, -58); ctx.lineTo(-9, 50); ctx.moveTo(12, -58); ctx.lineTo(9, 50); ctx.moveTo(-35, -20); ctx.quadraticCurveTo(0, -26, 35, -20); ctx.moveTo(-31, 16); ctx.quadraticCurveTo(0, 12, 31, 16); ctx.stroke();\n  ctx.fillStyle = INK; ctx.fillRect(-24, 46, 48, 9);\n  ctx.fillStyle = '#FFF6D8'; ctx.beginPath(); ctx.ellipse(0, 38 - Math.sin(t * 9) * 2, 5, 9 + Math.sin(t * 13) * 2, 0, 0, 6.2832); ctx.fill();\n  ctx.restore();\n}\nfunction owl(x, y, look, t) {\n  ctx.save(); ctx.translate(x, y); ctx.fillStyle = INK;\n  ctx.beginPath(); ctx.ellipse(0, 0, 40, 58, 0, 0, 6.2832); ctx.fill();\n  ctx.beginPath(); ctx.arc(0, -66, 36, 0, 6.2832); ctx.fill();\n  ctx.beginPath(); ctx.moveTo(-34, -84); ctx.lineTo(-30, -118); ctx.lineTo(-14, -94); ctx.moveTo(34, -84); ctx.lineTo(30, -118); ctx.lineTo(14, -94); ctx.fill();\n  ctx.beginPath(); ctx.moveTo(-10, 56); ctx.lineTo(-4, 74); ctx.lineTo(2, 56); ctx.moveTo(4, 56); ctx.lineTo(10, 74); ctx.lineTo(16, 56); ctx.fill();\n  const blink = (t % 3.1) < .14;\n  for (const s of [-1, 1]) {\n    if (blink) { ctx.strokeStyle = GLOW; ctx.lineWidth = 2.5; ctx.beginPath(); ctx.moveTo(s * 14 - 8, -70); ctx.lineTo(s * 14 + 8, -70); ctx.stroke(); continue; }\n    ctx.fillStyle = GLOW; ctx.beginPath(); ctx.arc(s * 14, -70, 10, 0, 6.2832); ctx.fill();\n    ctx.fillStyle = INK; ctx.beginPath(); ctx.arc(s * 14 + look * 4, -70, 4.5, 0, 6.2832); ctx.fill();\n  }\n  ctx.fillStyle = INK; ctx.beginPath(); ctx.moveTo(-5, -60); ctx.lineTo(0, -48); ctx.lineTo(5, -60); ctx.fill();\n  ctx.restore();\n}\n\n// ---------- proscenium: red velvet, gold trim, opening curtains\nfunction velvet(x, w, y0, y1) {\n  const gr = ctx.createLinearGradient(x, 0, x + w, 0);\n  for (let k = 0; k <= 8; k++) gr.addColorStop(k / 8, k % 2 ? '#6E1518' : '#36090B');\n  ctx.fillStyle = gr; ctx.fillRect(x, y0, w, y1 - y0);\n}\nfunction frame(t) {\n  const open = ease(span(t, .15, 1.9));\n  if (open < 1) {\n    const cw = (SX1 - SX0) / 2 * (1 - open);\n    velvet(SX0, cw + 30, SY0, SY1); velvet(SX1 - cw - 30, cw + 30, SY0, SY1);\n  }\n  ctx.fillStyle = '#0A0403'; ctx.fillRect(0, SY1, W, H - SY1);\n  ctx.fillStyle = '#3B1D0C'; ctx.fillRect(0, SY1, W, 16); ctx.fillStyle = '#B8862E'; ctx.fillRect(0, SY1 + 16, W, 3);\n  velvet(0, SX0 + 10, 0, SY1 + 16); velvet(SX1 - 10, W - SX1 + 10, 0, SY1 + 16);\n  velvet(0, W, 0, SY0 - 20);\n  ctx.fillStyle = '#5A1012';\n  for (let x = 0; x < W + 60; x += 60) { ctx.beginPath(); ctx.arc(x + 30, SY0 - 20, 30, 0, Math.PI); ctx.fill(); }\n  ctx.strokeStyle = '#B8862E'; ctx.lineWidth = 3;\n  ctx.beginPath(); for (let x = 0; x < W + 60; x += 60) ctx.arc(x + 30, SY0 - 20, 30, Math.PI, 0, true); ctx.stroke();\n  ctx.beginPath(); ctx.moveTo(0, SY0 - 22); ctx.lineTo(W, SY0 - 22); ctx.stroke();\n}\n\njsoncut.onFrame(() => {\n  const t = jsoncut.time, cam = camAt(t), fxS = foxXAt(t), fxW = cam + fxS;\n  ctx.fillStyle = '#000'; ctx.fillRect(0, 0, W, H);\n  ctx.save(); ctx.beginPath(); ctx.rect(SX0, SY0, SX1 - SX0, SY1 - SY0); ctx.clip();\n  // backlight with a candle flicker, dimming into night\n  const fl = .94 + .04 * Math.sin(t * 7.3) + .02 * Math.sin(t * 13.1 + 1);\n  const lg = ctx.createRadialGradient(540, 860, 60, 540, 900, 980); lg.addColorStop(0, '#FFD48A'); lg.addColorStop(.55, '#F09A3A'); lg.addColorStop(1, '#7A3010');\n  ctx.fillStyle = lg; ctx.fillRect(0, 0, W, H);\n  ctx.fillStyle = `rgba(30,8,4,${(1 - fl) + .46 * ease(span(t, 11, 19))})`; ctx.fillRect(0, 0, W, H);\n  // the moon rises behind the hill\n  const my = lerp(1740, 900, ease(span(t, 12.5, 19.5))), mb = span(t, 12.5, 14);\n  if (mb > 0) {\n    ctx.save(); ctx.globalCompositeOperation = 'lighter';\n    const mh = ctx.createRadialGradient(600, my, 200, 600, my, 520); mh.addColorStop(0, `rgba(255,220,150,${.45 * mb})`); mh.addColorStop(1, 'rgba(255,200,120,0)');\n    ctx.fillStyle = mh; ctx.fillRect(0, 0, W, H); ctx.restore();\n    ctx.fillStyle = `rgba(255,244,214,${mb})`; ctx.beginPath(); ctx.arc(600, my, 235, 0, 6.2832); ctx.fill();\n    ctx.fillStyle = `rgba(200,150,90,${.12 * mb})`; for (const [dx, dy, r] of [[-70, -50, 46], [60, 40, 34], [-10, 100, 28], [90, -80, 22]]) { ctx.beginPath(); ctx.arc(600 + dx, my + dy, r, 0, 6.2832); ctx.fill(); }\n  }\n  if (t > 21) { // the lantern melts into the moon\n    ctx.save(); ctx.globalCompositeOperation = 'lighter'; const b = Math.sin(span(t, 21, 23) * Math.PI) * .5;\n    const bg = ctx.createRadialGradient(600, my, 100, 600, my, 560); bg.addColorStop(0, `rgba(255,236,190,${b})`); bg.addColorStop(1, 'rgba(255,220,160,0)'); ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H); ctx.restore();\n  }\n  // far forest, soft because it sits further from the scrim\n  ctx.save(); ctx.globalAlpha = 1 - .65 * ease(span(t, 13, 19));\n  ctx.fillStyle = 'rgba(74,32,12,.32)'; ctx.beginPath(); ctx.moveTo(0, H);\n  for (let x = 0; x <= W; x += 20) ctx.lineTo(x, 1180 - 60 * Math.sin((x + cam * .25) * .004) - 40 * Math.sin((x + cam * .25) * .011 + 2)); ctx.lineTo(W, H); ctx.fill();\n  for (let i = Math.floor(cam * .45 / 170) - 2; i < Math.floor((cam * .45 + W) / 170) + 2; i++) {\n    const sp = FAR[Math.floor(hash(i + 300) * 4)], sc = .5 + hash(i + 310) * .25, x = i * 170 + hash(i + 320) * 70 - cam * .45;\n    ctx.drawImage(sp, x - sp.width * sc / 2, 1300 - sp.height * sc + 30 * sc, sp.width * sc, sp.height * sc);\n  }\n  ctx.restore();\n  // ground, grass and the mid forest\n  ctx.fillStyle = INK; ctx.beginPath(); ctx.moveTo(0, H);\n  for (let x = 0; x <= W; x += 10) ctx.lineTo(x, groundY(x + cam)); ctx.lineTo(W, H); ctx.fill();\n  ctx.beginPath();\n  for (let wx = Math.floor(cam / 14) * 14; wx < cam + W; wx += 14) { const x = wx - cam, y = groundY(wx), h = 10 + hash(wx * .07) * 26, lean = (hash(wx * .13) - .5) * 14 + Math.sin(t * 2 + wx * .01) * 3; ctx.moveTo(x - 5, y + 2); ctx.lineTo(x + lean, y - h); ctx.lineTo(x + 5, y + 2); }\n  ctx.fill();\n  for (let i = 0; i < 12; i++) {\n    const wx = 150 + i * 255 + hash(i + 50) * 90; if (wx - cam < -320 || wx - cam > W + 320) continue;\n    const sp = TREES[Math.floor(hash(i + 60) * 6)], sc = .78 + hash(i + 70) * .3, x = wx - cam;\n    ctx.drawImage(sp, x - 280 * sc, groundY(wx) + 24 - 1300 * sc, 560 * sc, 1300 * sc);\n  }\n  // the old snag with the owl\n  const ox = 2050 - cam, oy = groundY(2050);\n  if (ox > -300 && ox < W + 300) {\n    ctx.fillStyle = INK; ctx.beginPath(); ctx.moveTo(ox - 30, oy + 10); ctx.lineTo(ox - 16, oy - 720); ctx.lineTo(ox - 4, oy - 760); ctx.lineTo(ox + 6, oy - 728); ctx.lineTo(ox + 18, oy - 745); ctx.lineTo(ox + 30, oy + 10); ctx.fill();\n    ctx.strokeStyle = INK; ctx.lineCap = 'round'; ctx.lineWidth = 16; ctx.beginPath(); ctx.moveTo(ox, oy - 540); ctx.quadraticCurveTo(ox + 90, oy - 570, ox + 190, oy - 560); ctx.stroke();\n    owl(ox + 120, oy - 628, Math.max(-1, Math.min(1, (fxS - ox) / 300)), t);\n  }\n  // fireflies\n  const vis = span(t, 6, 8) * (1 - .6 * span(t, 18, 21));\n  if (vis > 0) { ctx.save(); ctx.globalCompositeOperation = 'lighter';\n    for (let j = 0; j < 26; j++) { const x = SX0 + ((hash(j) * 1000 - cam * .8 + t * 14) % 1000 + 1000) % 1000, y = 640 + hash(j + 3) * 620 + Math.sin(t * 1.1 + j * 2) * 30, a = vis * (.45 + .55 * Math.sin(t * 3 + j * 1.7)); if (a <= 0) continue;\n      const fg = ctx.createRadialGradient(x, y, 0, x, y, 18); fg.addColorStop(0, `rgba(255,236,150,${a})`); fg.addColorStop(1, 'rgba(255,200,90,0)'); ctx.fillStyle = fg; ctx.fillRect(x - 18, y - 18, 36, 36); }\n    ctx.restore(); }\n  // the lantern drifts ahead, then rises into the moon\n  const follow = (tt, fs) => [lerp(760, fs + 330, ease(span(tt, 1.5, 3.2))) + 30 * Math.sin(tt * 1.3), 860 + 50 * Math.sin(tt * .9) - 140 * ease(span(tt, 8, 14))];\n  let [lx, ly] = follow(t, fxS), ls = 1;\n  if (t > 17.2) { const [x17, y17] = follow(17.2, 330), p = ease(span(t, 17.2, 21.4)); lx = lerp(x17, 600, p) + Math.sin(t * 1.3) * 20 * (1 - p); ly = lerp(y17, my, p); ls = 1 - .72 * p; }\n  const la = 1 - span(t, 21, 22.2);\n  if (la > 0) { ctx.strokeStyle = `rgba(18,8,5,${.55 * (1 - span(t, 16.5, 17.5))})`; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(lx, ly - 60 * ls); ctx.lineTo(lx - 50, SY0); ctx.stroke(); }\n  lantern(lx, ly, ls, la, t);\n  // the fox and its control rod\n  const fy = groundY(fxW), slope = Math.atan2(groundY(fxW + 60) - groundY(fxW - 60), 120), sit = ease(span(t, 19.2, 20.4));\n  const moving = t > .6 && t < 19.2 ? 1 : 0, phase = fxW / 46;\n  const look = Math.max(-.6, Math.min(.15, Math.atan2(ly - (fy - 170), lx - (fxS + 140)) * .55));\n  ctx.strokeStyle = 'rgba(18,8,5,.7)'; ctx.lineWidth = 4; ctx.beginPath(); ctx.moveTo(fxS - 12, fy - 105); ctx.lineTo(fxS - 70, SY1); ctx.stroke();\n  fox(fxS, fy, phase, moving, sit, slope, look, t);\n  // close ferns, blurred because they almost touch the light\n  for (let i = Math.floor(cam * 1.5 / 430) - 1; i < Math.floor((cam * 1.5 + W) / 430) + 2; i++) {\n    const sp = FERNS[i & 1], sc = .35 + hash(i + 400) * .2, x = i * 430 + hash(i + 410) * 180 - cam * 1.5;\n    ctx.drawImage(sp, x - sp.width * sc / 2, SY1 + 40 - sp.height * sc, sp.width * sc, sp.height * sc);\n  }\n  // paper grain and a warm vignette on the scrim\n  ctx.save(); ctx.globalCompositeOperation = 'multiply'; ctx.globalAlpha = .55; ctx.drawImage(PAPER, SX0, SY0); ctx.restore();\n  const v = ctx.createRadialGradient(540, 860, 420, 540, 860, 1050); v.addColorStop(0, 'rgba(0,0,0,0)'); v.addColorStop(1, 'rgba(25,6,2,.55)'); ctx.fillStyle = v; ctx.fillRect(0, 0, W, H);\n  ctx.restore();\n  frame(t);\n});\n"
    }
  ],
  "aspectVariants": [],
  "compositionKind": "video",
  "canvasBackground": "#000000",
  "editabilityTarget": "hybrid"
}
