From 916969c96f661e60d93c16891b394a50a9903d39 Mon Sep 17 00:00:00 2001 From: Mischa Date: Fri, 19 Jun 2026 22:33:48 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20journey=20line=20=E2=80=94=20Catmull-Ro?= =?UTF-8?q?m=20spline=20curve,=20dotted=20subordinate=20style=20under=20GP?= =?UTF-8?q?X=20tracks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- themes/intotheeast/js/maplibre-utils.js | 84 +++++++++++++++++-------- 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/themes/intotheeast/js/maplibre-utils.js b/themes/intotheeast/js/maplibre-utils.js index 2aa501f..eee2b27 100644 --- a/themes/intotheeast/js/maplibre-utils.js +++ b/themes/intotheeast/js/maplibre-utils.js @@ -10,18 +10,46 @@ } /* - * Progressively draw the journey line using a requestAnimationFrame loop. - * coords: [[lng, lat], ...] in chronological order. - * sourceId: the MapLibre source id to update each frame. + * Catmull-Rom spline through waypoints → dense interpolated coords. + * Produces a smooth curve that passes through every entry dot. + * steps: interpolated points per segment (16 is plenty for daily entries). */ - function animateJourneyLine(map, coords, sourceId) { - if (coords.length < 2) return; + function catmullRomSpline(coords, steps) { + 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]; - for (var i = 1; i < coords.length; i++) { - var dx = coords[i][0] - coords[i - 1][0]; - var dy = coords[i][1] - coords[i - 1][1]; + for (var i = 1; i < splineCoords.length; i++) { + var dx = splineCoords[i][0] - splineCoords[i - 1][0]; + var dy = splineCoords[i][1] - splineCoords[i - 1][1]; segDist.push(segDist[i - 1] + Math.sqrt(dx * dx + dy * dy)); } var totalDist = segDist[segDist.length - 1]; @@ -29,20 +57,20 @@ var startTime = performance.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 eased = 1 - Math.pow(1 - t, 3); /* ease-out cubic */ + var eased = 1 - Math.pow(1 - t, 3); var target = eased * totalDist; - var animCoords = [coords[0]]; - for (var j = 1; j < coords.length; j++) { + var animCoords = [splineCoords[0]]; + for (var j = 1; j < splineCoords.length; j++) { if (segDist[j] <= target) { - animCoords.push(coords[j]); + animCoords.push(splineCoords[j]); } else { var frac = (target - segDist[j - 1]) / (segDist[j] - segDist[j - 1]); animCoords.push([ - coords[j - 1][0] + (coords[j][0] - coords[j - 1][0]) * frac, - coords[j - 1][1] + (coords[j][1] - coords[j - 1][1]) * frac + splineCoords[j - 1][0] + (splineCoords[j][0] - splineCoords[j - 1][0]) * frac, + splineCoords[j - 1][1] + (splineCoords[j][1] - splineCoords[j - 1][1]) * frac ]); break; } @@ -56,31 +84,33 @@ } /* - * Add a journey line source + two layers (glow + main) to a loaded map, - * then animate or draw instantly based on prefers-reduced-motion. + * Add a journey line to a loaded map — dotted, subordinate style so GPX + * tracks read as the primary route where they exist. + * coords: [[lng, lat], ...] raw waypoints (daily entry positions). */ function addJourneyLine(map, coords, sourceId) { if (coords.length < 2) return; - map.addSource(sourceId, { type: 'geojson', data: lineFeature([coords[0]]) }); + var splineCoords = catmullRomSpline(coords, 16); - map.addLayer({ - 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.addSource(sourceId, { type: 'geojson', data: lineFeature([splineCoords[0]]) }); map.addLayer({ id: sourceId + '-line', type: 'line', source: sourceId, 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; if (reducedMotion) { - map.getSource(sourceId).setData(lineFeature(coords)); + map.getSource(sourceId).setData(lineFeature(splineCoords)); } else { - animateJourneyLine(map, coords, sourceId); + animateJourneyLine(map, splineCoords, sourceId); } }