// Stacktree Labs — single-file React landing page. // Hosted via index.html which loads React + Babel + Tailwind from CDNs, // so this file can ship as raw JSX with no build step. Deploy by // pushing to GitHub Pages (see .github/workflows/pages.yml). const { useEffect, useRef, useState, useCallback } = React; // ─── Particle background (canvas, no library) ────────────────────────── // Lightweight starfield with cursor parallax. Cheaper than Three.js, // scales with viewport, pauses when tab is hidden. function ParticleField() { const ref = useRef(null); useEffect(() => { const canvas = ref.current; if (!canvas) return; const ctx = canvas.getContext('2d'); let raf = 0; let particles = []; let mouse = { x: 0, y: 0 }; const dpr = Math.min(window.devicePixelRatio || 1, 2); function resize() { canvas.width = window.innerWidth * dpr; canvas.height = window.innerHeight * dpr; canvas.style.width = window.innerWidth + 'px'; canvas.style.height = window.innerHeight + 'px'; ctx.scale(dpr, dpr); } function spawn() { const count = Math.min(180, Math.floor(window.innerWidth / 8)); particles = Array.from({ length: count }, () => ({ x: Math.random() * window.innerWidth, y: Math.random() * window.innerHeight, z: Math.random() * 1.0 + 0.2, // depth (0.2 .. 1.2) vx: (Math.random() - 0.5) * 0.12, vy: (Math.random() - 0.5) * 0.12, r: Math.random() * 1.2 + 0.4, c: Math.random() > 0.85 ? '#39FF14' : '#00BFFF', a: Math.random() * 0.5 + 0.25, })); } function tick() { ctx.clearRect(0, 0, window.innerWidth, window.innerHeight); for (const p of particles) { // Parallax: deeper particles move less with cursor. const px = p.x + (mouse.x - window.innerWidth / 2) * 0.012 * p.z; const py = p.y + (mouse.y - window.innerHeight / 2) * 0.012 * p.z; ctx.beginPath(); ctx.arc(px, py, p.r * p.z, 0, Math.PI * 2); ctx.fillStyle = p.c; ctx.globalAlpha = p.a * p.z; ctx.fill(); p.x += p.vx; p.y += p.vy; if (p.x < -10) p.x = window.innerWidth + 10; if (p.x > window.innerWidth + 10) p.x = -10; if (p.y < -10) p.y = window.innerHeight + 10; if (p.y > window.innerHeight + 10) p.y = -10; } ctx.globalAlpha = 1; // Connect close-by particles with hairline lines for a constellation feel. for (let i = 0; i < particles.length; i++) { for (let j = i + 1; j < particles.length; j++) { const a = particles[i], b = particles[j]; const dx = a.x - b.x, dy = a.y - b.y; const d2 = dx * dx + dy * dy; if (d2 < 9000) { const alpha = (1 - d2 / 9000) * 0.18; ctx.strokeStyle = `rgba(0,191,255,${alpha})`; ctx.lineWidth = 0.6; ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke(); } } } raf = requestAnimationFrame(tick); } function onMove(e) { mouse.x = e.clientX; mouse.y = e.clientY; } function onVisibility() { if (document.hidden) cancelAnimationFrame(raf); else raf = requestAnimationFrame(tick); } resize(); spawn(); raf = requestAnimationFrame(tick); window.addEventListener('resize', () => { resize(); spawn(); }); window.addEventListener('mousemove', onMove); document.addEventListener('visibilitychange', onVisibility); return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', resize); window.removeEventListener('mousemove', onMove); document.removeEventListener('visibilitychange', onVisibility); }; }, []); return (