How to render a svg circle using start and endAngle
Asked Answered
M

2

5

I have rendered the svg circle using start and endAngle. It was worked fine. But when i render the complete circle(startAngle as 70 and endAngle as 70) the output is huge different(except the 0, 90, 180, 270). what i made wrongly for this code?

            function getPathArc(center, start, end, radius) {
                end -= this.isCompleteAngle(start, end) ? 0.0001 : 0;
                var degree = end - start;
                degree = degree < 0 ? (degree + 360) : degree;
                return this.getCirclePath(
                    center, getLocationFromAngle(start, radius, center),
                    getLocationFromAngle(end, radius, center), radius, (degree < 180) ? 0 : 1
                );
            }

            function getCirclePath(center, start, end, radius, clockWise) {
                return 'M ' + start.x + ' ' + start.y + ' A ' + radius + ' ' +
                    radius + ' 0 ' + clockWise + ' 1 ' + end.x + ' ' + end.y;
            }

            function getLocationFromAngle(degree, radius, center) {
                var radian = (degree * Math.PI) / 180;
                return {
                    x : Math.cos(radian) * radius + center.x,
                    y : Math.sin(radian) * radius + center.y
                }
            }

            function isCompleteAngle(startAngle, endAngle) {
                var totalAngle = endAngle - startAngle;
                totalAngle = totalAngle <= 0 ? (totalAngle + 360) : totalAngle;
                return Math.floor(totalAngle / 360) !== 0;
            }

Sample Link: https://jsfiddle.net/fNPvf/43106/

enter image description here

The Green circle is rendered correctly because the start and endAngle is 90 even if i change the angle as 0, 180, 270 and 360 it will work. but The actual problem is Red circle which i use 70 even the issue will come except those angles(0, 90, 180, 270, 360).

how to clear this issue?

Me answered 14/7, 2017 at 4:54 Comment(0)
C
5

This difference between the circles is due to the effects of numerical accuracy. Due to the way SVG arcs and floating point arithmetic works, minuscule changes in the start and end points can be exaggerated in the final arc. The bigger the angle that your arc spans, the more the effect comes into play.

To draw the arc, the renderer needs to first determine the centre of the circle. If you try to make an arc of 360 degrees, your start and end points are going to be almost the same.

Consider the following illustration:

enter image description here

The green and red points are the start and end points of the arc. The grey point is the centre of the circle that the render calculates in order to draw the arc.

The arc in the illustration represents roughly 359 degrees. Now imagine moving the red and the green points closer together. The calculation required to determine the centre point is going to become more and more susceptible to inaccuracies in the start and end coordinates and in the floating point arithmetic functions.

Add to that the fact that the sin() and cos() functions are only approximations of the sin and cos curves. Remember that browser Javascript engines have to balance speed and accuracy.

Then add to that the fact that most (if not all) SVG rendering engines approximate arcs using bezier curves. But beziers can not perfectly represent a circular arc.

Hopefully you can now see why you are getting the results you are. Trying to represent large angles with a single arc is a bad idea. My personal recommendation is use at least three or four arcs for a full circle.

function getPathArc(center, start, end, radius) {
  if (end == start) end += 360;
  var degree = end - start;
  degree = degree < 0 ? (degree + 360) : degree;
  var points = [];
  points.push( getLocationFromAngle(start, radius, center) );
  points.push( getLocationFromAngle(start+degree/3, radius, center) );
  points.push( getLocationFromAngle(start+degree*2/3, radius, center) );
  points.push( getLocationFromAngle(end, radius, center) );
  return this.getCirclePath(points, radius, (degree < 180) ? 0 : 1);
}
			
function getCirclePath(points, radius, clockWise) {
  return ['M', points[0].x, points[0].y,
          'A', radius, radius, 0, 0, clockWise, points[1].x, points[1].y,
          'A', radius, radius, 0, 0, clockWise, points[2].x, points[2].y,
          'A', radius, radius, 0, 0, clockWise, points[3].x, points[3].y
         ].join(' ');
}
			
function getLocationFromAngle(degree, radius, center) {
  var radian = (degree * Math.PI) / 180;
  return {
    x : Math.cos(radian) * radius + center.x,
    y : Math.sin(radian) * radius + center.y
  }
}
			
document.getElementById("arc1").setAttribute("d", getPathArc({x:250,y:250}, 90, 90, 200));
document.getElementById("arc2").setAttribute("d", getPathArc({x:250,y:250}, 70, 70, 200));
<svg width="500" height="500">
  <path id="arc1" fill="none" stroke="green" stroke-width="8" />
  <path id="arc2" fill="none" stroke="red" stroke-width="3" />
</svg>
Carboy answered 14/7, 2017 at 10:14 Comment(1)
Great answer, I knew some SVG specialist (you) would shed some light here.Magical
M
1

I'll suggest a solution which works, but whose precisely mechanism I can't explain (Edit: explained in @LeBeau answer)

This is the culprit in your code:

end -= this.isCompleteAngle(start, end) ? 0.0001 : 0;

We can clearly see that if we increase the value when the ternary is true, that is, increasing the amount of subtraction of the end variable, the bizarre effect disappears. Let's show it.

With a very small subtraction, just 0.00001, to make bizarre effect more noticeable:

function getPathArc(center, start, end, radius) {
  end -= this.isCompleteAngle(start, end) ? 0.00001 : 0;
  var degree = end - start;
  degree = degree < 0 ? (degree + 360) : degree;
  return this.getCirclePath(
    center, getLocationFromAngle(start, radius, center),
    getLocationFromAngle(end, radius, center), radius, (degree < 180) ? 0 : 1
  );
}

function getCirclePath(center, start, end, radius, clockWise) {

  return 'M ' + start.x + ' ' + start.y + ' A ' + radius + ' ' +
    radius + ' 0 ' + clockWise + ' 1 ' + end.x + ' ' + end.y;
}

function getLocationFromAngle(degree, radius, center) {
  var radian = (degree * Math.PI) / 180;
  return {
    x: Math.cos(radian) * radius + center.x,
    y: Math.sin(radian) * radius + center.y
  }
}

function isCompleteAngle(startAngle, endAngle) {
  var totalAngle = endAngle - startAngle;
  totalAngle = totalAngle <= 0 ? (totalAngle + 360) : totalAngle;
  return Math.floor(totalAngle / 360) !== 0;
}

window.onload = function() {
  document.getElementById("arc1").setAttribute("d", getPathArc({
    x: 250,
    y: 250
  }, 90, 90, 200));
  document.getElementById("arc2").setAttribute("d", getPathArc({
    x: 250,
    y: 250
  }, 70, 70, 200));
};
<svg width="500" height="500">
  <path id="arc1" fill="none" stroke="green" stroke-width="2" />
  <path id="arc2" fill="none" stroke="red" stroke-width="2" />
</svg>

Now with a bigger subtraction, 0.01:

function getPathArc(center, start, end, radius) {
  end -= this.isCompleteAngle(start, end) ? 0.01 : 0;
  var degree = end - start;
  degree = degree < 0 ? (degree + 360) : degree;
  return this.getCirclePath(
    center, getLocationFromAngle(start, radius, center),
    getLocationFromAngle(end, radius, center), radius, (degree < 180) ? 0 : 1
  );
}

function getCirclePath(center, start, end, radius, clockWise) {

  return 'M ' + start.x + ' ' + start.y + ' A ' + radius + ' ' +
    radius + ' 0 ' + clockWise + ' 1 ' + end.x + ' ' + end.y;
}

function getLocationFromAngle(degree, radius, center) {
  var radian = (degree * Math.PI) / 180;
  return {
    x: Math.cos(radian) * radius + center.x,
    y: Math.sin(radian) * radius + center.y
  }
}

function isCompleteAngle(startAngle, endAngle) {
  var totalAngle = endAngle - startAngle;
  totalAngle = totalAngle <= 0 ? (totalAngle + 360) : totalAngle;
  return Math.floor(totalAngle / 360) !== 0;
}

window.onload = function() {
  document.getElementById("arc1").setAttribute("d", getPathArc({
    x: 250,
    y: 250
  }, 90, 90, 200));
  document.getElementById("arc2").setAttribute("d", getPathArc({
    x: 250,
    y: 250
  }, 70, 70, 200));
};
<svg width="500" height="500">
  <path id="arc1" fill="none" stroke="green" stroke-width="2" />
  <path id="arc2" fill="none" stroke="red" stroke-width="2" />
</svg>

Do you see? The two arcs overlap if the subtraction is bigger.

Therefore, the solution is increasing the difference in end -= someValue.

Further investigation:

While investigating this, I console.loged every variable. They are almost the same.

Then, I copied both elements in both snippets. To my surprise, they are almost the same. Have a look, these are the two paths in the first snippet:

<path id="arc1" fill="none" stroke="green" stroke-width="2" 
    d="M 250 450 A 200 200 0 1 1 250.0349065848627 449.9999969538258"></path>
<path id="arc2" fill="none" stroke="red" stroke-width="2" 
    d="M 318.40402866513375 437.93852415718163 A 200 200 0 1 1 318.40406146659313 437.9385122184236"></path>

Now the two paths produced by the second snippet:

<path id="arc1" fill="none" stroke="green" stroke-width="2" 
    d="M 250 450 A 200 200 0 1 1 250.0000349065851 449.99999999999693"></path>
<path id="arc2" fill="none" stroke="red" stroke-width="2" 
    d="M 318.40402866513375 437.93852415718163 A 200 200 0 1 1 318.4368290834931 437.92658253955653"></path>

As you can see, the red paths are are almost identical, except for the x axis starting/ending values. It seems that, the more similar they are, the bigger the efect.

To test this, lets see what happens if the values are exactly the same (the black circle marks the origin):

<svg width="500" height="700">
  <path id="arc1" fill="none" stroke="green" stroke-width="2" d="M 250 450 A 200 200 0 1 1 250.0349065848627 449.9999969538258"></path>
  <path id="arc2" fill="none" stroke="red" stroke-width="2" d="M 318.4368290834931 437.93852415718163 A 200 200 0 1 1 318.4368290834931 437.92658253955653"></path>
  <circle cx="318.4368290834931" cy="437.93852415718163" r="4"></circle>
</svg>

As I said before, I cannot explain this. But I'm sure one of the SVG specialists here shortly will.

Magical answered 14/7, 2017 at 6:13 Comment(1)
@Gerado Thanks dude for valuable comment.Me

© 2022 - 2024 — McMap. All rights reserved.