feat: beacon system — click to place glowing markers in 3D world
Some checks failed
CI / validate (pull_request) Failing after 21s
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:
97
app.js
97
app.js
@@ -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;
|
||||
|
||||
|
||||
@@ -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 · right-click to clear
|
||||
</div>
|
||||
|
||||
<script type="module" src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user