{
  "id": "opus-showcase-06-vintage-infinite-zoom",
  "css": "",
  "fps": 30,
  "html": "<div style=\"left:0px;top:0px;width:1080px;height:1350px;background:#EDE3CC;border-radius:0px\" data-jc-track=\"0\" data-jc-duration=\"18\" 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:1350px;overflow:hidden;box-sizing:border-box;background:transparent\" data-jc-track=\"1\" data-jc-duration=\"18\" data-jc-start=\"0\" data-jc-name=\"Collage zoom / mountains-watch, cliffs-lens, dunes-shell, print texture (own code)\" data-jc-kind=\"custom\" data-jc-clip=\"\" id=\"custom-1\"></div><div style=\"left:290px;top:1200px;width:500px;height:84px;background:#F1E8D6;border-radius:18px\" data-jc-track=\"2\" data-jc-duration=\"18\" data-jc-start=\"0\" data-jc-name=\"Collage label / paper strip\" data-jc-kind=\"shape\" data-jc-clip=\"\" id=\"shape-2\"></div><div data-jc-animation=\"{&quot;tracks&quot;:[{&quot;id&quot;:&quot;label-text-kf-opacity-track&quot;,&quot;property&quot;:&quot;opacity&quot;,&quot;keyframes&quot;:[{&quot;id&quot;:&quot;label-text-kf-opacity-track-key-1&quot;,&quot;time&quot;:0,&quot;value&quot;:1,&quot;easing&quot;:&quot;ease-in-out&quot;},{&quot;id&quot;:&quot;label-text-kf-opacity-track-key-2&quot;,&quot;time&quot;:18,&quot;value&quot;:1,&quot;easing&quot;:&quot;ease-in-out&quot;}]}]}\" style=\"left:290px;top:1200px;width:500px;height:84px;color:#2B2A28;font-family:Cormorant Garamond;font-size:40px;font-weight:700;text-align:center;white-space:normal;background:transparent;display:grid;align-content:center\" data-jc-track=\"3\" data-jc-duration=\"18\" data-jc-start=\"0\" data-jc-name=\"Collage label / The endless album\" data-jc-kind=\"text\" data-jc-clip=\"\" id=\"text-1\">THE ENDLESS ALBUM</div>",
  "name": "The Endless Album · Vintage infinite zoom",
  "width": 1080,
  "format": "jsoncut-html",
  "height": 1350,
  "values": {},
  "version": 1,
  "duration": 18,
  "safeArea": {
    "top": 0,
    "left": 0,
    "right": 0,
    "bottom": 0
  },
  "variables": [],
  "customBlocks": [
    {
      "inputs": {
        "seed": 6
      },
      "adapter": "canvas-2d",
      "layerId": "custom-1",
      "javascript": "// Endless vintage collage zoom: every world hides the next one inside a cut-out object. Seamless 18 s loop.\nconst ctx = context, W = width, H = height, CX = W / 2, CY = H / 2;\nconst R = 120;                         // window radius inside every object\nconst HALF = Math.hypot(CX, CY);       // half diagonal of the frame\nconst EX = Math.ceil(HALF) + 24;       // worlds extend past the frame so every round window is filled\nconst K = R / HALF, Z = 1 / K;         // nested scale and zoom per world\nconst PERIOD = 6;\nconst hash = n => { const q = Math.sin(n * 127.1 + 311.7) * 43758.5453; return q - Math.floor(q); };\n\n// ---------- screen-space print texture (halftone dots, paper grain)\nconst dot = document.createElement('canvas'); dot.width = dot.height = 9;\n{ const g = dot.getContext('2d'); g.fillStyle = '#000'; g.beginPath(); g.arc(4.5, 4.5, 1.9, 0, 6.28); g.fill(); }\nconst dots = ctx.createPattern(dot, 'repeat');\nconst paper = document.createElement('canvas'); paper.width = paper.height = 384;\n{ const g = paper.getContext('2d'); for (let i = 0; i < 14000; i++) { g.fillStyle = hash(i) > .5 ? 'rgba(255,250,235,.5)' : 'rgba(70,50,30,.45)'; g.fillRect(hash(i * 2.3) * 384, hash(i * 3.7) * 384, 1.3, 1.3); } }\nconst grain = ctx.createPattern(paper, 'repeat');\n\nconst poly = (pts, fill) => { ctx.beginPath(); pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y)); ctx.closePath(); ctx.fillStyle = fill; ctx.fill(); };\nconst ridge = (base, amp, freq, seed, fill) => {\n  const pts = [[-EX, EX]];\n  for (let x = -EX; x <= EX; x += 18) pts.push([x, base - amp * (.55 + .45 * Math.sin(x * freq + seed)) - amp * .35 * Math.abs(Math.sin(x * freq * 2.3 + seed * 2))]);\n  pts.push([EX, EX]); poly(pts, fill);\n};\nconst vgrad = (y0, y1, a, b) => { const g = ctx.createLinearGradient(0, y0, 0, y1); g.addColorStop(0, a); g.addColorStop(1, b); return g; };\n\n// ---------- the three worlds (local coordinates, window centred at 0,0)\nconst worlds = [\n  { // snowy mountain lake\n    land() {\n      ctx.fillStyle = vgrad(-CY, 80, '#EDE3CC', '#B9D2C9'); ctx.fillRect(-EX, -EX, 2 * EX, 2 * EX);\n      ctx.fillStyle = 'rgba(255,248,230,.8)'; ctx.beginPath(); ctx.arc(250, -420, 70, 0, 6.28); ctx.fill();\n      ridge(40, 330, .006, 1.3, '#5E8C8C');\n      for (let i = 0; i < 6; i++) { const x = -420 + i * 170, y = 40 - 330 * (.55 + .45 * Math.sin(x * .006 + 1.3)) - 330 * .35 * Math.abs(Math.sin(x * .006 * 2.3 + 2.6)); poly([[x - 50, y + 70], [x, y - 6], [x + 50, y + 70]], '#F4EFE4'); }\n      ridge(110, 150, .011, 4.1, '#1F4E5A');\n      ctx.fillStyle = vgrad(110, CY, '#6FA3A0', '#2E6870'); ctx.fillRect(-EX, 110, 2 * EX, EX);\n      ctx.strokeStyle = 'rgba(237,227,204,.55)'; ctx.lineWidth = 3;\n      for (let l = 0; l < 14; l++) { const y = 150 + l * 34, x = -300 + hash(l) * 400; ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + 120 + hash(l + 9) * 140, y); ctx.stroke(); }\n      for (let p = 0; p < 16; p++) { const x = p < 8 ? -540 + p * 34 : 300 + (p - 8) * 34, h = 90 + hash(p) * 80; poly([[x - 22, 118], [x, 118 - h], [x + 22, 118]], '#16343B'); }\n    },\n    object(t) { // pocket watch on a chain\n      ctx.strokeStyle = '#2B2A28'; ctx.lineWidth = 6;\n      for (let c = 0; c < 26; c++) { ctx.beginPath(); ctx.ellipse(0, -205 - c * 22, 7, 12, 0, 0, 6.28); ctx.stroke(); }\n      ctx.fillStyle = '#C9C6BE'; ctx.fillRect(-14, -206, 28, 26); ctx.beginPath(); ctx.arc(0, -212, 16, 0, 6.28); ctx.fill();\n      ring(0, 0, 176, R, '#D9D6CF');\n      ctx.strokeStyle = '#2B2A28'; ctx.lineWidth = 7; ctx.beginPath(); ctx.arc(0, 0, 172, 0, 6.28); ctx.stroke();\n      for (let k = 0; k < 60; k++) { const a = k / 60 * 6.2832, r0 = k % 5 ? 150 : 136; ctx.lineWidth = k % 5 ? 2 : 5; ctx.beginPath(); ctx.moveTo(Math.cos(a) * r0, Math.sin(a) * r0); ctx.lineTo(Math.cos(a) * 160, Math.sin(a) * 160); ctx.stroke(); }\n    },\n    glass(t) {\n      ctx.strokeStyle = '#2B2A28'; ctx.lineCap = 'round';\n      const m = t / 18 * 6.2832 * 3, h = t / 18 * 6.2832;\n      ctx.lineWidth = 6; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(Math.sin(m) * 100, -Math.cos(m) * 100); ctx.stroke();\n      ctx.lineWidth = 9; ctx.beginPath(); ctx.moveTo(0, 0); ctx.lineTo(Math.sin(h) * 66, -Math.cos(h) * 66); ctx.stroke();\n      ctx.fillStyle = '#2B2A28'; ctx.beginPath(); ctx.arc(0, 0, 9, 0, 6.28); ctx.fill();\n      sheen();\n    },\n  },\n  { // sea cliffs\n    land() {\n      ctx.fillStyle = vgrad(-CY, 60, '#F0E4C8', '#D9C9A0'); ctx.fillRect(-EX, -EX, 2 * EX, 2 * EX);\n      ctx.fillStyle = vgrad(40, CY, '#3C5A86', '#22324F'); ctx.fillRect(-EX, 40, 2 * EX, EX);\n      ctx.strokeStyle = 'rgba(217,164,65,.7)'; ctx.lineWidth = 3;\n      for (let l = 0; l < 22; l++) { const y = 70 + l * 38; ctx.beginPath(); for (let x = -EX; x <= EX; x += 30) ctx.lineTo(x, y + Math.sin(x * .03 + l) * 6); ctx.stroke(); }\n      poly([[-EX, EX], [-EX, -330], [-430, -360], [-330, -250], [-280, -90], [-230, 60], [-150, 200], [-170, EX]], '#B9822F');\n      poly([[-EX, EX], [-EX, -200], [-420, -150], [-330, 40], [-260, 260], [-300, EX]], '#7A4F1E');\n      poly([[EX, EX], [EX, -260], [430, -300], [360, -120], [300, 80], [250, 260], [270, EX]], '#A8732A');\n      ctx.strokeStyle = '#2B2A28'; ctx.lineWidth = 4;\n      for (let b = 0; b < 7; b++) { const x = -200 + hash(b + 3) * 400, y = -480 + hash(b + 8) * 200, s = 14 + hash(b) * 12; ctx.beginPath(); ctx.moveTo(x - s, y - s * .4); ctx.quadraticCurveTo(x - s * .4, y - s * .7, x, y); ctx.quadraticCurveTo(x + s * .4, y - s * .7, x + s, y - s * .4); ctx.stroke(); }\n    },\n    object(t) { // vintage camera lens\n      ring(0, 0, 206, R, '#2B2A28');\n      for (const [r, c] of [[204, '#3A3936'], [186, '#D9D6CF'], [168, '#2B2A28'], [150, '#8E8B84'], [136, '#1C1B1A']]) { ctx.strokeStyle = c; ctx.lineWidth = 14; ctx.beginPath(); ctx.arc(0, 0, r - 7, 0, 6.28); ctx.stroke(); }\n      ctx.strokeStyle = '#D9D6CF'; ctx.lineWidth = 3;\n      for (let k = 0; k < 90; k++) { const a = k / 90 * 6.2832; ctx.beginPath(); ctx.moveTo(Math.cos(a) * 192, Math.sin(a) * 192); ctx.lineTo(Math.cos(a) * 204, Math.sin(a) * 204); ctx.stroke(); }\n      ctx.fillStyle = '#D9D6CF'; for (let k = 0; k < 12; k++) { const a = -1.2 + k * .2; ctx.fillRect(Math.cos(a) * 157 - 2, Math.sin(a) * 157 - 2, 4, 4); }\n    },\n    glass(t) {\n      ctx.save(); ctx.globalCompositeOperation = 'screen';\n      for (const [r, a, col] of [[108, .35, 'rgba(150,120,255,'], [80, .28, 'rgba(90,200,255,'], [40, .22, 'rgba(255,220,140,']]) { ctx.strokeStyle = col + a + ')'; ctx.lineWidth = 10; ctx.beginPath(); ctx.arc(-14, -14, r, 3.6, 5); ctx.stroke(); }\n      ctx.restore(); sheen();\n    },\n  },\n  { // desert dunes\n    land() {\n      ctx.fillStyle = vgrad(-CY, 120, '#F4E2C4', '#E8B87A'); ctx.fillRect(-EX, -EX, 2 * EX, 2 * EX);\n      ctx.fillStyle = '#C8552F'; ctx.beginPath(); ctx.arc(0, -250, 230, 0, 6.28); ctx.fill();\n      ctx.strokeStyle = 'rgba(244,226,196,.9)'; ctx.lineWidth = 10; for (let s = 0; s < 6; s++) { ctx.beginPath(); ctx.moveTo(-260, -230 + s * 34); ctx.lineTo(260, -230 + s * 34); ctx.stroke(); }\n      ridge(160, 120, .004, .7, '#E2A35C');\n      ridge(300, 140, .006, 2.9, '#C9763C');\n      ridge(470, 120, .005, 5.1, '#9A3B22');\n      for (const [x, h] of [[-390, 170], [420, 130]]) { ctx.fillStyle = '#4C2A1A'; ctx.fillRect(x - 12, 300 - h, 24, h); ctx.fillRect(x - 46, 300 - h * .7, 22, 12); ctx.fillRect(x - 46, 300 - h * .95, 12, h * .3); ctx.fillRect(x + 24, 300 - h * .55, 22, 12); ctx.fillRect(x + 34, 300 - h * .8, 12, h * .3); }\n    },\n    object(t) { // nautilus shell around the opening\n      const pts = [];\n      for (let k = 0; k <= 120; k++) { const a = k / 120 * Math.PI * 3.2, r = (R + 12) * Math.exp(a * .11); pts.push([Math.cos(a - 1.2) * r, Math.sin(a - 1.2) * r]); }\n      ctx.beginPath(); pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y)); ctx.arc(0, 0, R, 0, 6.28, true); ctx.closePath();\n      ctx.fillStyle = '#E3DED3'; ctx.fill('evenodd');\n      ctx.strokeStyle = '#3A3530'; ctx.lineWidth = 4;\n      for (let k = 6; k < 120; k += 7) { const a = k / 120 * Math.PI * 3.2, r = (R + 12) * Math.exp(a * .11), r0 = r * .78; ctx.beginPath(); ctx.moveTo(Math.cos(a - 1.2) * Math.max(R, r0), Math.sin(a - 1.2) * Math.max(R, r0)); ctx.quadraticCurveTo(Math.cos(a - 1.05) * r * .92, Math.sin(a - 1.05) * r * .92, Math.cos(a - 1.2) * r, Math.sin(a - 1.2) * r); ctx.stroke(); }\n      ctx.lineWidth = 7; ctx.beginPath(); pts.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y)); ctx.stroke();\n      ctx.beginPath(); ctx.arc(0, 0, R + 3, 0, 6.28); ctx.stroke();\n    },\n    glass(t) { sheen(.12); },\n  },\n];\n\nfunction ring(x, y, r1, r0, fill) {\n  ctx.save();\n  ctx.shadowColor = 'rgba(0,0,0,.45)'; ctx.shadowBlur = 24; ctx.shadowOffsetY = 10;\n  ctx.fillStyle = '#FBF8F1'; ctx.beginPath(); ctx.arc(x, y, r1 + 12, 0, 6.28); ctx.arc(x, y, r0, 0, 6.28, true); ctx.fill('evenodd');\n  ctx.restore();\n  ctx.fillStyle = fill; ctx.beginPath(); ctx.arc(x, y, r1, 0, 6.28); ctx.arc(x, y, r0, 0, 6.28, true); ctx.fill('evenodd');\n}\nfunction sheen(a = .22) {\n  const g = ctx.createLinearGradient(-R, -R, R, R); g.addColorStop(0, `rgba(255,255,255,${a})`); g.addColorStop(.45, 'rgba(255,255,255,0)'); g.addColorStop(1, `rgba(255,255,255,${a * .4})`);\n  ctx.fillStyle = g; ctx.beginPath(); ctx.arc(0, 0, R, 0, 6.28); ctx.fill();\n}\n\nfunction drawWorld(i, t, depth, fade) {\n  const w = worlds[i % 3];\n  w.land(); w.object(t);\n  if (depth < 3) {\n    ctx.save(); ctx.beginPath(); ctx.arc(0, 0, R, 0, 6.28); ctx.clip();\n    ctx.scale(K, K); drawWorld(i + 1, t, depth + 1, 1);\n    ctx.restore();\n  }\n  // the glass of the world we are flying through dissolves before the hand-over, so nothing pops\n  ctx.save(); ctx.globalAlpha = fade; w.glass(t); ctx.restore();\n}\n\njsoncut.onFrame(() => {\n  const t = jsoncut.time, loop = t % (PERIOD * 3);\n  const i = Math.floor(loop / PERIOD), u = (loop % PERIOD) / PERIOD;\n  const S = Math.pow(Z, u), g = Math.min(1, Math.max(0, (u - .55) / .35));\n  ctx.fillStyle = '#EDE3CC'; ctx.fillRect(0, 0, W, H);\n  ctx.save(); ctx.translate(CX, CY); ctx.scale(S, S); drawWorld(i, loop, 0, 1 - g * g * (3 - 2 * g)); ctx.restore();\n  // print texture, grain, flicker, dust and vignette in screen space\n  ctx.save(); ctx.globalCompositeOperation = 'multiply'; ctx.globalAlpha = .12; ctx.fillStyle = dots; ctx.fillRect(0, 0, W, H); ctx.restore();\n  ctx.save(); ctx.globalAlpha = .22; ctx.translate((Math.floor(t * 12) * 37) % 384, (Math.floor(t * 12) * 91) % 384); ctx.fillStyle = grain; ctx.fillRect(-384, -384, W + 768, H + 768); ctx.restore();\n  const f = Math.floor(t * 12);\n  ctx.fillStyle = `rgba(40,25,10,${.03 + .05 * hash(f)})`; ctx.fillRect(0, 0, W, H);\n  ctx.strokeStyle = 'rgba(40,30,20,.35)'; ctx.lineWidth = 1.5;\n  for (let s = 0; s < 3; s++) if (hash(f * 3 + s) > .6) { const x = hash(f + s * 7) * W; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x + (hash(f + s) - .5) * 30, H); ctx.stroke(); }\n  const v = ctx.createRadialGradient(CX, CY, H * .3, CX, CY, HALF);\n  v.addColorStop(0, 'rgba(0,0,0,0)'); v.addColorStop(1, 'rgba(30,18,8,.55)'); ctx.fillStyle = v; ctx.fillRect(0, 0, W, H);\n});\n"
    }
  ],
  "aspectVariants": [],
  "compositionKind": "video",
  "canvasBackground": "#000000",
  "editabilityTarget": "hybrid"
}
