0

I'm trying to calculate a 2 concentric arcs (cubic bezier) from a given arc (quadratic bezier). I figured I could calculate control points for the cubic at 1/3 and 2/3 but it doesn't quite match up.

  var u = 1 / 3; // fraction of curve where Px1 and Py1 are 
  var v = 2 / 3; // fraction of curve where Px2 and Py2 are 
  //Calculate control points (Cx1, Cy1, Cx2, Cy2)
  var a = 3 * (1 - u) * (1 - u) * u;
  var b = 3 * (1 - u) * u * u;
  var c = 3 * (1 - v) * (1 - v) * v;
  var d = 3 * (1 - v) * v * v;
  var det = a * d - b * c;
  var Qx1 = Px1 - ((1 - u) * (1 - u) * (1 - u) * Px0 + u * u * u * Px3);
  var Qy1 = Py1 - ((1 - u) * (1 - u) * (1 - u) * Py0 + u * u * u * Py3);
  var Qx2 = Px2 - ((1 - v) * (1 - v) * (1 - v) * Px0 + v * v * v * Px3);
  var Qy2 = Py2 - ((1 - v) * (1 - v) * (1 - v) * Py0 + v * v * v * Py3);
  var Cx1 = (d * Qx1 - b * Qx2) / det;
  var Cy1 = (d * Qy1 - b * Qy2) / det;
  var Cx2 = ((-c) * Qx1 + a * Qx2) / det;
  var Cy2 = ((-c) * Qy1 + a * Qy2) / det;
  ctx.beginPath();
  ctx.moveTo(Px0, Py0);
  ctx.bezierCurveTo(Cx1, Cy1, Cx2, Cy2, Px3, Py3);
  ctx.strokeStyle = "#0000FF";
  ctx.stroke();

Are the control points also dependent on the radius of the arc or something entirely different? Is a cubic bezier even a good option for drawing a concentric arc? Quadratic bezier definitely does not work and cubic definitely got me closer to what I need.

Here is the link: http://codepen.io/davidreed0/full/zGqPxQ/

Use the position slider to move the ellipse.

JasonMArcher
  • 14,195
  • 22
  • 56
  • 52
dreed75
  • 115
  • 1
  • 9
  • 1
    You're using 2 different sets of largely irreconcilable terms: "cubic & quadratic Bezier curves" relate to curved paths while "arc & radius" relate to [semi-]circular paths. Those kinds of curves cannot represent circular paths & visa-versa. Please clarify what you're trying to do. :-) – markE Jun 01 '15 at 20:06
  • Why not just use an arc() and calculate the start/end angles? –  Jun 01 '15 at 20:14
  • @K3N, That's what I was thinking. Do you understand what the question wants...I'm not clear? :-/ – markE Jun 01 '15 at 20:19
  • @markE I'm not entirely sure, I assume he wants the centers of the blue and green to line up (concentric) and that the blue covers/outlines/embeds (?) the green ellipse, but I don't know if beziers are a requirement. I do think arcs (or perhaps the new ellipse) would be easier and more accurate to deal with. –  Jun 01 '15 at 20:19
  • I use "arc" to describe the curve. Sorry. It is not an actual arc. The quadratic bezier is given--meaning I can't change that. I am trying to "match" it with a curve above and a curve below. Obviously, I can't just draw another quadratic curve above and below because the curve above or below is smaller or larger than the given middle curve. – dreed75 Jun 01 '15 at 20:54

1 Answers1

0

The question as it stands is a bit unclear about requirements. Here is in any case an approach that does not require much calculations, but takes advantage of draw operations to visualize about the same as shown in the codepen.

The main steps are:

  • At an off-screen canvas:
  • Define a line thickness with radius set to the green area
  • Define round caps for the line
  • Draw the Bezier line with solid color
  • Draw the result into main canvas with various offsets relative to the thickness of the blue line.
  • Clear the center and you will have the blue outline
  • Implement a manual Bezier so you can draw the green arc/ellipse at any point within that shape

Radius/diameter can be expanded. If you need variable radius you can just use the Bezier formula to plot a series of blue arcs on top of each other instead.

Proof-of-concept

This will show the process step-by-step.

Step 1

On an off-screen canvas (shown on-screen here for demo, we'll switch in the next step):

step1snap

var c = document.querySelector("canvas"),
    ctx = c.getContext("2d"),
    dia = 90;                                        // diameter of graphics

ctx.strokeStyle = "blue";                            // color
ctx.lineWidth = dia;                                 // line-width = dia
ctx.lineCap = "round";                               // round caps

// draw bezier (quadratic, one control point)
ctx.moveTo(dia, dia);
ctx.quadraticCurveTo(300, 230, c.width - dia, dia);
ctx.stroke();
<canvas width=600 height=300></canvas>

Done. We now have the main shape. Adjust points as needed.

Step 2

As we now have the main shape we will create the outline using this shape:

  • Draw this to main canvas offset it circle (f.ex. 8 position around main area)
  • Knock out the center using comp. mode "destination-out" to leave only the outline

step2snap

var c = document.querySelector("canvas"),
    co = document.createElement("canvas"),
    ctx = c.getContext("2d"),
    ctxo = co.getContext("2d"),
    dia = 90;

co.width = c.width;
co.height = c.height;

ctxo.strokeStyle = "blue";
ctxo.lineWidth = dia;
ctxo.lineCap = "round";

// draw bezier (quadratic here, one control point)
ctxo.moveTo(dia, dia);
ctxo.quadraticCurveTo(300, 230, c.width - dia, dia);
ctxo.stroke();

// draw multiple times to main canvas
var thickness = 1, angle = 0, step = Math.PI * 0.25;

for(; angle < Math.PI * 2; angle += step) {
  var x = thickness * Math.cos(angle),
      y = thickness * Math.sin(angle);
  ctx.drawImage(co, x, y);
}

// punch out center
ctx.globalCompositeOperation = "destination-out";
ctx.drawImage(co, 0, 0);
<canvas width=600 height=300></canvas>

Step 3

Plot the green circle using a custom implementation of the canvas. First we back up a copy of the resulting blue outline so we can redraw it on top of the free circle. We can reuse out off-line canvas for that, just clear it and draw back the result (reset transforms):

The only calculation we need from here is for the quadratic Bezier where we supply t in the range [0, 1] to get a point:

function getQuadraticPoint(z0x, z0y, cx, cy, z1x, z1y, t) {

  var t1 = (1 - t),       // (1 - t)
      t12 = t1 * t1,      // (1 - t) ^ 2
      t2 = t * t,         // t ^ 2
      t21tt = 2 * t1 * t; // 2(1-t)t

  return {
    x: t12 * z0x + t21tt * cx + t2 * z1x,
    y: t12 * z0y + t21tt * cy + t2 * z1y
  }
}

The result will be (using values closer to original codepen):

step3snap

var c = document.querySelector("canvas"),
    co = document.createElement("canvas"),
    ctx = c.getContext("2d"),
    ctxo = co.getContext("2d"),
    radius = 150,
    dia = radius * 2;

co.width = c.width;
co.height = c.height;

ctxo.translate(2,2);           // to avoid clipping of edges in this demo
ctxo.strokeStyle = "blue";
ctxo.lineWidth = dia;
ctxo.lineCap = "round";

// draw bezier (quadratic here, one control point)
ctxo.moveTo(radius, radius);
ctxo.quadraticCurveTo(300, 230, c.width - radius - 6, radius);
ctxo.stroke();

// draw multiple times to main canvas
var thickness = 1, angle = 0, step = Math.PI * 0.25;
for(; angle < Math.PI * 2; angle += step) {
  var x = thickness * Math.cos(angle),
      y = thickness * Math.sin(angle);
  ctx.drawImage(co, x, y);
}

// punch out center
ctx.globalCompositeOperation = "destination-out";
ctx.drawImage(co, 0, 0);

// back-up result by reusing off-screen canvas
ctxo.clearRect(0, 0, co.width, co.height);
ctxo.drawImage(c, 0, 0);

// Step 3: draw the green circle at any point
ctx.globalCompositeOperation = "source-over";  // normal comp. mode
ctx.fillStyle = "#9f9";
ctx.strokeStyle = "#090";

var t = 0, dlt = 0.01;

(function loop(){
  
  ctx.clearRect(0, 0, c.width, c.height);
  t += dlt;
  
  // calc position based on t [0, 1] and the same points as for the blue
  var pos = getQuadraticPoint(radius, radius, 300, 230, c.width - radius, radius, t);
  
  // draw the arc
  ctx.beginPath();
  ctx.arc(pos.x + 2, pos.y + 2, radius, 0, 2*Math.PI);
  ctx.fill();
  
  // draw center line
  ctx.beginPath();
  ctx.moveTo(radius, radius);
  ctx.quadraticCurveTo(300, 230, c.width - radius - 6, radius);
  ctx.stroke();

  // draw blue outline on top
  ctx.drawImage(co, 0, 0);
  
  if (t <0  || t >= 1) dlt = -dlt;  // ping-pong for demo
  requestAnimationFrame(loop);
})();

// formula for quadr. curve is: B(t) = (1-t)^2 * Z0 + 2(1-t)t * C + t^2 * Z1
function getQuadraticPoint(z0x, z0y, cx, cy, z1x, z1y, t) {

  var t1 = (1 - t),       // (1 - t)
      t12 = t1 * t1,      // (1 - t) ^ 2
      t2 = t * t,         // t ^ 2
      t21tt = 2 * t1 * t; // 2(1-t)t

  return {
    x: t12 * z0x + t21tt * cx + t2 * z1x,
    y: t12 * z0y + t21tt * cy + t2 * z1y
  }
}
<canvas width=600 height=600></canvas>

Example using non 1:1 axis by scale:

var c = document.querySelector("canvas"),
    co = document.createElement("canvas"),
    ctx = c.getContext("2d"),
    ctxo = co.getContext("2d"),
    radius = 150,
    dia = radius * 2;

co.width = c.width;
co.height = c.height;

ctxo.translate(2,2);           // to avoid clipping of edges in this demo
ctxo.strokeStyle = "blue";
ctxo.lineWidth = dia;
ctxo.lineCap = "round";

// draw bezier (quadratic here, one control point)
ctxo.moveTo(radius, radius);
ctxo.quadraticCurveTo(300, 230, c.width - radius - 6, radius);
ctxo.stroke();

// draw multiple times to main canvas
var thickness = 2, angle = 0, step = Math.PI * 0.25;
for(; angle < Math.PI * 2; angle += step) {
  var x = thickness * Math.cos(angle),
      y = thickness * Math.sin(angle);
  ctx.drawImage(co, x, y);
}

// punch out center
ctx.globalCompositeOperation = "destination-out";
ctx.drawImage(co, 0, 0);

// back-up result by reusing off-screen canvas
ctxo.setTransform(1,0,0,1,0,0);  // remove scale
ctxo.clearRect(0, 0, co.width, co.height);
ctxo.drawImage(c, 0, 0);

// Step 3: draw the green circle at any point
ctx.globalCompositeOperation = "source-over";  // normal comp. mode
ctx.fillStyle = "#9f9";
ctx.strokeStyle = "#090";

ctx.scale(1, 0.4);            // create ellipse

var t = 0, dlt = 0.01;

(function loop(){
  
  ctx.clearRect(0, 0, c.width, c.height * 1 / 0.4);
  t += dlt;
  
  // calc position based on t [0, 1] and the same points as for the blue
  var pos = getQuadraticPoint(radius, radius, 300, 230, c.width - radius, radius, t);
  
  // draw the arc
  ctx.beginPath();
  ctx.arc(pos.x + 2, pos.y + 2, radius, 0, 2*Math.PI);
  ctx.fill();
  
  // draw center line
  ctx.beginPath();
  ctx.moveTo(radius, radius);
  ctx.quadraticCurveTo(300, 230, c.width - radius - 6, radius);
  ctx.stroke();

  // draw blue outline on top
  ctx.drawImage(co, 0, 0);
  
  if (t <0  || t >= 1) dlt = -dlt;  // ping-pong for demo
  requestAnimationFrame(loop);
})();

// formula for quadr. curve is: B(t) = (1-t)^2 * Z0 + 2(1-t)t * C + t^2 * Z1
function getQuadraticPoint(z0x, z0y, cx, cy, z1x, z1y, t) {

  var t1 = (1 - t),       // (1 - t)
      t12 = t1 * t1,      // (1 - t) ^ 2
      t2 = t * t,         // t ^ 2
      t21tt = 2 * t1 * t; // 2(1-t)t

  return {
    x: t12 * z0x + t21tt * cx + t2 * z1x,
    y: t12 * z0y + t21tt * cy + t2 * z1y
  }
}
<canvas width=600 height=600></canvas>
Community
  • 1
  • 1
  • Wow! Thanks for the detailed answer! I really like it and may switch to this idea. This would perfectly solve my problem if the axes were equal. In my example, which wasn't entirely clear, the x and y axes are slightly different. In the real world they could be even more different than each other by a factor of 2. I'll probably mark this answer as correct since it is a useful way to solve the problem and will help most people with a similar problem. I would like you to look at my modified example to see what I mean about the different axes: http://codepen.io/davidreed0/full/GJrVev/ – dreed75 Jun 01 '15 at 22:10
  • I may just force my axes to be equal and then I could fully use your answer for my problem. – dreed75 Jun 01 '15 at 22:12
  • @dreed75 it may be possible to get away with a simple scale applied to the canvas for one of the axis. There is also the option to use ellipse (it's part of the canvas standard but not all browsers support it) using the built-in or a custom one. Just render the outline following a fine-resolution t-value. –  Jun 01 '15 at 22:23
  • @dreed75 added an example for this at the bottom (collapsed), but the better approach is to draw an actual ellipse as this will keep the inner arc correct) –  Jun 01 '15 at 22:29
  • Gotcha. Thanks! I also love your getQuadraticPoint(). I was trying to find a way to do this in canvas. That was the only reason I was using svg for that quadratic Bezier curve since it had a built-in function for getting the point at a certain percentage. – dreed75 Jun 01 '15 at 22:29
  • @dreed75 ah yeah, I made that for a pseudo context (retro graphics) so I was forced to convert the formula to JS :) –  Jun 01 '15 at 22:31