import * as THREE from 'three';
export function initHero3D() {
const container = document.getElementById('hero-canvas');
if (!container) return;
while (container.firstChild) {
container.removeChild(container.firstChild);
}
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0f1117, 0.03);
const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, 12);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); container.appendChild(renderer.domElement);
const group = new THREE.Group();
const width = 40;
const depth = 40;
const widthSegments = 60;
const depthSegments = 40;
const geometry = new THREE.PlaneGeometry(width, depth, widthSegments, depthSegments);
const vertexShader = `
uniform float time;
uniform float scrollY;
varying float vElevation;
varying vec2 vUv;
void main() {
vUv = uv;
vec3 pos = position;
// Create a moving wave effect
// The "varve" layers undulate
float wave1 = sin(pos.x * 0.2 + time * 0.5) * 1.0;
float wave2 = cos(pos.y * 0.3 + time * 0.3) * 0.5;
// Apply elevation
pos.z += wave1 + wave2;
// Save elevation for fragment shader coloring
vElevation = pos.z;
vec4 modelPosition = modelMatrix * vec4(pos, 1.0);
vec4 viewPosition = viewMatrix * modelPosition;
vec4 projectedPosition = projectionMatrix * viewPosition;
gl_Position = projectedPosition;
gl_PointSize = 2.0;
}
`;
const fragmentShader = `
uniform vec3 color;
varying float vElevation;
varying vec2 vUv;
void main() {
// Fade out at edges
float alpha = 1.0 - smoothstep(0.3, 0.5, distance(vUv, vec2(0.5)));
// Height-based glow
float glow = smoothstep(-1.0, 2.0, vElevation);
// Final color mixing
vec3 finalColor = mix(color * 0.5, color * 2.0, glow);
gl_FragColor = vec4(finalColor, alpha * 0.6);
}
`;
const material = new THREE.ShaderMaterial({
vertexShader,
fragmentShader,
uniforms: {
time: { value: 0 },
color: { value: new THREE.Color(0x38bdf8) }
},
transparent: true,
depthWrite: false,
blending: THREE.AdditiveBlending,
wireframe: true });
const planeTop = new THREE.Points(geometry, material);
planeTop.rotation.x = -Math.PI / 2 + 0.2; planeTop.position.y = -2;
group.add(planeTop);
const material2 = material.clone();
material2.uniforms.color.value = new THREE.Color(0x4d5960); const planeBottom = new THREE.Points(geometry, material2);
planeBottom.rotation.x = -Math.PI / 2 + 0.2;
planeBottom.position.y = -5; group.add(planeBottom);
scene.add(group);
let mouseX = 0;
let mouseY = 0;
const windowHalfX = window.innerWidth / 2;
const windowHalfY = window.innerHeight / 2;
document.addEventListener('mousemove', (event) => {
mouseX = (event.clientX - windowHalfX) * 0.0001;
mouseY = (event.clientY - windowHalfY) * 0.0001;
});
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const elapsedTime = clock.getElapsedTime();
material.uniforms.time.value = elapsedTime;
material2.uniforms.time.value = elapsedTime + 2.0;
camera.position.x += (mouseX * 10 - camera.position.x) * 0.05;
camera.position.y += (-mouseY * 10 + 5 - camera.position.y) * 0.05;
camera.lookAt(0, 0, 0);
group.rotation.y = Math.sin(elapsedTime * 0.1) * 0.1;
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
}