// DottedSurface — animated Three.js wave grid of dots. // Ported from the shadcn/next-themes component to this project's setup: // • Three.js loaded via CDN global (window.THREE) instead of `import * as THREE` // • container-sized (ResizeObserver) instead of window-sized + `fixed inset-0` // • dark-theme palette hardcoded (site is all-black) — light grey dots // • no `cn()` / no next-themes // // Usage: — renders an absolutely-positioned, // pointer-events:none canvas that fills its nearest positioned ancestor. const { useEffect: useE_ds, useRef: useR_ds } = React; function DottedSurface({ style = {}, opacity = 0.45 }) { const containerRef = useR_ds(null); const stateRef = useR_ds(null); useE_ds(() => { const THREE = window.THREE; const container = containerRef.current; if (!THREE || !container) return; const SEPARATION = 150; const AMOUNTX = 40; const AMOUNTY = 60; const w = () => container.clientWidth || window.innerWidth; const h = () => container.clientHeight || window.innerHeight; const scene = new THREE.Scene(); scene.fog = new THREE.Fog(0x000000, 2000, 10000); const camera = new THREE.PerspectiveCamera(60, w() / h(), 1, 10000); camera.position.set(0, 355, 1220); const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true }); renderer.setPixelRatio(window.devicePixelRatio); renderer.setSize(w(), h()); renderer.setClearColor(0x000000, 0); container.appendChild(renderer.domElement); renderer.domElement.style.display = "block"; const positions = []; const colors = []; const geometry = new THREE.BufferGeometry(); for (let ix = 0; ix < AMOUNTX; ix++) { for (let iy = 0; iy < AMOUNTY; iy++) { const x = ix * SEPARATION - (AMOUNTX * SEPARATION) / 2; const z = iy * SEPARATION - (AMOUNTY * SEPARATION) / 2; positions.push(x, 0, z); // dark-theme dots — soft light grey colors.push(180, 180, 180); } } geometry.setAttribute("position", new THREE.Float32BufferAttribute(positions, 3)); geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3)); const material = new THREE.PointsMaterial({ size: 8, vertexColors: true, transparent: true, opacity: opacity, sizeAttenuation: true, }); const points = new THREE.Points(geometry, material); scene.add(points); let count = 0; let animationId; const animate = () => { animationId = requestAnimationFrame(animate); const positionAttribute = geometry.attributes.position; const arr = positionAttribute.array; let i = 0; for (let ix = 0; ix < AMOUNTX; ix++) { for (let iy = 0; iy < AMOUNTY; iy++) { const index = i * 3; arr[index + 1] = Math.sin((ix + count) * 0.3) * 50 + Math.sin((iy + count) * 0.5) * 50; i++; } } positionAttribute.needsUpdate = true; renderer.render(scene, camera); count += 0.1; }; const handleResize = () => { camera.aspect = w() / h(); camera.updateProjectionMatrix(); renderer.setSize(w(), h()); }; const ro = new ResizeObserver(handleResize); ro.observe(container); window.addEventListener("resize", handleResize); animate(); stateRef.current = { scene, renderer, geometry, material, animationId }; return () => { ro.disconnect(); window.removeEventListener("resize", handleResize); cancelAnimationFrame(animationId); geometry.dispose(); material.dispose(); renderer.dispose(); if (renderer.domElement && renderer.domElement.parentNode === container) { container.removeChild(renderer.domElement); } }; }, [opacity]); return (