[claude] Split app.js into modules (#143) #170
209
app.js
209
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();
|
||||
|
||||
16
modules/controls.js
vendored
Normal file
16
modules/controls.js
vendored
Normal file
@@ -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);
|
||||
});
|
||||
86
modules/effects.js
vendored
Normal file
86
modules/effects.js
vendored
Normal file
@@ -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);
|
||||
25
modules/scene.js
Normal file
25
modules/scene.js
Normal file
@@ -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);
|
||||
23
modules/ui.js
Normal file
23
modules/ui.js
Normal file
@@ -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';
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user