UNPKG

path2d

Version:

Path2D API for node. Can be used for server-side rendering with canvas

831 lines (825 loc) 25.8 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; 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); // src/index.ts var index_exports = {}; __export(index_exports, { Path2D: () => Path2D, applyPath2DToCanvasRenderingContext: () => applyPath2DToCanvasRenderingContext, applyRoundRectToCanvasRenderingContext2D: () => applyRoundRectToCanvasRenderingContext2D, applyRoundRectToPath2D: () => applyRoundRectToPath2D, parsePath: () => parsePath, roundRect: () => roundRect }); module.exports = __toCommonJS(index_exports); // src/parse-path.ts var ARG_LENGTH = { a: 7, // arc: rx, ry, x-axis-rotation, large-arc-flag, sweep-flag, x, y c: 6, // cubic curve: x1, y1, x2, y2, x, y h: 1, // horizontal line: x l: 2, // line: x, y m: 2, // move: x, y q: 4, // quadratic curve: x1, y1, x, y s: 4, // smooth cubic curve: x2, y2, x, y t: 2, // smooth quadratic curve: x, y v: 1, // vertical line: y z: 0 // close path: no arguments }; var SEGMENT_PATTERN = /([astvzqmhlc])([^astvzqmhlc]*)/gi; var NUMBER = /-?[0-9]*\.?[0-9]+(?:e[-+]?\d+)?/gi; function parseValues(args) { const numbers = args.match(NUMBER); return numbers ? numbers.map(Number) : []; } function parsePath(path) { const data = []; const p = String(path).trim(); if (p[0] !== "M" && p[0] !== "m") { return data; } p.replace(SEGMENT_PATTERN, (_, command, args) => { const theArgs = parseValues(args); let type = command.toLowerCase(); let theCommand = command; if (type === "m" && theArgs.length > 2) { data.push([theCommand, ...theArgs.splice(0, 2)]); type = "l"; theCommand = theCommand === "m" ? "l" : "L"; } if (theArgs.length < ARG_LENGTH[type]) { return ""; } data.push([theCommand, ...theArgs.splice(0, ARG_LENGTH[type])]); while (theArgs.length >= ARG_LENGTH[type] && theArgs.length && ARG_LENGTH[type]) { data.push([theCommand, ...theArgs.splice(0, ARG_LENGTH[type])]); } return ""; }); return data; } // src/path2d.ts function rotatePoint(point, angle) { const nx = point.x * Math.cos(angle) - point.y * Math.sin(angle); const ny = point.y * Math.cos(angle) + point.x * Math.sin(angle); point.x = nx; point.y = ny; } function translatePoint(point, dx, dy) { point.x += dx; point.y += dy; } function scalePoint(point, s) { point.x *= s; point.y *= s; } var Path2D = class _Path2D { /** Internal storage for path commands */ #commands; /** * Creates a new Path2D object. * * @param path - Optional path to initialize from. Can be another Path2D object or an SVG path string * * @example * ```typescript * // Empty path * const path1 = new Path2D(); * * // From SVG path string * const path2 = new Path2D("M10,10 L100,100 Z"); * * // Copy from another Path2D * const path3 = new Path2D(path1); * ``` */ constructor(path) { this.#commands = []; if (path && path instanceof _Path2D) { this.#commands.push(...path.#commands); } else if (path) { this.#commands = parsePath(path); } } /** * Adds a custom command to the path's command list. * This is primarily used internally for extending functionality. * * @param command - The path command to add */ addCustomCommand(command) { this.#commands.push(command); } /** * Adds the commands from another Path2D object to this path. * * @param path - The Path2D object whose commands should be added to this path * * @example * ```typescript * const path1 = new Path2D("M10,10 L20,20"); * const path2 = new Path2D("L30,30 Z"); * path1.addPath(path2); // path1 now contains both sets of commands * ``` */ addPath(path) { if (path && path instanceof _Path2D) { this.#commands.push(...path.#commands); } } /** * Moves the starting point of a new sub-path to the specified coordinates. * * @param x - The x-coordinate of the new starting point * @param y - The y-coordinate of the new starting point * * @example * ```typescript * const path = new Path2D(); * path.moveTo(10, 10); * ``` */ moveTo(x, y) { this.#commands.push(["M", x, y]); } /** * Connects the last point in the current sub-path to the specified coordinates with a straight line. * * @param x - The x-coordinate of the end point * @param y - The y-coordinate of the end point * * @example * ```typescript * const path = new Path2D(); * path.moveTo(10, 10); * path.lineTo(100, 100); * ``` */ lineTo(x, y) { this.#commands.push(["L", x, y]); } /** * Adds a circular arc to the current path. * * @param x - The x-coordinate of the arc's center * @param y - The y-coordinate of the arc's center * @param radius - The arc's radius * @param startAngle - The starting angle in radians * @param endAngle - The ending angle in radians * @param counterclockwise - Whether the arc should be drawn counterclockwise (default: false) * * @example * ```typescript * const path = new Path2D(); * path.arc(50, 50, 25, 0, Math.PI * 2); // Full circle * path.arc(100, 100, 30, 0, Math.PI, true); // Half circle, counterclockwise * ``` */ arc(x, y, radius, startAngle, endAngle, counterclockwise) { this.#commands.push(["AC", x, y, radius, startAngle, endAngle, !!counterclockwise]); } /** * Adds an arc to the current path with the given control points and radius. * * @param x1 - The x-coordinate of the first control point * @param y1 - The y-coordinate of the first control point * @param x2 - The x-coordinate of the second control point * @param y2 - The y-coordinate of the second control point * @param r - The arc's radius * * @example * ```typescript * const path = new Path2D(); * path.moveTo(20, 20); * path.arcTo(100, 20, 100, 100, 50); * ``` */ arcTo(x1, y1, x2, y2, r) { this.#commands.push(["AT", x1, y1, x2, y2, r]); } /** * Adds an elliptical arc to the current path. * * @param x - The x-coordinate of the ellipse's center * @param y - The y-coordinate of the ellipse's center * @param radiusX - The ellipse's major-axis radius * @param radiusY - The ellipse's minor-axis radius * @param rotation - The rotation angle of the ellipse in radians * @param startAngle - The starting angle in radians * @param endAngle - The ending angle in radians * @param counterclockwise - Whether the arc should be drawn counterclockwise (default: false) * * @example * ```typescript * const path = new Path2D(); * path.ellipse(50, 50, 30, 20, Math.PI / 4, 0, Math.PI * 2); * ``` */ ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise) { this.#commands.push(["E", x, y, radiusX, radiusY, rotation, startAngle, endAngle, !!counterclockwise]); } /** * Closes the current sub-path by connecting the last point to the first point with a straight line. * * @example * ```typescript * const path = new Path2D(); * path.moveTo(10, 10); * path.lineTo(100, 10); * path.lineTo(100, 100); * path.closePath(); // Creates a triangle * ``` */ closePath() { this.#commands.push(["Z"]); } /** * Adds a cubic Bézier curve to the current path. * * @param cp1x - The x-coordinate of the first control point * @param cp1y - The y-coordinate of the first control point * @param cp2x - The x-coordinate of the second control point * @param cp2y - The y-coordinate of the second control point * @param x - The x-coordinate of the end point * @param y - The y-coordinate of the end point * * @example * ```typescript * const path = new Path2D(); * path.moveTo(20, 20); * path.bezierCurveTo(20, 100, 200, 100, 200, 20); * ``` */ bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) { this.#commands.push(["C", cp1x, cp1y, cp2x, cp2y, x, y]); } /** * Adds a quadratic Bézier curve to the current path. * * @param cpx - The x-coordinate of the control point * @param cpy - The y-coordinate of the control point * @param x - The x-coordinate of the end point * @param y - The y-coordinate of the end point * * @example * ```typescript * const path = new Path2D(); * path.moveTo(20, 20); * path.quadraticCurveTo(100, 100, 200, 20); * ``` */ quadraticCurveTo(cpx, cpy, x, y) { this.#commands.push(["Q", cpx, cpy, x, y]); } /** * Adds a rectangle to the current path. * * @param x - The x-coordinate of the rectangle's top-left corner * @param y - The y-coordinate of the rectangle's top-left corner * @param width - The rectangle's width * @param height - The rectangle's height * * @example * ```typescript * const path = new Path2D(); * path.rect(10, 10, 100, 50); * ``` */ rect(x, y, width, height) { this.#commands.push(["R", x, y, width, height]); } /** * Adds a rounded rectangle to the current path. * * @param x - The x-coordinate of the rectangle's top-left corner * @param y - The y-coordinate of the rectangle's top-left corner * @param w - The rectangle's width * @param h - The rectangle's height * @param radii - The corner radii. Can be a number, DOMPointInit, or array of up to 4 values * * @example * ```typescript * const path = new Path2D(); * path.roundRect(10, 10, 100, 50, 10); // All corners with radius 10 * path.roundRect(10, 70, 100, 50, [10, 20]); // Different horizontal/vertical radii * path.roundRect(10, 130, 100, 50, [5, 10, 15, 20]); // Each corner different * ``` */ roundRect(x, y, w, h, radii) { if (typeof radii === "undefined") { this.#commands.push(["RR", x, y, w, h, 0]); } else { this.#commands.push(["RR", x, y, w, h, radii]); } } /** * Builds the path in a canvas rendering context by executing all stored commands. * This method translates the internal path commands into actual canvas drawing operations. * * @param ctx - The canvas rendering context to draw the path in * * @internal This method is primarily used internally by the polyfill system * to render Path2D objects on contexts that don't natively support them. */ buildPathInCanvas(ctx) { let x = 0; let y = 0; let endAngle; let startAngle; let largeArcFlag; let sweepFlag; let endPoint; let midPoint; let angle; let lambda; let t1; let t2; let x1; let y1; let r; let rx; let ry; let w; let h; let pathType; let centerPoint; let ccw; let radii; let cpx = null; let cpy = null; let qcpx = null; let qcpy = null; let startPoint = null; let currentPoint = null; ctx.beginPath(); for (let i = 0; i < this.#commands.length; ++i) { pathType = this.#commands[i][0]; if (pathType !== "S" && pathType !== "s" && pathType !== "C" && pathType !== "c") { cpx = null; cpy = null; } if (pathType !== "T" && pathType !== "t" && pathType !== "Q" && pathType !== "q") { qcpx = null; qcpy = null; } let c; switch (pathType) { case "m": case "M": c = this.#commands[i]; if (pathType === "m") { x += c[1]; y += c[2]; } else { x = c[1]; y = c[2]; } if (pathType === "M" || !startPoint) { startPoint = { x, y }; } ctx.moveTo(x, y); break; case "l": c = this.#commands[i]; x += c[1]; y += c[2]; ctx.lineTo(x, y); break; case "L": c = this.#commands[i]; x = c[1]; y = c[2]; ctx.lineTo(x, y); break; case "H": c = this.#commands[i]; x = c[1]; ctx.lineTo(x, y); break; case "h": c = this.#commands[i]; x += c[1]; ctx.lineTo(x, y); break; case "V": c = this.#commands[i]; y = c[1]; ctx.lineTo(x, y); break; case "v": c = this.#commands[i]; y += c[1]; ctx.lineTo(x, y); break; case "a": case "A": c = this.#commands[i]; if (currentPoint === null) { throw new Error("This should never happen"); } if (pathType === "a") { x += c[6]; y += c[7]; } else { x = c[6]; y = c[7]; } rx = c[1]; ry = c[2]; angle = c[3] * Math.PI / 180; largeArcFlag = !!c[4]; sweepFlag = !!c[5]; endPoint = { x, y }; midPoint = { x: (currentPoint.x - endPoint.x) / 2, y: (currentPoint.y - endPoint.y) / 2 }; rotatePoint(midPoint, -angle); lambda = midPoint.x * midPoint.x / (rx * rx) + midPoint.y * midPoint.y / (ry * ry); if (lambda > 1) { lambda = Math.sqrt(lambda); rx *= lambda; ry *= lambda; } centerPoint = { x: rx * midPoint.y / ry, y: -(ry * midPoint.x) / rx }; t1 = rx * rx * ry * ry; t2 = rx * rx * midPoint.y * midPoint.y + ry * ry * midPoint.x * midPoint.x; if (sweepFlag !== largeArcFlag) { scalePoint(centerPoint, Math.sqrt((t1 - t2) / t2) || 0); } else { scalePoint(centerPoint, -Math.sqrt((t1 - t2) / t2) || 0); } startAngle = Math.atan2((midPoint.y - centerPoint.y) / ry, (midPoint.x - centerPoint.x) / rx); endAngle = Math.atan2(-(midPoint.y + centerPoint.y) / ry, -(midPoint.x + centerPoint.x) / rx); rotatePoint(centerPoint, angle); translatePoint(centerPoint, (endPoint.x + currentPoint.x) / 2, (endPoint.y + currentPoint.y) / 2); ctx.save(); ctx.translate(centerPoint.x, centerPoint.y); ctx.rotate(angle); ctx.scale(rx, ry); ctx.arc(0, 0, 1, startAngle, endAngle, !sweepFlag); ctx.restore(); break; case "C": c = this.#commands[i]; cpx = c[3]; cpy = c[4]; x = c[5]; y = c[6]; ctx.bezierCurveTo(c[1], c[2], cpx, cpy, x, y); break; case "c": c = this.#commands[i]; ctx.bezierCurveTo(c[1] + x, c[2] + y, c[3] + x, c[4] + y, c[5] + x, c[6] + y); cpx = c[3] + x; cpy = c[4] + y; x += c[5]; y += c[6]; break; case "S": c = this.#commands[i]; if (cpx === null || cpy === null) { cpx = x; cpy = y; } ctx.bezierCurveTo(2 * x - cpx, 2 * y - cpy, c[1], c[2], c[3], c[4]); cpx = c[1]; cpy = c[2]; x = c[3]; y = c[4]; break; case "s": c = this.#commands[i]; if (cpx === null || cpy === null) { cpx = x; cpy = y; } ctx.bezierCurveTo(2 * x - cpx, 2 * y - cpy, c[1] + x, c[2] + y, c[3] + x, c[4] + y); cpx = c[1] + x; cpy = c[2] + y; x += c[3]; y += c[4]; break; case "Q": c = this.#commands[i]; qcpx = c[1]; qcpy = c[2]; x = c[3]; y = c[4]; ctx.quadraticCurveTo(qcpx, qcpy, x, y); break; case "q": c = this.#commands[i]; qcpx = c[1] + x; qcpy = c[2] + y; x += c[3]; y += c[4]; ctx.quadraticCurveTo(qcpx, qcpy, x, y); break; case "T": c = this.#commands[i]; if (qcpx === null || qcpy === null) { qcpx = x; qcpy = y; } qcpx = 2 * x - qcpx; qcpy = 2 * y - qcpy; x = c[1]; y = c[2]; ctx.quadraticCurveTo(qcpx, qcpy, x, y); break; case "t": c = this.#commands[i]; if (qcpx === null || qcpy === null) { qcpx = x; qcpy = y; } qcpx = 2 * x - qcpx; qcpy = 2 * y - qcpy; x += c[1]; y += c[2]; ctx.quadraticCurveTo(qcpx, qcpy, x, y); break; case "z": case "Z": if (startPoint) { x = startPoint.x; y = startPoint.y; } startPoint = null; ctx.closePath(); break; case "AC": c = this.#commands[i]; x = c[1]; y = c[2]; r = c[3]; startAngle = c[4]; endAngle = c[5]; ccw = c[6]; ctx.arc(x, y, r, startAngle, endAngle, ccw); break; case "AT": c = this.#commands[i]; x1 = c[1]; y1 = c[2]; x = c[3]; y = c[4]; r = c[5]; ctx.arcTo(x1, y1, x, y, r); break; case "E": c = this.#commands[i]; x = c[1]; y = c[2]; rx = c[3]; ry = c[4]; angle = c[5]; startAngle = c[6]; endAngle = c[7]; ccw = c[8]; ctx.save(); ctx.translate(x, y); ctx.rotate(angle); ctx.scale(rx, ry); ctx.arc(0, 0, 1, startAngle, endAngle, ccw); ctx.restore(); break; case "R": c = this.#commands[i]; x = c[1]; y = c[2]; w = c[3]; h = c[4]; startPoint = { x, y }; ctx.rect(x, y, w, h); break; case "RR": c = this.#commands[i]; x = c[1]; y = c[2]; w = c[3]; h = c[4]; radii = c[5]; startPoint = { x, y }; ctx.roundRect(x, y, w, h, radii); break; default: throw new Error(`Invalid path command: ${pathType}`); } if (!currentPoint) { currentPoint = { x, y }; } else { currentPoint.x = x; currentPoint.y = y; } } } }; // src/round-rect.ts function isPointObject(point) { return point !== null && typeof point === "object" && ("x" in point || "y" in point) && (typeof point.x === "number" || typeof point.y === "number" || typeof point.x === "undefined" || typeof point.y === "undefined"); } function normalizeRadius(radius) { if (typeof radius === "number") { return { x: radius, y: radius }; } return { x: typeof radius.x === "number" ? radius.x : 0, y: typeof radius.y === "number" ? radius.y : 0 }; } function roundRect(x, y, width, height, radii = 0) { if (typeof radii === "number") { radii = [radii]; } else if (isPointObject(radii)) { radii = [radii]; } else if (!Array.isArray(radii)) { return; } if (Array.isArray(radii)) { if (radii.length === 0 || radii.length > 4) { throw new RangeError( `Failed to execute 'roundRect' on '${this.constructor.name}': ${radii.length} radii provided. Between one and four radii are necessary.` ); } radii.forEach((v) => { if (isPointObject(v)) { const point = v; if (typeof point.x === "number" && point.x < 0) { throw new RangeError( `Failed to execute 'roundRect' on '${this.constructor.name}': Radius value ${point.x} is negative.` ); } if (typeof point.y === "number" && point.y < 0) { throw new RangeError( `Failed to execute 'roundRect' on '${this.constructor.name}': Radius value ${point.y} is negative.` ); } } else if (typeof v !== "number") { throw new TypeError( `Failed to execute 'roundRect' on '${this.constructor.name}': Radius value ${v} is not a number or DOMPointInit.` ); } else if (typeof v === "number" && v < 0) { throw new RangeError( `Failed to execute 'roundRect' on '${this.constructor.name}': Radius value ${v} is negative.` ); } }); } const normalizedRadii = radii.map(normalizeRadius); if (radii.length === 1 && normalizedRadii[0].x === 0 && normalizedRadii[0].y === 0) { this.rect(x, y, width, height); return; } const maxRadiusX = width / 2; const maxRadiusY = height / 2; const tl = { x: Math.min(maxRadiusX, normalizedRadii[0].x), y: Math.min(maxRadiusY, normalizedRadii[0].y) }; let tr = tl; let br = tl; let bl = tl; if (normalizedRadii.length === 2) { tr = { x: Math.min(maxRadiusX, normalizedRadii[1].x), y: Math.min(maxRadiusY, normalizedRadii[1].y) }; bl = tr; } if (normalizedRadii.length === 3) { tr = { x: Math.min(maxRadiusX, normalizedRadii[1].x), y: Math.min(maxRadiusY, normalizedRadii[1].y) }; bl = tr; br = { x: Math.min(maxRadiusX, normalizedRadii[2].x), y: Math.min(maxRadiusY, normalizedRadii[2].y) }; } if (normalizedRadii.length === 4) { tr = { x: Math.min(maxRadiusX, normalizedRadii[1].x), y: Math.min(maxRadiusY, normalizedRadii[1].y) }; br = { x: Math.min(maxRadiusX, normalizedRadii[2].x), y: Math.min(maxRadiusY, normalizedRadii[2].y) }; bl = { x: Math.min(maxRadiusX, normalizedRadii[3].x), y: Math.min(maxRadiusY, normalizedRadii[3].y) }; } this.moveTo(x, y + height - bl.y); if (tl.x === tl.y && tl.x > 0) { this.arcTo(x, y, x + tl.x, y, tl.x); } else if (tl.x > 0 || tl.y > 0) { this.ellipse(x + tl.x, y + tl.y, tl.x, tl.y, 0, Math.PI, Math.PI * 1.5, false); } else { this.lineTo(x, y); } this.lineTo(x + width - tr.x, y); if (tr.x === tr.y && tr.x > 0) { this.arcTo(x + width, y, x + width, y + tr.y, tr.x); } else if (tr.x > 0 || tr.y > 0) { this.ellipse(x + width - tr.x, y + tr.y, tr.x, tr.y, 0, Math.PI * 1.5, 0, false); } else { this.lineTo(x + width, y); } this.lineTo(x + width, y + height - br.y); if (br.x === br.y && br.x > 0) { this.arcTo(x + width, y + height, x + width - br.x, y + height, br.x); } else if (br.x > 0 || br.y > 0) { this.ellipse(x + width - br.x, y + height - br.y, br.x, br.y, 0, 0, Math.PI * 0.5, false); } else { this.lineTo(x + width, y + height); } this.lineTo(x + bl.x, y + height); if (bl.x === bl.y && bl.x > 0) { this.arcTo(x, y + height, x, y + height - bl.y, bl.x); } else if (bl.x > 0 || bl.y > 0) { this.ellipse(x + bl.x, y + height - bl.y, bl.x, bl.y, 0, Math.PI * 0.5, Math.PI, false); } else { this.lineTo(x, y + height); } this.closePath(); this.moveTo(x, y); } // src/apply.ts function applyPath2DToCanvasRenderingContext(CanvasRenderingContext2D) { if (!CanvasRenderingContext2D) return; const cClip = CanvasRenderingContext2D.prototype.clip; const cFill = CanvasRenderingContext2D.prototype.fill; const cStroke = CanvasRenderingContext2D.prototype.stroke; const cIsPointInPath = CanvasRenderingContext2D.prototype.isPointInPath; CanvasRenderingContext2D.prototype.clip = function clip(...args) { if (args[0] instanceof Path2D) { const path = args[0]; const fillRule2 = args[1] !== void 0 ? args[1] : "nonzero"; path.buildPathInCanvas(this); cClip.apply(this, [fillRule2]); return; } const fillRule = args[0] !== void 0 ? args[0] : "nonzero"; cClip.apply(this, [fillRule]); }; CanvasRenderingContext2D.prototype.fill = function fill(...args) { if (args[0] instanceof Path2D) { const path = args[0]; const fillRule2 = args[1] !== void 0 ? args[1] : "nonzero"; path.buildPathInCanvas(this); cFill.apply(this, [fillRule2]); return; } const fillRule = args[0] !== void 0 ? args[0] : "nonzero"; cFill.apply(this, [fillRule]); }; CanvasRenderingContext2D.prototype.stroke = function stroke(path) { if (path) { path.buildPathInCanvas(this); } cStroke.apply(this); }; CanvasRenderingContext2D.prototype.isPointInPath = function isPointInPath(...args) { if (args[0] instanceof Path2D) { const path = args[0]; const x = args[1]; const y = args[2]; const fillRule = args[3] !== void 0 ? args[3] : "nonzero"; path.buildPathInCanvas(this); return cIsPointInPath.apply(this, [x, y, fillRule]); } return cIsPointInPath.apply(this, args); }; } function applyRoundRectToCanvasRenderingContext2D(CanvasRenderingContext2D) { if (CanvasRenderingContext2D && !CanvasRenderingContext2D.prototype.roundRect) { CanvasRenderingContext2D.prototype.roundRect = roundRect; } } function applyRoundRectToPath2D(P2D) { if (P2D && !P2D.prototype.roundRect) { P2D.prototype.roundRect = roundRect; } } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { Path2D, applyPath2DToCanvasRenderingContext, applyRoundRectToCanvasRenderingContext2D, applyRoundRectToPath2D, parsePath, roundRect });