/* ============================================================ AR — MouseField.jsx Campo de partículas ciano/azul que reage ao cursor (inspiração Antigravity). Leve, no fundo, sem competir com texto/botões. A REAÇÃO AO MOUSE funciona em qualquer desktop com ponteiro fino (mover o mouse é ação do usuário). A deriva automática/cintilação respeita "reduzir movimento". Em touch/telas pequenas, desenha um campo estático. Renderiza um canvas absoluto — colocar em pai position:relative. ============================================================ */ const MouseField = ({ density = 1 }) => { const canvasRef = React.useRef(null); React.useEffect(() => { const canvas = canvasRef.current; if (!canvas) return; if (window.innerWidth < 860) return; // pula em telas pequenas (perf + foco) const reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches; const fine = window.matchMedia('(pointer: fine)').matches; const ambient = !reduce; // deriva/cintilação automática const animate = fine || ambient; // roda o loop se há mouse-fino OU movimento ambiente const ctx = canvas.getContext('2d'); let w = 0, h = 0, dpr = Math.min(window.devicePixelRatio || 1, 2); let particles = []; let raf = 0; const mouse = { x: -9999, y: -9999, tx: -9999, ty: -9999, active: false }; const palette = [ 'rgba(0,212,255,', // ciano 'rgba(61,139,255,', // azul 'rgba(0,94,255,', // azul profundo 'rgba(125,176,255,',// azul claro ]; const resize = () => { const parent = canvas.parentElement; w = parent.clientWidth; h = parent.clientHeight; canvas.width = w * dpr; canvas.height = h * dpr; canvas.style.width = w + 'px'; canvas.style.height = h + 'px'; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); build(); if (!animate) drawStatic(); }; const build = () => { const count = Math.round((w * h) / 14000 * density); particles = []; for (let i = 0; i < count; i++) { const ang = Math.random() * Math.PI * 2; particles.push({ x: Math.random() * w, y: Math.random() * h, len: 4 + Math.random() * 7, rot: ang, vx: (Math.random() - 0.5) * 0.13, vy: (Math.random() - 0.5) * 0.13, baseA: 0.09 + Math.random() * 0.15, a: 0, color: palette[(Math.random() * palette.length) | 0], tw: Math.random() * Math.PI * 2, }); } }; const drawDash = (p, a, rot, extra) => { const hx = Math.cos(rot) * p.len * 0.5, hy = Math.sin(rot) * p.len * 0.5; ctx.strokeStyle = p.color + a.toFixed(3) + ')'; ctx.lineWidth = 1.2 + extra * 1.1; ctx.beginPath(); ctx.moveTo(p.x - hx, p.y - hy); ctx.lineTo(p.x + hx, p.y + hy); ctx.stroke(); }; const drawStatic = () => { ctx.clearRect(0, 0, w, h); ctx.lineCap = 'round'; for (const p of particles) drawDash(p, p.baseA, p.rot, 0); }; const onMove = (e) => { const r = canvas.getBoundingClientRect(); const x = e.clientX - r.left, y = e.clientY - r.top; // só ativa quando o cursor está sobre a área do hero if (x < -40 || y < -40 || x > w + 40 || y > h + 40) { mouse.active = false; return; } mouse.tx = x; mouse.ty = y; mouse.active = true; if (mouse.x < -1000) { mouse.x = x; mouse.y = y; } }; const onLeave = () => { mouse.active = false; }; const R = 165; // raio de influência (maior → efeito mais perceptível) const tick = () => { ctx.clearRect(0, 0, w, h); ctx.lineCap = 'round'; // suaviza o seguimento do cursor mouse.x += (mouse.tx - mouse.x) * 0.18; mouse.y += (mouse.ty - mouse.y) * 0.18; // aura suave seguindo o cursor (deixa o efeito inconfundível) if (mouse.active) { const g = ctx.createRadialGradient(mouse.x, mouse.y, 0, mouse.x, mouse.y, R); g.addColorStop(0, 'rgba(0,212,255,0.10)'); g.addColorStop(0.6, 'rgba(0,94,255,0.045)'); g.addColorStop(1, 'rgba(0,94,255,0)'); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(mouse.x, mouse.y, R, 0, Math.PI * 2); ctx.fill(); } for (const p of particles) { if (ambient) { p.x += p.vx; p.y += p.vy; if (p.x < -10) p.x = w + 10; if (p.x > w + 10) p.x = -10; if (p.y < -10) p.y = h + 10; if (p.y > h + 10) p.y = -10; p.tw += 0.018; } let a = p.baseA + (ambient ? Math.sin(p.tw) * 0.04 : 0); let extra = 0; let rot = p.rot; if (mouse.active) { const dx = p.x - mouse.x, dy = p.y - mouse.y; const dist = Math.hypot(dx, dy); if (dist < R) { const f = 1 - dist / R; extra = f * 0.6; // brilho perto do cursor const push = f * f * 1.4; // repulsão gentil p.x += (dx / (dist || 1)) * push; p.y += (dy / (dist || 1)) * push; rot = Math.atan2(dy, dx) * 0.4 + p.rot * 0.6; } } a = Math.min(0.85, a + extra); p.a = a; drawDash(p, a, rot, extra); if (extra > 0.16) { ctx.fillStyle = p.color + (extra * 0.5).toFixed(3) + ')'; ctx.beginPath(); ctx.arc(p.x, p.y, 1.3 + extra * 2.4, 0, Math.PI * 2); ctx.fill(); } } // fios conectivos perto do cursor if (mouse.active) { for (let i = 0; i < particles.length; i++) { const p = particles[i]; const dm = Math.hypot(p.x - mouse.x, p.y - mouse.y); if (dm > R) continue; for (let j = i + 1; j < particles.length; j++) { const q = particles[j]; const d = Math.hypot(p.x - q.x, p.y - q.y); if (d < 70) { const dq = Math.hypot(q.x - mouse.x, q.y - mouse.y); if (dq > R) continue; const o = (1 - d / 70) * (1 - dm / R) * 0.3; ctx.strokeStyle = 'rgba(0,212,255,' + o.toFixed(3) + ')'; ctx.lineWidth = 0.8; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(q.x, q.y); ctx.stroke(); } } } } raf = requestAnimationFrame(tick); }; resize(); window.addEventListener('resize', resize); if (animate) { raf = requestAnimationFrame(tick); if (fine) { window.addEventListener('mousemove', onMove, { passive: true }); document.addEventListener('mouseleave', onLeave); } } return () => { cancelAnimationFrame(raf); window.removeEventListener('resize', resize); window.removeEventListener('mousemove', onMove); document.removeEventListener('mouseleave', onLeave); }; }, [density]); // máscara: afina partículas sobre a coluna de texto (inferior-esquerda) const mask = 'radial-gradient(120% 130% at 72% 26%, #000 38%, rgba(0,0,0,0.5) 70%, transparent 100%)'; return (