183 lines
4.8 KiB
TypeScript
183 lines
4.8 KiB
TypeScript
// Adopted from: https://github.com/pshihn/bezier-points/blob/master/lib/index.ts
|
||
|
||
import type { Point } from './types.ts';
|
||
import { Vector } from './Vector.ts';
|
||
|
||
// Distance squared from a point p to the line segment vw
|
||
function distanceToSegmentSq(p: Point, v: Point, w: Point): number {
|
||
const l2 = Vector.distanceSquared(v, w);
|
||
if (l2 === 0) {
|
||
return Vector.distanceSquared(p, v);
|
||
}
|
||
let t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
|
||
t = Math.max(0, Math.min(1, t));
|
||
return Vector.distanceSquared(p, Vector.lerp(v, w, t));
|
||
}
|
||
|
||
function lerp(a: Point, b: Point, t: number): Point {
|
||
return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };
|
||
}
|
||
|
||
// Adapted from https://seant23.wordpress.com/2010/11/12/offset-bezier-curves/
|
||
function flatness(points: readonly Point[], offset: number): number {
|
||
const p1 = points[offset + 0];
|
||
const p2 = points[offset + 1];
|
||
const p3 = points[offset + 2];
|
||
const p4 = points[offset + 3];
|
||
|
||
let ux = 3 * p2.x - 2 * p1.x - p4.x;
|
||
ux *= ux;
|
||
let uy = 3 * p2.y - 2 * p1.y - p4.y;
|
||
uy *= uy;
|
||
let vx = 3 * p3.x - 2 * p4.x - p1.x;
|
||
vx *= vx;
|
||
let vy = 3 * p3.y - 2 * p4.y - p1.y;
|
||
vy *= vy;
|
||
|
||
if (ux < vx) {
|
||
ux = vx;
|
||
}
|
||
|
||
if (uy < vy) {
|
||
uy = vy;
|
||
}
|
||
|
||
return ux + uy;
|
||
}
|
||
|
||
function getPointsOnBezierCurveWithSplitting(
|
||
points: readonly Point[],
|
||
offset: number,
|
||
tolerance: number,
|
||
newPoints?: Point[]
|
||
): Point[] {
|
||
const outPoints = newPoints || [];
|
||
if (flatness(points, offset) < tolerance) {
|
||
const p0 = points[offset + 0];
|
||
if (outPoints.length) {
|
||
const d = Vector.distance(outPoints[outPoints.length - 1], p0);
|
||
if (d > 1) {
|
||
outPoints.push(p0);
|
||
}
|
||
} else {
|
||
outPoints.push(p0);
|
||
}
|
||
outPoints.push(points[offset + 3]);
|
||
} else {
|
||
// subdivide
|
||
const t = 0.5;
|
||
const p1 = points[offset + 0];
|
||
const p2 = points[offset + 1];
|
||
const p3 = points[offset + 2];
|
||
const p4 = points[offset + 3];
|
||
|
||
const q1 = lerp(p1, p2, t);
|
||
const q2 = lerp(p2, p3, t);
|
||
const q3 = lerp(p3, p4, t);
|
||
|
||
const r1 = lerp(q1, q2, t);
|
||
const r2 = lerp(q2, q3, t);
|
||
|
||
const red = lerp(r1, r2, t);
|
||
|
||
getPointsOnBezierCurveWithSplitting([p1, q1, r1, red], 0, tolerance, outPoints);
|
||
getPointsOnBezierCurveWithSplitting([red, r2, q3, p4], 0, tolerance, outPoints);
|
||
}
|
||
return outPoints;
|
||
}
|
||
|
||
export function simplify(points: readonly Point[], distance: number): Point[] {
|
||
return simplifyPoints(points, 0, points.length, distance);
|
||
}
|
||
|
||
// Ramer–Douglas–Peucker algorithm
|
||
// https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
|
||
export function simplifyPoints(
|
||
points: readonly Point[],
|
||
start: number,
|
||
end: number,
|
||
epsilon: number,
|
||
newPoints?: Point[]
|
||
): Point[] {
|
||
const outPoints = newPoints || [];
|
||
|
||
// find the most distance point from the endpoints
|
||
const s = points[start];
|
||
const e = points[end - 1];
|
||
let maxDistSq = 0;
|
||
let maxNdx = 1;
|
||
for (let i = start + 1; i < end - 1; ++i) {
|
||
const distSq = distanceToSegmentSq(points[i], s, e);
|
||
if (distSq > maxDistSq) {
|
||
maxDistSq = distSq;
|
||
maxNdx = i;
|
||
}
|
||
}
|
||
|
||
// if that point is too far, split
|
||
if (Math.sqrt(maxDistSq) > epsilon) {
|
||
simplifyPoints(points, start, maxNdx + 1, epsilon, outPoints);
|
||
simplifyPoints(points, maxNdx, end, epsilon, outPoints);
|
||
} else {
|
||
if (!outPoints.length) {
|
||
outPoints.push(s);
|
||
}
|
||
outPoints.push(e);
|
||
}
|
||
|
||
return outPoints;
|
||
}
|
||
|
||
export function pointsOnBezierCurves(points: readonly Point[], tolerance: number = 0.15, distance?: number): Point[] {
|
||
const newPoints: Point[] = [];
|
||
const numSegments = (points.length - 1) / 3;
|
||
for (let i = 0; i < numSegments; i++) {
|
||
const offset = i * 3;
|
||
getPointsOnBezierCurveWithSplitting(points, offset, tolerance, newPoints);
|
||
}
|
||
if (distance && distance > 0) {
|
||
return simplifyPoints(newPoints, 0, newPoints.length, distance);
|
||
}
|
||
return newPoints;
|
||
}
|
||
|
||
export function getSvgPathFromStroke(stroke: number[][]): string {
|
||
if (stroke.length === 0) return '';
|
||
|
||
for (const point of stroke) {
|
||
point[0] = Math.round(point[0] * 100) / 100;
|
||
point[1] = Math.round(point[1] * 100) / 100;
|
||
}
|
||
|
||
const d = stroke.reduce(
|
||
(acc, [x0, y0], i, arr) => {
|
||
const [x1, y1] = arr[(i + 1) % arr.length];
|
||
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
|
||
return acc;
|
||
},
|
||
['M', ...stroke[0], 'Q']
|
||
);
|
||
|
||
d.push('Z');
|
||
return d.join(' ');
|
||
}
|
||
|
||
export function verticesToPolygon(vertices: Point[]): string {
|
||
if (vertices.length === 0) return '';
|
||
|
||
return `polygon(${vertices.map((vertex) => `${vertex.x}px ${vertex.y}px`).join(', ')})`;
|
||
}
|
||
|
||
const vertexRegex = /(?<x>-?([0-9]*[.])?[0-9]+),\s*(?<y>-?([0-9]*[.])?[0-9]+)/;
|
||
|
||
export function parseVertex(str: string): Point | null {
|
||
const results = vertexRegex.exec(str);
|
||
|
||
if (results === null) return null;
|
||
|
||
return {
|
||
x: Number(results.groups?.x),
|
||
y: Number(results.groups?.y),
|
||
};
|
||
}
|