Forgive me for the long code example, but I couldn't figure out how to properly explain my question with any less code:
let c = document.querySelector("canvas");
let ctx = c.getContext("2d");
class BezierCurve {
constructor(x1, y1, cpX, cpY, x2, y2) {
this.f = 0;
this.x1 = x1;
this.y1 = y1;
this.cpX = cpX;
this.cpY = cpY;
this.x2 = x2;
this.y2 = y2;
this.pointCache = this.calcPoints();
}
calcX(t) { return (1 - t) * (1 - t) * this.x1 + 2 * (1 - t) * t * this.cpX + t * t * this.x2; }
calcY(t) { return (1 - t) * (1 - t) * this.y1 + 2 * (1 - t) * t * this.cpY + t * t * this.y2; }
calcPoints() {
const step = 0.001, segments = [];
for (let i = 0; i <= 1 - step; i += step) {
let dx = this.calcX(i) - this.calcX(i + step);
let dy = this.calcY(i) - this.calcY(i + step);
segments.push(Math.sqrt(dx * dx + dy * dy));
}
const len = segments.reduce((a, c) => a + c, 0);
let result = [], l = 0, co = 0;
for (let i = 0; i < segments.length; i++) {
l += segments[i];
co += step;
result.push({ t: l / len, co });
}
return result;
}
draw() {
ctx.beginPath();
ctx.moveTo(this.x1, this.y1);
ctx.quadraticCurveTo(this.cpX, this.cpY, this.x2, this.y2);
ctx.stroke();
}
tick(amount = 0.001) {
this.f = this.f < 1 ? this.f + amount : 0;
}
}
function drawCircle(x, y, r) {
ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.fill();
}
let a = new BezierCurve(25, 25, 80, 250, 100, 50);
let b = new BezierCurve(225, 25, 280, 250, 300, 50);
function draw(curve, fraction) {
let x = curve.calcX(fraction);
let y = curve.calcY(fraction);
curve.draw();
drawCircle(x, y, 5);
curve.tick();
}
// Inefficient but using this instead of binary search just to save space in code example
function findClosestNumInArray(arr, goal) {
return arr.reduce((prev, cur) => Math.abs(cur.t - goal) < Math.abs(prev.t - goal) ? cur : prev);
}
function drawLoop(elapsed) {
c.width = 600;
c.height = 600;
draw(a, a.f);
let closest = findClosestNumInArray(b.pointCache, b.f).co;
draw(b, closest);
requestAnimationFrame(drawLoop);
}
drawLoop(0);
<canvas></canvas>
Okay, so, to explain what's going on: if you hit Run code snippet
you'll see that there are two curves, which I'll refer to as a
(left one) and b
(right one).
You may notice that the dot moving along a's curve starts off fast, then slows down around the curve, and then speeds up again. This is despite the fractional part being incremented by a constant 0.001
each frame.
The dot for b
on the other hand moves at a constant velocity throughout the entire iteration. This is because for b
I use the pointCache
mapping that I precompute for the curve. This function calcPoints
generates a mapping such that the input fractional component t
is associated with the "proper" actual percentage along the curve co
.
Anyways, this all works, but my issue is that the precomputation calcPoints
is expensive, and referencing a lookup table to find the actual fractional part along the line for a percentage is inexact and requires significant memory usage. I was wondering if there was a better way.
What I'm looking for is a way to do something like curve.calcX(0.5)
and actually get the 50% mark along the curve. Because currently the existing equation does not do this, and I instead have to do this costly workaround.
x1
,y1
,cpX
,cpY
,x2
, andy2
variables to describe the curve. I don't have equations likex = 3t - t^3
like in the linked resource – BennecalcX
andcalcY
are exactly those equations, functions oft
using a number of constants. – Fatiguex = 3t - t^3
. The first code snippet in your previous question is exactly that. – Bimestrial