feat: add breadcrumb trail showing glowing path through star field
Some checks failed
CI / validate (pull_request) Has been cancelled
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:
71
app.js
71
app.js
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user