parts-store/assets/hero.js

48 lines
2.5 KiB
JavaScript

// 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);
})();
}