/* DottedSurface — Three.js animated wave of dots, scoped to its container */ const { useEffect: useEffectDS, useRef: useRefDS } = React; function DottedSurface({ className = '', style = {}, dark = false, density = 1 }) { const containerRef = useRefDS(null); useEffectDS(() => { const container = containerRef.current; if (!container || !window.THREE) return; const THREE = window.THREE; const SEPARATION = 150; const AMOUNTX = Math.round(40 * density); const AMOUNTY = Math.round(60 * density); // Initial size from container const getSize = () => ({ w: container.clientWidth || window.innerWidth, h: container.clientHeight || 600 }); let { w, h } = getSize(); const scene = new THREE.Scene(); scene.fog = new THREE.Fog(0xffffff, 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(Math.min(window.devicePixelRatio, 2)); renderer.setSize(w, h); renderer.setClearColor(0xffffff, 0); renderer.domElement.style.display = 'block'; container.appendChild(renderer.domElement); // Particles 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 y = 0; const z = iy * SEPARATION - (AMOUNTY * SEPARATION) / 2; positions.push(x, y, z); if (dark) { colors.push(0.78, 0.88, 0.90); // soft cyan-white } else { colors.push(0.04, 0.06, 0.10); // near-ink dark } } } geometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3)); geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); const material = new THREE.PointsMaterial({ size: 7, vertexColors: true, transparent: true, opacity: dark ? 0.65 : 0.55, sizeAttenuation: true }); const points = new THREE.Points(geometry, material); scene.add(points); let count = 0; let rafId = 0; let stopped = false; const animate = () => { if (stopped) return; rafId = requestAnimationFrame(animate); const posAttr = geometry.attributes.position; const pos = posAttr.array; let i = 0; for (let ix = 0; ix < AMOUNTX; ix++) { for (let iy = 0; iy < AMOUNTY; iy++) { const idx = i * 3; pos[idx + 1] = Math.sin((ix + count) * 0.3) * 50 + Math.sin((iy + count) * 0.5) * 50; i++; } } posAttr.needsUpdate = true; renderer.render(scene, camera); count += 0.08; }; const handleResize = () => { const sz = getSize(); w = sz.w; h = sz.h; camera.aspect = w / h; camera.updateProjectionMatrix(); renderer.setSize(w, h); }; let ro; if (window.ResizeObserver) { ro = new ResizeObserver(handleResize); ro.observe(container); } else { window.addEventListener('resize', handleResize); } animate(); return () => { stopped = true; cancelAnimationFrame(rafId); if (ro) ro.disconnect(); else window.removeEventListener('resize', handleResize); scene.traverse(obj => { if (obj.isPoints) { obj.geometry.dispose(); if (Array.isArray(obj.material)) obj.material.forEach(m => m.dispose()); else obj.material.dispose(); } }); renderer.dispose(); if (renderer.domElement && container.contains(renderer.domElement)) { container.removeChild(renderer.domElement); } }; }, [dark, density]); return (
); } window.DottedSurface = DottedSurface;