fix: centripetal Catmull-Rom spline to prevent overshooting near close markers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
This commit is contained in:
2026-06-21 18:20:26 +02:00
parent 51ab99b839
commit b6c9d0b2ac
+41 -13
View File
@@ -10,28 +10,56 @@
}
/*
* 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).
* Centripetal Catmull-Rom spline (α=0.5) through waypoints → dense coords.
* Parameterising by √chord-length prevents the large bow that uniform
* parameterisation produces when consecutive points are very close together
* but their neighbours are far away (e.g. two entries both in Berlin).
*/
function catmullRomSpline(coords, steps) {
if (coords.length < 2) return coords;
steps = steps || 16;
var alpha = 0.5;
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)];
/* Phantom endpoints via reflection so the spline reaches the first and last real points */
var ext = [
[2*coords[0][0] - coords[1][0], 2*coords[0][1] - coords[1][1]]
].concat(coords).concat([
[2*coords[coords.length-1][0] - coords[coords.length-2][0],
2*coords[coords.length-1][1] - coords[coords.length-2][1]]
]);
function segT(a, b) {
var dx = b[0]-a[0], dy = b[1]-a[1];
return Math.pow(Math.max(Math.sqrt(dx*dx + dy*dy), 1e-10), alpha);
}
for (var i = 1; i < ext.length - 2; i++) {
var p0 = ext[i-1], p1 = ext[i], p2 = ext[i+1], p3 = ext[i+2];
var t0 = 0;
var t1 = t0 + segT(p0, p1);
var t2 = t1 + segT(p1, p2);
var t3 = t2 + segT(p2, p3);
for (var s = 0; s < steps; s++) {
var t = s / steps;
var t2 = t * t;
var t3 = t2 * t;
var t = t1 + (t2 - t1) * s / steps;
var a1x = (t1-t)/(t1-t0)*p0[0] + (t-t0)/(t1-t0)*p1[0];
var a1y = (t1-t)/(t1-t0)*p0[1] + (t-t0)/(t1-t0)*p1[1];
var a2x = (t2-t)/(t2-t1)*p1[0] + (t-t1)/(t2-t1)*p2[0];
var a2y = (t2-t)/(t2-t1)*p1[1] + (t-t1)/(t2-t1)*p2[1];
var a3x = (t3-t)/(t3-t2)*p2[0] + (t-t2)/(t3-t2)*p3[0];
var a3y = (t3-t)/(t3-t2)*p2[1] + (t-t2)/(t3-t2)*p3[1];
var b1x = (t2-t)/(t2-t0)*a1x + (t-t0)/(t2-t0)*a2x;
var b1y = (t2-t)/(t2-t0)*a1y + (t-t0)/(t2-t0)*a2y;
var b2x = (t3-t)/(t3-t1)*a2x + (t-t1)/(t3-t1)*a3x;
var b2y = (t3-t)/(t3-t1)*a2y + (t-t1)/(t3-t1)*a3y;
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)
(t2-t)/(t2-t1)*b1x + (t-t1)/(t2-t1)*b2x,
(t2-t)/(t2-t1)*b1y + (t-t1)/(t2-t1)*b2y
]);
}
}