import * as THREE from "three"; import type { SceneFactory } from "../stage"; import { createPackets, glowGeometry, glowLines, glowPoints, smooth, stagger, } from "../materials"; /** * Work: an isometric infrastructure stack — edge, app, data, storage * layers as gridded plates, services as nodes, traffic flowing between * layers. Entry: plates drop into place bottom-up, then links light up. */ const LAYERS = 4; const SPACING = 0.62; const SIZE = 1.9; const GRID = 8; const SERVICES_PER_LAYER = 5; const LINKS_PER_GAP = 6; const stack: SceneFactory = (stage) => { const { scene, colors } = stage; const root = new THREE.Group(); root.rotation.set(0.62, Math.PI / 4, 0); scene.add(root); const half = SIZE / 2; const cell = SIZE / GRID; const layerY = (i: number) => (i - (LAYERS - 1) / 2) * SPACING; type Layer = { group: THREE.Group; gridMat: THREE.ShaderMaterial; borderMat: THREE.ShaderMaterial; nodeMat: THREE.ShaderMaterial; services: THREE.Vector3[]; }; const layers: Layer[] = []; for (let li = 0; li < LAYERS; li++) { const group = new THREE.Group(); const grid: number[] = []; for (let k = 1; k < GRID; k++) { const c = -half + k * cell; grid.push(c, 0, -half, c, 0, half, -half, 0, c, half, 0, c); } const gridMat = glowLines({ color: colors.line, opacity: 0.1 }); group.add(new THREE.LineSegments(glowGeometry(grid), gridMat)); const b = [ [-half, -half], [half, -half], [half, half], [-half, half], ]; const border: number[] = []; for (let k = 0; k < 4; k++) { const [x1, z1] = b[k]!; const [x2, z2] = b[(k + 1) % 4]!; border.push(x1!, 0, z1!, x2!, 0, z2!); } // corner ticks give plates a "hardware" feel b.forEach(([x, z]) => { border.push(x!, 0, z!, x!, -0.06, z!); }); const borderMat = glowLines({ color: colors.line, opacity: 0.55 }); group.add(new THREE.LineSegments(glowGeometry(border), borderMat)); const services: THREE.Vector3[] = []; const used = new Set(); while (services.length < SERVICES_PER_LAYER) { const gx = 1 + Math.floor(Math.random() * (GRID - 1)); const gz = 1 + Math.floor(Math.random() * (GRID - 1)); if (used.has(`${gx},${gz}`)) continue; used.add(`${gx},${gz}`); services.push( new THREE.Vector3(-half + gx * cell, 0, -half + gz * cell), ); } const nodeMat = glowPoints(stage, { color: li === LAYERS - 1 ? colors.accent : colors.line, size: 9, }); group.add( new THREE.Points( glowGeometry(services.flatMap((s) => [s.x, s.y, s.z])), nodeMat, ), ); root.add(group); layers.push({ group, gridMat, borderMat, nodeMat, services }); } // ---- inter-layer links ---- type Link = { lower: number; a: number; b: number }; const links: Link[] = []; for (let li = 0; li < LAYERS - 1; li++) { for (let k = 0; k < LINKS_PER_GAP; k++) { links.push({ lower: li, a: Math.floor(Math.random() * SERVICES_PER_LAYER), b: Math.floor(Math.random() * SERVICES_PER_LAYER), }); } } const linkGeo = glowGeometry(new Float32Array(links.length * 6), { dynamic: true, }); const linkMat = glowLines({ color: colors.accent, opacity: 0.28 }); root.add(new THREE.LineSegments(linkGeo, linkMat)); // packet paths are rebuilt in place each frame as layers bob const linkPaths = links.map(() => [ new THREE.Vector3(), new THREE.Vector3(), ]); const packets = createPackets(stage, linkPaths, { perPath: 1, speed: 0.5, trail: 6, size: 8, }); root.add(packets.points); const world = (li: number, v: THREE.Vector3, out: THREE.Vector3) => out.copy(v).add(layers[li]!.group.position); return { frame: { width: 3.6, height: 4.1 }, update(t, dt) { const e = stage.entry; layers.forEach((L, i) => { const local = stagger(e, i, LAYERS, 0.45); const bob = stage.reducedMotion ? 0 : Math.sin(t * 0.8 + i * 1.3) * 0.035; L.group.position.y = layerY(i) + bob + (1 - local) * 1.6; L.gridMat.uniforms.uOpacity!.value = 0.1 * local; L.borderMat.uniforms.uOpacity!.value = 0.55 * local; L.nodeMat.uniforms.uOpacity!.value = smooth( (local - 0.6) / 0.4, ); }); const pos = linkGeo.attributes.position as THREE.BufferAttribute; links.forEach((lk, i) => { const pa = world( lk.lower, layers[lk.lower]!.services[lk.a]!, linkPaths[i]![0]!, ); const pb = world( lk.lower + 1, layers[lk.lower + 1]!.services[lk.b]!, linkPaths[i]![1]!, ); pos.setXYZ(i * 2, pa.x, pa.y, pa.z); pos.setXYZ(i * 2 + 1, pb.x, pb.y, pb.z); }); pos.needsUpdate = true; const linksOn = smooth((e - 0.7) / 0.3); linkMat.uniforms.uOpacity!.value = 0.28 * linksOn; if (stage.reducedMotion) return; packets.update(dt, linksOn); root.rotation.y = Math.PI / 4 + Math.sin(t * 0.15) * 0.15 + stage.pointer.x * 0.35; root.rotation.x = 0.62 + stage.pointer.y * 0.12; }, }; }; export default stack;