// hero.js — a live 3D lattice for the hero: a slab of nodes (a via-lattice motif) with near-neighbour // links, slowly rotating and parallaxing to the pointer. Off-white nodes, a scatter of lime. three.js. import * as THREE from 'three'; const canvas = document.getElementById('hero-canvas'); if (canvas) { const r = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }); r.setPixelRatio(Math.min(devicePixelRatio, 2)); const scene = new THREE.Scene(); const cam = new THREE.PerspectiveCamera(42, 1, 0.1, 100); cam.position.set(0, 0, 15); // scatter nodes across a wide, shallow slab const N = 260, W = 26, H = 12, D = 5, pts = []; for (let i = 0; i < N; i++) pts.push(new THREE.Vector3((Math.random() - 0.5) * W, (Math.random() - 0.5) * H, (Math.random() - 0.5) * D)); // links between near neighbours const linePos = []; for (let i = 0; i < N; i++) for (let j = i + 1; j < N; j++) { if (pts[i].distanceTo(pts[j]) < 2.4) { linePos.push(pts[i].x, pts[i].y, pts[i].z, pts[j].x, pts[j].y, pts[j].z); } } const lg = new THREE.BufferGeometry(); lg.setAttribute('position', new THREE.Float32BufferAttribute(linePos, 3)); const lines = new THREE.LineSegments(lg, new THREE.LineBasicMaterial({ color: 0xf4f3ee, transparent: true, opacity: 0.08 })); // node points — most off-white, a few lime accents const pg = new THREE.BufferGeometry(); pg.setAttribute('position', new THREE.Float32BufferAttribute(pts.flatMap((p) => [p.x, p.y, p.z]), 3)); const colors = []; for (let i = 0; i < N; i++) { const lime = Math.random() < 0.12; colors.push(lime ? 0.78 : 0.96, lime ? 1.0 : 0.95, lime ? 0.18 : 0.93); } pg.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3)); const points = new THREE.Points(pg, new THREE.PointsMaterial({ size: 0.13, vertexColors: true, transparent: true, opacity: 0.9 })); const group = new THREE.Group(); group.add(lines); group.add(points); scene.add(group); const resize = () => { const w = canvas.clientWidth, h = canvas.clientHeight; r.setSize(w, h, false); cam.aspect = w / h; cam.updateProjectionMatrix(); }; resize(); addEventListener('resize', resize); let mx = 0, my = 0, tx = 0, ty = 0, tick = 0; addEventListener('pointermove', (e) => { tx = (e.clientX / innerWidth - 0.5); ty = (e.clientY / innerHeight - 0.5); }); (function loop() { requestAnimationFrame(loop); tick += 0.0016; mx += (tx - mx) * 0.04; my += (ty - my) * 0.04; group.rotation.y = tick + mx * 0.5; group.rotation.x = Math.sin(tick * 0.6) * 0.08 + my * 0.3; r.render(scene, cam); })(); }