Skip to content

Instantly share code, notes, and snippets.

@ngbrown
Last active April 14, 2024 18:06
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ngbrown/89410bff16f844e9cc23a364fb6930e1 to your computer and use it in GitHub Desktop.
Save ngbrown/89410bff16f844e9cc23a364fb6930e1 to your computer and use it in GitHub Desktop.
SVG Path Builder (TypeScript)
// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#path_commands
// https://developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths
// M moveto (x y)+
// Z closepath (none)
// L lineto (x y)+
// H horizontal lineto x+
// V vertical lineto y+
// C curveto (x1 y1 x2 y2 x y)+
// S smooth curveto (x2 y2 x y)+
// Q quadratic Bézier curveto (x1 y1 x y)+
// T smooth quadratic Bézier curveto (x y)+
// A elliptical arc (rx ry x-axis-rotation large-arc-flag sweep-flag x y)+
export class SvgPathBuilder {
private _pathCommands: string[] = [];
private _continuationCommand: string | null = null;
private _precision = 8;
private _compactMode = true;
toString() {
return this._pathCommands.join(" ");
}
move(x: number, y: number): this {
return this._push("M", `${this._n(x)},${this._n(y)}`);
}
moveOffset(dx: number, dy: number): this {
return this._push("m", `${this._n(dx)},${this._n(dy)}`);
}
line(x: number, y: number): this {
return this._push("L", `${this._n(x)},${this._n(y)}`);
}
lineOffset(dx: number, dy: number): this {
return this._push("l", `${this._n(dx)},${this._n(dy)}`);
}
arc(
rx: number,
ry: number,
angle: number,
largeArcFlag: boolean,
sweepFlag: boolean,
x: number,
y: number
): this {
return this._push(
"A",
`${this._n(rx)},${this._n(ry)} ${this._n(angle)},${
largeArcFlag ? 1 : 0
},${sweepFlag ? 1 : 0} ${this._n(x)},${this._n(y)}`
);
}
arcOffset(
rx: number,
ry: number,
angle: number,
largeArcFlag: boolean,
sweepFlag: boolean,
dx: number,
dy: number
): this {
return this._push(
"a",
`${this._n(rx)},${this._n(ry)} ${this._n(angle)},${
largeArcFlag ? 1 : 0
},${sweepFlag ? 1 : 0} ${this._n(dx)},${this._n(dy)}`
);
}
/**
* Draw a closed circle with four arc segments
* @param x Center point of circle
* @param y Center point of circle
* @param r Radius of circle
* @param sweepFlag clockwise turning arc (true) or else counterclockwise turning arc (false), which is the default.
*/
circle(x: number, y: number, r: number, sweepFlag = false): this {
if (sweepFlag) {
return this.move(x, y - r)
.arcOffset(r, r, 0, false, true, r, r)
.arcOffset(r, r, 0, false, true, -r, r)
.arcOffset(r, r, 0, false, true, -r, -r)
.arcOffset(r, r, 0, false, true, r, -r)
.closePath();
} else {
return this.move(x, y - r)
.arcOffset(r, r, 0, false, false, -r, r)
.arcOffset(r, r, 0, false, false, r, r)
.arcOffset(r, r, 0, false, false, r, -r)
.arcOffset(r, r, 0, false, false, -r, -r)
.closePath();
}
}
closePath() {
return this._push("Z");
}
private _push(command: string, parameters?: string): this {
if (parameters == null) {
this._pathCommands.push(command);
} else if (this._continuationCommand === command) {
this._pathCommands.push(parameters);
} else {
this._pathCommands.push(`${command}${parameters}`);
}
this._continuationCommand =
command === "M" ? "L" : command === "m" ? "l" : command;
return this;
}
private _n(value: number): string {
// noinspection SuspiciousTypeOfGuard
if (typeof value !== "number") {
throw Error("Value is not a number");
}
if (value === 0) {
return "0";
}
if (Number.isInteger(value)) {
return value.toString();
}
const withPrecision = value.toPrecision(this._precision);
if (this._compactMode) {
const rawValue = value.toString();
return rawValue.length < withPrecision.length ? rawValue : withPrecision;
} else {
return withPrecision;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment