feat: add breadcrumb trail showing glowing path through star field
Some checks failed
CI / validate (pull_request) Has been cancelled

Tracks camera position in star-local space at 0.25s intervals, rendering
faint cyan-blue glowing dots that fade over 12 seconds. Crumbs rotate with
the star field so they stay embedded in space as you explore.

Fixes #142
This commit is contained in:
Alexander Whitestone
2026-03-24 00:04:47 -04:00
parent 7eca0fba5d
commit 558b8ef1bb

71
app.js
View File

@@ -126,6 +126,71 @@ window.addEventListener('resize', () => {
renderer.setSize(window.innerWidth, window.innerHeight);
});
// === BREADCRUMB TRAIL ===
const TRAIL_MAX = 100;
const TRAIL_LIFETIME = 12; // seconds before a crumb fades out
const TRAIL_INTERVAL = 0.25; // seconds between crumb placements
let lastCrumbTime = -TRAIL_INTERVAL;
const trailPosArr = new Float32Array(TRAIL_MAX * 3);
const trailColArr = new Float32Array(TRAIL_MAX * 3);
const trailGeo = new THREE.BufferGeometry();
trailGeo.setAttribute('position', new THREE.BufferAttribute(trailPosArr, 3));
trailGeo.setAttribute('color', new THREE.BufferAttribute(trailColArr, 3));
trailGeo.setDrawRange(0, 0);
const trailMat = new THREE.PointsMaterial({
size: 2.5,
sizeAttenuation: true,
transparent: true,
opacity: 0.9,
vertexColors: true,
});
const trailMesh = new THREE.Points(trailGeo, trailMat);
scene.add(trailMesh);
// crumbs store positions in star-local space so they stay embedded in the star field
const crumbs = []; // { lx, ly, lz, born }
function addCrumb(elapsed) {
// Project camera position into star-local space to track the viewing path
const invQ = stars.quaternion.clone().invert();
const localCam = camera.position.clone().applyQuaternion(invQ);
crumbs.push({ lx: localCam.x, ly: localCam.y, lz: localCam.z, born: elapsed });
if (crumbs.length > TRAIL_MAX) crumbs.shift();
lastCrumbTime = elapsed;
}
function updateTrail(elapsed) {
// Remove expired crumbs
while (crumbs.length > 0 && elapsed - crumbs[0].born > TRAIL_LIFETIME) {
crumbs.shift();
}
const n = crumbs.length;
for (let i = 0; i < n; i++) {
const c = crumbs[i];
const age = elapsed - c.born;
const t = 1 - age / TRAIL_LIFETIME;
const fade = t * t; // quadratic fade for a softer tail
trailPosArr[i * 3] = c.lx;
trailPosArr[i * 3 + 1] = c.ly;
trailPosArr[i * 3 + 2] = c.lz;
// Faint cyan-blue glow matching the accent palette
trailColArr[i * 3] = 0.1 * fade;
trailColArr[i * 3 + 1] = 0.5 * fade;
trailColArr[i * 3 + 2] = 1.0 * fade;
}
trailGeo.attributes.position.needsUpdate = true;
trailGeo.attributes.color.needsUpdate = true;
trailGeo.setDrawRange(0, n);
// Trail rotates with the star field so crumbs stay embedded in space
trailMesh.rotation.x = stars.rotation.x;
trailMesh.rotation.y = stars.rotation.y;
}
// === ANIMATION LOOP ===
const clock = new THREE.Clock();
@@ -146,6 +211,12 @@ function animate() {
// Subtle pulse on constellation opacity
constellationLines.material.opacity = 0.12 + Math.sin(elapsed * 0.5) * 0.06;
// Breadcrumb trail — drop a crumb every interval
if (elapsed - lastCrumbTime >= TRAIL_INTERVAL) {
addCrumb(elapsed);
}
updateTrail(elapsed);
renderer.render(scene, camera);
}