toosoon-utils
Version:
Utility functions & classes
368 lines (367 loc) • 15.6 kB
JavaScript
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;
}
}