feat: breadcrumb trail — faint glowing path showing where you've walked
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:
84
app.js
84
app.js
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user