← 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();
})();