SVG stroke-dasharray offset not consistent

3.9k views Asked by At

I have a series of circles with borders comprised of smaller circles which I will call 'dots'. I am then animating the circles by rotating them with CSS3's transform, each by either 5 or 15 degrees (alternating) more than the last starting with the middle circle not rotating at all. This alternating of degrees is due to the original offset of half of them of 5deg

The animation itself works great, but the offset between each dot in a circle is not consistent. This is made obvious when the animation completes, some dots jump back. If they were all off by a consistent amount then it would be an error in my calculations, but dots around the same circle jump different amounts, meaning that they are offset different amounts to begin with. Vals, in his example at the end of his answer, also shows this inconsistency in offset

Here is how each circle is set up. The spacing in between each dot was determined by using the formula spacing = (radius × 2) × 3.14159265 ÷ numberOfCircles. The .001 is to allow Chrome to see the dots

<circle cx="30" cy="30" r="radius" stroke-dasharray="0.001, spacing" stroke="color"/>

Here is the demo jsFiddle

Can anyone help me fix this SVG rendering offset bug?

EDIT

vals and squeamish ossifrage both provided wonderfully working alternative solutions to the problem. However, I'm still looking to actually fix the offset/rendering issue if such a thing is possible

2

There are 2 answers

6
vals On

I think that there are 2 slight mistakes in your setup.

The first is that the spacing of your dots is the sum of the 2 parameters to the stroke-dash array. Since the first parameter is always 0.001, the second one should be the result of your formula minus 0.001.

The second one is that you are placing 36 dots around the circle. That gives 10 degrees for the angle from dot to dot. So, your animations should specificy 10deg, 20deg, 30deg for the series, and not 15deg 30deg 45deg ... That creates a jump of 5 deg at the end of every cycle.

I think that I have it more or less working

fiddle

There was also an issue with the initial rotation; I hope that now it is what you wanted.

And also, there was some kind of round up due to the small size of the svg; setting it to 600 square works much better.

I have also added a line at 10 deg to check the correct alignment of the dots.

CSS

body {
    background: black;
    padding: 0;
    margin: 0;
}
circle {
    fill: none;             
    stroke-width: 10;
    stroke-linecap: round;
}
circle { -webkit-transform-origin: center center; -moz-transform-origin: center center; transform-origin: center center;
  -webkit-animation-duration: 3s; 
-webkit-animation-timing-function: ease-in-out;
-webkit-animation-iteration-count: infinite;}
circle:nth-child(2)  { -webkit-animation-name:second; 
                       -moz-animation:second  3s ease-in-out infinite; 
                       animation:second  3s ease-in-out infinite;}
circle:nth-child(3)  { -webkit-animation-name:third; -moz-animation:third   3s ease-in-out infinite; animation:third   3s ease-in-out infinite; }
circle:nth-child(4)  { -webkit-animation-name:fourth; -moz-animation:fourth  3s ease-in-out infinite; animation:fourth  3s ease-in-out infinite; }
circle:nth-child(5)  { -webkit-animation-name:fifth; -moz-animation:fifth   3s ease-in-out infinite; animation:fifth   3s ease-in-out infinite; }
circle:nth-child(6)  { -webkit-animation-name:sixth; -moz-animation:sixth   3s ease-in-out infinite; animation:sixth   3s ease-in-out infinite; }
circle:nth-child(7)  { -webkit-animation-name:seventh; -moz-animation:seventh 3s ease-in-out infinite; animation:seventh 3s ease-in-out infinite; }
circle:nth-child(8)  { -webkit-animation-name:eighth; -moz-animation:eighth  3s ease-in-out infinite; animation:eighth  3s ease-in-out infinite; }
circle:nth-child(9)  { -webkit-animation-name:ninth; -moz-animation:ninth   3s ease-in-out infinite; animation:ninth   3s ease-in-out infinite; }
circle:nth-child(10) { 
    -webkit-animation-name:tenth; 
    -moz-animation:tenth   3s ease-in-out infinite; 
    animation:tenth   3s ease-in-out infinite; 
    -webkit-transform: rotate(10deg);}
@-webkit-keyframes second {   0% { -webkit-transform:rotate(5deg)  }
                            100% { -webkit-transform:rotate(15deg) } }
@-webkit-keyframes third {  100% { -webkit-transform:rotate(20deg) } }
@-webkit-keyframes fourth  {  0% { -webkit-transform:rotate(5deg)  }
                            100% { -webkit-transform:rotate(35deg) } }
@-webkit-keyframes fifth {  100% { -webkit-transform:rotate(40deg) } }
@-webkit-keyframes sixth   {  0% { -webkit-transform:rotate(5deg)  }
                            100% { -webkit-transform:rotate(55deg) } }
@-webkit-keyframes seventh {100% { -webkit-transform:rotate(60deg) } }
@-webkit-keyframes eighth  {  0% { -webkit-transform:rotate(5deg)  }
                            100% { -webkit-transform:rotate(75deg) } }
@-webkit-keyframes ninth  {   0% { -webkit-transform:rotate(0deg)  }
                            100% { -webkit-transform:rotate(80deg) } }
@-webkit-keyframes tenth  {   0% { -webkit-transform:rotate(5deg)  }
                            100% { -webkit-transform:rotate(95deg) } }
@-moz-keyframes second  {   0% { -moz-transform:rotate(5deg)  }
                          100% { -moz-transform:rotate(15deg)  } }
@-moz-keyframes third   { 100% { -moz-transform:rotate(20deg)  } }
@-moz-keyframes fourth  {   0% { -moz-transform:rotate(5deg)  }
                          100% { -moz-transform:rotate(35deg)  } }
@-moz-keyframes fifth   { 100% { -moz-transform:rotate(40deg)  } }
@-moz-keyframes sixth   {   0% { -moz-transform:rotate(5deg)  }
                          100% { -moz-transform:rotate(55deg)  } }
@-moz-keyframes seventh { 100% { -moz-transform:rotate(60deg)  } }
@-moz-keyframes eighth  {   0% { -moz-transform:rotate(5deg)  }
                          100% { -moz-transform:rotate(75deg) } }
@-moz-keyframes ninth   { 100% { -moz-transform:rotate(80deg) } }
@-moz-keyframes tenth   {   0% { -moz-transform:rotate(5deg)  }
                          100% { -moz-transform:rotate(95deg) } }

line {
    stroke-width: 1;
    -webkit-transform-origin: left center;
    -webkit-transform: rotate(-10deg);
}

And also optimized a little bit the styles

Well, after a lot of time spent with that issue, I am almost sure that there is some kind of bug in some kind of rounding / precision.

I have changed the idea fully to avoid this issue. The target will be to have the circles make full circles before ending the animation, so that the beginning and the end of the animation are always in sync.

Since that generates an huge keyframes style, I want to reuse it; to achieve this I have grouped the circles in a nested way; and applied the animation to every group:

HTML

<svg viewBox="0 0 60 60">
    <g class="g">
    <circle cx="30" cy="30" r="10" stroke-dasharray="0.001, 1.745" stroke="hsl(120, 100%, 50%)"/>
    <g class="g">
    <circle cx="30" cy="30" r="12" stroke-dasharray="0.001, 2.094" stroke="hsl(108, 100%, 50%)" class="c2"/>
    <g class="g">
    <circle cx="30" cy="30" r="14" stroke-dasharray="0.001, 2.443" stroke="hsl(96, 100%, 50%)"/>
    <g class="g">
    <circle cx="30" cy="30" r="16" stroke-dasharray="0.001, 2.793" stroke="hsl(84, 100%, 50%)"  class="c2"/>
    <g class="g">
    <circle cx="30" cy="30" r="18" stroke-dasharray="0.001, 3.142" stroke="hsl(72, 100%, 50%)"/>
    <g class="g">
    <circle cx="30" cy="30" r="20" stroke-dasharray="0.001, 3.491" stroke="hsl(60, 100%, 50%)" class="c2"/>
    <g class="g">
    <circle cx="30" cy="30" r="22" stroke-dasharray="0.001, 3.840" stroke="hsl(48, 100%, 50%)"/>
    <g class="g">
     <circle cx="30" cy="30" r="24" stroke-dasharray="0.001, 4.189" stroke="hsl(36, 100%, 50%)"  class="c2"/>
    <g class="g">
    <circle cx="30" cy="30" r="26" stroke-dasharray="0.001, 4.538" stroke="hsl(24, 100%, 50%)"/>
    <g class="g">
    <circle cx="30" cy="30" r="28" stroke-dasharray="0.001, 4.887" stroke="hsl(12, 100%, 50%)"  class="c2"/>
    </g></g></g></g></g></g></g></g></g></g>
</svg>

(yes, back to the low resolution !)

CSS

body {
    background: black;
    padding: 0;
    margin: 0;
}

circle {
    fill: none;             
    stroke-width: 1;
    stroke-linecap: round;
}

.g { 
    -webkit-transform-origin: center center; -moz-transform-origin: center center;                                         transform-origin: center center;
    -webkit-animation-duration: 108s; 
    -webkit-animation-timing-function: ease-in-out;
    -webkit-animation-iteration-count: infinite; 
    -webkit-animation-name: anim; 
    -moz-animation:second  3s ease-in-out infinite; 
     animation:second  3s ease-in-out infinite;}

.c2 {
    -webkit-transform-origin: center center;
    -webkit-transform: rotate(5deg); 
} 

@-webkit-keyframes anim {   0% { -webkit-transform:rotate(0deg)}
                        2.778% { -webkit-transform:rotate(10deg)}
                        5.56% { -webkit-transform:rotate(20deg)}
                        8.33% { -webkit-transform:rotate(30deg)}
                       11.11% { -webkit-transform:rotate(40deg)}
                       13.89% { -webkit-transform:rotate(50deg)}
                       16.67% { -webkit-transform:rotate(60deg)}
                       19.44% { -webkit-transform:rotate(70deg)}
                       22.22% { -webkit-transform:rotate(80deg)}
                       25% { -webkit-transform:rotate(90deg)}
                       27.78% { -webkit-transform:rotate(100deg)}
                       30.56% { -webkit-transform:rotate(110deg)}
                       33.33% { -webkit-transform:rotate(120deg)}
                       36.11% { -webkit-transform:rotate(130deg)}
                       38.89% { -webkit-transform:rotate(140deg)}
                       41.67% { -webkit-transform:rotate(150deg)}
                       44.44% { -webkit-transform:rotate(160deg)}
                       47.22% { -webkit-transform:rotate(170deg)}
                       50%    { -webkit-transform:rotate(180deg)}
                       52.78% { -webkit-transform:rotate(190deg)}
                       55.56% { -webkit-transform:rotate(200deg)}
                       58.33% { -webkit-transform:rotate(210deg)}
                       61.11% { -webkit-transform:rotate(220deg)}
                       63.89% { -webkit-transform:rotate(230deg)}
                       66.67% { -webkit-transform:rotate(240deg)}
                       69.44% { -webkit-transform:rotate(250deg)}
                       72.22% { -webkit-transform:rotate(260deg)}
                       75%    { -webkit-transform:rotate(270deg)}
                       77.78% { -webkit-transform:rotate(280deg)}
                       80.56% { -webkit-transform:rotate(290deg)}
                       83.33% { -webkit-transform:rotate(300deg)}
                       86.11% { -webkit-transform:rotate(310deg)}
                       88.89% { -webkit-transform:rotate(320deg)}
                       91.67% { -webkit-transform:rotate(330deg)}
                       94.44% { -webkit-transform:rotate(340deg)}
                       97.22% { -webkit-transform:rotate(350deg)}
                     100%     { -webkit-transform:rotate(360deg)}
}

And new demo (Sorry, only webkit)

This is my attempt to investigate the errors. I have changed the system, instead of animation, I have 2 sets of circles, one in color and another in black over it, and rotated 10 deg. The color circles shouldn't show; the offset is a measure off the error. (may be you need to scroll to see the circles

offsets demo

1
r3mainer On

I think the problem here is that the dashed line drawing algorithm uses a fast approximation of line length instead of actually integrating the path length along each segment, which would presumably be quite slow.

If you draw the dots as individual circles instead of using dashed lines, then this problem goes away. The following PHP script will do the calculations for you:

<svg viewBox="0 0 60 60">
<g transform="translate(30,30)">
<?php
define("PI",3.141592654);
for ($i=0; $i<10; $i++) {
  $r = 10 + 2 * $i;
  $hue = 120 - 12 * $i;
  echo "<g id=\"ring_$i\">\n";
  for ($th=0; $th<360; $th+=10) {
    $theta = ($th + 5 * $i) * PI / 180;
    $x = $r * sin($theta);
    $y = $r * cos($theta);
    printf("  <circle cx=\"%.5f\" cy=\"%.5f\" r=\"0.45\" fill=\"hsl(%d,100%%,50%%)\"/>\n",$x,$y,$hue);
  }
  echo "</g>\n";
}
?></g>
</svg>

And here's the CSS (vendor-specific rules omitted for brevity):

body { background: black; padding: 0; margin: 0; }
g#ring_0 { transform:rotate(5deg); }
g#ring_1 { animation:second 3s ease-in-out infinite; }
g#ring_2 { animation:third 3s ease-in-out infinite; }
g#ring_3 { animation:fourth 3s ease-in-out infinite; }
g#ring_4 { animation:fifth 3s ease-in-out infinite; }
g#ring_5 { animation:sixth 3s ease-in-out infinite; }
g#ring_6 { animation:seventh 3s ease-in-out infinite; }
g#ring_7 { animation:eighth 3s ease-in-out infinite; }
g#ring_8 { animation:ninth 3s ease-in-out infinite; }
g#ring_9 { animation:tenth 3s ease-in-out infinite; }
keyframes second { 100% { transform:rotate(10deg) } }
keyframes third { 100% { transform:rotate(20deg) } }
keyframes fourth { 100% { transform:rotate(30deg) } }
keyframes fifth { 100% { transform:rotate(40deg) } }
keyframes sixth { 100% { transform:rotate(50deg) } }
keyframes seventh { 100% { transform:rotate(60deg) } }
keyframes eighth { 100% { transform:rotate(70deg) } }
keyframes ninth { 100% { transform:rotate(80deg) } }
keyframes tenth { 100% { transform:rotate(90deg) } }

You can see it working here: JSFiddle