// surgx-landing-v3.jsx — SurgX Landing page, design v3. // Mounts #root; IIFE pattern (same as browse-v3, Babel-race safe). // Blank-over-wrong throughout: every fetch failure omits the section, never fakes. // Reuses .sxv3 / .sx token root from surgx-pdp-v3.css; layout classes prefixed lx-. (() => { const { useState, useEffect, useRef, useCallback, useMemo } = React; /* ---- shared search core (surgx-search-core.js, loaded before this script) ---- */ const { str, normalizeVendor, matchesQuery } = window.SurgxSearchCore; /* ---- hero mode param (dark gate) ---- */ const _heroParam = new URLSearchParams(window.location.search).get('hero'); /* ================================================================ HeroConstellation — 3D point-cloud canvas of the real taxonomy. Hub nodes = specialties (Fibonacci sphere), system nodes clustered around each hub. Perspective projection + depth-fade. Auto-rotates ~1 rev/40s with pointer parallax on desktop. Renders ONLY when taxonomy is truthy AND ?hero=3d. prefers-reduced-motion: reduce → single static frame, no rAF. ================================================================ */ function HeroConstellation({ taxonomy }) { const canvasRef = useRef(null); const stateRef = useRef(null); useEffect(() => { if (!taxonomy || !taxonomy.specialties || !taxonomy.specialties.length) return; const canvas = canvasRef.current; if (!canvas) return; const ctx = canvas.getContext('2d'); if (!ctx) return; /* ---- build 3D point cloud from taxonomy ---- */ const specs = taxonomy.specialties; const N = specs.length; const TAU = Math.PI * 2; const GOLDEN = (1 + Math.sqrt(5)) / 2; const HUB_R = 1.0; /* sphere radius for hub distribution */ /* Fibonacci sphere placement for hubs */ const hubs = []; const systems = []; for (let i = 0; i < N; i++) { const theta = Math.acos(1 - 2 * (i + 0.5) / N); const phi = TAU * i / GOLDEN; const hx = HUB_R * Math.sin(theta) * Math.cos(phi); const hy = HUB_R * Math.sin(theta) * Math.sin(phi); const hz = HUB_R * Math.cos(theta); const cnt = specs[i].count || 1; const r = Math.max(3, Math.min(8, Math.sqrt(cnt) * 1.4)); hubs.push({ x: hx, y: hy, z: hz, r: r, idx: i }); /* system nodes — jittered sphere around hub, capped at 14 */ const sysCount = Math.min(cnt, 14); for (let j = 0; j < sysCount; j++) { const st = Math.acos(1 - 2 * (j + 0.5) / Math.max(sysCount, 1)); const sp = TAU * j / GOLDEN; const spread = 0.12 + Math.random() * 0.06; const sx = hx + spread * Math.sin(st) * Math.cos(sp); const sy = hy + spread * Math.sin(st) * Math.sin(sp); const sz = hz + spread * Math.cos(st); systems.push({ x: sx, y: sy, z: sz, hub: i }); } } /* ---- rotation state ---- */ const rot = { ay: 0, targetAy: 0, ax: 0.25 }; /* fixed X tilt ~14deg */ const SPEED = TAU / 40; /* 1 rev per 40s */ let mouseInfluence = { dx: 0, dy: 0 }; let rafId = 0; let lastT = 0; let paused = false; /* ---- sizing (DPR-aware) ---- */ const dpr = Math.min(window.devicePixelRatio || 1, 2); function resize() { const rect = canvas.getBoundingClientRect(); canvas.width = rect.width * dpr; canvas.height = rect.height * dpr; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } resize(); /* ---- projection helpers ---- */ function project(px, py, pz, w, h) { const cosAy = Math.cos(rot.ay); const sinAy = Math.sin(rot.ay); const cosAx = Math.cos(rot.ax); const sinAx = Math.sin(rot.ax); /* rotate Y then X */ const x1 = px * cosAy - pz * sinAy; const z1 = px * sinAy + pz * cosAy; const y1 = py * cosAx - z1 * sinAx; const z2 = py * sinAx + z1 * cosAx; const f = 2.8; const scale = f / (f + z2); return { sx: w * 0.5 + x1 * scale * w * 0.38, sy: h * 0.5 + y1 * scale * h * 0.38, scale: scale, z: z2 }; } /* ---- palette ---- */ const HUB_COLOR = '#0B4A72'; const SYS_COLOR = '#6FA6C6'; const EDGE_BASE = 'rgba(11,74,114,'; /* ---- frame ---- */ function frame(t) { if (paused) { rafId = requestAnimationFrame(frame); return; } const dt = lastT ? (t - lastT) / 1000 : 0.016; lastT = t; /* auto-rotate + mouse parallax with easing */ rot.targetAy += SPEED * dt; rot.ay += (rot.targetAy + mouseInfluence.dx - rot.ay) * 0.06; const rect = canvas.getBoundingClientRect(); const w = rect.width; const h = rect.height; if (canvas.width !== w * dpr || canvas.height !== h * dpr) resize(); ctx.clearRect(0, 0, w, h); /* project all points once */ const pH = hubs.map(p => project(p.x, p.y, p.z, w, h)); const pS = systems.map(p => project(p.x, p.y, p.z, w, h)); /* edges (hub → system), low alpha with depth — darkened for visibility on the white landing background (Drew walk 2026-06-10: lines too faint) */ for (let i = 0; i < pS.length; i++) { const s = pS[i]; const hProj = pH[systems[i].hub]; const depthAlpha = Math.max(0.10, Math.min(0.24, 0.24 * s.scale)); ctx.beginPath(); ctx.moveTo(hProj.sx, hProj.sy); ctx.lineTo(s.sx, s.sy); ctx.strokeStyle = EDGE_BASE + depthAlpha + ')'; ctx.lineWidth = 0.6; ctx.stroke(); } /* system nodes */ for (let i = 0; i < pS.length; i++) { const s = pS[i]; const alpha = Math.max(0.2, Math.min(0.85, 0.85 * s.scale)); const r = Math.max(0.8, 1.5 * s.scale); ctx.beginPath(); ctx.arc(s.sx, s.sy, r, 0, TAU); ctx.fillStyle = SYS_COLOR; ctx.globalAlpha = alpha; ctx.fill(); } /* hub nodes (on top) */ for (let i = 0; i < pH.length; i++) { const h2 = pH[i]; const alpha = Math.max(0.35, Math.min(1, 1.0 * h2.scale)); const r = Math.max(2, hubs[i].r * h2.scale); ctx.beginPath(); ctx.arc(h2.sx, h2.sy, r, 0, TAU); ctx.fillStyle = HUB_COLOR; ctx.globalAlpha = alpha; ctx.fill(); } ctx.globalAlpha = 1; rafId = requestAnimationFrame(frame); } /* ---- reduced motion: single static frame ---- */ const prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (prefersReduced) { rot.ay = 0.4; /* slight rotation so it's not head-on */ frame(0); /* no rAF loop */ stateRef.current = { cleanup: () => {} }; return; } rafId = requestAnimationFrame(frame); /* ---- pointer parallax (desktop only, no touch) ---- */ function onMouseMove(e) { const rect = canvas.getBoundingClientRect(); const nx = ((e.clientX - rect.left) / rect.width - 0.5) * 2; const ny = ((e.clientY - rect.top) / rect.height - 0.5) * 2; mouseInfluence.dx = nx * 0.3; } canvas.addEventListener('mousemove', onMouseMove); /* ---- visibility / intersection pause ---- */ function onVisChange() { paused = document.hidden; } document.addEventListener('visibilitychange', onVisChange); let observer = null; if (typeof IntersectionObserver !== 'undefined') { observer = new IntersectionObserver(([entry]) => { if (!entry.isIntersecting) paused = true; else if (!document.hidden) paused = false; }, { threshold: 0 }); observer.observe(canvas); } /* ---- cleanup ---- */ stateRef.current = { cleanup: () => { cancelAnimationFrame(rafId); canvas.removeEventListener('mousemove', onMouseMove); document.removeEventListener('visibilitychange', onVisChange); if (observer) observer.disconnect(); } }; return () => { if (stateRef.current && stateRef.current.cleanup) stateRef.current.cleanup(); }; }, [taxonomy]); if (!taxonomy || !taxonomy.specialties || !taxonomy.specialties.length) return null; return (