Skip to content

Commit af6ddae

Browse files
committed
Internal lib - Use monotone cubic interpolation to create smooth paths without dips
1 parent 75d9b50 commit af6ddae

File tree

2 files changed

+39
-29
lines changed

2 files changed

+39
-29
lines changed

src/lib.js

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -735,38 +735,48 @@ export function createStraightPath(points) {
735735
return arr.join(' ').trim()
736736
}
737737

738-
export function createSmoothPath(points, smoothing = 0.2) {
739-
function line(pointA, pointB) {
740-
const lengthX = pointB.x - pointA.x;
741-
const lengthY = pointB.y - pointA.y;
742-
return {
743-
length: Math.sqrt(Math.pow(lengthX, 2) + Math.pow(lengthY, 2)),
744-
angle: Math.atan2(lengthY, lengthX)
745-
};
738+
// Monotone cubic interpolation
739+
export function createSmoothPath(points) {
740+
if (points.length < 2) return '0,0';
741+
742+
const n = points.length - 1;
743+
const path = [`${checkNaN(points[0].x)},${checkNaN(points[0].y)}`];
744+
const dx = [], dy = [], slopes = [], tangents = [];
745+
746+
for (let i = 0; i < n; i += 1) {
747+
dx[i] = points[i + 1].x - points[i].x;
748+
dy[i] = points[i + 1].y - points[i].y;
749+
slopes[i] = dy[i] / dx[i];
746750
}
747-
function controlPoint(current, previous, next, reverse) {
748-
const p = previous || current;
749-
const n = next || current;
750-
const o = line(p, n);
751-
752-
const angle = o.angle + (reverse ? Math.PI : 0);
753-
const length = o.length * smoothing;
751+
tangents[0] = slopes[0];
752+
tangents[n] = slopes[n - 1];
754753

755-
const x = current.x + Math.cos(angle) * length;
756-
const y = current.y + Math.sin(angle) * length;
757-
return { x, y };
758-
}
759-
function bezierCommand(point, i, a) {
760-
const cps = controlPoint(a[i - 1], a[i - 2], point);
761-
const cpe = controlPoint(point, a[i - 1], a[i + 1], true);
762-
return `C ${checkNaN(cps.x)},${checkNaN(cps.y)} ${checkNaN(cpe.x)},${checkNaN(cpe.y)} ${checkNaN(point.x)},${checkNaN(point.y)}`;
754+
for (let i = 1; i < n; i += 1) {
755+
if (slopes[i - 1] * slopes[i] <= 0) {
756+
tangents[i] = 0;
757+
} else {
758+
const commonSlope = (slopes[i - 1] + slopes[i]) / 2;
759+
tangents[i] = commonSlope;
760+
}
763761
}
764-
const d = points.filter(p => !!p).reduce((acc, point, i, a) => i === 0
765-
? `${checkNaN(point.x)},${checkNaN(point.y)} `
766-
: `${acc} ${bezierCommand(point, i, a)} `
767-
, '');
768762

769-
return d;
763+
for (let i = 0; i < n; i += 1) {
764+
const x1 = points[i].x;
765+
const y1 = points[i].y;
766+
const x2 = points[i + 1].x;
767+
const y2 = points[i + 1].y;
768+
769+
const m1 = tangents[i];
770+
const m2 = tangents[i + 1];
771+
772+
const controlX1 = x1 + (x2 - x1) / 3;
773+
const controlY1 = y1 + m1 * (x2 - x1) / 3;
774+
const controlX2 = x2 - (x2 - x1) / 3;
775+
const controlY2 = y2 - m2 * (x2 - x1) / 3;
776+
777+
path.push(`C ${checkNaN(controlX1)},${checkNaN(controlY1)} ${checkNaN(controlX2)},${checkNaN(controlY2)} ${checkNaN(x2)},${checkNaN(y2)}`);
778+
}
779+
return path.join(' ');
770780
}
771781

772782
export function createUid() {

tests/lib.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -654,7 +654,7 @@ describe('createSmoothPath', () => {
654654
{ x: 9, y: 3 },
655655
]
656656

657-
expect(createSmoothPath(points)).toBe('1,3 C 1.2000000000000002,2.6 1.6,1 2,1 C 2.4,1 2.6,3 3,3 C 3.4,3 3.6,1 4,1 C 4.4,1 4.6,3 5,3 C 5.4,3 5.6,1 6,1 C 6.4,1 6.6,3 7,3 C 7.4,3 7.6,1 8,1 C 8.4,1 8.8,2.6 9,3 ')
657+
expect(createSmoothPath(points)).toBe("1,3 C 1.3333333333333333,2.3333333333333335 1.6666666666666667,1 2,1 C 2.3333333333333335,1 2.6666666666666665,3 3,3 C 3.3333333333333335,3 3.6666666666666665,1 4,1 C 4.333333333333333,1 4.666666666666667,3 5,3 C 5.333333333333333,3 5.666666666666667,1 6,1 C 6.333333333333333,1 6.666666666666667,3 7,3 C 7.333333333333333,3 7.666666666666667,1 8,1 C 8.333333333333334,1 8.666666666666666,2.3333333333333335 9,3")
658658
})
659659
})
660660

0 commit comments

Comments
 (0)