0

I am trying to place evenly-spaced markers/dots on a quadratic curve drawn with HTML Canvas API. Found a nice article explaining how the paths are calculated in the first place, at determining coordinates on canvas curve.

There is a formula, at the end, to calculate the angle:

function getQuadraticAngle(t, sx, sy, cp1x, cp1y, ex, ey) {
  var dx = 2*(1-t)*(cp1x-sx) + 2*t*(ex-cp1x);
  var dy = 2*(1-t)*(cp1y-sy) + 2*t*(ey-cp1y);
  return Math.PI / 2 - Math.atan2(dx, dy);
}

The x/y pairs that we pass, are the current position, the control point and the end position of the curve - exactly what is needed to pass to the canvas context, and t is a value from 0 to 1. Unless I somehow misunderstood the referenced article.

I want to do something very similar - place my markers over the distance specified s, rather than use t. This means, unless I am mistaken, that I need to calculate the length of the "curved path" and from there, I could probably use the above formula.

I found a solution for the length in JavaScript at length of quadratic curve. The formula is similar to: length of the curve formula.

And added the below function:

function quadraticBezierLength(x1, y1, x2, y2, x3, y3) {
  let a, b, c, d, e, u, a1, e1, c1, d1, u1, v1x, v1y;

  v1x = x2 * 2;
  v1y = y2 * 2;
  d = x1 - v1x + x3;
  d1 = y1 - v1y + y3;
  e = v1x - 2 * x1;
  e1 = v1y - 2 * y1;
  c1 = a = 4 * (d * d + d1 * d1);
  c1 += b = 4 * (d * e + d1 * e1);
  c1 += c = e * e + e1 * e1;
  c1 = 2 * Math.sqrt(c1);
  a1 = 2 * a * (u = Math.sqrt(a));
  u1 = b / u;
  a = 4 * c * a - b * b;
  c = 2 * Math.sqrt(c);
  return (
    (a1 * c1 + u * b * (c1 - c) + a * Math.log((2 * u + u1 + c1) / (u1 + c))) /
    (4 * a1)
  );
}

Now, I am trying to space markers evenly. I thought that making "entropy" smooth - dividing the total length by the step length would result in the n markers, so going using the 1/nth step over t would do the trick. However, this does not work. The correlation between t and distance on the curve is not linear.

How do I solve the equation "backwards" - knowing the control point, the start, and the length of the curved path, calculate the end-point?

Igor Shmukler
  • 1,742
  • 3
  • 15
  • 48
  • Found another solution at https://gamedev.stackexchange.com/questions/6009/bezier-curve-arc-length and it seems to be the same as "my" `quadraticBezierLength()`, and it too gives a wrong result. – Igor Shmukler Nov 27 '22 at 19:51
  • 1
    Given `quadraticBezierLength(100, 100,200,100.001,300,100) == 200.0000000030346` and that the result is always > than the distance between end points the function looks good. Note that the function fails if the control point is on the line between end points – Blindman67 Nov 27 '22 at 19:59
  • Based on https://pomax.github.io/bezierinfo/#tracing there is not a good approximation for points at distance, at least not for the generic bezier curve case. The numerical (Newton) solution is always an option, yet a formula would have been much better. – Igor Shmukler Nov 29 '22 at 00:48
  • However, it seems that the tracing example covers a cubic function case - two control points. I know that quartic is the highest-order polynomial equation that can be solved by radicals in the general case. Perhaps here too a solution for quadratic equations exists [while a generic one for higher-order curves does not], or at least a good approximation for such a solution, without having to resort to numeric solutions. ??? – Igor Shmukler Nov 29 '22 at 02:22

1 Answers1

0

Not sure I fully understand what you mean by "space markers evenly" but I do have some code that I did with curves and markers that maybe can help you ...

Run the code below, it should output a canvas like this:

function drawBezierCurve(p0, p1, p2, p3) {
  distance = 0
  last = null
  for (let t = 0; t <= 1; t += 0.0001) {
    const x = Math.pow(1 - t, 3) * p0[0] + 3 * Math.pow(1 - t, 2) * t * p1[0] + 3 * (1 - t) * Math.pow(t, 2) * p2[0] + Math.pow(t, 3) * p3[0];
    const y = Math.pow(1 - t, 3) * p0[1] + 3 * Math.pow(1 - t, 2) * t * p1[1] + 3 * (1 - t) * Math.pow(t, 2) * p2[1] + Math.pow(t, 3) * p3[1];
    ctx.lineTo(x, y);
    if (last) {
      distance += Math.sqrt((x - last[0]) ** 2 + (y - last[1]) ** 2)
      if (distance >= 30) {
        ctx.rect(x - 1, y - 1, 2, 2);
        distance = 0
      }
    }
    last = [x, y]
  }
}

const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');

ctx.beginPath();
drawBezierCurve([0, 0], [40, 300], [200, -90], [300, 150]);
ctx.stroke();
<canvas id="canvas" width=300 height=150></canvas>

I created the drawBezierCurve function, there I'm using a parametric equation of a bezier curve and then I use lineTo to draw between points, and also we get a distance between points, the points are very close so my thinking is OK to use the Pythagorean theorem to calculate the distance, and the markers are just little rectangles.

Helder Sepulveda
  • 15,500
  • 4
  • 29
  • 56
  • You used numeric approach for cubic curve, it seems. The distance that you are calculating is the length of a straight line between two points, not the length of the curved path. – Igor Shmukler Dec 19 '22 at 09:40
  • @IgorShmukler Yes I mentioned that ... but those points are very close to each other, in my example there are hundreds of points between markers ... while not an exact calculation it is a good approximation ... at the end we are drawing on a screen made of finite pixels – Helder Sepulveda Dec 19 '22 at 14:00
  • I agree, depending on the requirements, your solution might be enough. It is not terribly relevant to my use-case, though. ;) – Igor Shmukler Dec 19 '22 at 20:41