giovedì 3 maggio 2018

SVG arc path explained drawing circular sectors

I found quite hard to understand how svg arc path works. In the W3 docs is not explained well, and even though there are some attempts to explain them in other major sites e.g. MozillaO'Relly Common wiki and a StackOverflow question maybe the clearest was this one by Pindari but still, I could find an easy image to understand what change what. In this tutorial, I will focus on drawing circular sectors.

The elliptical arc curve commands for drawing circular sectors

The elliptical arc curve is denoted by the capital "A" using the absolute reference system and the lower case "a" for using a relative reference system. Let's start using an absolute reference system.

The W3 definition of the parameters is the following: 

A rx ry x-axis-rotation large-arc-flag sweep-flag xf yf

We create  500 x 500 svg sand box:

<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="5cm" height="5cm" viewBox="0 0 500 500"
xmlns="http://www.w3.org/2000/svg" version="1.1">
<title>Undrestanding ARC svg</title>
<desc>Simple absolute arc</desc>
<!-- here will go the code -->
</svg>


From now on all the code reported is supposed to be inside the svg tag. If you want to test it online without creating files on your PC you can use this applet (press the magnifying glass for running the code).

1.1 The simplest semi-circle

  1. We centre the object moving it at the coordinate 250,250 using the command M 250 250 (these are the starting coordinates of our arc!). 
  2.  If we want a semi-circle the radius will be the same for ry and rx, we set it to 100 (NOTE: the radius is always relative of course). 
  3. If we started a 250,250 and we have a radius of 100 hence a diameter of 200 the end point of our semicircle will be 450,250 as shown in the figure.
  4. Finally, we will close the path using "Z" command.
This is the code that you need:

<g stroke="none" fill="lime">
<path d="
M 250 250
A 100 100 0 0 0 450 250
Z"/> 
</g>

And here the results labelled with all the parameters.



Behind the scene, the centre "c" will be computed from the starting point from the previous command (M) and the xf,yf of the "A" command.

1.2 The bottom left quarter of a circle

Now making a quarter of a circle seems easy, we keep the same starting point it will end at x 350 and y 350. But if we change only these parameters the results will be a circular segment (see next figure a).
So we need to add a line with the command "L" that connects the final point xf,yf to the centre of the circle (xc = 350, yc = 250).

<g stroke="none" fill="lime">
<path d="
M 250 250
A 100 100 0 0 0 350 350
L 350 250
Z"/> 
</g>

The figure b seems promising. 


1.3 The bottom right quarter of a circle

For making the bottom right quarter of circle we have to change the starting point:
Setting  M to 350 350 and A to 100 100 0 0 0 450 250 will do the trick. We can also invert the starting and ending coordinate and then use the sweep_flag to obtain the same results: 
M 450 250       
A 100 100 0 0 1 350 350
                        ^ Sweep_flag
In our cases maybe is more easy to just change the starting and ending points.

1.4 Drawing sectors larger than 180°

Let's come back to our original semi-circle when we try to draw sectors lareger than 180° we experience some problems fig a.
<g stroke="none" fill="lime">
<path d="
M 250 250
A 100 100 0 0 0 350 150
Z"/> 
</g>

We can actually notice that there are two arcs with the same radius, starting point and ending point that can be described (Figure below). One bigger (b) and one smaller (a) it's not intuitive but they have the same radius just the centre is different! Maybe it's time to use the large-arc-flag to select the larger. Changing A to 100 100 0 1 0 350 150  and adding as usual L 350 250 will fix this issue.

1.5 All the others sectors

We covered most of the cases with sectors varying of 90° or pi/2. We understood that arc function is using a cartesian reference system not a polar one, so we have always to specify the starting and ending point of the arc. But what if  I want to describe a circular sector from 22° to 78° with radius 42 and centred at 67,85? We can try to find a method to get the output using these three simple inputs. I will use Python for this task starting using the values already tested.

x_center = 350
y_center = 250
r = 100

From math classes, we remember that conventionally the position 0° is on the right part of the circle. So if we want to draw a sector starting from 0° going to 180° the starting position will be x_center + radius and the ending point x_center - radius. ( The simples semicircle example with end-start positions inverted). 
If the end angle is 45° the x position will be x_center + r*cos(45°) and y y_center. While if the end angle is 90° the y position will be y_center - r*sin(90°). We need the minus sign because the to left corner is the origin of our reference system in a svg file! The same holds true for the start angle.
So using numpy given any start and end angle the inputs for the arc function can be calculated like this:

import numpy as np 
startangle = 5
endangle = 125
radiansconversion = np.pi/180.
xstartpoint = xcenter + r*np.cos(startangle*radiansconversion)
ystartpoint = ycenter - r*np.sin(startangle*radiansconversion)
xendpoint = xcenter + r*np.cos(endangle*radiansconversion)
yendpoint = ycenter - r*np.sin(endangle*radiansconversion)
large_arc_flag = 0

Numpy trigonometric functions use radians so we have to convert the angles in radians using a factor.
Now we have to deal with the problem described in §1.4 that can be solved with simple an if statement:

if endangle - startangle > 180: large_arc_flag = 1

1.6 An exception: the circle

This method works well for every angle where start angle is less than end angle and the difference is less than 360°. There are some workarounds to create circles using arcs but the best thing to d, in my opinion,n is to create directly a circle. 


1.7 Wrapping all up in a function

We can wrap up all our efforts in a Python function which will append to an .svg file the code:

def write_svgarc(xcenter,ycenter,r,startangle,endangle,output='arc.svg'):
    if startangle > endangle:
        raise ValueError("startangle must be smaller than endangle")
 
    if endangle - startangle < 360:
        large_arc_flag = 0
        radiansconversion = np.pi/180.
        xstartpoint = xcenter + r*np.cos(startangle*radiansconversion)
        ystartpoint = ycenter - r*np.sin(startangle*radiansconversion)
        xendpoint = xcenter + r*np.cos(endangle*radiansconversion)
        yendpoint = ycenter - r*np.sin(endangle*radiansconversion)

        if endangle - startangle > 180: large_arc_flag = 1
        with open(output,'a') as f:
            f.write(r""" <path d=" """)
            f.write("M %s %s" %(xstartpoint,ystartpoint))
            f.write("A %s %s 0 %s 0 %s %s"
                    %(r,r,large_arc_flag,xendpoint,yendpoint))
            f.write("L %s %s" %(xcenter,ycenter))
            f.write(r""" Z"/> """ )
 
    else:
        with open(output,'a') as f:
            f.write(r"""<circle cx="%s" cy="%s" r="%s"/>"""
                    %(xcenter,ycenter,r))

Remember to put everything inside a proper svg environment!