class InterpolatedBezierCurve {
originalPoints;
curTruncatedPoints;
arrowHeadLeft;
arrowHeadRight;
constructor(points) {
if (!Array.isArray(points)) {
throw `expected points to be an array but received: ${points}`
}
if (points.length !== 4) {
throw `expected points to have length 4 but received length: %{points.length}`
}
this.originalPoints = [...points];
this.curTruncatedPoints = [...points];
this.arrowHeadLeft = glm.vec2.create();
this.arrowHeadRight = glm.vec2.create();
this.cacheArrowHeads();
}
update(currentTimestamp, interpolator) {
const t = interpolator.t(currentTimestamp);
if (t > 1) {
return;
}
this.curTruncatedPoints = this._truncate(0, t);
}
static evaluate(cp, t) {
// (1 - t) ^ 3 P_0 + 3t.(1 - t) ^ 2 P_1 + 3t ^ 2.(1 - t) P_2 + t ^ 3 P_3
const oneMinusT = (1 - t)
const c0 = oneMinusT * oneMinusT * oneMinusT;
const c1 = 3 * t * oneMinusT * oneMinusT;
const c2 = 3 * t * t * oneMinusT;
const c3 = t * t * t;
// CONSIDER: can be optimized to use just one out parameter
const a = glm.vec2.scale(glm.vec2.create(), cp[0], c0);
const b = glm.vec2.scale(glm.vec2.create(), cp[1], c1);
const c = glm.vec2.scale(glm.vec2.create(), cp[2], c2);
const d = glm.vec2.scale(glm.vec2.create(), cp[3], c3);
let r = glm.vec2.create();
r = glm.vec2.add(r, a, b);
r = glm.vec2.add(r, r, c);
r = glm.vec2.add(r, r, d);
return r;
}
static evaluateTangent(cp, t) {
// (-3 + 6t + -3t ^ 2) P_0 + (3 - 12t + 9t ^ 2) P_1 + (6t - 9t ^ 2) P_2 + 3t ^ 2.P_3
const c0 = -3 + (6 * t) - 3 * (t * t);
const c1 = 3 - (12 * t) + 9 * (t * t);
const c2 = (6 * t) - 9 * (t * t);
const c3 = 3 * (t * t);
// CONSIDER: can be optimized to use just one out parameter
const a = glm.vec2.scale(glm.vec2.create(), cp[0], c0);
const b = glm.vec2.scale(glm.vec2.create(), cp[1], c1);
const c = glm.vec2.scale(glm.vec2.create(), cp[2], c2);
const d = glm.vec2.scale(glm.vec2.create(), cp[3], c3);
let r = glm.vec2.create();
r = glm.vec2.add(r, a, b);
r = glm.vec2.add(r, r, c);
r = glm.vec2.add(r, r, d);
return r;
}
cacheArrowHeads(length, angleOffsetFromCurve) {
let headTangent = glm.vec2.sub(glm.vec2.create(), this.curTruncatedPoints[3], this.curTruncatedPoints[2]);
headTangent = glm.vec2.normalize(headTangent, headTangent);
headTangent = glm.vec2.scale(headTangent, headTangent, length);
let pointAtHeadTangent = glm.vec2.add(headTangent, this.curTruncatedPoints[3], headTangent);
this.arrowHeadLeft = glm.vec2.rotate(this.arrowHeadLeft, pointAtHeadTangent, this.curTruncatedPoints[3], Math.PI - angleOffsetFromCurve);
this.arrowHeadRight = glm.vec2.rotate(this.arrowHeadRight, this.arrowHeadLeft, this.curTruncatedPoints[3], 2 * angleOffsetFromCurve);
}
_truncate(tStart, tEnd) {
// Yeah I know my usecase will always make tStart = 0, but keeping it general.
const q0 = InterpolatedBezierCurve.evaluate(this.originalPoints, tStart);
const q3 = InterpolatedBezierCurve.evaluate(this.originalPoints, tEnd);
let tan0 = InterpolatedBezierCurve.evaluateTangent(this.originalPoints, tStart);
tan0 = glm.vec2.scale(tan0, tan0, (tEnd - tStart) / 3);
let tan3 = InterpolatedBezierCurve.evaluateTangent(this.originalPoints, tEnd);
tan3 = glm.vec2.scale(tan3, tan3, (tEnd - tStart) / 3);
const q1 = glm.vec2.add(glm.vec2.create(), q0, tan0);
const q2 = glm.vec2.sub(glm.vec2.create(), q3, tan3);
return [q0, q1, q2, q3];
}
}