UNPKG

@joemaddalone/path

Version:

a simple svg path generation utility

1,050 lines (1,045 loc) 42.5 kB
"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;