feat: journey line — Catmull-Rom spline curve, dotted subordinate style under GPX tracks
This commit is contained in:
@@ -10,18 +10,46 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Progressively draw the journey line using a requestAnimationFrame loop.
|
* Catmull-Rom spline through waypoints → dense interpolated coords.
|
||||||
* coords: [[lng, lat], ...] in chronological order.
|
* Produces a smooth curve that passes through every entry dot.
|
||||||
* sourceId: the MapLibre source id to update each frame.
|
* steps: interpolated points per segment (16 is plenty for daily entries).
|
||||||
*/
|
*/
|
||||||
function animateJourneyLine(map, coords, sourceId) {
|
function catmullRomSpline(coords, steps) {
|
||||||
if (coords.length < 2) return;
|
if (coords.length < 2) return coords;
|
||||||
|
steps = steps || 16;
|
||||||
|
var out = [];
|
||||||
|
|
||||||
|
for (var i = 0; i < coords.length - 1; i++) {
|
||||||
|
var p0 = coords[Math.max(i - 1, 0)];
|
||||||
|
var p1 = coords[i];
|
||||||
|
var p2 = coords[i + 1];
|
||||||
|
var p3 = coords[Math.min(i + 2, coords.length - 1)];
|
||||||
|
|
||||||
|
for (var s = 0; s < steps; s++) {
|
||||||
|
var t = s / steps;
|
||||||
|
var t2 = t * t;
|
||||||
|
var t3 = t2 * t;
|
||||||
|
out.push([
|
||||||
|
0.5 * ((2*p1[0]) + (-p0[0]+p2[0])*t + (2*p0[0]-5*p1[0]+4*p2[0]-p3[0])*t2 + (-p0[0]+3*p1[0]-3*p2[0]+p3[0])*t3),
|
||||||
|
0.5 * ((2*p1[1]) + (-p0[1]+p2[1])*t + (2*p0[1]-5*p1[1]+4*p2[1]-p3[1])*t2 + (-p0[1]+3*p1[1]-3*p2[1]+p3[1])*t3)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push(coords[coords.length - 1]);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Progressively draw the journey line using a requestAnimationFrame loop.
|
||||||
|
* splineCoords: dense interpolated coords from catmullRomSpline().
|
||||||
|
*/
|
||||||
|
function animateJourneyLine(map, splineCoords, sourceId) {
|
||||||
|
if (splineCoords.length < 2) return;
|
||||||
|
|
||||||
/* Cumulative Euclidean distance between waypoints */
|
|
||||||
var segDist = [0];
|
var segDist = [0];
|
||||||
for (var i = 1; i < coords.length; i++) {
|
for (var i = 1; i < splineCoords.length; i++) {
|
||||||
var dx = coords[i][0] - coords[i - 1][0];
|
var dx = splineCoords[i][0] - splineCoords[i - 1][0];
|
||||||
var dy = coords[i][1] - coords[i - 1][1];
|
var dy = splineCoords[i][1] - splineCoords[i - 1][1];
|
||||||
segDist.push(segDist[i - 1] + Math.sqrt(dx * dx + dy * dy));
|
segDist.push(segDist[i - 1] + Math.sqrt(dx * dx + dy * dy));
|
||||||
}
|
}
|
||||||
var totalDist = segDist[segDist.length - 1];
|
var totalDist = segDist[segDist.length - 1];
|
||||||
@@ -29,20 +57,20 @@
|
|||||||
var startTime = performance.now();
|
var startTime = performance.now();
|
||||||
|
|
||||||
function frame(now) {
|
function frame(now) {
|
||||||
if (!map.getSource(sourceId)) return; /* map was removed */
|
if (!map.getSource(sourceId)) return;
|
||||||
var t = Math.min((now - startTime) / DURATION, 1);
|
var t = Math.min((now - startTime) / DURATION, 1);
|
||||||
var eased = 1 - Math.pow(1 - t, 3); /* ease-out cubic */
|
var eased = 1 - Math.pow(1 - t, 3);
|
||||||
var target = eased * totalDist;
|
var target = eased * totalDist;
|
||||||
|
|
||||||
var animCoords = [coords[0]];
|
var animCoords = [splineCoords[0]];
|
||||||
for (var j = 1; j < coords.length; j++) {
|
for (var j = 1; j < splineCoords.length; j++) {
|
||||||
if (segDist[j] <= target) {
|
if (segDist[j] <= target) {
|
||||||
animCoords.push(coords[j]);
|
animCoords.push(splineCoords[j]);
|
||||||
} else {
|
} else {
|
||||||
var frac = (target - segDist[j - 1]) / (segDist[j] - segDist[j - 1]);
|
var frac = (target - segDist[j - 1]) / (segDist[j] - segDist[j - 1]);
|
||||||
animCoords.push([
|
animCoords.push([
|
||||||
coords[j - 1][0] + (coords[j][0] - coords[j - 1][0]) * frac,
|
splineCoords[j - 1][0] + (splineCoords[j][0] - splineCoords[j - 1][0]) * frac,
|
||||||
coords[j - 1][1] + (coords[j][1] - coords[j - 1][1]) * frac
|
splineCoords[j - 1][1] + (splineCoords[j][1] - splineCoords[j - 1][1]) * frac
|
||||||
]);
|
]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -56,31 +84,33 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Add a journey line source + two layers (glow + main) to a loaded map,
|
* Add a journey line to a loaded map — dotted, subordinate style so GPX
|
||||||
* then animate or draw instantly based on prefers-reduced-motion.
|
* tracks read as the primary route where they exist.
|
||||||
|
* coords: [[lng, lat], ...] raw waypoints (daily entry positions).
|
||||||
*/
|
*/
|
||||||
function addJourneyLine(map, coords, sourceId) {
|
function addJourneyLine(map, coords, sourceId) {
|
||||||
if (coords.length < 2) return;
|
if (coords.length < 2) return;
|
||||||
|
|
||||||
map.addSource(sourceId, { type: 'geojson', data: lineFeature([coords[0]]) });
|
var splineCoords = catmullRomSpline(coords, 16);
|
||||||
|
|
||||||
map.addLayer({
|
map.addSource(sourceId, { type: 'geojson', data: lineFeature([splineCoords[0]]) });
|
||||||
id: sourceId + '-glow', type: 'line', source: sourceId,
|
|
||||||
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
|
||||||
paint: { 'line-color': ACCENT, 'line-width': 6, 'line-opacity': 0.18 }
|
|
||||||
});
|
|
||||||
|
|
||||||
map.addLayer({
|
map.addLayer({
|
||||||
id: sourceId + '-line', type: 'line', source: sourceId,
|
id: sourceId + '-line', type: 'line', source: sourceId,
|
||||||
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
layout: { 'line-join': 'round', 'line-cap': 'round' },
|
||||||
paint: { 'line-color': ACCENT, 'line-width': 2.5, 'line-opacity': 0.85 }
|
paint: {
|
||||||
|
'line-color': ACCENT,
|
||||||
|
'line-width': 2,
|
||||||
|
'line-opacity': 0.45,
|
||||||
|
'line-dasharray': [0, 2.5]
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
if (reducedMotion) {
|
if (reducedMotion) {
|
||||||
map.getSource(sourceId).setData(lineFeature(coords));
|
map.getSource(sourceId).setData(lineFeature(splineCoords));
|
||||||
} else {
|
} else {
|
||||||
animateJourneyLine(map, coords, sourceId);
|
animateJourneyLine(map, splineCoords, sourceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user