UNPKG

toosoon-utils

Version:
431 lines (430 loc) 17 kB
import { EPSILON, PI } from '../../constants'; import { arc, distance, ellipse, isCoincident, isCollinear, line } from '../../geometry'; import LineCurve from '../curves/LineCurve'; import PolylineCurve from '../curves/PolylineCurve'; import QuadraticBezierCurve from '../curves/QuadraticBezierCurve'; import CubicBezierCurve from '../curves/CubicBezierCurve'; import CatmullRomCurve from '../curves/CatmullRomCurve'; import SplineCurve from '../curves/SplineCurve'; import EllipseCurve from '../curves/EllipseCurve'; import ArcCurve from '../curves/ArcCurve'; import Path from './Path'; /** * Utility class for manipulating connected curves providing methods similar to the 2D Canvas API * * @exports * @class PathContext * @extends Path */ export default class PathContext extends Path { /** * Path current offset */ currentPosition = [NaN, NaN]; /** * Create a path from the given list of points * * @param {Point[]} points Array of points defining the path * @param {Point[]} type Type of curve used for creating the path * @return {this} */ setFromPoints(points, type) { this.moveTo(...points[0]); if (type === 'polyline') return this.polylineTo(points); if (type === 'spline') return this.splineTo(points); for (let i = 1, l = points.length; i < l; i++) { this.lineTo(...points[i]); } return this; } /** * Begin the path * Reset `currentPosition` */ beginPath() { this._setCurrentPosition(NaN, NaN); } /** * Draw a line from the ending position to the beginning position of the path * Add an instance of {@link LineCurve} to the path */ closePath() { const startPoint = this.curves[0]?.getPoint(0); const endPoint = this.curves[this.curves.length - 1]?.getPoint(1); if (!isCoincident(...startPoint, ...endPoint)) this.add(new LineCurve(...endPoint, ...startPoint)); this._setCurrentPosition(...endPoint); return this; } /** * Move {@link Path#currentPosition} to the coordinates specified by `x` and `y` * * @param {number} x X-axis coordinate of the point * @param {number} y Y-axis coordinate of the point * @return {this} */ moveTo(x, y) { this._setCurrentPosition(x, y); return this; } /** * Draw a line from the current position to the position specified by `x` and `y` * Add an instance of {@link LineCurve} to the path * * @param {number} x X-axis coordinate of the point * @param {number} y Y-axis coordinate of the point * @return {this} */ lineTo(x, y) { if (this.currentPosition.every(isNaN)) return this.moveTo(x, y); const curve = new LineCurve(...this.currentPosition, x, y); this.add(curve); this._setCurrentPosition(x, y); return this; } /** * Draw a Polyline curve from the current position through the given points * Add an instance of {@link PolylineCurve} to the path * * @param {Point[]} points Array of points defining the curve * @returns {this} */ polylineTo(points) { if (this.currentPosition.every(isNaN)) this.moveTo(...points[0]); const curve = new PolylineCurve([this.currentPosition].concat(points)); this.add(curve); this._setCurrentPosition(...points[points.length - 1]); return this; } /** * Draw an Arc curve from the current position, tangential to the 2 segments created by both control points * Add an instance of {@link ArcCurve} to the path * * @param {number} x1 X-axis coordinate of the first control point * @param {number} y1 Y-axis coordinate of the first control point * @param {number} x2 X-axis coordinate of the second control point * @param {number} y2 Y-axis coordinate of the second control point * @param {number} radius Arc radius (Must be non-negative) * @returns {this} */ arcTo(x1, y1, x2, y2, radius) { if (this.currentPosition.every(isNaN)) this.moveTo(x1, y1); const x0 = this.currentPosition[0]; const y0 = this.currentPosition[1]; if (radius < 0) { throw new Error(`IndexSizeError: Failed to execute 'arcTo' on 'PathContext': The radius provided (${radius}) is negative.`); } if (isCoincident(x0, y0, x1, y1)) { return this; } if (isCollinear(x0, y0, x1, y1, x2, y2)) { return this.lineTo(x2, y2); } const l01 = distance(x0, y0, x1, y1); const l21 = distance(x2, y2, x1, y1); const [t1x, t1y] = line(radius / l01, x1, y1, x0, y0); const [t2x, t2y] = line(radius / l21, x1, y1, x2, y2); const v1x = (t1x - x1) / radius; const v1y = (t1y - y1) / radius; const v2x = (t2x - x1) / radius; const v2y = (t2y - y1) / radius; const normalX = v2y - v1y; const normalY = v1x - v2x; const cx = x1 + normalX * radius; const cy = y1 + normalY * radius; const startAngle = Math.atan2(t1y - cy, t1x - cx); const endAngle = Math.atan2(t2y - cy, t2x - cx); let clockwise = endAngle - startAngle > 0; if (clockwise && endAngle - startAngle > PI) clockwise = false; else if (!clockwise && startAngle - endAngle > PI) clockwise = true; this.lineTo(t1x, t1y); this.arc(cx, cy, radius, startAngle, endAngle, !clockwise); return this; } /** * Draw a Quadratic Bézier curve from the current position to the end point specified by `x` and `y`, using the control point specified by `cpx` and `cpy` * Add an instance of {@link QuadraticBezierCurve} to the path * * @param {number} cpx X-axis coordinate of the control point * @param {number} cpy Y-axis coordinate of the control point * @param {number} x2 X-axis coordinate of the end point * @param {number} y2 Y-axis coordinate of the end point * @return {this} */ quadraticCurveTo(cpx, cpy, x2, y2) { if (this.currentPosition.every(isNaN)) this.moveTo(cpx, cpy); const curve = new QuadraticBezierCurve(...this.currentPosition, cpx, cpy, x2, y2); this.add(curve); this._setCurrentPosition(x2, y2); return this; } /** * Draw a Cubic Bézier curve from the current position to the end point specified by `x` and `y`, using the control point specified by (`cp1x`, `cp1y`) and (`cp2x`, `cp2y`) * Add an instance of {@link CubicBezierCurve} to the path * * @param {number} cp1x X-axis coordinate of the first control point * @param {number} cp1y Y-axis coordinate of the first control point * @param {number} cp2x X-axis coordinate of the second control point * @param {number} cp2y Y-axis coordinate of the second control point * @param {number} x2 X-axis coordinate of the end point * @param {number} y2 Y-axis coordinate of the end point * @return {this} */ bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x2, y2) { if (this.currentPosition.every(isNaN)) this.moveTo(cp1x, cp1y); const curve = new CubicBezierCurve(...this.currentPosition, cp1x, cp1y, cp2x, cp2y, x2, y2); this.add(curve); this._setCurrentPosition(x2, y2); return this; } /** * Draw a Catmull-Rom curve from the current position to the end point specified by `x` and `y`, using the control points specified by (`cp1x`, `cp1y`) and (`cp2x`, `cp2y`) * Add an instance of {@link CatmullRomCurve} to the path * * @param {number} cp1x X-axis coordinate of the first control point * @param {number} cp1y Y-axis coordinate of the first control point * @param {number} cp2x X-axis coordinate of the second control point * @param {number} cp2y Y-axis coordinate of the second control point * @param {number} x2 X-axis coordinate of the end point * @param {number} y2 Y-axis coordinate of the end point * @return {this} */ catmullRomCurveTo(cp1x, cp1y, cp2x, cp2y, x2, y2) { if (this.currentPosition.every(isNaN)) this.moveTo(cp1x, cp1y); const curve = new CatmullRomCurve(...this.currentPosition, cp1x, cp1y, cp2x, cp2y, x2, y2); this.add(curve); this._setCurrentPosition(x2, y2); return this; } /** * Draw a Spline curve from the current position through the given points * Add an instance of {@link SplineCurve} to the path * * @param {Point[]} points Array of points defining the curve * @return {this} */ splineTo(points) { if (this.currentPosition.every(isNaN)) this.moveTo(...points[0]); const curve = new SplineCurve([this.currentPosition].concat(points)); this.add(curve); this._setCurrentPosition(...points[points.length - 1]); return this; } /** * Draw an Ellispe curve which is centered at (`cx`, `cy`) position * Add an instance of {@link EllipseCurve} to the path * * @param {number} cx X-axis coordinate of the center of the circle * @param {number} cy Y-axis coordinate of the center of the circle * @param {number} rx X-radius of the ellipse * @param {number} ry Y-radius of the ellipse * @param {number} [rotation] Rotation angle of the ellipse (in radians), counterclockwise from the positive X-axis * @param {number} [startAngle] Start angle of the arc (in radians) * @param {number} [endAngle] End angle of the arc (in radians) * @param {boolean} [counterclockwise] Flag indicating the direction of the arc * @return {this} */ ellipse(cx, cy, rx, ry, rotation, startAngle, endAngle, counterclockwise) { const start = ellipse(0, cx, cy, rx, ry, rotation, startAngle, endAngle, counterclockwise); const end = ellipse(1, cx, cy, rx, ry, rotation, startAngle, endAngle, counterclockwise); if (this.currentPosition.every(isNaN)) this.moveTo(...start); else if (!isCoincident(...this.currentPosition, ...start)) this.lineTo(...start); if (rx <= EPSILON && ry <= EPSILON) return this; const curve = new EllipseCurve(cx, cy, rx, ry, rotation, startAngle, endAngle, counterclockwise); this.add(curve); this._setCurrentPosition(...end); return this; } /** * Draw an Arc curve which is centered at (`cx`, `cy`) position * Add an instance of {@link ArcCurve} to the path * * @param {number} cx X-axis coordinate of the center of the circle * @param {number} cy Y-axis coordinate of the center of the circle * @param {number} radius Radius of the circle * @param {number} [startAngle] Start angle of the arc (in radians) * @param {number} [endAngle] End angle of the arc (in radians) * @param {boolean} [counterclockwise] Flag indicating the direction of the arc * @return {this} */ arc(cx, cy, radius, startAngle, endAngle, counterclockwise) { const start = arc(0, cx, cy, radius, startAngle, endAngle, counterclockwise); const end = arc(1, cx, cy, radius, startAngle, endAngle, counterclockwise); if (this.currentPosition.every(isNaN)) this.moveTo(...start); else if (!isCoincident(...this.currentPosition, ...start)) this.lineTo(...start); if (radius <= EPSILON) return this; const curve = new ArcCurve(cx, cy, radius, startAngle, endAngle, counterclockwise); this.add(curve); this._setCurrentPosition(...end); return this; } /** * Draw a rectangular path from the start position specified by `x` and `y` to the end position using `width` and `height` * Add an instance of {@link PolylineCurve} to the path * * @param {number} x X-axis coordinate of the rectangle starting point * @param {number} y Y-axis coordinate of the rectangle starting point * @param {number} width Rectangle width (Positive values are to the right and negative to the left) * @param {number} height Rectangle height (Positive values are down, and negative are up) * @return {this} */ rect(x, y, width, height) { if (this.currentPosition.every(isNaN)) this.moveTo(x, y); else if (!isCoincident(...this.currentPosition, x, y)) this.lineTo(x, y); const curve = new PolylineCurve([ [x + width, y], [x + width, y + height], [x, y + height], [x, y] ]); this.add(curve); this._setCurrentPosition(x, y); return this; } /** * Draw a rounded rectangular path from the start position specified by `x` and `y` to the end position using `width` and `height` * * @param {number} x X-axis coordinate of the rectangle starting point * @param {number} y Y-axis coordinate of the rectangle starting point * @param {number} width Rectangle width (Positive values are to the right and negative to the left) * @param {number} height Rectangle height (Positive values are down, and negative are up) * @param {number|number[]} radius Radius of the circular arc to be used for the corners of the rectangle * @return {this} */ roundRect(x, y, width, height, radius) { let topLeftRadius = typeof radius === 'number' ? radius : radius[0]; let topRightRadius = typeof radius === 'number' ? radius : radius[1] ?? radius[0]; let bottomRightRadius = typeof radius === 'number' ? radius : radius[2] ?? topLeftRadius; let bottomLeftRadius = typeof radius === 'number' ? radius : radius[3] ?? topRightRadius; const maxRadius = Math.min(width / 2, height / 2); topLeftRadius = Math.min(topLeftRadius, maxRadius); topRightRadius = Math.min(topRightRadius, maxRadius); bottomRightRadius = Math.min(bottomRightRadius, maxRadius); bottomLeftRadius = Math.min(bottomLeftRadius, maxRadius); if (this.currentPosition.every(isNaN)) this.moveTo(x + topLeftRadius, y); else if (!isCoincident(...this.currentPosition, x, y)) this.lineTo(x + topLeftRadius, y); // Top-Right corner if (topRightRadius > 0) { this.lineTo(x + width - topRightRadius, y); this.arcTo(x + width, y, x + width, y + topRightRadius, topRightRadius); } else { this.lineTo(x + width, y); } // Bottom-Right corner if (bottomRightRadius > 0) { this.lineTo(x + width, y + height - bottomRightRadius); this.arcTo(x + width, y + height, x + width - bottomRightRadius, y + height, bottomRightRadius); } else { this.lineTo(x + width, y + height); } // Bottom-Left corner if (bottomLeftRadius > 0) { this.lineTo(x + bottomLeftRadius, y + height); this.arcTo(x, y + height, x, y + height - bottomLeftRadius, bottomLeftRadius); } else { this.lineTo(x, y + height); } // Top-Left corner if (topLeftRadius > 0) { this.lineTo(x, y + topLeftRadius); this.arcTo(x, y, x + topLeftRadius, y, topLeftRadius); } else { this.lineTo(x, y); } return this; } // public transform() {} // public translate(x: number, y: number) {} // public rotate(angle: number) {} // public restore() {} _setCurrentPosition(x, y) { this.currentPosition[0] = x; this.currentPosition[1] = y; } canvas; fillStyle; strokeStyle; lineWidth; lineCap; lineJoin; lineDashOffset; fillText; strokeText; fill; stroke; clearRect; fillRect; strokeRect; drawImage; clip; createLinearGradient; createRadialGradient; createConicGradient; createPattern; createImageData; getImageData; putImageData; imageSmoothingEnabled; imageSmoothingQuality; font; fontKerning; fontStretch; fontVariantCaps; letterSpacing; textAlign; textBaseline; textRendering; wordSpacing; direction; measureText; filter; globalAlpha; globalCompositeOperation; miterLimit; save; restore; reset; transform; translate; rotate; scale; getTransform; setTransform; resetTransform; getLineDash; setLineDash; shadowBlur; shadowColor; shadowOffsetX; shadowOffsetY; drawFocusIfNeeded; isPointInPath; isPointInStroke; getContextAttributes; isContextLost; }