feat: breadcrumb trail — faint glowing path showing where you've walked
Some checks failed
CI / validate (pull_request) Failing after 10s
CI / auto-merge (pull_request) Has been skipped

WASD keys move the camera across the glass platform. As you walk,
a faint accent-colored line and glowing dot markers trace your path,
pulsing gently to match the existing ambient glow aesthetic. Trail
is capped at 80 crumbs; oldest are dropped as you continue moving.

Fixes #142
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Alexander Whitestone
2026-03-24 00:39:03 -04:00
parent 1dc82b656f
commit 8cc5fd020e

84
app.js
View File

@@ -232,6 +232,58 @@ glassPlatformGroup.add(voidLight);
scene.add(glassPlatformGroup);
// === WALKING (WASD) ===
const walkOffset = new THREE.Vector3(0, 0, 0);
const walkCamPos = new THREE.Vector3();
const WALK_SPEED = 0.07;
const WALK_CLAMP = 4.0;
const walkKeys = { w: false, a: false, s: false, d: false };
document.addEventListener('keydown', (e) => {
if (e.key === 'w' || e.key === 'W') walkKeys.w = true;
if (e.key === 'a' || e.key === 'A') walkKeys.a = true;
if (e.key === 's' || e.key === 'S') walkKeys.s = true;
if (e.key === 'd' || e.key === 'D') walkKeys.d = true;
});
document.addEventListener('keyup', (e) => {
if (e.key === 'w' || e.key === 'W') walkKeys.w = false;
if (e.key === 'a' || e.key === 'A') walkKeys.a = false;
if (e.key === 's' || e.key === 'S') walkKeys.s = false;
if (e.key === 'd' || e.key === 'D') walkKeys.d = false;
});
// === BREADCRUMB TRAIL ===
const CRUMB_MAX = 80;
const CRUMB_MIN_DIST_SQ = 0.12; // ~0.35 units min gap between crumbs
/** @type {THREE.Vector3[]} */
const crumbs = [];
let crumbLastX = 0;
let crumbLastZ = 0;
// Faint line tracing the path walked
const trailGeo = new THREE.BufferGeometry();
const trailMat = new THREE.LineBasicMaterial({
color: NEXUS.colors.accent,
transparent: true,
opacity: 0.22,
depthWrite: false,
});
const trailLine = new THREE.Line(trailGeo, trailMat);
scene.add(trailLine);
// Glowing dots — individual breadcrumb markers
const crumbDotGeo = new THREE.BufferGeometry();
const crumbDotMat = new THREE.PointsMaterial({
color: NEXUS.colors.accent,
size: 0.22,
sizeAttenuation: true,
transparent: true,
opacity: 0.45,
depthWrite: false,
});
const crumbDots = new THREE.Points(crumbDotGeo, crumbDotMat);
scene.add(crumbDots);
// === MOUSE-DRIVEN ROTATION ===
let mouseX = 0;
let mouseY = 0;
@@ -443,11 +495,39 @@ function animate() {
requestAnimationFrame(animate);
const elapsed = clock.getElapsedTime();
// Update walking (only when in normal ground-level view)
if (!overviewMode && !photoMode) {
if (walkKeys.w) walkOffset.z -= WALK_SPEED;
if (walkKeys.s) walkOffset.z += WALK_SPEED;
if (walkKeys.a) walkOffset.x -= WALK_SPEED;
if (walkKeys.d) walkOffset.x += WALK_SPEED;
walkOffset.x = Math.max(-WALK_CLAMP, Math.min(WALK_CLAMP, walkOffset.x));
walkOffset.z = Math.max(-WALK_CLAMP, Math.min(WALK_CLAMP, walkOffset.z));
}
// Smooth camera transition for overview mode
const targetT = overviewMode ? 1 : 0;
overviewT += (targetT - overviewT) * 0.04;
camera.position.lerpVectors(NORMAL_CAM, OVERVIEW_CAM, overviewT);
camera.lookAt(0, 0, 0);
walkCamPos.set(NORMAL_CAM.x + walkOffset.x, NORMAL_CAM.y, NORMAL_CAM.z + walkOffset.z);
camera.position.lerpVectors(walkCamPos, OVERVIEW_CAM, overviewT);
camera.lookAt(walkOffset.x * (1 - overviewT), 0, walkOffset.z * (1 - overviewT));
// Record breadcrumb when player has moved far enough
const crumbFx = walkOffset.x;
const crumbFz = walkOffset.z;
const crumbDx = crumbFx - crumbLastX;
const crumbDz = crumbFz - crumbLastZ;
if (crumbDx * crumbDx + crumbDz * crumbDz >= CRUMB_MIN_DIST_SQ) {
crumbLastX = crumbFx;
crumbLastZ = crumbFz;
crumbs.push(new THREE.Vector3(crumbFx, 0.015, crumbFz));
if (crumbs.length > CRUMB_MAX) crumbs.shift();
if (crumbs.length >= 2) trailGeo.setFromPoints(crumbs);
crumbDotGeo.setFromPoints(crumbs);
}
// Pulse the trail glow
trailMat.opacity = 0.14 + Math.sin(elapsed * 1.1) * 0.08;
crumbDotMat.opacity = 0.35 + Math.sin(elapsed * 1.9) * 0.13;
// Slow auto-rotation — suppressed during overview and photo mode
const rotationScale = photoMode ? 0 : (1 - overviewT);