From b65384516f63107b1838a536eab7062f7f25781b Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Tue, 24 Mar 2026 00:03:29 -0400 Subject: [PATCH] refactor: split app.js into scene, effects, controls, ui modules Extract logical sections of app.js into focused ES modules under modules/: - modules/scene.js: NEXUS palette, scene/camera/renderer setup - modules/effects.js: star field and constellation lines - modules/controls.js: mouse-driven rotation and resize handler - modules/ui.js: debug mode toggle app.js becomes the orchestrator: imports all modules, runs animation loop. Fixes #143 Co-Authored-By: Claude Sonnet 4.6 --- app.js | 209 +++++++------------------------------------- modules/controls.js | 16 ++++ modules/effects.js | 86 ++++++++++++++++++ modules/scene.js | 25 ++++++ modules/ui.js | 23 +++++ 5 files changed, 184 insertions(+), 175 deletions(-) create mode 100644 modules/controls.js create mode 100644 modules/effects.js create mode 100644 modules/scene.js create mode 100644 modules/ui.js diff --git a/app.js b/app.js index 6e85528..9011771 100644 --- a/app.js +++ b/app.js @@ -1,183 +1,15 @@ import * as THREE from 'three'; - -// === COLOR PALETTE === -const NEXUS = { - colors: { - bg: 0x000008, - starCore: 0xffffff, - starDim: 0x8899cc, - constellationLine: 0x334488, - constellationFade: 0x112244, - accent: 0x4488ff, - } -}; - -// === SCENE SETUP === -const scene = new THREE.Scene(); -scene.background = new THREE.Color(NEXUS.colors.bg); - -const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000); -camera.position.set(0, 0, 5); - -const renderer = new THREE.WebGLRenderer({ antialias: true }); -renderer.setPixelRatio(window.devicePixelRatio); -renderer.setSize(window.innerWidth, window.innerHeight); -document.body.appendChild(renderer.domElement); - -// === STAR FIELD === -const STAR_COUNT = 800; -const STAR_SPREAD = 400; -const CONSTELLATION_DISTANCE = 30; // max distance to draw a line between stars - -const starPositions = []; -const starGeo = new THREE.BufferGeometry(); -const posArray = new Float32Array(STAR_COUNT * 3); -const sizeArray = new Float32Array(STAR_COUNT); - -for (let i = 0; i < STAR_COUNT; i++) { - const x = (Math.random() - 0.5) * STAR_SPREAD; - const y = (Math.random() - 0.5) * STAR_SPREAD; - const z = (Math.random() - 0.5) * STAR_SPREAD; - posArray[i * 3] = x; - posArray[i * 3 + 1] = y; - posArray[i * 3 + 2] = z; - sizeArray[i] = Math.random() * 2.5 + 0.5; - starPositions.push(new THREE.Vector3(x, y, z)); -} - -starGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3)); -starGeo.setAttribute('size', new THREE.BufferAttribute(sizeArray, 1)); - -const starMaterial = new THREE.PointsMaterial({ - color: NEXUS.colors.starCore, - size: 0.6, - sizeAttenuation: true, - transparent: true, - opacity: 0.9, -}); - -const stars = new THREE.Points(starGeo, starMaterial); -scene.add(stars); - -// === CONSTELLATION LINES === -// Connect nearby stars with faint lines, limited to avoid clutter -function buildConstellationLines() { - const linePositions = []; - const MAX_CONNECTIONS_PER_STAR = 3; - const connectionCount = new Array(STAR_COUNT).fill(0); - - for (let i = 0; i < STAR_COUNT; i++) { - if (connectionCount[i] >= MAX_CONNECTIONS_PER_STAR) continue; - - // Find nearest neighbors - const neighbors = []; - for (let j = i + 1; j < STAR_COUNT; j++) { - if (connectionCount[j] >= MAX_CONNECTIONS_PER_STAR) continue; - const dist = starPositions[i].distanceTo(starPositions[j]); - if (dist < CONSTELLATION_DISTANCE) { - neighbors.push({ j, dist }); - } - } - - // Sort by distance and connect closest ones - neighbors.sort((a, b) => a.dist - b.dist); - const toConnect = neighbors.slice(0, MAX_CONNECTIONS_PER_STAR - connectionCount[i]); - - for (const { j } of toConnect) { - linePositions.push( - starPositions[i].x, starPositions[i].y, starPositions[i].z, - starPositions[j].x, starPositions[j].y, starPositions[j].z - ); - connectionCount[i]++; - connectionCount[j]++; - } - } - - const lineGeo = new THREE.BufferGeometry(); - lineGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(linePositions), 3)); - - const lineMat = new THREE.LineBasicMaterial({ - color: NEXUS.colors.constellationLine, - transparent: true, - opacity: 0.18, - }); - - return new THREE.LineSegments(lineGeo, lineMat); -} - -const constellationLines = buildConstellationLines(); -scene.add(constellationLines); - -// === MOUSE-DRIVEN ROTATION === -let mouseX = 0; -let mouseY = 0; -let targetRotX = 0; -let targetRotY = 0; - -document.addEventListener('mousemove', (e) => { - mouseX = (e.clientX / window.innerWidth - 0.5) * 2; - mouseY = (e.clientY / window.innerHeight - 0.5) * 2; -}); - -// === RESIZE HANDLER === -window.addEventListener('resize', () => { - camera.aspect = window.innerWidth / window.innerHeight; - camera.updateProjectionMatrix(); - renderer.setSize(window.innerWidth, window.innerHeight); -}); - -// === ANIMATION LOOP === -const clock = new THREE.Clock(); - -function animate() { - requestAnimationFrame(animate); - const elapsed = clock.getElapsedTime(); - - // Slow auto-rotation - targetRotX += (mouseY * 0.3 - targetRotX) * 0.02; - targetRotY += (mouseX * 0.3 - targetRotY) * 0.02; - - stars.rotation.x = targetRotX + elapsed * 0.01; - stars.rotation.y = targetRotY + elapsed * 0.015; - - constellationLines.rotation.x = stars.rotation.x; - constellationLines.rotation.y = stars.rotation.y; - - // Subtle pulse on constellation opacity - constellationLines.material.opacity = 0.12 + Math.sin(elapsed * 0.5) * 0.06; - - renderer.render(scene, camera); -} - -animate(); - -// === DEBUG MODE === -let debugMode = false; - -document.getElementById('debug-toggle').addEventListener('click', () => { - debugMode = !debugMode; - document.getElementById('debug-toggle').style.backgroundColor = debugMode - ? 'var(--color-text-muted)' - : 'var(--color-secondary)'; - console.log(`Debug mode ${debugMode ? 'enabled' : 'disabled'}`); - - if (debugMode) { - // Example: Visualize all collision boxes and light sources - // Replace with actual logic when available - document.querySelectorAll('.collision-box').forEach(el => el.style.outline = '2px solid red'); - document.querySelectorAll('.light-source').forEach(el => el.style.outline = '2px dashed yellow'); - } else { - document.querySelectorAll('.collision-box, .light-source').forEach(el => { - el.style.outline = 'none'; - }); - } -}); - -// === WEBSOCKET CLIENT === +import { scene, camera, renderer } from './modules/scene.js'; +import { stars, constellationLines } from './modules/effects.js'; +import { mouse } from './modules/controls.js'; +import { initDebug } from './modules/ui.js'; import { wsClient } from './ws-client.js'; +// === INIT === +initDebug(); wsClient.connect(); +// === WEBSOCKET EVENTS === window.addEventListener('player-joined', (event) => { console.log('Player joined:', event.detail); }); @@ -193,3 +25,30 @@ window.addEventListener('chat-message', (event) => { window.addEventListener('beforeunload', () => { wsClient.disconnect(); }); + +// === ANIMATION LOOP === +const clock = new THREE.Clock(); +let targetRotX = 0; +let targetRotY = 0; + +function animate() { + requestAnimationFrame(animate); + const elapsed = clock.getElapsedTime(); + + // Slow auto-rotation + targetRotX += (mouse.y * 0.3 - targetRotX) * 0.02; + targetRotY += (mouse.x * 0.3 - targetRotY) * 0.02; + + stars.rotation.x = targetRotX + elapsed * 0.01; + stars.rotation.y = targetRotY + elapsed * 0.015; + + constellationLines.rotation.x = stars.rotation.x; + constellationLines.rotation.y = stars.rotation.y; + + // Subtle pulse on constellation opacity + constellationLines.material.opacity = 0.12 + Math.sin(elapsed * 0.5) * 0.06; + + renderer.render(scene, camera); +} + +animate(); diff --git a/modules/controls.js b/modules/controls.js new file mode 100644 index 0000000..4e0a92b --- /dev/null +++ b/modules/controls.js @@ -0,0 +1,16 @@ +import { camera, renderer } from './scene.js'; + +// === MOUSE-DRIVEN ROTATION === +export const mouse = { x: 0, y: 0 }; + +document.addEventListener('mousemove', (e) => { + mouse.x = (e.clientX / window.innerWidth - 0.5) * 2; + mouse.y = (e.clientY / window.innerHeight - 0.5) * 2; +}); + +// === RESIZE HANDLER === +window.addEventListener('resize', () => { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); +}); diff --git a/modules/effects.js b/modules/effects.js new file mode 100644 index 0000000..d6eab12 --- /dev/null +++ b/modules/effects.js @@ -0,0 +1,86 @@ +import * as THREE from 'three'; +import { NEXUS, scene } from './scene.js'; + +// === STAR FIELD === +const STAR_COUNT = 800; +const STAR_SPREAD = 400; +const CONSTELLATION_DISTANCE = 30; // max distance to draw a line between stars + +const starPositions = []; +const starGeo = new THREE.BufferGeometry(); +const posArray = new Float32Array(STAR_COUNT * 3); +const sizeArray = new Float32Array(STAR_COUNT); + +for (let i = 0; i < STAR_COUNT; i++) { + const x = (Math.random() - 0.5) * STAR_SPREAD; + const y = (Math.random() - 0.5) * STAR_SPREAD; + const z = (Math.random() - 0.5) * STAR_SPREAD; + posArray[i * 3] = x; + posArray[i * 3 + 1] = y; + posArray[i * 3 + 2] = z; + sizeArray[i] = Math.random() * 2.5 + 0.5; + starPositions.push(new THREE.Vector3(x, y, z)); +} + +starGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3)); +starGeo.setAttribute('size', new THREE.BufferAttribute(sizeArray, 1)); + +const starMaterial = new THREE.PointsMaterial({ + color: NEXUS.colors.starCore, + size: 0.6, + sizeAttenuation: true, + transparent: true, + opacity: 0.9, +}); + +export const stars = new THREE.Points(starGeo, starMaterial); +scene.add(stars); + +// === CONSTELLATION LINES === +// Connect nearby stars with faint lines, limited to avoid clutter +function buildConstellationLines() { + const linePositions = []; + const MAX_CONNECTIONS_PER_STAR = 3; + const connectionCount = new Array(STAR_COUNT).fill(0); + + for (let i = 0; i < STAR_COUNT; i++) { + if (connectionCount[i] >= MAX_CONNECTIONS_PER_STAR) continue; + + // Find nearest neighbors + const neighbors = []; + for (let j = i + 1; j < STAR_COUNT; j++) { + if (connectionCount[j] >= MAX_CONNECTIONS_PER_STAR) continue; + const dist = starPositions[i].distanceTo(starPositions[j]); + if (dist < CONSTELLATION_DISTANCE) { + neighbors.push({ j, dist }); + } + } + + // Sort by distance and connect closest ones + neighbors.sort((a, b) => a.dist - b.dist); + const toConnect = neighbors.slice(0, MAX_CONNECTIONS_PER_STAR - connectionCount[i]); + + for (const { j } of toConnect) { + linePositions.push( + starPositions[i].x, starPositions[i].y, starPositions[i].z, + starPositions[j].x, starPositions[j].y, starPositions[j].z + ); + connectionCount[i]++; + connectionCount[j]++; + } + } + + const lineGeo = new THREE.BufferGeometry(); + lineGeo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(linePositions), 3)); + + const lineMat = new THREE.LineBasicMaterial({ + color: NEXUS.colors.constellationLine, + transparent: true, + opacity: 0.18, + }); + + return new THREE.LineSegments(lineGeo, lineMat); +} + +export const constellationLines = buildConstellationLines(); +scene.add(constellationLines); diff --git a/modules/scene.js b/modules/scene.js new file mode 100644 index 0000000..2f5cb3f --- /dev/null +++ b/modules/scene.js @@ -0,0 +1,25 @@ +import * as THREE from 'three'; + +// === COLOR PALETTE === +export const NEXUS = { + colors: { + bg: 0x000008, + starCore: 0xffffff, + starDim: 0x8899cc, + constellationLine: 0x334488, + constellationFade: 0x112244, + accent: 0x4488ff, + } +}; + +// === SCENE SETUP === +export const scene = new THREE.Scene(); +scene.background = new THREE.Color(NEXUS.colors.bg); + +export const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 2000); +camera.position.set(0, 0, 5); + +export const renderer = new THREE.WebGLRenderer({ antialias: true }); +renderer.setPixelRatio(window.devicePixelRatio); +renderer.setSize(window.innerWidth, window.innerHeight); +document.body.appendChild(renderer.domElement); diff --git a/modules/ui.js b/modules/ui.js new file mode 100644 index 0000000..8aafd03 --- /dev/null +++ b/modules/ui.js @@ -0,0 +1,23 @@ +// === DEBUG MODE === +export function initDebug() { + let debugMode = false; + + document.getElementById('debug-toggle').addEventListener('click', () => { + debugMode = !debugMode; + document.getElementById('debug-toggle').style.backgroundColor = debugMode + ? 'var(--color-text-muted)' + : 'var(--color-secondary)'; + console.log(`Debug mode ${debugMode ? 'enabled' : 'disabled'}`); + + if (debugMode) { + // Example: Visualize all collision boxes and light sources + // Replace with actual logic when available + document.querySelectorAll('.collision-box').forEach(el => el.style.outline = '2px solid red'); + document.querySelectorAll('.light-source').forEach(el => el.style.outline = '2px dashed yellow'); + } else { + document.querySelectorAll('.collision-box, .light-source').forEach(el => { + el.style.outline = 'none'; + }); + } + }); +} -- 2.43.0