plotboilerplate
Version:
A simple javascript plotting boilerplate for 2d stuff.
1,062 lines • 45.5 kB
JavaScript
"use strict";
/**
* @author Ikaros Kappler
* @date 2013-08-15
* @modified 2018-08-16 Added a closure. Removed the wrapper class 'IKRS'. Replaced class THREE.Vector2 by Vertex class.
* @modified 2018-11-19 Added the fromArray(Array) function.
* @modified 2018-11-28 Added the locateCurveByPoint(Vertex) function.
* @modified 2018-12-04 Added the toSVGPathData() function.
* @modified 2019-03-20 Added JSDoc tags.
* @modified 2019-03-23 Changed the signatures of getPoint, getPointAt and getTangent (!version 2.0).
* @modified 2019-12-02 Fixed the updateArcLength function. It used the wrong pointAt function (was renamed before).
* @modified 2020-02-06 Added the getSubCurveAt(number,number) function.
* @modified 2020-02-06 Fixed a serious bug in the arc lenght calculation (length was never reset, urgh).
* @modified 2020-02-07 Added the isInstance(any) function.
* @modified 2020-02-10 Added the reverse() function.
* @modified 2020-02-10 Fixed the translate(...) function (returning 'this' was missing).
* @modified 2020-03-24 Ported this class from vanilla JS to Typescript.
* @modified 2020-06-03 Added the getBounds() function.
* @modified 2020-07-14 Changed the moveCurvePoint(...,Vertex) to moveCurvePoint(...,XYCoords), which is more generic.
* @modified 2020-07-24 Added the getClosestT function and the helper function locateIntervalByDistance(...).
* @modified 2021-01-20 Added UID.
* @modified 2022-02-02 Added the `destroy` method.
* @modified 2022-02-02 Cleared the `toSVGPathData` function (deprecated). Use `drawutilssvg` instead.
* @modified 2022-10-17 The `CubicBezierCurve` class now implements the new `PathSegment` interface.
* @modified 2023-09-30 Added the function `CubicbezierCurve.getSubCurve(number,number)` – similar to `getSubCurveAt(...)` but with absolute position parameters.
* @modified 2023-10-07 Added the `trimEnd`, `trimEndAt`, `trimStart`, `trimStartAt` methods.
* @modified 2025-04-09 Added the `CubicBezierCurve.move` method to match the convention – which just calls `translate`.
* @modified 2025-04-09 Modified the `CubicBezierCurve.translate` method: chaning parameter `Vertex` to more generalized `XYCoords`.
* @modified 2025-04-13 Changed visibility of `CubicBezierCurve.utils` from 'private' to 'public'.
* @modified 2025-04-13 Added helper function `CubicBezierCurve.utils.bezierCoeffs`.
* @modified 2025-04-13 Added helper functopn `CubicBezierCurve.utils.sgn(number)` for division safe sign calculation.
* @modified 2025-03-13 Class `CubicBezierCurve` is now implementing interface `Intersectable`.
* @modified 2025-04-18 Added evaluation method for cubic Bézier curves `CubicBezierCurve.utils.evaluateT`.
* @modified 2025-04-18 Refactored method `CubicBezierCurve.getPointAt` to use `evaluateT`.
* @modified 2025-04-18 Fixed the `CubicBezierCurve.getBounds` method: now returning the real bounding box. Before it was an approximated one.
* @modified 2025-ß4-18 Added helper methods for bounding box calculation `CubucBezierCurve.util.cubicPolyMinMax` and `cubicPoly`.
* @version 2.9.0
*
* @file CubicBezierCurve
* @public
**/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CubicBezierCurve = void 0;
var Bounds_1 = require("./Bounds");
var UIDGenerator_1 = require("./UIDGenerator");
var Vertex_1 = require("./Vertex");
var Vector_1 = require("./Vector");
/**
* @classdesc A refactored cubic bezier curve class.
*
* @requires Bounds
* @requires Vertex
* @requires Vector
* @requires XYCoords
* @requires UID
* @requires UIDGenerator
*/
var CubicBezierCurve = /** @class */ (function () {
/**
* The constructor.
*
* @constructor
* @name CubicBezierCurve
* @param {Vertex} startPoint - The Bézier curve's start point.
* @param {Vertex} endPoint - The Bézier curve's end point.
* @param {Vertex} startControlPoint - The Bézier curve's start control point.
* @param {Vertex} endControlPoint - The Bézier curve's end control point.
**/
function CubicBezierCurve(startPoint, endPoint, startControlPoint, endControlPoint) {
/** @constant {number} */
this.START_POINT = CubicBezierCurve.START_POINT;
/** @constant {number} */
this.START_CONTROL_POINT = CubicBezierCurve.START_CONTROL_POINT;
/** @constant {number} */
this.END_CONTROL_POINT = CubicBezierCurve.END_CONTROL_POINT;
/** @constant {number} */
this.END_POINT = CubicBezierCurve.END_POINT;
this.uid = UIDGenerator_1.UIDGenerator.next();
this.startPoint = startPoint;
this.startControlPoint = startControlPoint;
this.endPoint = endPoint;
this.endControlPoint = endControlPoint;
this.curveIntervals = 30;
// An array of vertices
this.segmentCache = [];
// An array of floats
this.segmentLengths = [];
// float
// this.arcLength = null;
this.updateArcLengths();
}
/**
* Move the given curve point (the start point, end point or one of the two
* control points).
*
* @method moveCurvePoint
* @param {number} pointID - The numeric identicator of the point to move. Use one of the four eBezierPoint constants.
* @param {XYCoords} moveAmount - The amount to move the specified point by.
* @param {boolean} moveControlPoint - Move the control points along with their path point (if specified point is a path point).
* @param {boolean} updateArcLengths - Specifiy if the internal arc segment buffer should be updated.
* @instance
* @memberof CubicBezierCurve
* @return {void}
**/
CubicBezierCurve.prototype.moveCurvePoint = function (pointID, moveAmount, moveControlPoint, updateArcLengths) {
if (pointID == this.START_POINT) {
this.getStartPoint().add(moveAmount);
if (moveControlPoint)
this.getStartControlPoint().add(moveAmount);
}
else if (pointID == this.START_CONTROL_POINT) {
this.getStartControlPoint().add(moveAmount);
}
else if (pointID == this.END_CONTROL_POINT) {
this.getEndControlPoint().add(moveAmount);
}
else if (pointID == this.END_POINT) {
this.getEndPoint().add(moveAmount);
if (moveControlPoint)
this.getEndControlPoint().add(moveAmount);
}
else {
console.log("[CubicBezierCurve.moveCurvePoint] pointID '".concat(pointID, "' invalid."));
}
if (updateArcLengths)
this.updateArcLengths();
};
/**
* Translate the whole curve by the given {x,y} amount: moves all four points.
*
* @method translate
* @param {XYCoords} amount - The amount to translate this curve by.
* @instance
* @memberof CubicBezierCurve
* @return {CubicBezierCurve} this (for chaining).
**/
CubicBezierCurve.prototype.translate = function (amount) {
this.startPoint.add(amount);
this.startControlPoint.add(amount);
this.endControlPoint.add(amount);
this.endPoint.add(amount);
return this;
};
/**
* Translate the whole curve by the given {x,y} amount: moves all four points.
*
* @method translate
* @param {XYCoords} amount - The amount to translate this curve by.
* @instance
* @memberof CubicBezierCurve
* @return {CubicBezierCurve} this (for chaining).
**/
CubicBezierCurve.prototype.move = function (amount) {
return this.translate(amount);
};
/**
* Reverse this curve, means swapping start- and end-point and swapping
* start-control- and end-control-point.
*
* @method reverse
* @instance
* @memberof CubicBezierCurve
* @return {CubicBezierCurve} this (for chaining).
**/
CubicBezierCurve.prototype.reverse = function () {
var tmp = this.startPoint;
this.startPoint = this.endPoint;
this.endPoint = tmp;
tmp = this.startControlPoint;
this.startControlPoint = this.endControlPoint;
this.endControlPoint = tmp;
return this;
};
/**
* Get the total curve length.<br>
* <br>
* As not all Bézier curved have a closed formula to calculate their lengths, this
* implementation uses a segment buffer (with a length of 30 segments). So the
* returned length is taken from the arc segment buffer.<br>
* <br>
* Note that if the curve points were changed and the segment buffer was not
* updated this function might return wrong (old) values.
*
* @method getLength
* @instance
* @memberof CubicBezierCurve
* @return {number} >= 0
**/
CubicBezierCurve.prototype.getLength = function () {
return this.arcLength;
};
/**
* Uptate the internal arc segment buffer and their lengths.<br>
* <br>
* All class functions update the buffer automatically; if any
* curve point is changed by other reasons you should call this
* function to keep actual values in the buffer.
*
* @method updateArcLengths
* @instance
* @memberof CubicBezierCurve
* @return {void}
**/
CubicBezierCurve.prototype.updateArcLengths = function () {
var pointA = this.startPoint.clone();
var pointB = new Vertex_1.Vertex(0, 0);
var curveStep = 1.0 / this.curveIntervals;
// Clear segment cache
this.segmentCache = [];
// Push start point into buffer
this.segmentCache.push(this.startPoint);
this.segmentLengths = [];
var newLength = 0.0;
var t = 0.0;
var tmpLength;
while (t <= 1.0) {
pointB = this.getPointAt(t);
// Store point into cache
this.segmentCache.push(pointB);
// Calculate segment length
tmpLength = pointA.distance(pointB);
this.segmentLengths.push(tmpLength);
newLength += tmpLength;
pointA = pointB;
t += curveStep;
}
this.arcLength = newLength;
};
/**
* Get a 't' (relative position on curve) with the closest distance to point 'p'.
*
* The returned number is 0.0 <= t <= 1.0. Use the getPointAt(t) function to retrieve the actual curve point.
*
* This function uses a recursive approach by cutting the curve into several linear segments.
*
* @method getClosestT
* @instance
* @memberof CubicBezierCurve
* @param {Vertex} p - The point to find the closest position ('t' on the curve).
* @return {number}
**/
CubicBezierCurve.prototype.getClosestT = function (p) {
// We would like to have an error that's not larger than 1.0.
var desiredEpsilon = 1.0;
var result = { t: 0, tPrev: 0.0, tNext: 1.0 };
var iteration = 0;
do {
result = this.locateIntervalByDistance(p, result.tPrev, result.tNext, this.curveIntervals);
iteration++;
// Be sure: stop after 4 iterations
} while (iteration < 4 && this.getPointAt(result.tPrev).distance(this.getPointAt(result.tNext)) > desiredEpsilon);
return result.t;
};
/**
* This helper function locates the 't' on a fixed step interval with the minimal distance
* between the curve (at 't') and the given point.
*
* Furthermore you must specify a sub curve (start 't' and end 't') you want to search on.
* Using tStart=0.0 and tEnd=1.0 will search on the full curve.
*
* @param {Vertex} p - The point to find the closest curve point for.
* @param {number} tStart - The start position (start 't' of the sub curve). Should be >= 0.0.
* @param {number} tEnd - The end position (end 't' of the sub curve). Should be <= 1.0.
* @param {number} stepCount - The number of steps to check within the interval.
*
* @return {object} - An object with t, tPrev and tNext (numbers).
**/
CubicBezierCurve.prototype.locateIntervalByDistance = function (p, tStart, tEnd, stepCount) {
var minIndex = -1;
var minDist = 0;
var t = 0.0;
var tDiff = tEnd - tStart;
for (var i = 0; i <= stepCount; i++) {
t = tStart + tDiff * (i / stepCount);
var vert = this.getPointAt(t);
var dist = vert.distance(p);
if (minIndex == -1 || dist < minDist) {
minIndex = i;
minDist = dist;
}
}
return {
t: tStart + tDiff * (minIndex / stepCount),
tPrev: tStart + tDiff * (Math.max(0, minIndex - 1) / stepCount),
tNext: tStart + tDiff * (Math.min(stepCount, minIndex + 1) / stepCount)
};
};
//--- BEGIN --- Implement interface `IBounded`
/**
* Get the bounds of this bezier curve.
*
* The bounds are approximated by the underlying segment buffer; the more segment there are,
* the more accurate will be the returned bounds.
*
* @method getClosestT
* @instance
* @memberof CubicBezierCurve
* @return {Bounds} The bounds of this ellipse.
**/
CubicBezierCurve.prototype.getBounds = function () {
// Thanks to Richard "RM" for the Bézier bounds calculatin
// https://jsfiddle.net/SalixAlba/QQnvm/4/
var xMinMax = CubicBezierCurve.utils.cubicPolyMinMax(this.startPoint.x, this.startControlPoint.x, this.endControlPoint.x, this.endPoint.x);
var xl = xMinMax.min;
var xh = xMinMax.max;
var yMinMax = CubicBezierCurve.utils.cubicPolyMinMax(this.startPoint.y, this.startControlPoint.y, this.endControlPoint.y, this.endPoint.y);
var yl = yMinMax.min;
var yh = yMinMax.max;
return Bounds_1.Bounds.computeFromVertices([
{ x: xl, y: yl },
{ x: xl, y: yh },
{ x: xh, y: yh },
{ x: xh, y: yl }
]);
};
//--- END --- Implement interface `IBounded`
/**
* Get the start point of the curve.<br>
* <br>
* This function just returns this.startPoint.
*
* @method getStartPoint
* @instance
* @memberof CubicBezierCurve
* @return {Vertex} this.startPoint
**/
CubicBezierCurve.prototype.getStartPoint = function () {
return this.startPoint;
};
/**
* Get the end point of the curve.<br>
* <br>
* This function just returns this.endPoint.
*
* @method getEndPoint
* @instance
* @memberof CubicBezierCurve
* @return {Vertex} this.endPoint
**/
CubicBezierCurve.prototype.getEndPoint = function () {
return this.endPoint;
};
/**
* Get the start control point of the curve.<br>
* <br>
* This function just returns this.startControlPoint.
*
* @method getStartControlPoint
* @instance
* @memberof CubicBezierCurve
* @return {Vertex} this.startControlPoint
**/
CubicBezierCurve.prototype.getStartControlPoint = function () {
return this.startControlPoint;
};
/**
* Get the end control point of the curve.<br>
* <br>
* This function just returns this.endControlPoint.
*
* @method getEndControlPoint
* @instance
* @memberof CubicBezierCurve
* @return {Vertex} this.endControlPoint
**/
CubicBezierCurve.prototype.getEndControlPoint = function () {
return this.endControlPoint;
};
/**
* Get one of the four curve points specified by the passt point ID.
*
* @method getEndControlPoint
* @param {number} id - One of START_POINT, START_CONTROL_POINT, END_CONTROL_POINT or END_POINT.
* @instance
* @memberof CubicBezierCurve
* @return {Vertex}
**/
CubicBezierCurve.prototype.getPointByID = function (id) {
if (id == this.START_POINT)
return this.startPoint;
if (id == this.END_POINT)
return this.endPoint;
if (id == this.START_CONTROL_POINT)
return this.startControlPoint;
if (id == this.END_CONTROL_POINT)
return this.endControlPoint;
throw new Error("Invalid point ID '".concat(id, "'."));
};
/**
* Get the curve point at a given position t, where t is in [0,1].<br>
* <br>
* @see Line.pointAt
*
* @method getPointAt
* @param {number} t - The position on the curve in [0,1] (0 means at
* start point, 1 means at end point, other values address points in bertween).
* @instance
* @memberof CubicBezierCurve
* @return {Vertex}
**/
CubicBezierCurve.prototype.getPointAt = function (t) {
// Perform some powerful math magic
// TODO: cleanup
// const x: number =
// this.startPoint.x * Math.pow(1.0 - t, 3) +
// this.startControlPoint.x * 3 * t * Math.pow(1.0 - t, 2) +
// this.endControlPoint.x * 3 * Math.pow(t, 2) * (1.0 - t) +
// this.endPoint.x * Math.pow(t, 3);
// const y: number =
// this.startPoint.y * Math.pow(1.0 - t, 3) +
// this.startControlPoint.y * 3 * t * Math.pow(1.0 - t, 2) +
// this.endControlPoint.y * 3 * Math.pow(t, 2) * (1.0 - t) +
// this.endPoint.y * Math.pow(t, 3);
var x = CubicBezierCurve.utils.evaluateT(this.startPoint.x, this.startControlPoint.x, this.endControlPoint.x, this.endPoint.x, t);
var y = CubicBezierCurve.utils.evaluateT(this.startPoint.y, this.startControlPoint.y, this.endControlPoint.y, this.endPoint.y, t);
return new Vertex_1.Vertex(x, y);
};
/**
* Get the curve point at a given position u, where u is in [0,arcLength].<br>
* <br>
* @see CubicBezierCurve.getPointAt
*
* @method getPoint
* @param {number} u - The position on the curve in [0,arcLength] (0 means at
* start point, arcLength means at end point, other values address points in bertween).
* @instance
* @memberof CubicBezierCurve
* @return {Vertex}
**/
CubicBezierCurve.prototype.getPoint = function (u) {
return this.getPointAt(u / this.arcLength);
};
/**
* Get the curve tangent vector at a given absolute curve position t in [0,1].<br>
* <br>
* Note that the returned tangent vector (end point) is not normalized and relative to (0,0).
*
* @method getTangent
* @param {number} t - The position on the curve in [0,1].
* @instance
* @memberof CubicBezierCurve
* @return {Vertex}
**/
CubicBezierCurve.prototype.getTangentAt = function (t) {
var a = this.getStartPoint();
var b = this.getStartControlPoint();
var c = this.getEndControlPoint();
var d = this.getEndPoint();
// This is the shortened one
var t2 = t * t;
// (1 - t)^2 = (1-t)*(1-t) = 1 - t - t + t^2 = 1 - 2*t + t^2
var nt2 = 1 - 2 * t + t2;
var tX = -3 * a.x * nt2 + b.x * (3 * nt2 - 6 * (t - t2)) + c.x * (6 * (t - t2) - 3 * t2) + 3 * d.x * t2;
var tY = -3 * a.y * nt2 + b.y * (3 * nt2 - 6 * (t - t2)) + c.y * (6 * (t - t2) - 3 * t2) + 3 * d.y * t2;
// Note: my implementation does NOT normalize tangent vectors!
return new Vertex_1.Vertex(tX, tY);
};
/**
* Trim off a start section of this curve. The position parameter `uValue` is the absolute position on the
* curve in `[0...arcLength]`.
* The remaining curve will be the one in the bounds `[uValue,1]` (so `[0.0,uValue]` is cut off).
*
* Note this function just converts the absolute parameter to a relative one and call `trimStartAt`.
*
* @method trimStart
* @instance
* @memberof CubicBezierCurve
* @param {number} uValue - The absolute position parameter where to cut off the head curve.
* @returns {CubicBezierCurve} `this` for chanining.
*/
CubicBezierCurve.prototype.trimStart = function (uValue) {
return this.trimStartAt(this.convertU2T(uValue));
};
/**
* Trim off a start section of this curve. The position parameter `t` is the relative position in [0..1].
* The remaining curve will be the one in the bounds `[uValue,1]` (so `[0.0,uValue]` is cut off).
*
* @method trimStartAt
* @instance
* @memberof CubicBezierCurve
* @param {number} t - The relative position parameter where to cut off the head curve.
* @returns {CubicBezierCurve} `this` for chanining.
*/
CubicBezierCurve.prototype.trimStartAt = function (t) {
var subCurbePoints = CubicBezierCurve.utils.getSubCurvePointsAt(this, t, 1.0);
this.startPoint.set(subCurbePoints[0]);
this.startControlPoint.set(subCurbePoints[2]);
this.endPoint.set(subCurbePoints[1]);
this.endControlPoint.set(subCurbePoints[3]);
this.updateArcLengths();
return this;
};
/**
* Trim off the end of this curve. The position parameter `uValue` is the absolute position on the
* curve in `[0...arcLength]`.
* The remaining curve will be the one in the bounds `[0,uValue]` (so `[1.0-uValue,1.0]` is cut off).
*
* Note this function just converts the absolute parameter to a relative one and call `trimEndAt`.
*
* @method trimEnd
* @instance
* @memberof CubicBezierCurve
* @param {number} uValue - The absolute position parameter where to cut off the tail curve.
* @returns {CubicBezierCurve} `this` for chanining.
*/
CubicBezierCurve.prototype.trimEnd = function (uValue) {
return this.trimEndAt(this.convertU2T(uValue));
};
/**
* Trim off the end of this curve. The position parameter `t` is the relative position in [0..1].
* The remaining curve will be the one in the bounds `[0,t]` (so `[1.0-t,1.0]` is cut off).
*
* @method trimEndAt
* @instance
* @memberof CubicBezierCurve
* @param {number} t - The relative position parameter where to cut off the tail curve.
* @returns {CubicBezierCurve} `this` for chanining.
*/
CubicBezierCurve.prototype.trimEndAt = function (t) {
var subCurbePoints = CubicBezierCurve.utils.getSubCurvePointsAt(this, 0.0, t);
this.startPoint.set(subCurbePoints[0]);
this.startControlPoint.set(subCurbePoints[2]);
this.endPoint.set(subCurbePoints[1]);
this.endControlPoint.set(subCurbePoints[3]);
this.updateArcLengths();
return this;
};
/**
* Get a sub curve at the given start end end positions (values on the curve's length, between 0 and curve.arcLength).
*
* tStart >= tEnd is allowed, you will get a reversed sub curve then.
*
* @method getSubCurve
* @param {number} tStart – The start position of the desired sub curve (must be in [0..arcLength]).
* @param {number} tEnd – The end position if the desired cub curve (must be in [0..arcLength]).
* @instance
* @memberof CubicBezierCurve
* @return {CubicBezierCurve} The sub curve as a new curve.
**/
CubicBezierCurve.prototype.getSubCurve = function (uStart, uEnd) {
return this.getSubCurveAt(this.convertU2T(uStart), this.convertU2T(uEnd));
};
/**
* Get a sub curve at the given start end end offsets (values between 0.0 and 1.0).
*
* tStart >= tEnd is allowed, you will get a reversed sub curve then.
*
* @method getSubCurveAt
* @param {number} tStart – The start offset of the desired sub curve (must be in [0..1]).
* @param {number} tEnd – The end offset if the desired cub curve (must be in [0..1]).
* @instance
* @memberof CubicBezierCurve
* @return {CubicBezierCurve} The sub curve as a new curve.
**/
CubicBezierCurve.prototype.getSubCurveAt = function (tStart, tEnd) {
// This 'splits' the curve at the given point at t.
var subCurbePoints = CubicBezierCurve.utils.getSubCurvePointsAt(this, tStart, tEnd);
return new CubicBezierCurve(subCurbePoints[0], subCurbePoints[1], subCurbePoints[2], subCurbePoints[3]);
};
/**
* Convert a relative curve position u to the absolute curve position t.
*
* @method convertU2t
* @param {number} u - The relative position on the curve in [0,arcLength].
* @instance
* @memberof CubicBezierCurve
* @return {number}
**/
CubicBezierCurve.prototype.convertU2T = function (u) {
return Math.max(0.0, Math.min(1.0, u / this.arcLength));
};
/**
* Get the curve tangent vector at a given relative position u in [0,arcLength].<br>
* <br>
* Note that the returned tangent vector (end point) is not normalized.
*
* @method getTangent
* @param {number} u - The position on the curve in [0,arcLength].
* @instance
* @memberof CubicBezierCurve
* @return {Vertex}
**/
CubicBezierCurve.prototype.getTangent = function (u) {
return this.getTangentAt(this.convertU2T(u));
};
/**
* Get the curve perpendicular at a given relative position u in [0,arcLength] as a vector.<br>
* <br>
* Note that the returned vector (end point) is not normalized.
*
* @method getPerpendicular
* @param {number} u - The relative position on the curve in [0,arcLength].
* @instance
* @memberof CubicBezierCurve
* @return {Vertex}
**/
CubicBezierCurve.prototype.getPerpendicular = function (u) {
return this.getPerpendicularAt(this.convertU2T(u));
};
/**
* Get the curve perpendicular at a given absolute position t in [0,1] as a vector.<br>
* <br>
* Note that the returned vector (end point) is not normalized.
*
* @method getPerpendicularAt
* @param {number} u - The absolute position on the curve in [0,1].
* @instance
* @memberof CubicBezierCurve
* @return {Vertex}
**/
CubicBezierCurve.prototype.getPerpendicularAt = function (t) {
var tangentVector = this.getTangentAt(t);
return new Vertex_1.Vertex(tangentVector.y, -tangentVector.x);
};
/**
* Clone this Bézier curve (deep clone).
*
* @method clone
* @instance
* @memberof CubicBezierCurve
* @return {CubicBezierCurve}
**/
CubicBezierCurve.prototype.clone = function () {
return new CubicBezierCurve(this.getStartPoint().clone(), this.getEndPoint().clone(), this.getStartControlPoint().clone(), this.getEndControlPoint().clone());
};
//---BEGIN PathSegment-------------------------
/**
* Get the tangent's end point at the start point of this segment.
*
* @method getStartTangent
* @memberof PathSegment
* @return {Vertex} The end point of the starting point's tangent.
*/
CubicBezierCurve.prototype.getStartTangent = function () {
return this.startControlPoint;
};
/**
* Get the tangent's end point at the end point of this segment.
*
* @method getEndTangent
* @memberof PathSegment
* @return {Vertex} The end point of the ending point's tangent.
*/
CubicBezierCurve.prototype.getEndTangent = function () {
return this.endControlPoint;
};
//---END PathSegment-------------------------
//--- BEGIN --- Implement interface `Intersectable`
/**
* Get all line intersections with this shape.
*
* This method returns all intersections (as vertices) with this shape. The returned array of vertices is in no specific order.
*
* @param {VertTuple} line - The line to find intersections with.
* @param {boolean} inVectorBoundsOnly - If set to true only intersecion points on the passed vector are returned (located strictly between start and end vertex).
* @returns {Array<Vertex>} - An array of all intersections with the shape outline.
*/
CubicBezierCurve.prototype.lineIntersections = function (line, inVectorBoundsOnly) {
var _this = this;
if (inVectorBoundsOnly === void 0) { inVectorBoundsOnly = false; }
var intersectionTs = this.lineIntersectionTs(line);
var intersectionPoints = intersectionTs.map(function (t) {
return _this.getPointAt(t);
});
if (inVectorBoundsOnly) {
// const maxDist = line.length();
return intersectionPoints.filter(function (vert) { return line.hasPoint(vert, true); });
}
else {
return intersectionPoints;
}
};
/**
* Get all line intersections of this polygon and their tangents along the shape.
*
* This method returns all intersection tangents (as vectors) with this shape. The returned array of vectors is in no specific order.
*
* @param line
* @param lineIntersectionTangents
* @returns
*/
CubicBezierCurve.prototype.lineIntersectionTangents = function (line, inVectorBoundsOnly) {
var _this = this;
if (inVectorBoundsOnly === void 0) { inVectorBoundsOnly = false; }
var intersectionTs = this.lineIntersectionTs(line);
var intersectionTangents = intersectionTs.map(function (t) {
var startPoint = _this.getPointAt(t);
var endPoint = _this.getTangentAt(t);
return new Vector_1.Vector(startPoint, endPoint.add(startPoint));
});
if (inVectorBoundsOnly) {
return intersectionTangents.filter(function (vec) { return line.hasPoint(vec.a, true); });
}
else {
return intersectionTangents;
}
};
//--- END --- Implement interface `Intersectable`
CubicBezierCurve.prototype.lineIntersectionTs = function (line) {
var A = line.b.y - line.a.y; // A=y2-y1
var B = line.a.x - line.b.x; // B=x1-x2
var C = line.a.x * (line.a.y - line.b.y) + line.a.y * (line.b.x - line.a.x); //C=x1*(y1-y2)+y1*(x2-x1)
// var bx = bezierCoeffs(px[0], px[1], px[2], px[3]);
// var by = bezierCoeffs(py[0], py[1], py[2], py[3]);
var bx = CubicBezierCurve.utils.bezierCoeffs(this.startPoint.x, this.startControlPoint.x, this.endControlPoint.x, this.endPoint.x);
var by = CubicBezierCurve.utils.bezierCoeffs(this.startPoint.y, this.startControlPoint.y, this.endControlPoint.y, this.endPoint.y);
var poly = Array(4);
poly[0] = A * bx[0] + B * by[0]; /*t^3*/
poly[1] = A * bx[1] + B * by[1]; /*t^2*/
poly[2] = A * bx[2] + B * by[2]; /*t*/
poly[3] = A * bx[3] + B * by[3] + C; /*1*/
var roots = CubicBezierCurve.utils.cubicRoots(poly);
return roots.filter(function (root) { return root != -1; });
};
/**
* Check if this and the specified curve are equal.<br>
* <br>
* All four points need to be equal for this, the Vertex.equals function is used.<br>
* <br>
* Please note that this function is not type safe (comparison with any object will fail).
*
* @method clone
* @param {CubicBezierCurve} curve - The curve to compare with.
* @instance
* @memberof CubicBezierCurve
* @return {boolean}
**/
CubicBezierCurve.prototype.equals = function (curve) {
// Note: in the earlier vanilla-JS version this was callable with plain objects.
// Let's see if this restricted version works out.
if (!curve)
return false;
if (!curve.startPoint || !curve.endPoint || !curve.startControlPoint || !curve.endControlPoint)
return false;
return (this.startPoint.equals(curve.startPoint) &&
this.endPoint.equals(curve.endPoint) &&
this.startControlPoint.equals(curve.startControlPoint) &&
this.endControlPoint.equals(curve.endControlPoint));
};
/**
* This function should invalidate any installed listeners and invalidate this object.
* After calling this function the object might not hold valid data any more and
* should not be used.
*/
CubicBezierCurve.prototype.destroy = function () {
this.startPoint.destroy();
this.endPoint.destroy();
this.startControlPoint.destroy();
this.endControlPoint.destroy();
this.isDestroyed = true;
};
/**
* Quick check for class instance.
* Is there a better way?
*
* @method isInstance
* @param {any} obj - Check if the passed object/value is an instance of CubicBezierCurve.
* @instance
* @memberof CubicBezierCurve
* @return {boolean}
**/
CubicBezierCurve.isInstance = function (obj) {
// Note: check this again
/* OLD VANILLA JS IMPLEMENTATION */
/* if( typeof obj != "object" )
return false;
function hasXY(v) {
return typeof v != "undefined" && typeof v.x == "number" && typeof v.y == "number";
}
return typeof obj.startPoint == "object" && hasXY(obj.startPoint)
&& typeof obj.endPoint == "object" && hasXY(obj.endPoint)
&& typeof obj.startControlPoint == "object" && hasXY(obj.startControlPoint)
&& typeof obj.endControlPoint == "object" && hasXY(obj.endControlPoint);
*/
return obj instanceof CubicBezierCurve;
};
/**
* Convert this curve to a JSON string.
*
* @method toJSON
* @param {boolean=} [prettyFormat=false] - If set to true the function will add line breaks.
* @instance
* @memberof CubicBezierCurve
* @return {string} The JSON data.
**/
CubicBezierCurve.prototype.toJSON = function (prettyFormat) {
var jsonString = "{ " + // begin object
(prettyFormat ? "\n\t" : "") +
'"startPoint" : [' +
this.getStartPoint().x +
"," +
this.getStartPoint().y +
"], " +
(prettyFormat ? "\n\t" : "") +
'"endPoint" : [' +
this.getEndPoint().x +
"," +
this.getEndPoint().y +
"], " +
(prettyFormat ? "\n\t" : "") +
'"startControlPoint": [' +
this.getStartControlPoint().x +
"," +
this.getStartControlPoint().y +
"], " +
(prettyFormat ? "\n\t" : "") +
'"endControlPoint" : [' +
this.getEndControlPoint().x +
"," +
this.getEndControlPoint().y +
"]" +
(prettyFormat ? "\n\t" : "") +
" }"; // end object
return jsonString;
};
/**
* Parse a Bézier curve from the given JSON string.
*
* @method fromJSON
* @param {string} jsonString - The JSON data to parse.
* @memberof CubicBezierCurve
* @static
* @throws An exception if the JSON string is malformed.
* @return {CubicBezierCurve}
**/
CubicBezierCurve.fromJSON = function (jsonString) {
var obj = JSON.parse(jsonString);
return CubicBezierCurve.fromObject(obj);
};
/**
* Try to convert the passed object to a CubicBezierCurve.
*
* @method fromObject
* @param {object} obj - The object to convert.
* @memberof CubicBezierCurve
* @static
* @throws An exception if the passed object is malformed.
* @return {CubicBezierCurve}
**/
CubicBezierCurve.fromObject = function (obj) {
if (typeof obj !== "object")
throw "Can only build from object.";
if (!obj.startPoint)
throw 'Object member "startPoint" missing.';
if (!obj.endPoint)
throw 'Object member "endPoint" missing.';
if (!obj.startControlPoint)
throw 'Object member "startControlPoint" missing.';
if (!obj.endControlPoint)
throw 'Object member "endControlPoint" missing.';
return new CubicBezierCurve(new Vertex_1.Vertex(obj.startPoint[0], obj.startPoint[1]), new Vertex_1.Vertex(obj.endPoint[0], obj.endPoint[1]), new Vertex_1.Vertex(obj.startControlPoint[0], obj.startControlPoint[1]), new Vertex_1.Vertex(obj.endControlPoint[0], obj.endControlPoint[1]));
};
/**
* Convert a 4-element array of vertices to a cubic bézier curve.
*
* @method fromArray
* @param {Vertex[]} arr - [ startVertex, endVertex, startControlVertex, endControlVertex ]
* @memberof CubicBezierCurve
* @throws An exception if the passed array is malformed.
* @return {CubicBezierCurve}
**/
CubicBezierCurve.fromArray = function (arr) {
if (!Array.isArray(arr))
throw "Can only build from object.";
if (arr.length != 4)
throw "Can only build from array with four elements.";
return new CubicBezierCurve(arr[0], arr[1], arr[2], arr[3]);
};
/** @constant {number} */
CubicBezierCurve.START_POINT = 0;
/** @constant {number} */
CubicBezierCurve.START_CONTROL_POINT = 1;
/** @constant {number} */
CubicBezierCurve.END_CONTROL_POINT = 2;
/** @constant {number} */
CubicBezierCurve.END_POINT = 3;
/**
* Helper utils.
*/
CubicBezierCurve.utils = {
evaluateT: function (p0, p1, p2, p3, t) {
return p0 * Math.pow(1.0 - t, 3) +
p1 * 3 * t * Math.pow(1.0 - t, 2) +
p2 * 3 * Math.pow(t, 2) * (1.0 - t) +
p3 * Math.pow(t, 3);
},
cubicPolyMinMax: function (p0, p1, p2, p3) {
// var polyX = CubicBezierCurve.utils.cubicPoly2(
// p0, // P[0].X, // bezierCurve.startPoint.x,
// p1, // P[1].X, // bezierCurve.startControlPoint.x,
// p2, // P[2].X, // bezierCurve.endControlPoint.x,
// p3 // P[3].X // bezierCurve.endPoint.x
// );
// var a = polyX.a;
// var b = polyX.b;
// var c = polyX.c;
// var disc = polyX.b * polyX.b - 4 * polyX.a * polyX.c;
var polyX = CubicBezierCurve.utils.cubicPoly(p0, // P[0].X, // bezierCurve.startPoint.x,
p1, // P[1].X, // bezierCurve.startControlPoint.x,
p2, // P[2].X, // bezierCurve.endControlPoint.x,
p3 // P[3].X // bezierCurve.endPoint.x
);
var a = polyX[0]; // .a;
var b = polyX[1]; // .b;
var c = polyX[2]; // .c;
//alert("a "+a+" "+b+" "+c);
// var disc = b * b - 4 * a * c;
var disc = polyX[1] * polyX[1] - 4 * polyX[0] * polyX[2];
// var polyX = CubicBezierCurve.utils.bezierCoeffs(p3,p2,p1,p0);
// var a = polyX[0]; //polyX.a;
// var b = polyX[1]; // .b;
// var c = polyX[2]; //.c;
// var disc = polyX[1] * polyX[1] - 4 * polyX[0] * polyX[2];
// var xl = Math.min(bCurve.endPoint.x, bCurve.startPoint.x); // P[0].X;
// var xh = Math.max(bCurve.endPoint.x, bCurve.startPoint.x); // P[0].X;
var xl = Math.min(p3, p0); // P[0].X;
var xh = Math.max(p3, p0); // P[0].X;
// if (P[3].X < xl) xl = P[3].X;
// if (P[3].X > xh) xh = P[3].X;
if (disc >= 0) {
var t1 = (-b + Math.sqrt(disc)) / (2 * a);
// alert("t1 " + t1);
if (t1 > 0 && t1 < 1) {
// var x1 = evalBez(PX, t1);
// var x1 = bCurve.getPointAt(t1).x;
var x1 = CubicBezierCurve.utils.evaluateT(p0, p1, p2, p3, t1); // bCurve.getPointAt(t1).x;
if (x1 < xl)
xl = x1;
if (x1 > xh)
xh = x1;
}
var t2 = (-b - Math.sqrt(disc)) / (2 * a);
// alert("t2 " + t2);
if (t2 > 0 && t2 < 1) {
// var x2 = evalBez(PX, t2);
// var x2 = bCurve.getPointAt(t2).x;
var x2 = CubicBezierCurve.utils.evaluateT(p0, p1, p2, p3, t2); //
if (x2 < xl)
xl = x2;
if (x2 > xh)
xh = x2;
}
}
return { min: xl, max: xh };
},
/**
* Get the points of a sub curve at the given start end end offsets (values between 0.0 and 1.0).
*
* tStart >= tEnd is allowed, you will get a reversed sub curve then.
*
* @method getSubCurvePointsAt
* @param {CubicBezierCurve} curve – The curve to get the sub curve points from.
* @param {number} tStart – The start offset of the desired sub curve (must be in [0..1]).
* @param {number} tEnd – The end offset if the desired cub curve (must be in [0..1]).
* @instance
* @memberof CubicBezierCurve
* @return {CubicBezierCurve} The sub curve as a new curve.
**/
getSubCurvePointsAt: function (curve, tStart, tEnd) {
var startVec = new Vector_1.Vector(curve.getPointAt(tStart), curve.getTangentAt(tStart));
var endVec = new Vector_1.Vector(curve.getPointAt(tEnd), curve.getTangentAt(tEnd).inv());
// Tangents are relative. Make absolute.
startVec.b.add(startVec.a);
endVec.b.add(endVec.a);
// This 'splits' the curve at the given point at t.
startVec.scale(0.33333333 * (tEnd - tStart));
endVec.scale(0.33333333 * (tEnd - tStart));
return [startVec.a, endVec.a, startVec.b, endVec.b];
},
/**
* Compute the cubic roots for the given cubic polynomial coefficients.
*
* Based on
* http://mysite.verizon.net/res148h4j/javascript/script_exact_cubic.html#the%20source%20code
* Inspired by
* https://www.particleincell.com/2013/cubic-line-intersection/
* Thanks to Stephan Schmitt and Particle-In-Cell!
*
* @param poly
* @returns
*/
cubicRoots: function (poly) {
var a = poly[0];
var b = poly[1];
var c = poly[2];
var d = poly[3];
var A = b / a;
var B = c / a;
var C = d / a;
var S, T, Im;
var Q = (3 * B - Math.pow(A, 2)) / 9;
var R = (9 * A * B - 27 * C - 2 * Math.pow(A, 3)) / 54;
var D = Math.pow(Q, 3) + Math.pow(R, 2); // polynomial discriminant
var ts = []; // Array();
if (D >= 0) {
// complex or duplicate roots
S = CubicBezierCurve.utils.sgn(R + Math.sqrt(D)) * Math.pow(Math.abs(R + Math.sqrt(D)), 1 / 3);
T = CubicBezierCurve.utils.sgn(R - Math.sqrt(D)) * Math.pow(Math.abs(R - Math.sqrt(D)), 1 / 3);
ts[0] = -A / 3 + (S + T); // real root
ts[1] = -A / 3 - (S + T) / 2; // real part of complex root
ts[2] = -A / 3 - (S + T) / 2; // real part of complex root
Im = Math.abs((Math.sqrt(3) * (S - T)) / 2); // complex part of root pair
// Mark complex roots to be discarded
if (Im != 0) {
ts[1] = -1;
ts[2] = -1;
}
} // distinct real roots
else {
var th = Math.acos(R / Math.sqrt(-Math.pow(Q, 3)));
ts[0] = 2 * Math.sqrt(-Q) * Math.cos(th / 3) - A / 3;
ts[1] = 2 * Math.sqrt(-Q) * Math.cos((th + 2 * Math.PI) / 3) - A / 3;
ts[2] = 2 * Math.sqrt(-Q) * Math.cos((th + 4 * Math.PI) / 3) - A / 3;
Im = 0.0;
}
// Discard all t's out of spec and sort the rest
return ts.filter(function (t) { return t >= 0 && t <= 1.0; }).sort();
},
/**
* Compute the Bézier coefficients from the given Bézier point coordinates.
*
* @param {number} p0 - The start point coordinate.
* @param {number} p1 - The start control point coordinate.
* @param {number} p2 - The end control point coordinate.
* @param {number} p3 - The end point coordinate.
* @returns {[number,number,number,number]}
*/
bezierCoeffs: function (p0, p1, p2, p3) {
var coeffs = [NaN, NaN, NaN, NaN]; //Array(4);
coeffs[0] = -p0 + 3 * p1 + -3 * p2 + p3;
coeffs[1] = 3 * p0 - 6 * p1 + 3 * p2;
coeffs[2] = -3 * p0 + 3 * p1;
coeffs[3] = p0;
return coeffs;
},
/**
* Calculate the cubic polynomial coefficients used to find the bounding box.
*
* @param {number} p0 - The start point coordinate.
* @param {number} p1 - The start control point coordinate.
* @param {number} p2 - The end control point coordinate.
* @param {number} p3 - The end point coordinate.
* @returns {[number,number,number]}
*/
cubicPoly: function (p0, p1, p2, p3) {
return [
3 * p3 - 9 * p2 + 9 * p1 - 3 * p0,
6 * p0 - 12 * p1 + 6 * p2,
3 * p1 - 3 * p0
];
},
/**
* sign of number, but is division safe: no zero returned :)
*/
sgn: function (x) {
return x < 0.0 ? -1 : 1;
}
};
return CubicBezierCurve;
}());
exports.CubicBezierCurve = CubicBezierCurve;
//# sourceMappingURL=CubicBezierCurve.js.map