feat: beacon system — click to place glowing markers in 3D world
Some checks failed
CI / validate (pull_request) Failing after 21s

- Click anywhere on canvas to place a cyan glowing beacon at depth 15
- Beacons pulse and rotate with animated glow sphere + orbiting ring
- Each beacon has a PointLight for environment illumination
- Right-click canvas to clear all beacons
- Max 10 beacons (oldest removed automatically)
- HUD shows active beacon count with glow styling

Fixes #138

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Alexander Whitestone
2026-03-24 00:04:43 -04:00
parent 7eca0fba5d
commit c5391b4488
3 changed files with 114 additions and 0 deletions

97
app.js
View File

@@ -146,11 +146,108 @@ function animate() {
// Subtle pulse on constellation opacity
constellationLines.material.opacity = 0.12 + Math.sin(elapsed * 0.5) * 0.06;
// Animate beacons
for (const beacon of beacons) {
const age = elapsed - beacon.userData.createdAt;
const pulse = 0.5 + Math.sin(age * 2.5) * 0.5; // 0..1
const glow = beacon.children[1];
if (glow) {
glow.material.opacity = 0.05 + pulse * 0.18;
glow.scale.setScalar(1 + pulse * 0.4);
}
const ring = beacon.children[2];
if (ring) {
ring.rotation.y += 0.025;
ring.material.opacity = 0.25 + pulse * 0.45;
}
}
renderer.render(scene, camera);
}
animate();
// === BEACON SYSTEM ===
const beacons = [];
const BEACON_COLOR = 0x00ffcc;
const MAX_BEACONS = 10;
function createBeacon(position) {
const group = new THREE.Group();
// Core sphere
const coreGeo = new THREE.SphereGeometry(0.12, 8, 8);
const coreMat = new THREE.MeshBasicMaterial({ color: BEACON_COLOR });
const core = new THREE.Mesh(coreGeo, coreMat);
group.add(core); // index 0
// Outer glow sphere
const glowGeo = new THREE.SphereGeometry(0.35, 8, 8);
const glowMat = new THREE.MeshBasicMaterial({
color: BEACON_COLOR,
transparent: true,
opacity: 0.15,
});
const glow = new THREE.Mesh(glowGeo, glowMat);
group.add(glow); // index 1
// Orbiting ring
const ringGeo = new THREE.RingGeometry(0.22, 0.28, 20);
const ringMat = new THREE.MeshBasicMaterial({
color: BEACON_COLOR,
transparent: true,
opacity: 0.6,
side: THREE.DoubleSide,
});
const ring = new THREE.Mesh(ringGeo, ringMat);
ring.rotation.x = Math.PI / 2;
group.add(ring); // index 2
// Point light for environment glow
const light = new THREE.PointLight(BEACON_COLOR, 2.0, 6);
group.add(light);
group.position.copy(position);
group.userData.createdAt = clock.getElapsedTime();
scene.add(group);
beacons.push(group);
// Remove oldest beacon when over limit
if (beacons.length > MAX_BEACONS) {
const oldest = beacons.shift();
scene.remove(oldest);
}
updateBeaconHUD();
}
function updateBeaconHUD() {
const el = document.getElementById('beacon-count');
if (el) el.textContent = beacons.length > 0 ? `${beacons.length}` : '';
}
// Click to place beacon
renderer.domElement.addEventListener('click', (e) => {
const ndc = new THREE.Vector3(
(e.clientX / window.innerWidth) * 2 - 1,
-(e.clientY / window.innerHeight) * 2 + 1,
0.5
);
ndc.unproject(camera);
const dir = ndc.sub(camera.position).normalize();
const pos = camera.position.clone().add(dir.multiplyScalar(15));
createBeacon(pos);
});
// Right-click to clear all beacons
renderer.domElement.addEventListener('contextmenu', (e) => {
e.preventDefault();
beacons.forEach(b => scene.remove(b));
beacons.length = 0;
updateBeaconHUD();
});
// === DEBUG MODE ===
let debugMode = false;

View File

@@ -36,6 +36,14 @@
<audio id="ambient-sound" src="ambient.mp3" loop></audio>
</div>
<!-- Beacon HUD -->
<div id="beacon-hud" class="hud-controls" style="position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); pointer-events: none;">
<span id="beacon-count" class="beacon-count-label"></span>
</div>
<div id="beacon-hint" class="hud-controls" style="position: absolute; bottom: 40px; left: 50%; transform: translateX(-50%); pointer-events: none; opacity: 0.35; font-size: 10px; color: var(--color-text-muted);">
click to place beacon &nbsp;·&nbsp; right-click to clear
</div>
<script type="module" src="app.js"></script>
</body>
</html>

View File

@@ -56,6 +56,15 @@ canvas {
background-color: var(--color-text-muted);
}
/* === BEACON HUD === */
.beacon-count-label {
color: #00ffcc;
font-family: var(--font-body);
font-size: 13px;
letter-spacing: 0.1em;
text-shadow: 0 0 8px #00ffcc;
}
/* === DEBUG MODE === */
#debug-toggle {
margin-left: 8px;