Last active
April 14, 2024 18:06
-
-
Save ngbrown/89410bff16f844e9cc23a364fb6930e1 to your computer and use it in GitHub Desktop.
SVG Path Builder (TypeScript)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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