diff --git a/app.js b/app.js index 4902f2f..bbb18fe 100644 --- a/app.js +++ b/app.js @@ -13,6 +13,9 @@ const NEXUS = { constellationLine: 0x334488, constellationFade: 0x112244, accent: 0x4488ff, + cableGlow: 0x0044cc, + particle: 0x44aaff, + nodeGlow: 0x2266ff, } }; @@ -116,6 +119,117 @@ function buildConstellationLines() { const constellationLines = buildConstellationLines(); scene.add(constellationLines); +// === STRUCTURE NODES === +// Fixed anchor points in 3D space that cables connect between +const STRUCTURE_NODES = [ + new THREE.Vector3( 0, 2, 0), // Nexus core + new THREE.Vector3( 6, -1, 2), // East tower + new THREE.Vector3( -6, 1, -2), // West tower + new THREE.Vector3( 2, 5, -3), // North spire + new THREE.Vector3( -3, -4, 4), // South relay + new THREE.Vector3( 0, -2, -6), // Deep node +]; + +// Icosahedron nodes at each structure +const nodeGroup = new THREE.Group(); +const nodeMat = new THREE.MeshBasicMaterial({ + color: NEXUS.colors.nodeGlow, + transparent: true, + opacity: 0.7, + wireframe: true, +}); +for (const pos of STRUCTURE_NODES) { + const geo = new THREE.IcosahedronGeometry(0.18, 1); + const mesh = new THREE.Mesh(geo, nodeMat); + mesh.position.copy(pos); + nodeGroup.add(mesh); +} +scene.add(nodeGroup); + +// === CABLES / CONDUITS === +// Pairs of structure nodes to connect with curved cables +const CABLE_PAIRS = [ + [0, 1], [0, 2], [0, 3], [0, 4], + [1, 3], [2, 4], [1, 5], [3, 5], +]; + +const PARTICLES_PER_CABLE = 8; // flowing particles per cable +const CABLE_TUBE_RADIUS = 0.018; +const CABLE_TUBE_SEGMENTS = 32; + +/** + * Builds a cubic Bezier curve between two points with a random mid-arc offset. + * @param {THREE.Vector3} a + * @param {THREE.Vector3} b + * @returns {THREE.CatmullRomCurve3} + */ +function makeCableCurve(a, b) { + const mid = a.clone().lerp(b, 0.5); + // Perpendicular arc offset — gives cables a gentle bow + const offset = new THREE.Vector3( + (Math.random() - 0.5) * 2.5, + (Math.random() - 0.5) * 2.5, + (Math.random() - 0.5) * 2.5, + ); + mid.add(offset); + return new THREE.CatmullRomCurve3([a.clone(), mid, b.clone()]); +} + +const cableGroup = new THREE.Group(); +const cables = []; // { curve, particles, offsets } + +const cableTubeMat = new THREE.MeshBasicMaterial({ + color: NEXUS.colors.cableGlow, + transparent: true, + opacity: 0.25, +}); + +const particleMat = new THREE.PointsMaterial({ + color: NEXUS.colors.particle, + size: 0.22, + sizeAttenuation: true, + transparent: true, + opacity: 0.9, +}); + +for (const [ai, bi] of CABLE_PAIRS) { + const curve = makeCableCurve(STRUCTURE_NODES[ai], STRUCTURE_NODES[bi]); + + // Tube mesh + const tubeGeo = new THREE.TubeGeometry(curve, CABLE_TUBE_SEGMENTS, CABLE_TUBE_RADIUS, 6, false); + const tube = new THREE.Mesh(tubeGeo, cableTubeMat); + cableGroup.add(tube); + + // Particles — each has a random t offset so they're staggered + const offsets = Array.from({ length: PARTICLES_PER_CABLE }, (_, i) => i / PARTICLES_PER_CABLE); + const pPositions = new Float32Array(PARTICLES_PER_CABLE * 3); + const pGeo = new THREE.BufferGeometry(); + pGeo.setAttribute('position', new THREE.BufferAttribute(pPositions, 3)); + const pMesh = new THREE.Points(pGeo, particleMat); + cableGroup.add(pMesh); + + cables.push({ curve, pGeo, offsets }); +} + +scene.add(cableGroup); + +/** + * Advance glowing particles along all cables. + * @param {number} elapsed — seconds since scene start + */ +function updateCableParticles(elapsed) { + const SPEED = 0.18; // full-cable traversal time in loops/sec + for (const { curve, pGeo, offsets } of cables) { + const pos = pGeo.attributes.position; + for (let i = 0; i < offsets.length; i++) { + const t = ((offsets[i] + elapsed * SPEED) % 1 + 1) % 1; + const pt = curve.getPoint(t); + pos.setXYZ(i, pt.x, pt.y, pt.z); + } + pos.needsUpdate = true; + } +} + // === MOUSE-DRIVEN ROTATION === let mouseX = 0; let mouseY = 0; @@ -250,6 +364,15 @@ function animate() { // Subtle pulse on constellation opacity constellationLines.material.opacity = 0.12 + Math.sin(elapsed * 0.5) * 0.06; + // Animate cables — rotate with stars, pulse tube opacity, advance particles + cableGroup.rotation.x = stars.rotation.x; + cableGroup.rotation.y = stars.rotation.y; + nodeGroup.rotation.x = stars.rotation.x; + nodeGroup.rotation.y = stars.rotation.y; + cableTubeMat.opacity = 0.18 + Math.sin(elapsed * 0.8) * 0.07; + nodeMat.opacity = 0.5 + Math.sin(elapsed * 1.2) * 0.3; + updateCableParticles(elapsed); + if (photoMode) { orbitControls.update(); composer.render();