6

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.

Ryan Peschel
  • 11,087
  • 19
  • 74
  • 136
  • As I commented on your [other recent question](https://stackoverflow.com/q/69847201) on this topic, you will probably have to do some calculus to handle this exactly. This link may help: https://www.ck12.org/c/calculus/parametric-formula-for-length-of-a-curve/lesson/Parametric-Forms-and-Calculus%253A-Length-of-a-Curve/ – Scott Sauyet Nov 05 '21 at 16:56
  • @ScottSauyet Aren't these for a curves of a different form? I saw your comment but I wasn't sure if that would apply to bezier curves as well – Ryan Peschel Nov 05 '21 at 16:57
  • It should be for any parameterized curve, including these. These bezier curves are quadratic, so there are probably exact solutions, but I haven't tried it through. – Scott Sauyet Nov 05 '21 at 16:58
  • Wouldn't these require finding an equation for the line somehow first though? All I have are `x1`, `y1`, `cpX`, `cpY`, `x2`, and `y2` variables to describe the curve. I don't have equations like `x = 3t - t^3` like in the linked resource – Ryan Peschel Nov 05 '21 at 16:59
  • `calcX` and `calcY` are exactly those equations, functions of `t` using a number of constants. – Scott Sauyet Nov 05 '21 at 17:01
  • I suppose I'm just a bit confused then. I am aware that calculating the arc length for a bezier curve can be incredibly hard (as answers like this show https://gamedev.stackexchange.com/a/125321/147192). I guess I was just wondering that since this question was slightly different that perhaps there may be a more efficient solution. In that I'm not technically looking for arc length, but instead for where the x% mark on a curve is. It's just that the first solution I found used arc lengths to find this. – Ryan Peschel Nov 05 '21 at 17:05
  • I didn't say it would be *easy*. :-). I assume a numerical integration tool would work fine, but probably wouldn't be any faster than what you're doing now. – Scott Sauyet Nov 05 '21 at 17:11
  • 1
    Just use a lookup table, and either snap to LUT values if you generated a large LUT, or use interpolation if you generated a sparse LUT. See https://pomax.github.io/bezierinfo/#tracing – Mike 'Pomax' Kamermans Nov 05 '21 at 18:16
  • Just for the record, you absolutely *do* have equations like `x = 3t - t^3`. The first code snippet in [your previous question](https://stackoverflow.com/questions/69847201/how-to-find-the-halfway-mark-on-a-bezier-curve) is exactly that. – user3386109 Nov 05 '21 at 19:03

2 Answers2

1

We can try to modify your method to be a bit more efficient. It is still not the exact solution you hope for but it might do the trick.

Instead of repeatedly evaluating the Bézier curve at parameter values differing by 0.001 (where you do not reuse the computation from the previous step) we could use the idea of subdivision. Do you know De Casteljau's algorithm? It not only evaluates the Bézier curve at a given parameter t, it also provides you with means to subdivide the curve in two: one Bézier curve that equals the original curve on the interval [0, t] and another one that equals the original curve on [t, 1]. Their control polygons are a much better approximation of the curves than the original control polygon.

So, you would proceed as follows:

  1. Use De Casteljau's algorithm to subdivide the curve at t=0.5.
  2. Use De Casteljau's algorithm to subdivide the first segment at t=0.25.
  3. Use De Casteljau's algorithm to subdivide the second segment at t=0.75.
  4. Proceed recursively in the same manner until prescribed depth. This depends on the precision you would like to achieve.

The control polygons of these segments will be your (piecewise linear) approximation of the original Bézier curve. Either use them to precompute the parameters as you have done so far; or plot this approximation directly instead of using quadraticCurveTo with the original curve. Generating this approximation should be much faster than your procedure.

You can read more about this idea in Sections 3.3, 3.4 and 3.5 of Prautzsch, Boehm and Paluszny: Bézier and B-spline techniques. They also provide an estimate how quickly does this procedure converge to the original curve.

Dominik Mokriš
  • 1,118
  • 1
  • 8
  • 29
0

Not totally sure this will work, but are you aware of Horner's Scheme for plotting Bezier points?

        /***************************************************************************
        //
        //   This routine plots out a bezier curve, with multiple calls to hornbez()
        //
        //***************************************************************************
        function bezierCalculate(context, NumberOfDots, color, dotSize) {
            // This routine uses Horner's Scheme to draw entire Bezier Line...

            for (var t = 0.0; t < 1.0001; t = t + 1.0 / NumberOfDots) {
                xTemp = hornbez(numberOfControlPoints - 1, "x", t);
                yTemp = hornbez(numberOfControlPoints - 1, "y", t);
                drawDot(context, xTemp, yTemp, dotSize, color);
            }
        }

        //***************************************************************************
        //
        //   This routine uses  Horner's scheme to compute one coordinate
        //   value of a  Bezier curve. Has to be called
        //   for each coordinate  (x,y, and/or z) of a control polygon.
        //   See Farin, pg 59,60.  Note:  This technique is also called
        //   "nested multiplication".
        //   Input:   degree: degree of curve.
        //            coeff:  array with coefficients of curve.
        //            t:      parameter value.
        //            Output: coordinate value.
        //
        //***************************************************************************
        function hornbez(degree, xORy, t) {
            var i;
            var n_choose_i; /* shouldn't be too large! */
            var fact, t1, aux;

            t1 = 1 - t;
            fact = 1;
            n_choose_i = 1;

            var aux = FrameControlPt[0][xORy] * t1;
            /* starting the evaluation loop */
            for (i = 1; i < degree; i++) {
                fact = fact * t;
                n_choose_i = n_choose_i * (degree - i + 1) / i; /* always int! */
                aux = (aux + fact * n_choose_i * FrameControlPt[i][xORy]) * t1;
            }
            aux = aux + fact * t * FrameControlPt[degree][xORy];

            return aux;
        }

Not sure exactly where you are going here, but here's a reference of something I wrote a while ago... And for the contents of just the Bezier iframe, see this... My implied question? Is Bezier the right curve for you?

zipzit
  • 3,778
  • 4
  • 35
  • 63
  • What is `FrameControlPt` in your code example? Also, I believe bezier curves are fine for me. Well, I'm not really aware of many other types to be honest. Why, is there another type of curve that can achieve similar looking results to bezier curves which have simpler mathematics to work in? I'm rather ignorant of these topic of math. – Ryan Peschel Nov 05 '21 at 17:34
  • `FrameControlPt` is an array of JS objects `{x:,y:}` that define the "frame" of the curve. Look at the code in the iframe via browser inspect tools, link above. It's all in the clear and documented with comments. And check the Information buttons on the advantages and disadvantages of each of the different types of curves. Obviously I'm coming at this discussion from "the theory of Computer Assisted Drafting" (CAD). Your FrameControlPt question makes me wonder.. how are you defining the shape of your desired curves? – zipzit Nov 05 '21 at 17:45
  • I'm defining the shape using control points. This page https://pomax.github.io/bezierjs/ has a lot of interactive demos that you can play with to see how they work. – Ryan Peschel Nov 05 '21 at 17:50
  • Makes sense. Thats a great reference, thanks. Curves of interest? 1) Conics (Circle, Ellipse, parabola) 2) Bezier 3) B-Spline and 4) Non-Uniform B-Spline and I would add in #5) Catmull-Rom curve. Again, I have no clue where you are going here? Design boat hulls and racing sails? Analyzing stock market curves? Predicting human behavior? Playing with website analytics? And to reduce computation time, do you really need a solid line to define a curve? Will a series of points do? – zipzit Nov 05 '21 at 18:11
  • @zipzit I do not see naything related to the question (am I missing something?) What I can see is Your code evaluates Bernstein polynomials of selected degree to compute the point on curve however I do not see any linearization of the `t` parameter which is what the question is about. – Spektre Nov 06 '21 at 10:05