[claude] Add constellation system — connect nearby stars with faint lines (#114) #155
158
app.js
158
app.js
@@ -1,12 +1,161 @@
|
|||||||
// ... existing code ...
|
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();
|
||||||
|
|
||||||
// === WEBSOCKET CLIENT ===
|
// === WEBSOCKET CLIENT ===
|
||||||
import { wsClient } from './ws-client.js';
|
import { wsClient } from './ws-client.js';
|
||||||
|
|
||||||
// Initialize WebSocket client
|
|
||||||
wsClient.connect();
|
wsClient.connect();
|
||||||
|
|
||||||
// Handle WebSocket events
|
|
||||||
window.addEventListener('player-joined', (event) => {
|
window.addEventListener('player-joined', (event) => {
|
||||||
console.log('Player joined:', event.detail);
|
console.log('Player joined:', event.detail);
|
||||||
});
|
});
|
||||||
@@ -19,9 +168,6 @@ window.addEventListener('chat-message', (event) => {
|
|||||||
console.log('Chat message:', event.detail);
|
console.log('Chat message:', event.detail);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean up on page unload
|
|
||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener('beforeunload', () => {
|
||||||
wsClient.disconnect();
|
wsClient.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ... existing code ...
|
|
||||||
|
|||||||
13
index.html
13
index.html
@@ -14,10 +14,17 @@
|
|||||||
<meta name="twitter:description" content="A sovereign 3D world">
|
<meta name="twitter:description" content="A sovereign 3D world">
|
||||||
<meta name="twitter:image" content="https://example.com/og-image.png">
|
<meta name="twitter:image" content="https://example.com/og-image.png">
|
||||||
<link rel="manifest" href="/manifest.json">
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<script type="importmap">
|
||||||
|
{
|
||||||
|
"imports": {
|
||||||
|
"three": "https://unpkg.com/three@0.183.0/build/three.module.js",
|
||||||
|
"three/addons/": "https://unpkg.com/three@0.183.0/examples/jsm/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- ... existing content ... -->
|
|
||||||
|
|
||||||
<!-- Top Right: Audio Toggle -->
|
<!-- Top Right: Audio Toggle -->
|
||||||
<div id="audio-control" class="hud-controls" style="position: absolute; top: 8px; right: 8px;">
|
<div id="audio-control" class="hud-controls" style="position: absolute; top: 8px; right: 8px;">
|
||||||
<button id="audio-toggle" class="chat-toggle-btn" aria-label="Toggle ambient sound" style="background-color: var(--color-primary); color: var(--color-bg); padding: 4px 8px; border-radius: 4px; font-size: 12px; cursor: pointer;">
|
<button id="audio-toggle" class="chat-toggle-btn" aria-label="Toggle ambient sound" style="background-color: var(--color-primary); color: var(--color-bg); padding: 4px 8px; border-radius: 4px; font-size: 12px; cursor: pointer;">
|
||||||
@@ -26,6 +33,6 @@
|
|||||||
<audio id="ambient-sound" src="ambient.mp3" loop></audio>
|
<audio id="ambient-sound" src="ambient.mp3" loop></audio>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ... existing content ... -->
|
<script type="module" src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
41
style.css
41
style.css
@@ -1,12 +1,51 @@
|
|||||||
|
/* === DESIGN SYSTEM — NEXUS === */
|
||||||
|
:root {
|
||||||
|
--color-bg: #000008;
|
||||||
|
--color-primary: #4488ff;
|
||||||
|
--color-secondary: #334488;
|
||||||
|
--color-text: #ccd6f6;
|
||||||
|
--color-text-muted: #4a5568;
|
||||||
|
--font-body: 'Courier New', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--color-bg);
|
||||||
|
color: var(--color-text);
|
||||||
|
font-family: var(--font-body);
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === HUD === */
|
||||||
|
.hud-controls {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
/* === AUDIO TOGGLE === */
|
/* === AUDIO TOGGLE === */
|
||||||
#audio-toggle {
|
#audio-toggle {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
background-color: var(--color-primary-primary);
|
background-color: var(--color-primary);
|
||||||
color: var(--color-bg);
|
color: var(--color-bg);
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-family: var(--font-body);
|
font-family: var(--font-body);
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
#audio-toggle:hover {
|
#audio-toggle:hover {
|
||||||
|
|||||||
Reference in New Issue
Block a user