Eric Andrew Lewis
Eric Andrew Lewis

Reputation: 1366

How to make a spiral in SVG?

I'd like to make an Archimedean spiral in SVG.

I created a spiral with four quadratic bezier points, but I'm not sure where I should put the control points for each to get a perfect Archimedean spiral:

<path class="spiral" 
  d="M100 50 
     C 100 116 12.5 99.5 12.5 50 
     C 12.5 0.5 75 9 75 50 
     C 75 83 37.5 74 37.5 50
     C 37.5 38 50 42 50 50"
  stroke="black" stroke-width="1" fill="none">

Upvotes: 5

Views: 6932

Answers (3)

Paul LeBeau
Paul LeBeau

Reputation: 101820

Do you need to use bezier curves? You can't get a perfect spiral with bezier curves, just like you can't get an exact circular arc.

You might want to consider using a <polyline> made of straight line segments.

// pathId the id of the path element to modify.
// centreX, centreY: the position of the centre of the spiral.
// startRadius: radius at start (inside) of spiral.
// endRadius: radius after one complete (360deg) rotation.
// quarterTurns: the number of quarter turns to generate.

function makeSpiral(pathId, centreX, centreY, startRadius, endRadius, quarterTurns)
{
  var pointsPerQuarter = 90;
  var radiusStep = (endRadius - startRadius) / 4 / pointsPerQuarter;
  var points = [];

  for (var i=0; i < quarterTurns * pointsPerQuarter; i++)
  {
    var radius = startRadius + radiusStep * i;
    var angle = i * Math.PI / 2 / pointsPerQuarter;
    points.push(radius * Math.cos(angle));
    points.push(radius * Math.sin(angle));
  }
  document.getElementById(pathId).setAttribute("points", points.join(','));
}


makeSpiral("spiral", 0, 0, 1, 2, 31);
<svg width="300" viewBox="-10 -10 20 20">

  <g class="arc" fill="none" stroke="blue" stroke-width="0.05">
    <polyline id="spiral" points=""/>
  </g>

</svg>

With beziers

Like I said, beziers can never be exact, but with care, you can get a very accurate approximation.

Let's start by imagining a quarter circle. The control points to approximate that turn out to have a ratio of around 0.55 of the radius. The exact value varies depending on whether you want to minimise the maximum error, or the average error, or pass through a specific point, etc.

You can read about one approach to calculate this here..

The first approach on that page, giving (4/3)*(sqrt(2) - 1) is the most common value used.

<svg width="300" viewBox="-0.5 -0.5 2 2">

  <g class="axes" stroke="black" stroke-width="0.01">
    <line x2="1.1" y2="0"/>
    <line x2="0" y2="1.1"/>
  </g>

  <g class="arc" fill="none" stroke="blue" stroke-width="0.01">
    <path d="M 1,0 C 1,0.552, 0.552,1, 0,1"/>
  </g>

</svg>

To make your spiral, you can just imagine the radius growing as you make each quarter circle step.

To make this easier, I'll use some JS to calculate our bezier values. I'll also include a reference spiral in red to see how accurate the bezier version is.

// pathId the id of the path element to modify.
// centreX, centreY: the position of the centre of the spiral.
// startRadius: radius at start (inside) of spiral.
// endRadius: radius after one complete (360deg) rotation.
// quarterTurns: the number of quarter turns to generate.

function makeSpiral(pathId, centreX, centreY, startRadius, endRadius, quarterTurns)
{
  var radiusStep = (endRadius - startRadius) / 4;
  var FACTOR = 0.5522847498;

  var step = 0;
  var radius = startRadius;
  var nextRadius = radius + radiusStep;
  var d = "M " + (centreX + startRadius) + "," + centreY;

  while (step < quarterTurns)
  {
    switch(step % 4)
    {
      case 0:
        d += "c" + [0, radius * FACTOR, -radius + nextRadius * FACTOR, nextRadius, -radius, nextRadius].join(',');
        break;        

      case 1:
        d += "c" + [-radius * FACTOR, 0, -nextRadius, -radius + nextRadius * FACTOR, -nextRadius, -radius].join(',');
        break;        

      case 2:
        d += "c" + [0, -radius * FACTOR, radius - nextRadius * FACTOR, -nextRadius, radius, -nextRadius].join(',');
        break;        

      case 3:
        d += "c" + [radius * FACTOR, 0, nextRadius, radius - nextRadius * FACTOR, nextRadius, radius].join(',');
        break;
    }
    step++;
    radius = nextRadius;
    nextRadius += radiusStep;
  }

  document.getElementById(pathId).setAttribute("d", d);
}


function makePolylineSpiral(pathId, centreX, centreY, startRadius, endRadius, quarterTurns)
{
  var pointsPerQuarter = 90;
  var radiusStep = (endRadius - startRadius) / 4 / pointsPerQuarter;
  var points = [];

  for (var i=0; i < quarterTurns * pointsPerQuarter; i++)
  {
    var radius = startRadius + radiusStep * i;
    var angle = i * Math.PI / 2 / pointsPerQuarter;
    points.push(centreX + radius * Math.cos(angle));
    points.push(centreY + radius * Math.sin(angle));
  }
  document.getElementById(pathId).setAttribute("points", points.join(','));
}


makePolylineSpiral("reference-spiral", 0, 0, 1, 2, 4);
makeSpiral("spiral", 0, 0, 1, 2, 4);
<svg width="300" viewBox="-2 -2 5 5">

  <g class="arc" fill="none" stroke="blue" stroke-width="0.1">
    <polyline id="reference-spiral" points="" stroke="red"/>
    <path id="spiral" d=""/>
  </g>

</svg>

Unfortunately we can see that the naive bezier version doesn't match the reference spiral very well. Your could try tweaking the control point ratio, but you'll find that it'll always look a bit wonky.

For a better approximation of the spiral, you'll need to use bezier curves that cover a smaller portion of the circle (ie. less than 90 degrees).

I'm not going to do that here, but you might want to try yourself. Personally, I'd stick with the <polyline> version. If you need fewer or more points, you can modify the pointsPerQuarter value.

Upvotes: 2

ccprog
ccprog

Reputation: 21821

I'd like to expand on a question by Zev Eisenberg at math.stackexchange. From that, Zev implented a solution as a C function. It uses quadratic bezier curves instead of cubic, but has the advantage that you can set the angles for the path sections freely, thus minimizing the error as you like.

Here is a Javascript port. Set the parameters to getPath to your liking (angles are in degree). thetaStep is the angle each path section covers. I think 30° gives pretty decent results.

function lineIntersection (m1, b1, m2, b2) {
    if (m1 === m2) {
        throw new Error("parallel slopes");
    }
    const x = (b2 - b1) / (m1 - m2);
    return {x: x, y: m1 * x + b1};
}

function pStr (point) {
  return `${point.x},${point.y} `;
}

function getPath (center, startRadius, spacePerLoop, startTheta, endTheta, thetaStep) {
    // Rename spiral parameters for the formula r = a + bθ
    const a = startRadius;  // start distance from center
    const b = spacePerLoop / Math.PI / 2; // space between each loop

    // convert angles to radians
    let oldTheta = newTheta = startTheta * Math.PI / 180;
    endTheta = endTheta * Math.PI / 180;
    thetaStep = thetaStep * Math.PI / 180;

    // radii
    let oldR,
        newR = a + b * newTheta;

    // start and end points
    const oldPoint = {x: 0, y: 0};
    const newPoint = {
        x: center.x + newR * Math.cos(newTheta), 
        y: center.y + newR * Math.sin(newTheta)
    };

    // slopes of tangents
    let oldslope,
        newSlope = (b * Math.sin(oldTheta) + (a + b * newTheta) * Math.cos(oldTheta)) /
                   (b * Math.cos(oldTheta) - (a + b * newTheta) * Math.sin(oldTheta));

    let path = "M " + pStr(newPoint);
    
    while (oldTheta < endTheta - thetaStep) {
        oldTheta = newTheta;
        newTheta += thetaStep;

        oldR = newR;
        newR = a + b * newTheta;

        oldPoint.x = newPoint.x;
        oldPoint.y = newPoint.y;
        newPoint.x = center.x + newR * Math.cos(newTheta);
        newPoint.y = center.y + newR * Math.sin(newTheta);

        // Slope calculation with the formula:
        // (b * sinΘ + (a + bΘ) * cosΘ) / (b * cosΘ - (a + bΘ) * sinΘ)
        const aPlusBTheta = a + b * newTheta;

        oldSlope = newSlope;
        newSlope = (b * Math.sin(newTheta) + aPlusBTheta * Math.cos(newTheta)) /
                   (b * Math.cos(newTheta) - aPlusBTheta * Math.sin(newTheta));

        const oldIntercept = -(oldSlope * oldR * Math.cos(oldTheta) - oldR * Math.sin(oldTheta));
        const newIntercept = -(newSlope * newR* Math.cos(newTheta) - newR * Math.sin(newTheta));

        const controlPoint = lineIntersection(oldSlope, oldIntercept, newSlope, newIntercept);

        // Offset the control point by the center offset.
        controlPoint.x += center.x;
        controlPoint.y += center.y;

        path += "Q " + pStr(controlPoint) + pStr(newPoint);
    }
    
    return path;
}

const path = getPath({x:400,y:400}, 0, 50, 0, 6*360, 30);

const spiral = document.querySelector('#spiral');
spiral.setAttribute("d", path);
<svg xmlns="http://www.w3.org/2000/svg" width="400" height="400" viewBox="0 0 800 800">
    <path id="spiral" d="" fill="none" stroke="black" stroke-width="3"/>
</svg>

Upvotes: 9

Alexandr_TT
Alexandr_TT

Reputation: 14545

To get the sprien code, you can use a vector editor, for example, Inkscape

In the vector editor toolbar, select the spiral (F9), where you can select the parameters of the spiral - the number of turns, the inner radius.

Save the file. We need the string <path> ... </ path> Copy it.

<svg xmlns="http://www.w3.org/2000/svg" width="50%" heihgt="50%" viewBox="0 150 744 1052">
<path d="m351 487c0 8-11 4-14-1-6-11 4-24 15-27 19-5 37 11 40 30 4 27-18 50-44 53-35 4-64-25-66-59-3-42 32-77 73-79 50-3 90 39 92 88 2 57-46 104-102 105-65 2-117-53-119-117-1-72 60-131 131-132 80-1 144 67 145 146 1 87-74 158-160 158-95 0-171-81-171-175 0-102 88-185 190-184 110 1 198 95 197 204C557 615 456 709 340 708 215 706 115 598 117 475 119 342 233 236 364 238 504 240 616 361 614 500 611 648 484 766 337 763 182 760 58 626 61 472 65 309 206 179 367 183c170 4 306 151 302 320-4 178-158 319-335 315" fill="none" stroke="grey" stroke-width="3"/>

Example of spiral animation

For the animation of the drawing of the spiral we will use the patch attribute stroke-dashoffset - indent from the beginning of the line. With a maximum indent equal to the length of the line (patch), the line is not visible. With stroke-dashoffset = "0" the line will be drawn completely.

In other words, to implement the drawing animation of the line, you need to decrease the margin from the maximum to zero.

Find the length of the line - var len = Math.round (path.getTotalLength ()); For our patch - 6265px

<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="50%" heihgt="50%" viewBox="0 150 744 1052" id="svg2" version="1">
   
     <path  stroke-dashoffset="6265" stroke-dasharray="6265" d="m351 487c0 8-11 4-14-1-6-11 4-24 15-27 19-5 37 11 40 30 4 27-18 50-44 53-35 4-64-25-66-59-3-42 32-77 73-79 50-3 90 39 92 88 2 57-46 104-102 105-65 2-117-53-119-117-1-72 60-131 131-132 80-1 144 67 145 146 1 87-74 158-160 158-95 0-171-81-171-175 0-102 88-185 190-184 110 1 198 95 197 204C557 615 456 709 340 708 215 706 115 598 117 475 119 342 233 236 364 238 504 240 616 361 614 500 611 648 484 766 337 763 182 760 58 626 61 472 65 309 206 179 367 183c170 4 306 151 302 320-4 178-158 319-335 315" style="fill:none;stroke:#000" stroke-width="2">
	 <animate attributeName="stroke-dashoffset" values="6265;0;6265;0" dur="15s" fill="freeze" /> 
	 </path>

Implementing animation with CSS

Beginning of animation when hovering over the cursor

#spiral {
  
  stroke: dodgerblue;
  stroke-width:4;
  fill:#FCFCFC;
  stroke-dasharray: 6265;
  stroke-dashoffset: 6265;
  transition: stroke-dashoffset 10s;
}

#spiral:hover {
  stroke-dashoffset: 0;
} 
svg text {
font-size:36px;
pointer-events:none;
}
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="50%" heihgt="50%" viewBox="0 150 744 1052" id="svg2" version="1"  >
   
     <path id="spiral"  stroke-dashoffset="6265" stroke-dasharray="6265"  d="m351 487c0 8-11 4-14-1-6-11 4-24 15-27 19-5 37 11 40 30 4 27-18 50-44 53-35 4-64-25-66-59-3-42 32-77 73-79 50-3 90 39 92 88 2 57-46 104-102 105-65 2-117-53-119-117-1-72 60-131 131-132 80-1 144 67 145 146 1 87-74 158-160 158-95 0-171-81-171-175 0-102 88-185 190-184 110 1 198 95 197 204C557 615 456 709 340 708 215 706 115 598 117 475 119 342 233 236 364 238 504 240 616 361 614 500 611 648 484 766 337 763 182 760 58 626 61 472 65 309 206 179 367 183c170 4 306 151 302 320-4 178-158 319-335 315" />
	  <text x="10" y="200"   > Mouse over </text>
</svg>

Upvotes: 5

Related Questions