// Three hero variants for loboLab. // 1. NeuralMesh — animated network of agents (default — user explicitly asked for this) // 2. WolfHowling — wolf silhouette howling at a glowing moon, particles + faint constellation // 3. DataStreams — vertical streams of orange data points + rotating ring of agent labels // // All three share a layered structure: // - for the background motion // - overlays for fixed glyphs // - children (the hero content) sit on top // // They listen to `intensity` ('low' | 'med' | 'high') and scale particle counts accordingly. const HERO_PALETTE = { ink: "#0A0B10", inkSoft: "#0F1118", orange: "#F08A2B", orangeBright: "#FF9D3F", orangeGlow: "rgba(240, 138, 43, 0.55)", slate: "#6B7A91", white: "#F5F6F8", }; // ───────────────────────────────────────────────────────────────────────────── // Hook: requestAnimationFrame loop with cleanup. Pauses when tab hidden. function useRaf(callback, deps = []) { React.useEffect(() => { let raf, last = performance.now(); const tick = (now) => { const dt = Math.min(0.05, (now - last) / 1000); last = now; callback(dt, now / 1000); raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); // eslint-disable-next-line react-hooks/exhaustive-deps }, deps); } // Resize a canvas to its CSS size × DPR. Returns [w,h] in CSS px. function fitCanvas(cv) { const dpr = Math.min(window.devicePixelRatio || 1, 2); const r = cv.getBoundingClientRect(); cv.width = Math.round(r.width * dpr); cv.height = Math.round(r.height * dpr); const ctx = cv.getContext("2d"); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); return [r.width, r.height, ctx]; } // ───────────────────────────────────────────────────────────────────────────── // 1. NEURAL MESH — drifting nodes with proximity edges; orange "active" pulses // travel along edges. The wolf eye-shape sits subtly behind everything. function NeuralMesh({ intensity = "med" }) { const cvRef = React.useRef(null); const stateRef = React.useRef({ nodes: [], pulses: [], w: 0, h: 0 }); const COUNT = intensity === "high" ? 110 : intensity === "low" ? 45 : 75; const LINK_DIST = 150; React.useEffect(() => { const cv = cvRef.current; if (!cv) return; const init = () => { const [w, h] = fitCanvas(cv); const nodes = Array.from({ length: COUNT }, () => ({ x: Math.random() * w, y: Math.random() * h, vx: (Math.random() - 0.5) * 14, vy: (Math.random() - 0.5) * 14, r: 1 + Math.random() * 1.8, active: Math.random() < 0.12, })); stateRef.current = { nodes, pulses: [], w, h }; }; init(); const onR = () => init(); window.addEventListener("resize", onR); return () => window.removeEventListener("resize", onR); }, [COUNT]); useRaf((dt, t) => { const cv = cvRef.current; if (!cv) return; const ctx = cv.getContext("2d"); const { nodes, pulses, w, h } = stateRef.current; if (!w) return; ctx.clearRect(0, 0, w, h); // soft radial vignette anchored slightly above center const grad = ctx.createRadialGradient(w * 0.5, h * 0.4, 0, w * 0.5, h * 0.4, Math.max(w, h) * 0.7); grad.addColorStop(0, "rgba(240,138,43,0.06)"); grad.addColorStop(1, "rgba(10,11,16,0)"); ctx.fillStyle = grad; ctx.fillRect(0, 0, w, h); // move nodes for (const n of nodes) { n.x += n.vx * dt; n.y += n.vy * dt; if (n.x < 0 || n.x > w) n.vx *= -1; if (n.y < 0 || n.y > h) n.vy *= -1; } // edges ctx.lineWidth = 1; for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { const a = nodes[i], b = nodes[j]; const dx = a.x - b.x, dy = a.y - b.y; const d = Math.hypot(dx, dy); if (d < LINK_DIST) { const alpha = (1 - d / LINK_DIST) * 0.22; // tint edges connecting two active nodes orange const isHot = a.active && b.active; ctx.strokeStyle = isHot ? `rgba(240,138,43,${alpha * 1.8})` : `rgba(120,140,170,${alpha})`; ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke(); } } } // nodes for (const n of nodes) { if (n.active) { ctx.fillStyle = HERO_PALETTE.orange; ctx.shadowBlur = 12; ctx.shadowColor = HERO_PALETTE.orangeGlow; } else { ctx.fillStyle = "rgba(180,195,220,0.55)"; ctx.shadowBlur = 0; } ctx.beginPath(); ctx.arc(n.x, n.y, n.r + (n.active ? 1.5 : 0), 0, Math.PI * 2); ctx.fill(); } ctx.shadowBlur = 0; // pulses traveling between random pairs of active nodes if (Math.random() < (intensity === "high" ? 0.18 : intensity === "low" ? 0.04 : 0.09)) { const actives = nodes.filter(n => n.active); if (actives.length > 1) { const a = actives[Math.floor(Math.random() * actives.length)]; const b = actives[Math.floor(Math.random() * actives.length)]; if (a !== b) pulses.push({ a, b, p: 0, life: 1.2 }); } } for (let i = pulses.length - 1; i >= 0; i--) { const pl = pulses[i]; pl.p += dt / pl.life; if (pl.p >= 1) { pulses.splice(i, 1); continue; } const x = pl.a.x + (pl.b.x - pl.a.x) * pl.p; const y = pl.a.y + (pl.b.y - pl.a.y) * pl.p; ctx.fillStyle = HERO_PALETTE.orangeBright; ctx.shadowBlur = 18; ctx.shadowColor = HERO_PALETTE.orangeGlow; ctx.beginPath(); ctx.arc(x, y, 2.5, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; } }, [intensity]); return (
); } // ───────────────────────────────────────────────────────────────────────────── // 2. WOLF HOWLING — user's idea. Lone wolf silhouette on a ridge, head tilted up, // against a glowing orange moon. Embers/particles drift up. Subtle constellation // behind the moon connects faintly to suggest "agents". function WolfHowling({ intensity = "med" }) { const cvRef = React.useRef(null); const stateRef = React.useRef({ embers: [], stars: [], w: 0, h: 0 }); const EMBER_COUNT = intensity === "high" ? 90 : intensity === "low" ? 30 : 55; const STAR_COUNT = intensity === "high" ? 80 : intensity === "low" ? 30 : 55; React.useEffect(() => { const cv = cvRef.current; if (!cv) return; const init = () => { const [w, h] = fitCanvas(cv); const embers = Array.from({ length: EMBER_COUNT }, () => ({ x: Math.random() * w, y: h * 0.6 + Math.random() * h * 0.4, vy: -8 - Math.random() * 22, vx: (Math.random() - 0.5) * 6, r: 0.6 + Math.random() * 1.6, a: 0.3 + Math.random() * 0.7, life: 1, })); const stars = Array.from({ length: STAR_COUNT }, () => ({ x: Math.random() * w, y: Math.random() * h * 0.6, r: 0.4 + Math.random() * 1.4, tw: Math.random() * Math.PI * 2, })); stateRef.current = { embers, stars, w, h }; }; init(); const onR = () => init(); window.addEventListener("resize", onR); return () => window.removeEventListener("resize", onR); }, [EMBER_COUNT, STAR_COUNT]); useRaf((dt, t) => { const cv = cvRef.current; if (!cv) return; const ctx = cv.getContext("2d"); const { embers, stars, w, h } = stateRef.current; if (!w) return; // Transparent canvas — the background image is rendered by the parent
. // We only draw atmospheric overlays (stars, constellation lines, embers and a // soft orange glow around the moon area) on top of the image. ctx.clearRect(0, 0, w, h); // Soft orange aura over the moon area of the image (upper-center) const mx = w * 0.5, my = h * 0.42, mr = Math.min(w, h) * 0.45; const moonGlow = ctx.createRadialGradient(mx, my, 0, mx, my, mr); moonGlow.addColorStop(0, "rgba(255,170,80,0.18)"); moonGlow.addColorStop(0.5, "rgba(240,138,43,0.06)"); moonGlow.addColorStop(1, "rgba(240,138,43,0)"); ctx.fillStyle = moonGlow; ctx.fillRect(0, 0, w, h); // Stars (mostly upper half so they don't sit on the ridge) for (const s of stars) { s.tw += dt * 2; const tw = 0.5 + 0.5 * Math.sin(s.tw + s.x); ctx.fillStyle = `rgba(255,210,140,${0.25 + tw * 0.45})`; ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2); ctx.fill(); } // Faint constellation lines connecting nearby stars (the "agents" thread) ctx.strokeStyle = "rgba(240,138,43,0.14)"; ctx.lineWidth = 0.6; for (let i = 0; i < stars.length; i++) { for (let j = i + 1; j < Math.min(i + 4, stars.length); j++) { const a = stars[i], b = stars[j]; const d = Math.hypot(a.x - b.x, a.y - b.y); if (d < 130) { ctx.beginPath(); ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke(); } } } // Embers for (const e of embers) { e.x += e.vx * dt; e.y += e.vy * dt; e.life -= dt * 0.18; if (e.life <= 0 || e.y < h * 0.1) { e.x = Math.random() * w; e.y = h * 0.85 + Math.random() * h * 0.1; e.life = 1; e.vy = -8 - Math.random() * 22; } const a = e.a * Math.max(0, e.life); ctx.fillStyle = `rgba(255,160,70,${a})`; ctx.shadowBlur = 8; ctx.shadowColor = "rgba(255,150,60,0.6)"; ctx.beginPath(); ctx.arc(e.x, e.y, e.r, 0, Math.PI * 2); ctx.fill(); } ctx.shadowBlur = 0; }, [intensity]); return (