You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
254 lines
7.1 KiB
254 lines
7.1 KiB
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,
|
|
};
|