import dolly from "#static/logos/dolly.svg?raw"; import { SVGPathData, SVGPathDataTransformer, type SVGCommand } from "svg-pathdata"; export const DOLLY = /\sd="([^"]+)"/.exec(dolly)![1]!; export interface Point { x: number; y: number; } export interface Node extends Point { in?: Point; out?: Point; } export type Contour = Node[]; const EPSILON = 1e-3; const same = (a: Point, b: Point) => Math.abs(a.x - b.x) < EPSILON && Math.abs(a.y - b.y) < EPSILON; const normalize = (d: string) => new SVGPathData(d) .toAbs() .transform(SVGPathDataTransformer.NORMALIZE_HVZ(false, true, true, false)) .normalizeST() .qtToC() .aToC().commands; export const parsePath = (d: string): Contour[] => { const contours: Contour[] = []; let contour: Contour = []; const close = () => { const first = contour[0]; const last = contour.at(-1); if (first && last && contour.length > 1 && same(first, last)) { first.in = last.in; contour.pop(); } if (contour.length) contours.push(contour); contour = []; }; for (const command of normalize(d)) { if (command.type === SVGPathData.MOVE_TO) { close(); contour.push({ x: command.x, y: command.y }); } else if (command.type === SVGPathData.LINE_TO) { contour.push({ x: command.x, y: command.y }); } else if (command.type === SVGPathData.CURVE_TO) { contour.at(-1)!.out = { x: command.x1, y: command.y1 }; contour.push({ x: command.x, y: command.y, in: { x: command.x2, y: command.y2 } }); } else if (command.type === SVGPathData.CLOSE_PATH) { close(); } } close(); return contours; }; const segment = (from: Node, to: Node): SVGCommand => from.out && to.in ? { type: SVGPathData.CURVE_TO, relative: false, x1: from.out.x, y1: from.out.y, x2: to.in.x, y2: to.in.y, x: to.x, y: to.y } : { type: SVGPathData.LINE_TO, relative: false, x: to.x, y: to.y }; export const toPath = (contours: Contour[]) => new SVGPathData( contours.flatMap((contour): SVGCommand[] => [ { type: SVGPathData.MOVE_TO, relative: false, x: contour[0]!.x, y: contour[0]!.y }, ...contour.map((node, n) => segment(node, contour[(n + 1) % contour.length]!)), { type: SVGPathData.CLOSE_PATH } ]) ) .round(1e4) .encode(); export const smooth = (node: Node) => { if (!node.in || !node.out) return false; const a = { x: node.in.x - node.x, y: node.in.y - node.y }; const b = { x: node.out.x - node.x, y: node.out.y - node.y }; const la = Math.hypot(a.x, a.y); const lb = Math.hypot(b.x, b.y); return la > 0 && lb > 0 && (a.x * b.x + a.y * b.y) / (la * lb) < -0.98; }; export const across = (anchor: Point, handle: Point, length: number): Point => { const dx = anchor.x - handle.x; const dy = anchor.y - handle.y; const l = Math.hypot(dx, dy) || 1; return { x: anchor.x + (dx / l) * length, y: anchor.y + (dy / l) * length }; };