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