← back to Model Wars

public/scene.js

85 lines

// scene.js — ambient cinematic layers: embers, cursor torchlight, parallax castle.
(function () {
  'use strict';

  // ── Parallax castle silhouette (inline SVG data-uri, no asset needed) ──
  const castle = encodeURIComponent(`
  <svg xmlns='http://www.w3.org/2000/svg' width='1600' height='520' viewBox='0 0 1600 520'>
    <defs><linearGradient id='g' x1='0' y1='0' x2='0' y2='1'>
      <stop offset='0' stop-color='#0a0a12'/><stop offset='1' stop-color='#05050a'/></linearGradient></defs>
    <g fill='url(#g)'>
      <rect x='0' y='360' width='1600' height='160'/>
      <path d='M120 360 V220 h40 v-30 h30 v30 h40 v-30 h30 v30 h40 V360 Z'/>
      <path d='M1300 360 V200 h50 v-34 h34 v34 h50 v-34 h34 v34 h50 V360 Z'/>
      <path d='M640 360 V150 l60 -60 l60 60 V360 Z'/>
      <path d='M760 360 V180 h80 v-40 h30 v40 h80 V360 Z'/>
      <path d='M300 360 V260 h60 v-20 h20 v20 h60 V360 Z'/>
      <path d='M980 360 V240 h70 v-26 h22 v26 h70 V360 Z'/>
      <path d='M540 360 V300 h40 v-16 h16 v16 h40 V360 Z'/>
    </g>
  </svg>`);
  const px = document.getElementById('parallax');
  if (px) px.style.backgroundImage = `url("data:image/svg+xml,${castle}")`;
  window.addEventListener('scroll', () => {
    if (px) px.style.transform = `translateY(${window.scrollY * 0.15}px)`;
  }, { passive: true });

  // ── Embers ─────────────────────────────────────────────
  const ec = document.getElementById('embers');
  const ex = ec.getContext('2d');
  let W, H, embers = [];
  function resize() {
    W = ec.width = window.innerWidth; H = ec.height = window.innerHeight;
    const t = document.getElementById('torch');
    t.width = W; t.height = H;
  }
  window.addEventListener('resize', resize); resize();

  function spawn() {
    return {
      x: Math.random() * W, y: H + Math.random() * 40,
      r: Math.random() * 2.2 + 0.6, vy: Math.random() * 0.9 + 0.35,
      vx: (Math.random() - 0.5) * 0.4, life: Math.random() * 0.7 + 0.3,
      hue: 18 + Math.random() * 22,
    };
  }
  for (let i = 0; i < 90; i++) { const e = spawn(); e.y = Math.random() * H; embers.push(e); }

  function drawEmbers() {
    ex.clearRect(0, 0, W, H);
    for (const e of embers) {
      e.y -= e.vy; e.x += e.vx + Math.sin(e.y * 0.01) * 0.25; e.life -= 0.0016;
      if (e.y < -10 || e.life <= 0) Object.assign(e, spawn());
      const a = Math.max(0, Math.min(1, e.life)) * 0.8;
      ex.beginPath();
      const grd = ex.createRadialGradient(e.x, e.y, 0, e.x, e.y, e.r * 4);
      grd.addColorStop(0, `hsla(${e.hue},100%,62%,${a})`);
      grd.addColorStop(1, 'hsla(20,100%,50%,0)');
      ex.fillStyle = grd;
      ex.arc(e.x, e.y, e.r * 4, 0, Math.PI * 2); ex.fill();
    }
    requestAnimationFrame(drawEmbers);
  }
  drawEmbers();

  // ── Interactive torchlight following cursor ────────────
  const tc = document.getElementById('torch');
  const tx = tc.getContext('2d');
  let mx = -999, my = -999, cx = -999, cy = -999;
  window.addEventListener('pointermove', (e) => { mx = e.clientX; my = e.clientY; }, { passive: true });
  function drawTorch() {
    cx += (mx - cx) * 0.12; cy += (my - cy) * 0.12;
    tx.clearRect(0, 0, W, H);
    if (cx > -500) {
      const flick = 1 + Math.sin(performance.now() * 0.02) * 0.05;
      const g = tx.createRadialGradient(cx, cy, 0, cx, cy, 240 * flick);
      g.addColorStop(0, 'rgba(255,180,90,0.16)');
      g.addColorStop(0.5, 'rgba(255,120,40,0.06)');
      g.addColorStop(1, 'rgba(0,0,0,0)');
      tx.fillStyle = g; tx.fillRect(0, 0, W, H);
    }
    requestAnimationFrame(drawTorch);
  }
  drawTorch();
})();