@joemaddalone/path
Version:
a simple svg path generation utility
1,050 lines (1,045 loc) • 42.5 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
// src/index.ts
var index_exports = {};
__export(index_exports, {
default: () => Path
});
module.exports = __toCommonJS(index_exports);
// src/docs.ts
var docs_default = {
A: { args: ["rx", "ry", "rotation", "arc", "sweep", "ex", "ey"] },
a: { args: ["rx", "ry", "rotation", "arc", "sweep", "ex", "ey"] },
C: { args: ["cx1", "cy1", "cx2", "cy2", "ex", "ey"] },
c: { args: ["cx1", "cy1", "cx2", "cy2", "ex", "ey"] },
H: { args: ["x"] },
h: { args: ["x"] },
L: { args: ["x", "y"] },
l: { args: ["x", "y"] },
M: { args: ["x", "y"] },
m: { args: ["x", "y"] },
Q: { args: ["cx", "cy", "ex", "ey"] },
q: { args: ["cx", "cy", "ex", "ey"] },
S: { args: ["cx", "cy", "ex", "ey"] },
s: { args: ["cx", "cy", "ex", "ey"] },
T: { args: ["ex", "ey"] },
t: { args: ["ex", "ey"] },
V: { args: ["y"] },
v: { args: ["y"] },
z: { args: [] }
};
// src/utils/math.ts
var angleInRadians = (angle) => angle * Math.PI / 180;
var polarToCartesian = (cx, cy, radius, angle) => {
const radians = angleInRadians(angle);
return {
x: cx + radius * Math.cos(radians),
y: cy + radius * Math.sin(radians)
};
};
var clockwisePoint = (cx, cy, radius, angle) => {
const a = angle - 90;
return polarToCartesian(cx, cy, radius, a);
};
var radialPoints = (radius, cx, cy, numOfPoints, offsetAngle = -0.5 * Math.PI, vertexSkip = 1) => {
radius = radius || 1e-10;
const baseAngle = 2 * Math.PI * vertexSkip / numOfPoints;
const vertexIndices = Array.from(
Array(numOfPoints >= 0 ? numOfPoints : 0).keys()
);
const precision = Math.max(0, 4 - Math.floor(Math.log10(radius)));
return vertexIndices.map((_, index) => {
const currentAngle = index * baseAngle + offsetAngle;
return [
parseFloat((cx + radius * Math.cos(currentAngle)).toFixed(precision)),
parseFloat((cy + radius * Math.sin(currentAngle)).toFixed(precision))
];
});
};
var positionByArray = (size, shape, sx, sy) => {
const response = [];
const halfSize = size / 2;
shape.forEach((r, ri) => {
r.forEach((c, ci) => {
if (c) {
response.push({
size,
cx: ci * size + halfSize + sx,
cy: ri * size + halfSize + sy,
ri,
ci,
value: c
});
}
});
});
return response;
};
// src/index.ts
var _Path = class _Path {
/**
* Constructor - Initializes a new Path instance
*
* Creates an empty path with no commands or attributes.
* Returns the instance for method chaining.
*
* @returns {Path} The initialized Path instance
*/
constructor() {
/** Array to store SVG path command strings */
__publicField(this, "pathData");
// ============================================================================
// SVG PATH COMMANDS - SHORTCUT METHODS
// ============================================================================
/** Move to position (x, y) - relative coordinates */
__publicField(this, "m", (x, y) => this.moveTo(x, y, true));
/** Move to position (x, y) - absolute coordinates */
__publicField(this, "M", (x, y) => this.moveTo(x, y));
/** Draw line to position (x, y) - relative coordinates */
__publicField(this, "l", (x, y) => this.lineTo(x, y, true));
/** Draw line to position (x, y) - absolute coordinates */
__publicField(this, "L", (x, y) => this.lineTo(x, y));
/** Draw horizontal line to x - absolute coordinates */
__publicField(this, "H", (x) => this.horizontalTo(x));
/** Draw horizontal line to x - relative coordinates */
__publicField(this, "h", (x) => this.horizontalTo(x, true));
/** Draw vertical line to y - absolute coordinates */
__publicField(this, "V", (y) => this.verticalTo(y));
/** Draw vertical line to y - relative coordinates */
__publicField(this, "v", (y) => this.verticalTo(y, true));
/** Draw quadratic curve - absolute coordinates */
__publicField(this, "Q", (cx, cy, ex, ey) => this.qCurve(cx, cy, ex, ey));
/** Draw quadratic curve - relative coordinates */
__publicField(this, "q", (cx, cy, ex, ey) => this.qCurve(cx, cy, ex, ey, true));
/** Draw smooth quadratic curve - absolute coordinates */
__publicField(this, "T", (ex, ey) => this.tCurveTo(ex, ey));
/** Draw smooth quadratic curve - relative coordinates */
__publicField(this, "t", (ex, ey) => this.tCurveTo(ex, ey, true));
/** Draw cubic curve - absolute coordinates */
__publicField(this, "C", (cx1, cy1, cx2, cy2, ex, ey) => this.cCurve(cx1, cy1, cx2, cy2, ex, ey));
/** Draw cubic curve - relative coordinates */
__publicField(this, "c", (cx1, cy1, cx2, cy2, ex, ey) => this.cCurve(cx1, cy1, cx2, cy2, ex, ey, true));
/** Draw smooth cubic curve - absolute coordinates */
__publicField(this, "S", (cx, cy, ex, ey) => this.sCurveTo(cx, cy, ex, ey));
/** Draw smooth cubic curve - relative coordinates */
__publicField(this, "s", (cx, cy, ex, ey) => this.sCurveTo(cx, cy, ex, ey, true));
/** Draw arc - absolute coordinates */
__publicField(this, "A", (rx, ry, rotation, arc, sweep, ex, ey) => this.arc(rx, ry, rotation, arc, sweep, ex, ey));
/** Draw arc - relative coordinates */
__publicField(this, "a", (rx, ry, rotation, arc, sweep, ex, ey) => this.arc(rx, ry, rotation, arc, sweep, ex, ey, true));
/** Close path - absolute coordinates */
__publicField(this, "Z", () => this.close());
/** Close path - relative coordinates */
__publicField(this, "z", () => this.close());
// ============================================================================
// FRIENDLY PATH COMMAND METHODS
// ============================================================================
/**
* Move SVG cursor to position (x, y)
*
* This is the foundation command that sets the starting point for subsequent
* drawing operations. If relative is true, coordinates are relative to the
* current cursor position.
*
* @param {number} x - X coordinate
* @param {number} y - Y coordinate
* @param {boolean} relative - Whether coordinates are relative (default: false)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "moveTo", (x, y, relative = false) => {
this.pathData.push(`${relative ? "m" : "M"}${x} ${y}`);
return this;
});
/**
* Draw a straight line to position (x, y)
*
* Creates a line segment from the current cursor position to the specified
* coordinates. If relative is true, coordinates are relative to current position.
*
* @param {number} x - X coordinate
* @param {number} y - Y coordinate
* @param {boolean} relative - Whether coordinates are relative (default: false)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "lineTo", (x, y, relative = false) => {
this.pathData.push(`${relative ? "l" : "L"}${x} ${y}`);
return this;
});
/**
* Draw a horizontal line to x coordinate
*
* Creates a horizontal line segment from the current cursor position.
* Only the x coordinate changes; y remains the same.
*
* @param {number} x - X coordinate
* @param {boolean} relative - Whether x is relative to current position (default: false)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "horizontalTo", (x, relative = false) => {
this.pathData.push(`${relative ? "h" : "H"}${x}`);
return this;
});
/**
* Draw a vertical line to y coordinate
*
* Creates a vertical line segment from the current cursor position.
* Only the y coordinate changes; x remains the same.
*
* @param {number} x - Y coordinate (parameter name is x for consistency)
* @param {boolean} relative - Whether y is relative to current position (default: false)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "verticalTo", (x, relative = false) => {
this.pathData.push(`${relative ? "v" : "V"}${x}`);
return this;
});
/**
* Draw a quadratic Bézier curve
*
* Creates a quadratic curve using a single control point (cx, cy) to define
* the curve shape, ending at (ex, ey). The curve will pass through the
* control point's influence area.
*
* @param {number} cx - Control point X coordinate
* @param {number} cy - Control point Y coordinate
* @param {number} ex - End point X coordinate
* @param {number} ey - End point Y coordinate
* @param {boolean} relative - Whether coordinates are relative (default: false)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "qCurve", (cx, cy, ex, ey, relative = false) => {
this.pathData.push(`${relative ? "q" : "Q"}${cx} ${cy} ${ex} ${ey}`);
return this;
});
/**
* Draw a smooth quadratic Bézier curve
*
* Creates a quadratic curve that smoothly continues from the previous curve.
* The control point is automatically calculated based on the previous curve's
* end point, creating a smooth transition.
*
* @param {number} ex - End point X coordinate
* @param {number} ey - End point Y coordinate
* @param {boolean} relative - Whether coordinates are relative (default: false)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "tCurveTo", (ex, ey, relative = false) => {
this.pathData.push(`${relative ? "t" : "T"}${ex} ${ey}`);
return this;
});
/**
* Draw a cubic Bézier curve
*
* Creates a cubic curve using two control points (cx1, cy1) and (cx2, cy2)
* to define the curve shape, ending at (ex, ey). This provides more control
* over the curve than quadratic curves.
*
* @param {number} cx1 - First control point X coordinate
* @param {number} cy1 - First control point Y coordinate
* @param {number} cx2 - Second control point X coordinate
* @param {number} cy2 - Second control point Y coordinate
* @param {number} ex - End point X coordinate
* @param {number} ey - End point Y coordinate
* @param {boolean} relative - Whether coordinates are relative (default: false)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "cCurve", (cx1, cy1, cx2, cy2, ex, ey, relative = false) => {
this.pathData.push(
`${relative ? "c" : "C"}${cx1} ${cy1} ${cx2} ${cy2} ${ex} ${ey}`
);
return this;
});
/**
* Draw a smooth cubic Bézier curve
*
* Creates a cubic curve that smoothly continues from the previous curve.
* The first control point is automatically calculated, while the second
* control point (cx, cy) is explicitly specified.
*
* @param {number} cx - Second control point X coordinate
* @param {number} cy - Second control point Y coordinate
* @param {number} ex - End point X coordinate
* @param {number} ey - End point Y coordinate
* @param {boolean} relative - Whether coordinates are relative (default: false)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "sCurveTo", (cx, cy, ex, ey, relative = false) => {
this.pathData.push(`${relative ? "s" : "S"}${cx} ${cy} ${ex} ${ey}`);
return this;
});
/**
* Draw an elliptical arc
*
* Creates an arc segment of an ellipse. The arc is defined by:
* - rx, ry: x and y radius of the ellipse
* - rotation: rotation of the ellipse in degrees
* - arc: large arc flag (0 = small arc, 1 = large arc)
* - sweep: sweep flag (0 = counterclockwise, 1 = clockwise)
* - ex, ey: end point coordinates
*
* @param {number} rx - X radius of the ellipse
* @param {number} ry - Y radius of the ellipse
* @param {number} rotation - Rotation of the ellipse in degrees
* @param {1|0} arc - Large arc flag (0 = small, 1 = large)
* @param {1|0} sweep - Sweep flag (0 = counterclockwise, 1 = clockwise)
* @param {number} ex - End point X coordinate
* @param {number} ey - End point Y coordinate
* @param {boolean} relative - Whether coordinates are relative (default: false)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "arc", (rx, ry, rotation, arc, sweep, ex, ey, relative = false) => {
this.pathData.push(
`${relative ? "a" : "A"}${rx} ${ry} ${rotation} ${arc} ${sweep} ${ex} ${ey}`
);
return this;
});
/**
* Close the current path
*
* Draws a straight line from the current position back to the starting point
* of the current subpath, effectively closing the shape.
*
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "close", () => {
this.pathData.push("z");
return this;
});
// ============================================================================
// CONVENIENCE DIRECTIONAL METHODS
// ============================================================================
/** Move down by specified pixels (positive y direction) */
__publicField(this, "down", (px) => this.v(px));
/** Move up by specified pixels (negative y direction) */
__publicField(this, "up", (px) => this.v(px * -1));
/** Move right by specified pixels (positive x direction) */
__publicField(this, "right", (px) => this.h(px));
/** Move left by specified pixels (negative x direction) */
__publicField(this, "left", (px) => this.h(px * -1));
// ============================================================================
// UTILITY AND OUTPUT METHODS
// ============================================================================
/**
* Get the path data as an array of command strings
*
* Returns the internal pathData array containing all SVG path commands
* that have been added to this path instance.
*
* @returns {string[]} Array of SVG path command strings
*/
__publicField(this, "toArray", () => this.pathData);
/**
* Convert the path to an SVG path data string
*
* Joins all path commands into a single string suitable for use
* as the 'd' attribute of an SVG path element.
*
* @returns {string} SVG path data string
*/
__publicField(this, "toString", () => this.pathData.join(""));
/**
* Convert path commands to structured array format
*
* Parses the path data into an array where each element contains:
* - First element: the command letter (M, L, Q, etc.)
* - Subsequent elements: the numeric parameters for that command
*
* @returns {Array<(string|number)[]>} Array of command arrays
*
* @example
* // For path "M10 10 L20 20"
* // Returns: [["M", 10, 10], ["L", 20, 20]]
*/
__publicField(this, "toCommands", () => {
return this.pathData.map((cmd) => {
const result = [cmd.substr(0, 1)];
const args = cmd.substr(1);
if (args.length) {
result.push(...args.split(" ").map(Number));
}
return result;
});
});
/**
* Convert path commands to annotated format with named parameters
*
* Uses the docs mapping to convert numeric parameters to named parameters
* based on the command type. This makes the path data more readable
* and self-documenting.
*
* @returns {Array<{fn: string, args?: Record<string, number>}>} Array of annotated commands
*
* @example
* // For path "M10 10 L20 20"
* // Returns: [{fn: "M", args: {x: 10, y: 10}}, {fn: "L", args: {x: 20, y: 20}}]
*/
__publicField(this, "toAnnotatedCommands", () => {
const commands = this.toCommands();
const mapped = commands.map((cmd) => {
const fn = String(cmd.shift());
const args = docs_default[fn]?.args;
if (args.length) {
return {
fn,
args: args.reduce((acc, argName, index) => {
acc[argName] = cmd[index];
return acc;
}, {})
};
} else {
return { fn };
}
});
return mapped;
});
/**
* Create an SVG path element from the current path
*
* Generates a DOM SVG path element with all the accumulated path data
* and attributes. This is useful for programmatically creating SVG elements
* that can be inserted into the DOM.
*
* @param {Record<string, any>} attributes - Additional attributes to apply to the element
* @returns {SVGPathElement} SVG path element
*/
__publicField(this, "toElement", (attributes = {}) => {
const addAttributes = { ...attributes };
const el = document.createElementNS("http://www.w3.org/2000/svg", "path");
Object.keys(addAttributes).forEach((key) => {
el.setAttribute(
key,
String(addAttributes[key])
);
});
el.setAttribute("d", this.toString());
return el;
});
// ============================================================================
// BASIC SHAPE METHODS
// ============================================================================
/**
* Create a circle
*
* Draws a perfect circle using two arc commands. The circle is centered
* at (cx, cy) with the specified size as diameter.
*
* @param {number} size - Diameter of the circle
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "circle", (size, cx, cy, centerEnd = true) => this.ellipse(size, size, cx, cy, centerEnd));
/**
* Create an ellipse
*
* Draws an ellipse using two arc commands. The ellipse is centered
* at (cx, cy) with the specified width and height as dimensions.
*
* @param {number} width - Width of the ellipse
* @param {number} height - Height of the ellipse
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "ellipse", (width, height, cx, cy, centerEnd = true) => {
const rx = width / 2;
const ry = height / 2;
this.M(cx + rx, cy).A(rx, ry, 0, 0, 1, cx - rx, cy).A(rx, ry, 0, 0, 1, cx + rx, cy).close();
if (centerEnd) {
this.M(cx, cy);
}
return this;
});
/**
* Create a kite shape
*
* Draws a kite (diamond-like shape) with adjustable height offset.
* The kite has four points: top, left, bottom, and right.
*
* @param {number} width - Width of the kite
* @param {number} height - Height of the kite
* @param {number} dh - Height offset for left/right points (defaults to height * 0.33)
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "kite", (width, height, dh, cx, cy, centerEnd = true) => {
dh = dh || Math.round(height * 0.33);
const [t, _, b] = _Path.radialPoints(height / 2, cx, cy, 4);
const h = Number(t[1]) + dh;
const points = [
[Number(t[0]), Number(t[1])],
[cx - width / 2, h],
[Number(b[0]), Number(b[1])],
[cx + width / 2, h]
];
this.polyline(points).close();
if (centerEnd) {
this.M(cx, cy);
}
return this;
});
/**
* Create a polygon from an array of points
*
* Draws a closed polygon by connecting the provided points in sequence.
* The polygon automatically closes by drawing a line back to the first point.
*
* @param {number[][]} points - Array of [x, y] coordinate pairs
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "polygon", (points) => {
this.polyline(points).close();
return this;
});
/**
* Create a polygram (star-like polygon)
*
* Draws a polygram by connecting vertices with a specified skip pattern.
* For example, with vertexSkip=2, it connects every other vertex, creating
* a star pattern.
*
* @param {number} size - Size of the polygram
* @param {number} points - Number of vertices
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {number} vertexSkip - How many vertices to skip when connecting (default: 2)
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "polygram", (size, points, cx, cy, vertexSkip = 2, centerEnd = true) => {
this.polygon(
_Path.radialPoints(size / 2, cx, cy, points, void 0, vertexSkip)
);
if (centerEnd) {
this.M(cx, cy);
}
return this;
});
/**
* Create a polyline from an array of points
*
* Draws a series of connected line segments through the provided points.
* Unlike polygon, this does not automatically close the shape.
*
* @param {number[][]} points - Array of [x, y] coordinate pairs
* @param {boolean} relative - Whether coordinates are relative (default: false)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "polyline", (points, relative = false) => {
const clone = [...points];
const start = clone.shift();
const move = relative ? this.m : this.M;
const line = relative ? this.l : this.L;
move.apply(null, start);
clone.forEach((val) => {
line.apply(null, val);
});
return this;
});
/**
* Create a rectangle
*
* Draws a rectangle centered at (cx, cy) with the specified width and height.
* Uses the convenience directional methods for clean, readable code.
*
* @param {number} width - Width of the rectangle
* @param {number} height - Height of the rectangle
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "rect", (width, height, cx, cy, centerEnd = true) => {
this.M(cx - width / 2, cy - height / 2).right(width).down(height).left(width).up(height);
if (centerEnd) {
this.M(cx, cy);
}
return this;
});
/**
* Create a regular polygon
*
* Draws a regular polygon with equal sides and angles. The polygon is
* centered at (cx, cy) and inscribed in a circle of the specified size.
*
* @param {number} size - Diameter of the circumscribed circle
* @param {number} sides - Number of sides in the polygon
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "regPolygon", (size, sides, cx, cy, centerEnd = true) => {
this.polygon(_Path.radialPoints(size / 2, cx, cy, sides));
if (centerEnd) {
this.M(cx, cy);
}
return this;
});
/**
* Create a rounded rectangle
*
* Draws a rectangle with rounded corners. The radius is automatically
* adjusted if it exceeds half the width or height of the rectangle.
*
* @param {number} width - Width of the rectangle
* @param {number} height - Height of the rectangle
* @param {number} radius - Corner radius
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "roundedRect", (width, height, radius, cx, cy, centerEnd = true) => {
const top = cy - height / 2;
const left = cx - width / 2;
const right = left + width;
const bottom = top + height;
let rx = Math.min(radius, width / 2);
rx = rx < 0 ? 0 : rx;
let ry = Math.min(radius, height / 2);
ry = ry < 0 ? 0 : ry;
const wr = Math.max(width - rx * 2, 0);
const hr = Math.max(height - ry * 2, 0);
this.M(left + rx, top).right(wr).A(rx, ry, 0, 0, 1, right, top + ry).down(hr).A(rx, ry, 0, 0, 1, right - rx, bottom).left(wr).A(rx, ry, 0, 0, 1, left, bottom - ry).up(hr).A(rx, ry, 0, 0, 1, left + rx, top).M(left, top);
if (centerEnd) {
this.M(cx, cy);
}
return this;
});
/**
* Create a rounded square
*
* Convenience method for creating a square with rounded corners.
*
* @param {number} size - Size of the square
* @param {number} radius - Corner radius
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "roundedSquare", (size, radius, cx, cy, centerEnd = true) => {
return this.roundedRect(size, size, radius, cx, cy, centerEnd);
});
/**
* Create a square
*
* Convenience method for creating a square (equal width and height).
*
* @param {number} size - Size of the square
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "square", (size, cx, cy, centerEnd = true) => {
return this.rect(size, size, cx, cy, centerEnd);
});
/**
* Create a star shape
*
* Draws a star by alternating between inner and outer radius points.
* The star has the specified number of points and uses two different
* radii to create the characteristic star appearance.
*
* @param {number} outerSize - Diameter of outer circle for star points
* @param {number} innerSize - Diameter of inner circle for star valleys
* @param {number} points - Number of star points
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "star", (outerSize, innerSize, points, cx, cy, centerEnd = true) => {
const innerRadius = innerSize / 2;
const outerRadius = outerSize / 2;
const increment = 360 / (points * 2);
const vertexIndices = Array.from({ length: points * 2 });
const verts = vertexIndices.map((p, i) => {
let radius = i % 2 == 0 ? outerRadius : innerRadius;
let degrees = increment * i;
const { x, y } = _Path.clockwisePoint(cx, cy, radius, degrees);
return [x, y];
});
this.polygon(verts);
if (centerEnd) {
this.M(cx, cy);
}
return this;
});
/**
* Create a triangle
*
* Draws an equilateral triangle centered at (cx, cy) with the specified size.
* The triangle points upward by default.
*
* @param {number} size - Size of the triangle (length of each side)
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "triangle", (size, cx, cy, centerEnd = true) => {
const sq3 = Math.sqrt(3);
const a = [cx, cy - sq3 / 3 * size];
const b = [cx - size / 2, cy + sq3 / 6 * size];
const c = [cx + size / 2, cy + sq3 / 6 * size];
this.polygon([a, b, c]);
if (centerEnd) {
this.M(cx, cy);
}
return this;
});
// ============================================================================
// COMPLEX SHAPE METHODS
// ============================================================================
/**
* Create a sector (pie slice)
*
* Draws a pie slice from startAngle to endAngle. The sector is filled
* and includes lines from the center to both edges.
*
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {number} size - Diameter of the circle
* @param {number} startAngle - Starting angle in degrees
* @param {number} endAngle - Ending angle in degrees
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "sector", (cx, cy, size, startAngle, endAngle, centerEnd = true) => {
const radius = size / 2;
const start = _Path.clockwisePoint(cx, cy, radius, endAngle);
const end = _Path.clockwisePoint(cx, cy, radius, startAngle);
const arcSweep = endAngle - startAngle <= 180 ? 0 : 1;
this.M(start.x, start.y).A(radius, radius, 0, arcSweep, 0, end.x, end.y).L(cx, cy).L(start.x, start.y);
if (centerEnd) {
this.M(cx, cy);
}
return this;
});
/**
* Create a segment (arc without center lines)
*
* Draws an arc segment from startAngle to endAngle without filling
* the center area. This creates just the curved edge.
*
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {number} size - Diameter of the circle
* @param {number} startAngle - Starting angle in degrees
* @param {number} endAngle - Ending angle in degrees
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "segment", (cx, cy, size, startAngle, endAngle, centerEnd = true) => {
const radius = size / 2;
const start = _Path.clockwisePoint(cx, cy, radius, endAngle);
const end = _Path.clockwisePoint(cx, cy, radius, startAngle);
const arcSweep = endAngle - startAngle <= 180 ? 0 : 1;
this.M(start.x, start.y).A(radius, radius, 0, arcSweep, 0, end.x, end.y);
if (centerEnd) {
this.M(cx, cy);
}
return this;
});
/**
* Create radial lines pattern
*
* Draws lines radiating from inner to outer circles at regular intervals.
* Creates a sunburst or starburst effect.
*
* @param {number} outerSize - Diameter of outer circle
* @param {number} innerSize - Diameter of inner circle
* @param {number} points - Number of radial lines
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "radialLines", (outerSize, innerSize, points, cx, cy, centerEnd = true) => {
const inner = _Path.radialPoints(innerSize / 2, cx, cy, points);
const outer = _Path.radialPoints(outerSize / 2, cx, cy, points);
inner.forEach((coords, index) => {
this.M(coords[0], coords[1]).L(outer[index][0], outer[index][1]);
});
if (centerEnd) {
this.M(cx, cy);
}
return this;
});
/**
* Create a lens shape
*
* Draws a lens (oval with pointed ends) using quadratic curves.
* The lens is centered at (cx, cy) with the specified width and height.
*
* @param {number} width - Width of the lens
* @param {number} height - Height of the lens
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "lens", (width, height, cx, cy, centerEnd = true) => {
this.M(cx - width / 2, cy).Q(cx, cy - height, cx + width / 2, cy).Q(cx, cy + height, cx - width / 2, cy);
if (centerEnd) {
this.M(cx, cy);
}
return this;
});
/**
* Create an omino pattern
*
* Draws a pattern based on a grid arrangement where each cell can be
* connected to its neighbors. The shape array defines which cells are
* occupied, and the method draws lines between adjacent cells.
*
* @param {number} size - Size of each grid cell
* @param {any[]} shape - 2D array defining the pattern (1 = occupied, 0 = empty)
* @param {number} sx - Starting X coordinate
* @param {number} sy - Starting Y coordinate
* @param {boolean} lined - Whether to always draw lines (default: false)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "omino", (size, shape, sx, sy, lined = false) => {
const arrangement = _Path.positionByArray(size, shape, sx, sy);
arrangement.forEach((r, _, arr) => {
const { cx, cy, ri, ci, size: size2 } = r;
const halfSize = size2 / 2;
const hasLeftSib = arr.find((a) => a.ri === ri && a.ci === ci - 1);
const hasRightSib = arr.find((a) => a.ri === ri && a.ci === ci + 1);
const hasUpSib = arr.find((a) => a.ri === ri - 1 && a.ci === ci);
const hasDownSib = arr.find((a) => a.ri === ri + 1 && a.ci === ci);
const left = cx - halfSize;
const right = cx + halfSize;
const top = cy - halfSize;
const bottom = cy + halfSize;
if (!hasLeftSib || lined) {
this.M(left, top);
this.v(size2);
}
if (!hasRightSib) {
this.M(right, top);
this.v(size2);
}
if (!hasUpSib || lined) {
this.M(left, top);
this.h(size2);
}
if (!hasDownSib) {
this.M(left, bottom);
this.h(size2);
}
});
return this;
});
/**
* Create an isometric cube
*
* Draws a hexagon (top view of cube) with lines extending from inner
* to outer points to create a 3D cube effect.
*
* @param {number} size - Size of the cube
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "isocube", (size, cx, cy, centerEnd = true) => {
this.regPolygon(size, 6, cx, cy, centerEnd);
const inner = _Path.radialPoints(0 / 2, cx, cy, 6);
const outer = _Path.radialPoints(size / 2, cx, cy, 6);
const top = [1, 3, 5];
top.forEach((index) => {
this.M(Number(inner[index][0]), Number(inner[index][1])).L(
Number(outer[index][0]),
Number(outer[index][1])
);
});
if (centerEnd) {
this.M(cx, cy);
}
return this;
});
// ============================================================================
// SYMMETRY PATTERN METHODS
// ============================================================================
/**
* Create a cross shape
*
* Draws a cross with horizontal and vertical lines intersecting at center.
* The cross extends width/2 pixels left and right, height/2 pixels up and down.
*
* @param {number} width - Width of the cross
* @param {number} height - Height of the cross
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "cross", (width, height, cx, cy, centerEnd = true) => {
const l = cx - width / 2;
const r = l + width;
const t = cy - height / 2;
const b = t + height;
this.M(l, cy).L(r, cy).M(cx, b).L(cx, t);
if (centerEnd) {
this.M(cx, cy);
}
return this;
});
/**
* Create an H-shaped symmetry pattern
*
* Draws an H shape with vertical lines on left and right sides,
* connected by a horizontal line in the center.
*
* @param {number} width - Width of the H pattern
* @param {number} height - Height of the H pattern
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "symH", (width, height, cx, cy, centerEnd = true) => {
const l = cx - width / 2;
const r = l + width;
const t = cy - height / 2;
const b = t + height;
this.M(l, t).L(l, b).M(l, cy).L(r, cy).M(r, t).L(r, b);
if (centerEnd) {
this.M(cx, cy);
}
return this;
});
/**
* Create an I-shaped symmetry pattern
*
* Draws an I shape with horizontal lines on top and bottom,
* connected by a vertical line in the center.
*
* @param {number} width - Width of the I pattern
* @param {number} height - Height of the I pattern
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "symI", (width, height, cx, cy, centerEnd = true) => {
const l = cx - width / 2;
const r = l + width;
const t = cy - height / 2;
const b = t + height;
this.M(l, t).L(r, t).M(cx, t).L(cx, b).M(l, b).L(r, b);
if (centerEnd) {
this.M(cx, cy);
}
return this;
});
/**
* Create a V-shaped symmetry pattern
*
* Draws a V shape with lines from the top corners meeting at the center bottom.
*
* @param {number} width - Width of the V pattern
* @param {number} height - Height of the V pattern
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "symV", (width, height, cx, cy, centerEnd = true) => {
const l = cx - width / 2;
const r = l + width;
const t = cy - height / 2;
const b = t + height;
this.M(l, t).L(cx, b).L(r, t);
if (centerEnd) {
this.M(cx, cy);
}
return this;
});
/**
* Create an X-shaped symmetry pattern
*
* Draws an X shape with diagonal lines crossing at the center.
*
* @param {number} width - Width of the X pattern
* @param {number} height - Height of the X pattern
* @param {number} cx - Center X coordinate
* @param {number} cy - Center Y coordinate
* @param {boolean} centerEnd - Whether to end at center (default: true)
* @returns {Path} The Path instance for chaining
*/
__publicField(this, "symX", (width, height, cx, cy, centerEnd = true) => {
const l = cx - width / 2;
const r = l + width;
const t = cy - height / 2;
const b = t + height;
this.M(l, t).L(r, b).M(l, b).L(r, t);
if (centerEnd) {
this.M(cx, cy);
}
return this;
});
this.pathData = [];
return this;
}
};
// ============================================================================
// STATIC UTILITY METHODS
// ============================================================================
/** Convert angle from degrees to radians */
__publicField(_Path, "angleInRadians", angleInRadians);
/** Convert polar coordinates (radius, angle) to Cartesian coordinates (x, y) */
__publicField(_Path, "polarToCartesian", polarToCartesian);
/** Calculate point position clockwise from center at given angle and radius */
__publicField(_Path, "clockwisePoint", clockwisePoint);
/** Generate array of points in a circle at given radius and center */
__publicField(_Path, "radialPoints", radialPoints);
/** Position elements in a grid based on array configuration */
__publicField(_Path, "positionByArray", positionByArray);
/**
* Macro system - Dynamically add methods to Path prototype
*
* This allows for runtime extension of the Path class with custom methods.
* Useful for adding domain-specific path building functionality.
*
* @param {string} name - The name of the method to add
* @param {Function} fn - The function to add as a method
* @returns {Function} The added function
*
* @example
* Path.macro('zigzag', function(width, height) {
* return this.M(0, 0).l(width/2, height).l(width/2, -height);
* });
*/
__publicField(_Path, "macro", (name, fn) => {
_Path.prototype[name] = fn;
return fn;
});
var Path = _Path;