104 lines
2.5 KiB
TypeScript
104 lines
2.5 KiB
TypeScript
import { getBoxToBoxArrow } from "perfect-arrows";
|
|
import { AbstractArrow } from "./abstract-arrow.ts";
|
|
import { pointsOnBezierCurves } from "./points-on-path.ts";
|
|
import { getStroke, StrokeOptions } from "perfect-freehand";
|
|
|
|
export type Arrow = [
|
|
/** The x position of the (padded) starting point. */
|
|
sx: number,
|
|
/** The y position of the (padded) starting point. */
|
|
sy: number,
|
|
/** The x position of the control point. */
|
|
cx: number,
|
|
/** The y position of the control point. */
|
|
cy: number,
|
|
/** The x position of the (padded) ending point. */
|
|
ex: number,
|
|
/** The y position of the (padded) ending point. */
|
|
ey: number,
|
|
/** The angle (in radians) for an ending arrowhead. */
|
|
ae: number,
|
|
/** The angle (in radians) for a starting arrowhead. */
|
|
as: number,
|
|
/** The angle (in radians) for a center arrowhead. */
|
|
ac: number
|
|
];
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"fc-connection": FolkConnection;
|
|
}
|
|
}
|
|
|
|
export class FolkConnection extends AbstractArrow {
|
|
static override tagName = "fc-connection";
|
|
|
|
#options: StrokeOptions = {
|
|
size: 7,
|
|
thinning: 0.5,
|
|
smoothing: 0.5,
|
|
streamline: 0.5,
|
|
simulatePressure: true,
|
|
// TODO: figure out how to expose these as attributes
|
|
easing: (t) => t,
|
|
start: {
|
|
taper: 50,
|
|
easing: (t) => t,
|
|
cap: true,
|
|
},
|
|
end: {
|
|
taper: 0,
|
|
easing: (t) => t,
|
|
cap: true,
|
|
},
|
|
};
|
|
|
|
override render() {
|
|
const { sourceRect, targetRect } = this;
|
|
|
|
const [sx, sy, cx, cy, ex, ey] = getBoxToBoxArrow(
|
|
sourceRect.x,
|
|
sourceRect.y,
|
|
sourceRect.width,
|
|
sourceRect.height,
|
|
targetRect.x,
|
|
targetRect.y,
|
|
targetRect.width,
|
|
targetRect.height
|
|
) as Arrow;
|
|
|
|
const points = pointsOnBezierCurves([
|
|
[sx, sy],
|
|
[cx, cy],
|
|
[ex, ey],
|
|
[ex, ey],
|
|
]);
|
|
|
|
const stroke = getStroke(points, this.#options);
|
|
const path = getSvgPathFromStroke(stroke);
|
|
this.style.clipPath = `path('${path}')`;
|
|
this.style.backgroundColor = "black";
|
|
}
|
|
}
|
|
|
|
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(" ");
|
|
}
|