blob: 07e9786f8426fe0119c3d063f5ef776abf3145ed [file] [log] [blame]
function floatEqual(a, b) {
return Math.abs(a - b) * 100000 <= Math.min(Math.abs(a), Math.abs(b));
}
function floatZero(f) {
return Math.abs(f) <= 0.00001;
}
function lerp(p0, p1, amount) {
return p0 * (1 - amount) + p1 * amount;
}
function lerpPoint(p0, p1, amount) {
return [lerp(p0[0], p1[0], amount), lerp(p0[1], p1[1], amount)];
}
function quadRoots(a, b, c) {
// no root
if (a === 0) return [];
var s = b * b - 4 * a * c;
// Complex roots
if (s < 0) return [];
var singleRoot = -b / (2 * a);
// 1 root
if (s === 0) return [singleRoot];
var delta = Math.sqrt(s) / (2 * a);
// 2 roots
return [singleRoot - delta, singleRoot + delta];
}
function polynomialCoefficients(p0, p1, p2, p3) {
return [
-p0 + 3 * p1 - 3 * p2 + p3,
3 * p0 - 6 * p1 + 3 * p2,
-3 * p0 + 3 * p1,
p0,
];
}
function singlePoint(p) {
return new PolynomialBezier(p, p, p, p, false);
}
function PolynomialBezier(p0, p1, p2, p3, linearize) {
if (linearize && pointEqual(p0, p1)) {
p1 = lerpPoint(p0, p3, 1 / 3);
}
if (linearize && pointEqual(p2, p3)) {
p2 = lerpPoint(p0, p3, 2 / 3);
}
var coeffx = polynomialCoefficients(p0[0], p1[0], p2[0], p3[0]);
var coeffy = polynomialCoefficients(p0[1], p1[1], p2[1], p3[1]);
this.a = [coeffx[0], coeffy[0]];
this.b = [coeffx[1], coeffy[1]];
this.c = [coeffx[2], coeffy[2]];
this.d = [coeffx[3], coeffy[3]];
this.points = [p0, p1, p2, p3];
}
PolynomialBezier.prototype.point = function (t) {
return [
(((this.a[0] * t) + this.b[0]) * t + this.c[0]) * t + this.d[0],
(((this.a[1] * t) + this.b[1]) * t + this.c[1]) * t + this.d[1],
];
};
PolynomialBezier.prototype.derivative = function (t) {
return [
(3 * t * this.a[0] + 2 * this.b[0]) * t + this.c[0],
(3 * t * this.a[1] + 2 * this.b[1]) * t + this.c[1],
];
};
PolynomialBezier.prototype.tangentAngle = function (t) {
var p = this.derivative(t);
return Math.atan2(p[1], p[0]);
};
PolynomialBezier.prototype.normalAngle = function (t) {
var p = this.derivative(t);
return Math.atan2(p[0], p[1]);
};
PolynomialBezier.prototype.inflectionPoints = function () {
var denom = this.a[1] * this.b[0] - this.a[0] * this.b[1];
if (floatZero(denom)) return [];
var tcusp = (-0.5 * (this.a[1] * this.c[0] - this.a[0] * this.c[1])) / denom;
var square = tcusp * tcusp - ((1 / 3) * (this.b[1] * this.c[0] - this.b[0] * this.c[1])) / denom;
if (square < 0) return [];
var root = Math.sqrt(square);
if (floatZero(root)) {
if (root > 0 && root < 1) return [tcusp];
return [];
}
return [tcusp - root, tcusp + root].filter(function (r) { return r > 0 && r < 1; });
};
PolynomialBezier.prototype.split = function (t) {
if (t <= 0) return [singlePoint(this.points[0]), this];
if (t >= 1) return [this, singlePoint(this.points[this.points.length - 1])];
var p10 = lerpPoint(this.points[0], this.points[1], t);
var p11 = lerpPoint(this.points[1], this.points[2], t);
var p12 = lerpPoint(this.points[2], this.points[3], t);
var p20 = lerpPoint(p10, p11, t);
var p21 = lerpPoint(p11, p12, t);
var p3 = lerpPoint(p20, p21, t);
return [
new PolynomialBezier(this.points[0], p10, p20, p3, true),
new PolynomialBezier(p3, p21, p12, this.points[3], true),
];
};
function extrema(bez, comp) {
var min = bez.points[0][comp];
var max = bez.points[bez.points.length - 1][comp];
if (min > max) {
var e = max;
max = min;
min = e;
}
// Derivative roots to find min/max
var f = quadRoots(3 * bez.a[comp], 2 * bez.b[comp], bez.c[comp]);
for (var i = 0; i < f.length; i += 1) {
if (f[i] > 0 && f[i] < 1) {
var val = bez.point(f[i])[comp];
if (val < min) min = val;
else if (val > max) max = val;
}
}
return {
min: min,
max: max,
};
}
PolynomialBezier.prototype.bounds = function () {
return {
x: extrema(this, 0),
y: extrema(this, 1),
};
};
PolynomialBezier.prototype.boundingBox = function () {
var bounds = this.bounds();
return {
left: bounds.x.min,
right: bounds.x.max,
top: bounds.y.min,
bottom: bounds.y.max,
width: bounds.x.max - bounds.x.min,
height: bounds.y.max - bounds.y.min,
cx: (bounds.x.max + bounds.x.min) / 2,
cy: (bounds.y.max + bounds.y.min) / 2,
};
};
function intersectData(bez, t1, t2) {
var box = bez.boundingBox();
return {
cx: box.cx,
cy: box.cy,
width: box.width,
height: box.height,
bez: bez,
t: (t1 + t2) / 2,
t1: t1,
t2: t2,
};
}
function splitData(data) {
var split = data.bez.split(0.5);
return [
intersectData(split[0], data.t1, data.t),
intersectData(split[1], data.t, data.t2),
];
}
function boxIntersect(b1, b2) {
return Math.abs(b1.cx - b2.cx) * 2 < b1.width + b2.width
&& Math.abs(b1.cy - b2.cy) * 2 < b1.height + b2.height;
}
function intersectsImpl(d1, d2, depth, tolerance, intersections, maxRecursion) {
if (!boxIntersect(d1, d2)) return;
if (depth >= maxRecursion || (d1.width <= tolerance && d1.height <= tolerance && d2.width <= tolerance && d2.height <= tolerance)) {
intersections.push([d1.t, d2.t]);
return;
}
var d1s = splitData(d1);
var d2s = splitData(d2);
intersectsImpl(d1s[0], d2s[0], depth + 1, tolerance, intersections, maxRecursion);
intersectsImpl(d1s[0], d2s[1], depth + 1, tolerance, intersections, maxRecursion);
intersectsImpl(d1s[1], d2s[0], depth + 1, tolerance, intersections, maxRecursion);
intersectsImpl(d1s[1], d2s[1], depth + 1, tolerance, intersections, maxRecursion);
}
PolynomialBezier.prototype.intersections = function (other, tolerance, maxRecursion) {
if (tolerance === undefined) tolerance = 2;
if (maxRecursion === undefined) maxRecursion = 7;
var intersections = [];
intersectsImpl(intersectData(this, 0, 1), intersectData(other, 0, 1), 0, tolerance, intersections, maxRecursion);
return intersections;
};
PolynomialBezier.shapeSegment = function (shapePath, index) {
var nextIndex = (index + 1) % shapePath.length();
return new PolynomialBezier(shapePath.v[index], shapePath.o[index], shapePath.i[nextIndex], shapePath.v[nextIndex], true);
};
PolynomialBezier.shapeSegmentInverted = function (shapePath, index) {
var nextIndex = (index + 1) % shapePath.length();
return new PolynomialBezier(shapePath.v[nextIndex], shapePath.i[nextIndex], shapePath.o[index], shapePath.v[index], true);
};
function crossProduct(a, b) {
return [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
];
}
function lineIntersection(start1, end1, start2, end2) {
var v1 = [start1[0], start1[1], 1];
var v2 = [end1[0], end1[1], 1];
var v3 = [start2[0], start2[1], 1];
var v4 = [end2[0], end2[1], 1];
var r = crossProduct(
crossProduct(v1, v2),
crossProduct(v3, v4)
);
if (floatZero(r[2])) return null;
return [r[0] / r[2], r[1] / r[2]];
}
function polarOffset(p, angle, length) {
return [
p[0] + Math.cos(angle) * length,
p[1] - Math.sin(angle) * length,
];
}
function pointDistance(p1, p2) {
return Math.hypot(p1[0] - p2[0], p1[1] - p2[1]);
}
function pointEqual(p1, p2) {
return floatEqual(p1[0], p2[0]) && floatEqual(p1[1], p2[1]);
}
export {
PolynomialBezier,
lineIntersection,
polarOffset,
pointDistance,
pointEqual,
floatEqual,
};