#nm-canvas{display:block;height:88%;width:auto;max-width:94%;border-radius:6px;image-rendering:pixelated}
.nm-cap{position:absolute;left:0;right:0;bottom:4%;text-align:center;font-size:clamp(9px,2.4vmin,12px);color:var(--muted)}
const canvas = document.getElementById('nm-canvas');
const ctx = canvas.getContext('2d');
const W = 128, H = 86;
canvas.width = W; canvas.height = H;
const bumps = [];
for (let i = 0; i < 9; i++) bumps.push({ x: Math.random() * W, y: Math.random() * H, r: 8 + Math.random() * 10 });
function heightAt(x, y) {
let h = 0;
for (const b of bumps) {
const d = Math.hypot(x - b.x, y - b.y) / b.r;
if (d < 1) h += Math.pow(Math.cos(d * Math.PI * 0.5), 2);
}
return h;
}
const nx = new Float32Array(W * H), ny = new Float32Array(W * H), nz = new Float32Array(W * H);
for (let y = 0; y < H; y++) {
for (let x = 0; x < W; x++) {
const hl = heightAt(x - 1, y), hr = heightAt(x + 1, y), hu = heightAt(x, y - 1), hd = heightAt(x, y + 1);
const vx = -(hr - hl) * 6, vy = -(hd - hu) * 6, vz = 1;
const len = Math.hypot(vx, vy, vz);
const i = y * W + x;
nx[i] = vx / len; ny[i] = vy / len; nz[i] = vz / len;
}
}
const img = ctx.createImageData(W, H);
function render(t) {
const ang = t * 0.0007;
const lx = Math.cos(ang), ly = Math.sin(ang) * 0.6, lz = 0.7;
const llen = Math.hypot(lx, ly, lz);
const Lx = lx / llen, Ly = ly / llen, Lz = lz / llen;
for (let i = 0; i < W * H; i++) {
const d = Math.max(0, nx[i] * Lx + ny[i] * Ly + nz[i] * Lz);
const base = 40 + d * 255 * 0.75;
const p = i * 4;
img.data[p] = base * 0.65 + 20;
img.data[p + 1] = base * 0.55 + 30;
img.data[p + 2] = base * 0.95 + 50;
img.data[p + 3] = 255;
}
ctx.putImageData(img, 0, 0);
requestAnimationFrame(render);
}
requestAnimationFrame(render);