// Fixed background layers — matches atopile.io home: // · faint orange grid (CSS background-image) // · soft radial glow (CSS radial-gradient) // · canvas "tracer" snakes walking the grid in brand orange // Canvas animation ported from apps/marketing-site/src/app/page.tsx. function Background() { const canvasRef = React.useRef(null); React.useEffect(() => { const C = canvasRef.current; if (!C) return; const X = C.getContext("2d"); if (!X) return; const G = 48; const NUM = 7; const DX = [1, 0, -1, 0]; const DY = [0, 1, 0, -1]; const snakes = []; let rafId = 0; let last = 0; const cols = () => Math.ceil(C.width / G) + 2; const rows = () => Math.ceil(C.height / G) + 2; const resize = () => { C.width = window.innerWidth; C.height = window.innerHeight; }; const pickDir = (gx, gy, exclude) => { const nc = cols(), nr = rows(); const out = []; for (let d = 0; d < 4; d += 1) { if (d === exclude) continue; const tx = gx + DX[d], ty = gy + DY[d]; if (tx >= 0 && tx < nc && ty >= 0 && ty < nr) out.push(d); } return out; }; const makeSnake = () => { const nc = cols(), nr = rows(); const gx = Math.floor(Math.random() * nc); const gy = Math.floor(Math.random() * nr); const dirs = pickDir(gx, gy, -1); const dir = dirs[Math.floor(Math.random() * dirs.length)]; return { gx, gy, dir, pts: [[gx * G, gy * G]], len: 12 + Math.floor(Math.random() * 8), speed: 0.45 + Math.random() * 0.5, alpha: 0.18 + Math.random() * 0.12, t: 0, runway: 4 + Math.floor(Math.random() * 8), }; }; const stepSnake = (s) => { s.gx += DX[s.dir]; s.gy += DY[s.dir]; const nc = cols(), nr = rows(); if (s.gx < 0 || s.gx >= nc || s.gy < 0 || s.gy >= nr) return false; s.pts.push([s.gx * G, s.gy * G]); if (s.pts.length > s.len + 1) s.pts.shift(); s.runway -= 1; const reverse = (s.dir + 2) % 4; const cw = (s.dir + 1) % 4; const ccw = (s.dir + 3) % 4; const fwdOk = s.gx + DX[s.dir] >= 0 && s.gx + DX[s.dir] < nc && s.gy + DY[s.dir] >= 0 && s.gy + DY[s.dir] < nr; const hitWall = !fwdOk; if (hitWall || (s.runway <= 0 && Math.random() < 0.5)) { let opts = []; [cw, ccw].forEach((d) => { const tx = s.gx + DX[d], ty = s.gy + DY[d]; if (tx >= 0 && tx < nc && ty >= 0 && ty < nr) opts.push(d); }); if (!hitWall && fwdOk) opts.push(s.dir); if (!opts.length) opts = pickDir(s.gx, s.gy, reverse); if (!opts.length) return false; s.dir = opts[Math.floor(Math.random() * opts.length)]; s.runway = 4 + Math.floor(Math.random() * 9); } return true; }; const updateSnake = (s, dt) => { s.t += s.speed * dt; while (s.t >= 1) { s.t -= 1; if (!stepSnake(s)) return makeSnake(); } return s; }; const drawSnake = (s) => { if (s.pts.length < 1) return; const lastCorner = s.pts[s.pts.length - 1]; const hx = lastCorner[0] + DX[s.dir] * s.t * G; const hy = lastCorner[1] + DY[s.dir] * s.t * G; const pts = s.pts.slice(); pts.push([hx, hy]); const n = pts.length; X.lineWidth = 1.5; X.lineCap = "square"; for (let i = 1; i < n; i += 1) { const a = Math.pow(i / (n - 1), 1.8) * s.alpha; X.strokeStyle = `rgba(249,80,21,${a.toFixed(3)})`; X.beginPath(); X.moveTo(pts[i - 1][0], pts[i - 1][1]); X.lineTo(pts[i][0], pts[i][1]); X.stroke(); } X.fillStyle = `rgba(249,80,21,${Math.min(s.alpha * 1.8, 0.8).toFixed(3)})`; X.beginPath(); X.arc(hx, hy, 2, 0, 6.2832); X.fill(); }; const frame = (ts) => { rafId = window.requestAnimationFrame(frame); const dt = Math.min((ts - last) / 1000, 0.05); last = ts; X.clearRect(0, 0, C.width, C.height); for (let i = 0; i < snakes.length; i += 1) { snakes[i] = updateSnake(snakes[i], dt); drawSnake(snakes[i]); } }; resize(); window.addEventListener("resize", resize); for (let i = 0; i < NUM; i += 1) { const s = makeSnake(); const warmup = 20 + Math.floor(Math.random() * 40); for (let j = 0; j < warmup; j += 1) stepSnake(s); snakes.push(s); } rafId = window.requestAnimationFrame(frame); return () => { window.cancelAnimationFrame(rafId); window.removeEventListener("resize", resize); }; }, []); // Portal the fixed layers directly into so they're siblings of // #root, not descendants. That keeps their "position: fixed" anchored to // the viewport regardless of any stacking context created on #root, and // guarantees they stay put while content scrolls. return ReactDOM.createPortal(