// App — root, global effects (grain, ink canvas, cursor), boot sequence, scroll spy function InkCanvas() { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); const isMobile = window.matchMedia('(max-width: 768px)').matches; const count = isMobile ? 10 : 25; let W = canvas.width = window.innerWidth; let H = canvas.height = window.innerHeight; const particles = Array.from({ length: count }, () => ({ x: Math.random() * W, y: Math.random() * H, r: 1 + Math.random() * 2, vy: 0.15 + Math.random() * 0.3, vx: (Math.random() - 0.5) * 0.15, red: Math.random() > 0.4, alpha: 0.15 + Math.random() * 0.25, })); let raf; const tick = () => { ctx.clearRect(0, 0, W, H); for (const p of particles) { p.y += p.vy; p.x += p.vx + Math.sin(p.y * 0.008) * 0.2; if (p.y > H + 10) { p.y = -10; p.x = Math.random() * W; } if (p.x < -10) p.x = W + 10; if (p.x > W + 10) p.x = -10; ctx.beginPath(); ctx.fillStyle = p.red ? `rgba(139,0,0,${p.alpha})` : `rgba(245,240,235,${p.alpha * 0.5})`; ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2); ctx.fill(); } raf = requestAnimationFrame(tick); }; tick(); const onResize = () => { W = canvas.width = window.innerWidth; H = canvas.height = window.innerHeight; }; window.addEventListener('resize', onResize); return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', onResize); }; }, []); return ; } function CustomCursor() { const ref = useRef(null); useEffect(() => { if (!window.matchMedia('(pointer: fine)').matches) return; const el = ref.current; const onMove = (e) => { if (!el) return; el.style.left = (e.clientX - 12) + 'px'; el.style.top = (e.clientY - 4) + 'px'; }; const onOver = (e) => { const t = e.target; if (!t) return; const isInt = t.closest('a,button,input,select,textarea,[role="button"]'); if (el) el.classList.toggle('hover', !!isInt); }; window.addEventListener('mousemove', onMove); window.addEventListener('mouseover', onOver); return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseover', onOver); }; }, []); return ( ); } function App() { const [booted, setBooted] = useState(false); const [active, setActive] = useState('inicio'); useEffect(() => { const ids = ['inicio','portafolio','estilos','tienda','sobre-mi','contacto']; const onScroll = () => { const y = window.scrollY + window.innerHeight * 0.35; let current = 'inicio'; for (const id of ids) { const el = document.getElementById(id); if (el && el.offsetTop <= y) current = id; } setActive(current); }; window.addEventListener('scroll', onScroll); onScroll(); return () => window.removeEventListener('scroll', onScroll); }, [booted]); return ( <>