UNPKG

toosoon-utils

Version:
368 lines (367 loc) 15.6 kB
import { EPSILON, PI } from '../../constants'; import { isCoincident, isCollinear } from '../../geometry'; import LineCurve from './LineCurve'; import PolylineCurve from './PolylineCurve'; import QuadraticBezierCurve from './QuadraticBezierCurve'; import CubicBezierCurve from './CubicBezierCurve'; import CatmullRomCurve from './CatmulRomCurve'; import SplineCurve from './SplineCurve'; import EllipseCurve from './EllipseCurve'; import ArcCurve from './ArcCurve'; import PathCurve from './PathCurve'; /** * Utility class for manipulating connected curves * * It works by providing methods for creating curves similar to the 2D Canvas API * * * @exports * @class CurvePath * @extends PathCurve */ export default class Path extends PathCurve { /** * 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; } /** * 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); if (radius < 0) { console.warn(`Path.arcTo()`, `Arc radius must be non-negative.`, radius); return this.lineTo(x2, y2); } const x0 = this.currentPosition[0]; const y0 = this.currentPosition[1]; // (x1, y1) is coincident with (x0, y0) if (isCoincident(x0, y0, x1, y1)) { return this; } // (x0, y0), (x1 ,y1) and (x2, y2) are collinear if (isCollinear(x0, y0, x1, y1, x2, y2) || radius <= EPSILON) { return this.lineTo(x2, y2); } const dx01 = x0 - x1; const dy01 = y2 - y1; const dx20 = x2 - x0; const dy20 = y2 - y0; const dx21 = x2 - x1; const dy21 = y2 - y1; const l20 = Math.hypot(dx20, dy20); const l21 = Math.hypot(dx21, dy21); const l01 = Math.hypot(dx01, dy01); const l = radius * Math.tan((PI - Math.acos((l21 + l01 - l20) / (2 * l21 * l01))) / 2); const t01 = l / l01; const t21 = l / l21; if (Math.abs(t01 - 1) > EPSILON) { return this.lineTo(x1 + t01 * dx01, y1 + t01 * dy01); } const startX = x1 + t01 * dx01; const startY = y1 + t01 * dy01; const endX = x1 + t21 * dx21; const endY = y1 + t21 * dy21; const normalX = dy01 * -1; const normalY = dx01; const centerX = (startX + endX) / 2; const centerY = (startY + endY) / 2; const d = Math.sqrt(radius ** 2 - l ** 2); const cx = centerX + (normalX / Math.hypot(normalX, normalY)) * d; const cy = centerY + (normalY / Math.hypot(normalX, normalY)) * d; const startAngle = Math.atan2(startY - cy, startX - cx); const endAngle = Math.atan2(endY - cy, endX - cx); const counterclockwise = dy01 * dx21 < dx01 * dy21; this.arc(x1 + t21 * dx21, y1 + t21 * dy21, radius, startAngle, endAngle, counterclockwise); 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 Catmul-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} */ catmulRomCurveTo(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) { if (this.currentPosition.every(isNaN)) this.moveTo(cx, cy); else if (!isCoincident(...this.currentPosition, cx, cy)) this.lineTo(cx, cy); const curve = new EllipseCurve(cx, cy, rx, ry, rotation, startAngle, endAngle, counterclockwise); this.add(curve); this._setCurrentPosition(...curve.getPoint(1)); 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) { if (this.currentPosition.every(isNaN)) this.moveTo(cx, cy); else if (!isCoincident(...this.currentPosition, cx, cy)) this.lineTo(cx, cy); const curve = new ArcCurve(cx, cy, radius, startAngle, endAngle, counterclockwise); this.add(curve); this._setCurrentPosition(...curve.getPoint(1)); 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; } /** * Add a line curve to close the curve path * Add an instance of {@link LineCurve} to the path */ closePath() { super.closePath(); const endPoint = this.curves[this.curves.length - 1]?.getPoint(1); this._setCurrentPosition(...endPoint); 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; } }