skip to Main Content

I want to draw a spiral, a circle where the radius steadily increases with the angle.

When searching for similar questions, the circle/spiral is always approximated by drawing a lot of lines with the lineTo method. The more lines are used, the more the circle/spiral is approximated. But using a lot of lines has a performance disadvantage, so the programmer needs to weight up "performance" vs "appearance".

The bezierCurveTo on the other hand could draw a perfect spiral, without a huge performance disadvantage, but I don’t know how to calculate the control points for the bezier method.

Playground for drawing the spiral with the lineTo method: JSFiddle Playground

context.save();
context.beginPath();
let offset = initialRadius;
for (let turn = 0; turn < turnCount; turn++) {
    for (let step = 0; step < stepCount; step++) {
        context.lineTo(offset, 0);
        offset += growthPerTurn / stepCount;
        context.rotate(360 / stepCount * Math.PI / 180);
    }
}
context.lineWidth = 3;
context.strokeStyle = "yellow";
context.stroke();
context.restore();

Here is my first attempt to approximate the spiral with bezier: JSFiddle Playground
(determined the value for temp through trial and error)

context.moveTo(initialRadius, 0);
const temp = 1.25;
for (let turn = 0; turn < turnCount; turn++) {
    for (let step = 1; step <= stepsPerTurn; step++) {
        const points = [
            [temp * calculateOffset(turn, step -0.66) * Math.cos(calculateAngle(step - 0.66)), temp * calculateOffset(turn, step -0.66) * Math.sin(calculateAngle(step - 0.66))],
            [temp * calculateOffset(turn, step -0.33) * Math.cos(calculateAngle(step - 0.33)), temp * calculateOffset(turn, step -0.33) * Math.sin(calculateAngle(step - 0.33))],
            [calculateOffset(turn, step) * Math.cos(calculateAngle(step)),calculateOffset(turn, step) * Math.sin(calculateAngle(step))],
        ];
        context.bezierCurveTo(points[0][0], points[0][1], points[1][0], points[1][1], points[2][0], points[2][1]);
    }
}

2

Answers


  1. How about a more mathematical approach…

    We can think of a spiral as a circle with a variable diameter that keeps getting smaller

    A circle

    We can draw a circle with many dot and a bit of trigonometry

    var ctx = document.getElementById("c").getContext("2d");
    
    var diam = 80
    var frame = 0
    
    function drawDot(x, y) {
      ctx.beginPath();
      ctx.arc(x, y, 1, 0, 2 * Math.PI);
      ctx.stroke();
    }
    
    function loop() {
      let angle = (frame++ % 360)
      let a = angle * Math.PI / 180
      drawDot(100 + diam * Math.cos(a), 100 + diam * Math.sin(a))
      if (angle == 360) clearInterval(inter)
    }
    
    inter = setInterval(loop, 10);
    <canvas id="c" width=200 height=200></canvas>

    A spiral

    Now that we know how to draw a circle we can make reduce the diameter on every frame

    var ctx = document.getElementById("c").getContext("2d");
    
    var diam = 80
    var frame = 0
    
    function drawDot(x, y) {
      ctx.beginPath();
      ctx.arc(x, y, 1, 0, 2 * Math.PI);
      ctx.stroke();
    }
    
    function loop() {
      let angle = (frame++ % 360)
      let a = angle * Math.PI / 180
      drawDot(100 + diam * Math.cos(a), 100 + diam * Math.sin(a))
      diam -= 0.05
      if (diam < 20) clearInterval(inter)
    }
    
    inter = setInterval(loop, 10);
    <canvas id="c" width=200 height=200></canvas>

    … and another example

    var ctx = document.getElementById("c").getContext("2d");
    
    var diam = 95
    var frame = 0
    var delta = 0
    
    function drawDot(x, y) {
      ctx.beginPath();
      ctx.arc(x, y, 1, 0, 2 * Math.PI);
      ctx.stroke();
    }
    
    function loop() {
      let angle = (frame++ % 360)
      let a = angle * Math.PI / 180
      drawDot(100 + diam * Math.cos(a), 100 + diam * Math.sin(a))
      diam -= delta
      if (angle % 45 == 0) delta += 0.01
      if (diam < 5) clearInterval(inter)
    }
    
    inter = setInterval(loop, 10);
    <canvas id="c" width=200 height=200></canvas>
    Login or Signup to reply.
  2. For best fit use line segments

    Using a bezier will likely be more of an approximation than using line segments..

    If you set the length of each line segment to be only a few pixels long you will get a very accurate spiral that at most will be only a fraction of a pixel out.

    In the snippet the change in angle is half the line segment length divided by the radius times PI.

    The radius is the current angle times a slope value radiusChange

    To prevent infinite loop ensure the lineSegLength is not zero
    To prevent a divide by zero check that the changing radius does not = zero

    const ctx = canvas.getContext("2d");
    function drawSpiral(x, y, radiusChange, maxRadius, lineSegLength = 1) {
      if (Math.abs(lineSegLength) > 0.5) {
        ctx.beginPath();
        ctx.lineTo(x, y);
        lineSegLength *= 0.5;
        var a = 0, r = radiusChange * lineSegLength;
        while (r < maxRadius && Math.abs(r) > 1e-5) {
          a += lineSegLength / r * Math.PI;   // next point at approx lineSegLength pixel distance 
          r = a * radiusChange;
          ctx.lineTo(x + Math.cos(a) * r, y + Math.sin(a) * r);
        }
        ctx.stroke();
      }
    }
    drawSpiral(200, 200, 4, 180);
    <canvas id="canvas" width="400" height="400"></canvas>

    Beziers are not suited to this task.

    To use a bezier of length > 2 pixels will be complicated and rather imprecise. Just as you can not draw a perfect circle using beziers I very much doubt they can create a good spiral.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search