update to use curveMonotoneX alg for smothing curves
This commit is contained in:
@@ -160,19 +160,46 @@ function linePathD(pts: [number, number][]): string {
|
||||
return pts.map((p, i) => `${i ? "L" : "M"}${p[0]},${p[1]}`).join(" ");
|
||||
}
|
||||
|
||||
// Catmull-Rom → cubic bezier, so the curve passes through every point (a plain bezier
|
||||
// smoothing would miss them).
|
||||
const sign = (x: number) => (x < 0 ? -1 : 1);
|
||||
|
||||
// Interior tangent for monotone-cubic interpolation (d3's curveMonotoneX): the lesser of
|
||||
// the two neighbouring secant slopes, and zero at a local extremum. This is the whole
|
||||
// point of the monotone curve — the tangent is capped so a segment can never bulge past
|
||||
// its endpoints, so an area fill can't dip below a value-0 point into negative space the
|
||||
// way a Catmull-Rom overshoot does.
|
||||
function monoTangent(x0: number, y0: number, x1: number, y1: number, x2: number, y2: number): number {
|
||||
const h0 = x1 - x0, h1 = x2 - x1;
|
||||
const s0 = h0 !== 0 ? (y1 - y0) / h0 : 0;
|
||||
const s1 = h1 !== 0 ? (y2 - y1) / h1 : 0;
|
||||
const p = (s0 * h1 + s1 * h0) / (h0 + h1);
|
||||
return (sign(s0) + sign(s1)) * Math.min(Math.abs(s0), Math.abs(s1), 0.5 * Math.abs(p)) || 0;
|
||||
}
|
||||
|
||||
// Endpoint tangent (d3 slope2): a parabola-end estimate constrained by the adjacent
|
||||
// interior tangent t, so the boundary segments don't overshoot either.
|
||||
function endTangent(x0: number, y0: number, x1: number, y1: number, t: number): number {
|
||||
const h = x1 - x0;
|
||||
return h !== 0 ? (3 * (y1 - y0) / h - t) / 2 : t;
|
||||
}
|
||||
|
||||
// Monotone cubic through every point, emitted as cubic beziers. A monotone interpolant
|
||||
// never overshoots its data, so the smoothed curve stays within the value range of each
|
||||
// pair of adjacent points.
|
||||
function smoothPathD(pts: [number, number][]): string {
|
||||
if (pts.length < 3) return linePathD(pts);
|
||||
const n = pts.length;
|
||||
if (n < 3) return linePathD(pts);
|
||||
const m: number[] = new Array(n);
|
||||
for (let i = 1; i < n - 1; i++) {
|
||||
m[i] = monoTangent(pts[i - 1][0], pts[i - 1][1], pts[i][0], pts[i][1], pts[i + 1][0], pts[i + 1][1]);
|
||||
}
|
||||
m[0] = endTangent(pts[0][0], pts[0][1], pts[1][0], pts[1][1], m[1]);
|
||||
m[n - 1] = endTangent(pts[n - 2][0], pts[n - 2][1], pts[n - 1][0], pts[n - 1][1], m[n - 2]);
|
||||
let d = `M${pts[0][0]},${pts[0][1]}`;
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const p0 = pts[i - 1] ?? pts[i];
|
||||
const p1 = pts[i];
|
||||
const p2 = pts[i + 1];
|
||||
const p3 = pts[i + 2] ?? p2;
|
||||
const c1x = p1[0] + (p2[0] - p0[0]) / 6, c1y = p1[1] + (p2[1] - p0[1]) / 6;
|
||||
const c2x = p2[0] - (p3[0] - p1[0]) / 6, c2y = p2[1] - (p3[1] - p1[1]) / 6;
|
||||
d += ` C${c1x},${c1y} ${c2x},${c2y} ${p2[0]},${p2[1]}`;
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
const dx = (pts[i + 1][0] - pts[i][0]) / 3;
|
||||
const c1x = pts[i][0] + dx, c1y = pts[i][1] + dx * m[i];
|
||||
const c2x = pts[i + 1][0] - dx, c2y = pts[i + 1][1] - dx * m[i + 1];
|
||||
d += ` C${c1x},${c1y} ${c2x},${c2y} ${pts[i + 1][0]},${pts[i + 1][1]}`;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user