add utils

This commit is contained in:
“chrisshank” 2024-11-01 23:13:59 -07:00
parent 44bbffaf05
commit d8888d484a
2 changed files with 166 additions and 11 deletions

View File

@ -1,7 +1,7 @@
import { getBoxToBoxArrow } from "perfect-arrows";
import { AbstractArrow } from "./abstract-arrow.ts";
import { pointsOnBezierCurves } from "./points-on-path.ts";
import { getStroke, StrokeOptions } from "perfect-freehand";
import { getBoxToBoxArrow } from 'perfect-arrows';
import { AbstractArrow } from './abstract-arrow.ts';
import { pointsOnBezierCurves } from './utils.ts';
import { getStroke, StrokeOptions } from 'perfect-freehand';
export type Arrow = [
/** The x position of the (padded) starting point. */
@ -26,12 +26,12 @@ export type Arrow = [
declare global {
interface HTMLElementTagNameMap {
"fc-connection": FolkConnection;
'fc-connection': FolkConnection;
}
}
export class FolkConnection extends AbstractArrow {
static override tagName = "fc-connection";
static override tagName = 'fc-connection';
#options: StrokeOptions = {
size: 7,
@ -77,12 +77,12 @@ export class FolkConnection extends AbstractArrow {
const stroke = getStroke(points, this.#options);
const path = getSvgPathFromStroke(stroke);
this.style.clipPath = `path('${path}')`;
this.style.backgroundColor = "black";
this.style.backgroundColor = 'black';
}
}
function getSvgPathFromStroke(stroke: number[][]): string {
if (stroke.length === 0) return "";
if (stroke.length === 0) return '';
for (const point of stroke) {
point[0] = Math.round(point[0] * 100) / 100;
@ -95,9 +95,9 @@ function getSvgPathFromStroke(stroke: number[][]): string {
acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
return acc;
},
["M", ...stroke[0], "Q"]
['M', ...stroke[0], 'Q']
);
d.push("Z");
return d.join(" ");
d.push('Z');
return d.join(' ');
}

155
src/arrows/utils.ts Normal file
View File

@ -0,0 +1,155 @@
// Adopted from: https://github.com/pshihn/bezier-points/blob/master/src/index.ts
export type Point = [number, number];
// distance between 2 points
function distance(p1: Point, p2: Point): number {
return Math.sqrt(distanceSq(p1, p2));
}
// distance between 2 points squared
function distanceSq(p1: Point, p2: Point): number {
return Math.pow(p1[0] - p2[0], 2) + Math.pow(p1[1] - p2[1], 2);
}
// Distance squared from a point p to the line segment vw
function distanceToSegmentSq(p: Point, v: Point, w: Point): number {
const l2 = distanceSq(v, w);
if (l2 === 0) {
return distanceSq(p, v);
}
let t = ((p[0] - v[0]) * (w[0] - v[0]) + (p[1] - v[1]) * (w[1] - v[1])) / l2;
t = Math.max(0, Math.min(1, t));
return distanceSq(p, lerp(v, w, t));
}
function lerp(a: Point, b: Point, t: number): Point {
return [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * 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[0] - 2 * p1[0] - p4[0];
ux *= ux;
let uy = 3 * p2[1] - 2 * p1[1] - p4[1];
uy *= uy;
let vx = 3 * p3[0] - 2 * p4[0] - p1[0];
vx *= vx;
let vy = 3 * p3[1] - 2 * p4[1] - p1[1];
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 = 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);
}
// RamerDouglasPeucker 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;
}