kld-contours
Version:
A collection of classes to represent and manipulate various vector shapes
367 lines (307 loc) • 10.4 kB
JavaScript
/**
* CubicBezier2D.js
*
* @copyright 2011-2019 Kevin Lindsey
* @module CubicBezier2D
*/
import {Polynomial, SqrtPolynomial} from "kld-polynomial";
import {Matrix2D, Point2D, Vector2D} from "kld-affine";
import BoundingBox2D from "./BoundingBox2D.js";
import Polygon2D from "./Polygon2D.js";
/**
* CubicBezier2D
*/
class CubicBezier2D {
/**
* @param {module:kld-affine.Point2D} p1
* @param {module:kld-affine.Point2D} p2
* @param {module:kld-affine.Point2D} p3
* @param {module:kld-affine.Point2D} p4
* @returns {module:kld-contours~CubicBezier2D}
*/
constructor(p1, p2, p3, p4) {
this.p1 = p1;
this.p2 = p2;
this.p3 = p3;
this.p4 = p4;
}
/**
* getPointAtParameter
*
* @param {number} t
* @returns {module:kld-affine.Point2D}
*/
getPointAtParameter(t) {
// TODO: validate t in [0,1]
// first round of lerps
const p5 = this.p1.lerp(this.p2, t);
const p6 = this.p2.lerp(this.p3, t);
const p7 = this.p3.lerp(this.p4, t);
// second round of lerps
const p8 = p5.lerp(p6, t);
const p9 = p6.lerp(p7, t);
return p8.lerp(p9, t);
}
/**
* splitAtParameter
*
* @param {number} t
* @returns {Array<module:kld-contours~CubicBezier2D>}
*/
splitAtParameter(t) {
// first round of lerps
const p5 = this.p1.lerp(this.p2, t);
const p6 = this.p2.lerp(this.p3, t);
const p7 = this.p3.lerp(this.p4, t);
// second round of lerps
const p8 = p5.lerp(p6, t);
const p9 = p6.lerp(p7, t);
// third round of lerps
const p10 = p8.lerp(p9, t);
return [
new CubicBezier2D(this.p1, p5, p8, p10),
new CubicBezier2D(p10, p9, p7, this.p4)
];
}
/**
* getBernsteinPolynomials
*
* @returns {{x: module:kld-polynomial.Polynomial, y: module:kld-polynomial.Polynomial}}
*/
getBernsteinPolynomials() {
let a, b, c;
// Start with Bezier using Bernstein polynomials for weighting functions:
// (1-t^3)P1 + 3t(1-t)^2P2 + 3t^2(1-t)P3 + t^3P4
//
// Expand and collect terms to form linear combinations of original Bezier
// controls. This ends up with a vector cubic in t:
// (-P1+3P2-3P3+P4)t^3 + (3P1-6P2+3P3)t^2 + (-3P1+3P2)t + P1
// /\ /\ /\ /\
// || || || ||
// c3 c2 c1 c0
// Calculate the coefficients
a = this.p1.multiply(-1);
b = this.p2.multiply(3);
c = this.p3.multiply(-3);
const c3 = a.add(b.add(c.add(this.p4)));
a = this.p1.multiply(3);
b = this.p2.multiply(-6);
c = this.p3.multiply(3);
const c2 = a.add(b.add(c));
a = this.p1.multiply(-3);
b = this.p2.multiply(3);
const c1 = a.add(b);
const c0 = this.p1;
return {
x: new Polynomial(c3.x, c2.x, c1.x, c0.x),
y: new Polynomial(c3.y, c2.y, c1.y, c0.y)
};
}
/**
* getArcLengthPolynomial
*
* @returns {module:kld-polynomial.SqrtPolynomial}
*/
getArcLengthPolynomial() {
// TODO: use Object.defineProperty
// TODO: use results from getBernsteinPolynomials but need getCoeficent in
// Polynomial first
/* eslint-disable-next-line no-prototype-builtins */
if (!this.hasOwnProperty("arcLengthPolynomial")) {
let a, b, c;
// convert to Bernstein polynomials
a = this.p1.multiply(-1);
b = this.p2.multiply(3);
c = this.p3.multiply(-3);
const c3 = a.add(b.add(c.add(this.p4)));
a = this.p1.multiply(3);
b = this.p2.multiply(-6);
c = this.p3.multiply(3);
const c2 = a.add(b.add(c));
a = this.p1.multiply(-3);
b = this.p2.multiply(3);
const c1 = a.add(b);
// ____________
// √ dx^2 + dy^2
//
this.arcLengthPolynomial = new SqrtPolynomial(
9 * (c3.x * c3.x + c3.y * c3.y),
12 * (c2.x * c3.x + c2.y * c3.y),
4 * (c2.x * c2.x + c2.y * c2.y) + 6 * (c1.x * c3.x + c1.y * c3.y),
4 * (c1.x * c2.x + c1.y * c2.y),
c1.x * c1.x + c1.y * c1.y
);
this.arcLength = this.arcLengthPolynomial.romberg(0, 1);
// this.arcLength = this.arcLengthPolynomial.simpson(0, 1);
}
return this.arcLengthPolynomial;
}
/**
* getParameterFromArcLength
*
* @param {number} arcLength
* @returns {number}
*/
getParameterFromArcLength(arcLength) {
// TODO: extract common Bezier code
// make sure the arc length polynomial and arc length values have been calculated
const arcLengthPolynomial = this.getArcLengthPolynomial();
let result;
if (arcLength <= 0) {
result = 0;
}
else if (arcLength >= this.arcLength) {
result = 1;
}
else {
const TOLERANCE = 1e-9;
const MAXITERS = 20;
result = arcLength / this.arcLength;
for (let i = 0; i <= MAXITERS; i++) {
const diff = arcLengthPolynomial.romberg(0, result) - arcLength;
if (Math.abs(diff) < TOLERANCE) {
break;
}
const value = arcLengthPolynomial.eval(result);
if (value === 0) {
break;
}
result -= diff / value;
}
}
return result;
}
/**
* getBoundingBox
*
* @returns {module:kld-contours~BoundingBox2D}
*/
getBoundingBox() {
const polys = this.getBernsteinPolynomials();
const dx = polys.x.getDerivative();
const dy = polys.y.getDerivative();
let roots = dx.getRootsInInterval(0, 1);
roots = roots.concat(dy.getRootsInInterval(0, 1));
// initialize min/max using the first and last points on the curve
let min = this.p1.min(this.p4);
let max = this.p1.max(this.p4);
// and now update min/max with points between p1 and p4
roots.forEach(function(t) {
if (0 <= t && t <= 1) {
const testPoint = new Point2D(
polys.x.eval(t),
polys.y.eval(t)
);
min = min.min(testPoint);
max = max.max(testPoint);
}
});
return new BoundingBox2D(
min.x,
min.y,
max.x - min.x,
max.y - min.y
);
}
/**
* getAlignedBoundingBox
*
* @returns {{ bbox: module:kld-contours~BoundingBox2D, transform: module:kld-affine.Matrix2D }}
*/
getAlignedBoundingBox() {
return {
bbox: this.getAlignedBezier().getBoundingBox(),
transform: Matrix2D
.translation(this.p1.x, this.p1.y)
.rotateFromVector(Vector2D.fromPoints(this.p1, this.p4))
};
}
/**
* getAlignedBezier
*
* @returns {module:kld-contours~CubicBezier2D}
*/
getAlignedBezier() {
const matrix = Matrix2D
.translation(this.p1.x, this.p1.y) // move p1 to the origin
.rotateFromVector(Vector2D.fromPoints(this.p1, this.p4)) // rotate vector from p1 to p4 to x-axis
.inverse(); // reverse transforms
// apply to each point and create new Bezier
return new CubicBezier2D(
this.p1.transform(matrix),
this.p2.transform(matrix),
this.p3.transform(matrix),
this.p4.transform(matrix)
);
}
/**
* toPolygon2D
*
* @param {number} [flatness]
* @returns {module:kld-contours~Polygon2D}
*/
toPolygon2D(flatness) {
const points = [];
const zeroVector = new Vector2D(0, 0);
flatness = flatness !== undefined ? flatness : 1.0;
// add first point
points.push(this.p1);
// add middle points
/**
* @param {number} p1
* @param {number} p2
* @param {number} p3
* @param {number} p4
*/
function tesselateInterior(p1, p2, p3, p4) {
// round 1
const p5 = p1.lerp(p2, 0.5);
const p6 = p2.lerp(p3, 0.5);
const p7 = p3.lerp(p4, 0.5);
// round 2
const p8 = p5.lerp(p6, 0.5);
const p9 = p6.lerp(p7, 0.5);
// round 3
const p10 = p8.lerp(p9, 0.5);
const baseline = Vector2D.fromPoints(p1, p4);
const tangent1 = Vector2D.fromPoints(p1, p2);
const tangent2 = Vector2D.fromPoints(p4, p3);
let dmax = 0;
if (zeroVector.equals(tangent1) === false) {
const perpendicular = baseline.perpendicular(tangent1);
dmax = perpendicular.length();
}
if (zeroVector.equals(tangent2) === false) {
const perpendicular = baseline.perpendicular(tangent2);
dmax = Math.max(dmax, perpendicular.length());
}
if (dmax > flatness) {
tesselateInterior(p1, p5, p8, p10);
points.push(new Point2D(p10.x, p10.y));
tesselateInterior(p10, p9, p7, p4);
}
else {
points.push(new Point2D(p10.x, p10.y));
}
}
// add interior points
tesselateInterior(this.p1, this.p2, this.p3, this.p4);
// add last point
points.push(this.p4);
return new Polygon2D(points);
}
/**
* toString
*
* @returns {string}
*/
toString() {
return (
"M" + this.p1.x + "," + this.p1.y + " " +
"C" + this.p2.x + "," + this.p2.y +
" " + this.p3.x + "," + this.p3.y +
" " + this.p4.x + "," + this.p4.y
);
}
}
export default CubicBezier2D;