kld-contours
Version:
A collection of classes to represent and manipulate various vector shapes
306 lines (255 loc) • 8 kB
JavaScript
/**
* QuadraticBezier2D.js
*
* @copyright 2011-2019 Kevin Lindsey
* @module QuadraticBezier2D
*/
import {Polynomial, SqrtPolynomial} from "kld-polynomial";
import {Matrix2D, Point2D, Vector2D} from "kld-affine";
import BoundingBox2D from "./BoundingBox2D.js";
import Polygon2D from "./Polygon2D.js";
/**
* QuadraticBezier2D
*/
class QuadraticBezier2D {
/**
* @param {module:kld-affine.Point2D} p1
* @param {module:kld-affine.Point2D} p2
* @param {module:kld-affine.Point2D} p3
* @returns {module:kld-contours~QuadraticBezier2D}
*/
constructor(p1, p2, p3) {
this.p1 = p1;
this.p2 = p2;
this.p3 = p3;
}
/**
* getPointAtParameter
*
* @param {number} t
* @returns {module:kld-affine.Point2D}
*/
getPointAtParameter(t) {
// TODO: validate t in [0,1]
// first round of lerps
const p4 = this.p1.lerp(this.p2, t);
const p5 = this.p2.lerp(this.p3, t);
return p4.lerp(p5, t);
}
/**
* splitAtParameter
*
* @param {number} t
* @returns {Array<module:kld-contours~QuadraticBezier2D>}
*/
splitAtParameter(t) {
// first round of lerps
const p4 = this.p1.lerp(this.p2, t);
const p5 = this.p2.lerp(this.p3, t);
// second round of lerps
const p6 = p4.lerp(p5, t);
return [
new QuadraticBezier2D(this.p1, p4, p6),
new QuadraticBezier2D(p6, p5, this.p3)
];
}
/**
* getBernsteinPolynomials
*
* @returns {{x: module:kld-polynomial.Polynomial, y: module:kld-polynomial.Polynomial}}
*/
getBernsteinPolynomials() {
let a; // temporary variables
a = this.p2.multiply(-2);
const c2 = this.p1.add(a.add(this.p3));
a = this.p1.multiply(-2);
const b = this.p2.multiply(2);
const c1 = a.add(b);
const c0 = this.p1;
return {
x: new Polynomial(c2.x, c1.x, c0.x),
y: new Polynomial(c2.y, c1.y, c0.y)
};
}
/**
* getArcLengthPolynomial
*
* @returns {module:kld-polynomial.SqrtPolynomial}
*/
getArcLengthPolynomial() {
/* eslint-disable-next-line no-prototype-builtins */
if (!this.hasOwnProperty("arcLengthPolynomial")) {
let a;
a = this.p2.multiply(-2);
const c2 = this.p1.add(a.add(this.p3));
a = this.p1.multiply(-2);
const b = this.p2.multiply(2);
const c1 = a.add(b);
// ____________
// √ dx^2 + dy^2
//
this.arcLengthPolynomial = new SqrtPolynomial(
4 * (c2.x * c2.x + c2.y * c2.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));
let min = this.p1.min(this.p3);
let max = this.p1.max(this.p3);
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.p3))
};
}
/**
* getAlignedBezier
*
* @returns {module:kld-contours~QuadraticBezier2D}
*/
getAlignedBezier() {
const matrix = Matrix2D
.translation(this.p1.x, this.p1.y) // move p1 to the origin
.rotateFromVector(Vector2D.fromPoints(this.p1, this.p3)) // rotate vector from p1 to p4 to x-axis
.inverse(); // reverse transforms
// apply to each point and create new Bezier
return new QuadraticBezier2D(
this.p1.transform(matrix),
this.p2.transform(matrix),
this.p3.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
*/
function tesselateInterior(p1, p2, p3) {
// round 1
const p4 = p1.lerp(p2, 0.5);
const p5 = p2.lerp(p3, 0.5);
// round 2
const p6 = p4.lerp(p5, 0.5);
const baseline = Vector2D.fromPoints(p1, p3);
const tangent = Vector2D.fromPoints(p1, p2);
let dmax = 0;
if (zeroVector.equals(tangent) === false) {
const perpendicular = baseline.perpendicular(tangent);
dmax = perpendicular.length();
}
if (dmax > flatness) {
tesselateInterior(p1, p4, p6);
points.push(new Point2D(p6.x, p6.y));
tesselateInterior(p6, p5, p3);
}
else {
points.push(new Point2D(p6.x, p6.y));
}
}
// add interior points
tesselateInterior(this.p1, this.p2, this.p3);
// add last point
points.push(this.p3);
return new Polygon2D(points);
}
/**
* toString
*
* @returns {string}
*/
toString() {
return (
"M" + this.p1.x + "," + this.p1.y + " " +
"Q" + this.p2.x + "," + this.p2.y +
" " + this.p3.x + "," + this.p3.y
);
}
}
export default QuadraticBezier2D;