// 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 (
);
}
window.DottedSurface = DottedSurface;