SVG Experiments
So first off, what is an SVG? Open an SVG with a text editor and it looks like slightly weird XML. Open it with a web browser however and it looks like an image.
Truth is it's kind of both.
SVG stands for Scalable Vector Graphics, and in short it's a way of creating images using XML (and maths). Instead of creating a square grid of pixels to make up an image SVG's define shapes and lines. The advantage of this is that you can zoom in and experience no loss of quality, but on the other hand you cannot easily create detailed images such as photos. For simple line drawings however they are perfect as they take up very little space, can be animated and interact with CSS. One neat way of using them is for a favicon, as you can define a media query in the SVG which responds to dark mode recolouring your image to avoid a dark image on dark tab and vice versa:
Here we can see a circle set to be red in light mode and blue in dark mode. You can swap between light mode and dark mode in browser and should be able to see the change in real time (depending on your browser).
Atoms
A fairly standard SVG to create when starting out is the atom, so we're going to go ahead and walk through this step by step.
First we need to know how to draw a shape. There are several ways of doing this, but the simplest is to use an inbuilt shape such as circle:
<svg version="1.1" width="500" height="500" viewBox="0 0 500 500" fill="none" stroke="white" xmlns="http://www.w3.org/2000/svg">
<circle cx="250" cy="250" r="240"/>
</svg>
Here you can think of us setting up a coordinate system from 0-500, with 0,0 being the top left corner and 500,500 the bottom corner. We then define a circle using cx (center x coordinate), cy (center y coordinate) and r (radius). So we have a circle with center 250,250 and radius 240, setting it squarely in the center of the canvas.
<svg version="1.1" width="500" height="500" viewBox="0 0 500 500" fill="none" stroke="white" xmlns="http://www.w3.org/2000/svg">
<path d="M 490 250 A 240 240 0 1 1 490 249"/>
</svg>
Here we've created a circle again, but this time we've used the path element. The path element is a real powerhouse, allowing you to create lines using straight lines, several different forms of curves and arcs (which is what we've done here). We've started by using M 490 250
which specifies an absolute move (so we don't care where we started) to the coordinate 490 250. We then have A 240 240 0 1 1 490 249
which is specifying the arc. The first pair of numbers 240 240
specify the radius of the arc. Here we have a circle rather than an ellipse as the x and y radius are the same. The next value 0
is the clockwise rotation measured in degrees. The next number 1
is the large arc flag which specifies whether we should use the larger "side" of the circle/ellipse. This is followed by the sweep flag which determines which direction the arc is drawn, or alternatively which of two arcs is used depending on how you visualise it. Finally we have the end point of our arc 490 249
.
That's a lot to work through, so lets go through them individually.
<svg version="1.1" width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M 250 250 A 50 50 0 1 1 250 249" stroke="red"/>
<path d="M 250 250 A 100 50 0 1 1 250 249" stroke="blue"/>
<path d="M 250 250 A 50 100 0 1 1 250 249" stroke="green"/>
<path d="M 250 250 A 100 100 0 1 1 250 249" stroke="white"/>
</svg>
Here we can see that by modifying the x and y radius we can create an ellipse rather than a circle or increase the size of the circle
<svg version="1.1" width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M 250 250 A 50 100 0 1 1 250 249" stroke="white"/>
<path d="M 250 250 A 50 100 -60 1 1 250 249" stroke="red"/>
<path d="M 250 250 A 50 100 60 1 1 250 249" stroke="blue"/>
<path d="M 250 250 A 50 100 180 1 1 250 249" stroke="green"/>
</svg>
Here we've rotated one ellipse 60 degrees anticlockwise and the other 60 degrees anticlockwise, with our final ellipse rotated 180 degrees.
There's something strange here though, we can't see our original white ellipse. Looking further we can also see that our clockwise and anticlockwise rotated ellipses are both on the left hand side of our starting point whereas we might expect the one rotated clockwise to be to the right. The reason for this is the next pair of numbers, the large arc flag
and the sweep flag
, so let's go ahead and change them a little.
<svg version="1.1" width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M 250 250 A 50 100 0 1 1 250 249" stroke="white"/>
<path d="M 250 250 A 50 100 -60 0 1 250 249" stroke="red"/>
<path d="M 250 250 A 50 100 60 0 1 250 249" stroke="blue"/>
<path d="M 250 250 A 50 100 180 0 1 250 249" stroke="green"/>
</svg>
Ok, so we've changed the last three ellipses large arc flag
to 0
and they appear to have vanished. So what's going? The reason for this is our ellipses are not full ellipses, our end point is 250 249
with our start point being 250 250
. Previously we've been drawing the largest segment of the arc, but by switching the flag we've specified that we want to draw the smaller segment which will be almost invisible (you may just see it if you zoom in). Our example is an extreme one as our start and end point are so close together, but if we were to move the end point further away it would become more apparent - so let's do that.
<svg version="1.1" width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M 250 250 A 50 100 0 1 1 250 200" stroke="white"/>
<path d="M 250 250 A 50 100 0 1 0 250 200" stroke="red"/>
</svg>
So here we can see we have two ellipses the same apart from the large arc flag
, but clearly there is a considerable difference between them. Looking closely though we can see that the second ellipse segment does not "fill in" the ellipse properly, it's facing the wrong way making it look wonky. This brings us to our next value the sweep flag
.
<svg version="1.1" width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M 250 250 A 50 100 0 1 1 250 200" stroke="white"/>
<path d="M 250 250 A 50 100 0 1 0 250 200" stroke="red"/>
</svg>
Here we've done the same as above, but kept the same value for the large arc flag
while changing the sweep flag
. We can see that this value has changed the direction in which the segment has been drawn. Effectively if you draw a straight line from the starting point to the end point the ellipse segment can be created on the left or the right of it (where "up" is towards the end point). If the value of the sweep flag
is 1
then it gets drawn on the left, otherwise if it's 0
it gets drawn on the right.
Finally our last two numbers are the coordinates of the end point, which should have been covered through the previous explanations. Simply put it's where the arc finishes, with the start of the arc being defined by the previous path point, in our case M 250 250
.
Now let's put some of this together.
<svg version="1.1" width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M 490 250 A 240 75 0 1 1 490 249.99" stroke="white"/>
<path d="M 419.67 419.67 A 240 75 45 1 1 419.67 419.66" stroke="white"/>
<path d="M 80.32 419.67 A 240 75 135 1 0 80.32 419.66" stroke="white"/>
</svg>
Oof. Let's not beat around the bush, this looks pretty terrible. So what went wrong? Well first I took the ellipse shape, messed with the y radius until it looked about right. Then I got lazy. I knew that I could define a circle where the first ellipses ends lay on the perimeter, so I did that then quickly calculated two other sets of coordinates on the circle, but see I didn't want to deal with trig so I did them at 45 degrees so I could use Pythagoras rather than at 60 degrees which would have properly given each ellipse an equal segment.
Ok so they should be at 60 degrees, but why am I doing the work here. Lets make the code do the heavy lifting. Introducing transform
specifically transform="rotate(angle x y)"
. I can just set this to a 60 degree angle, use the center of the ellipse and boom:
<svg version="1.1" width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M 490 250 A 240 75 0 1 1 490 249.99" stroke="white"/>
<g transform="rotate(60 250 250)">
<path d="M 490 250 A 240 75 0 1 1 490 249.99" stroke="white"/>
</g>
<g transform="rotate(120 250 250)">
<path d="M 490 250 A 240 75 0 1 1 490 249.99" stroke="white"/>
</g>
</svg>
Now that's much more like it, but we can go ahead and duplicate our original path to save us repeating ourselves.
<svg version="1.1" width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="orbit" d="M 490 250 A 240 75 0 1 1 490 249.99" stroke="white"/>
<use href="#orbit" transform="rotate(60 250 250)"/>
<use href="#orbit" transform="rotate(120 250 250)"/>
</svg>
Ok, now we're going to move onto animation, which is another great thing about SVGs. Here I've decided I want each path to have a small coloured "electron" orbiting along it.
Animation with SVGs is at the same time very simple and very complicated. You can use animate
tags to change a variable over a time period, and to some extent that's about it. The issue occurs in getting the animation to do what you want it to, rather than what you tell it to. Let me demonstrate:
<svg version="1.1" width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="orbit" stroke="white">
<animate attributeName="d" values="M 490 250 A 240 75 0 1 1 490 249.99;M 490 250 A 240 75 360 1 1 490 249.99" dur="10s" repeatCount="indefinite" />
</path>
<use href="#orbit" transform="rotate(60 250 250)"/>
<use href="#orbit" transform="rotate(120 250 250)"/>
</svg>
So here we've set the rotation part of the ellipse to animate between 0 and 360. So we might expect our ellipse perimeter to rotate around the set path. Well no, because it's going to rotate the actual ellipse, so that's already not good.
Worse however is that the ellipse is still using the same start and end coordinates, so as you can see it gets a bit messy...
Ok so lets assume we could calculate the new start and end positions for several points, we then run into another issue, the animation changes variables in a linear manner, so we would need to calculate these points using equal circumference values rather than angles, and would need them frequently enough that the motion was not noticeably jerky due to the radius using a quadratic term. And even then it's going to be a spinning ellipse rather than a moving path around the circumference of the ellipse.
So scrapping that, lets look at stroke-dasharray
instead. This can be used to create a dashed line for our path as so (we're moving back to one ellipse for simplicity):
<svg version="1.1" width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="orbit" d="M 490 250 A 240 75 0 1 1 490 249.99" stroke="white" stroke-dasharray="50 500"/>
</svg>
Here we've specified we want length 50 dashes and spaces of length 500 between them. Now adding stroke-dashoffset
we can delay the start of the dashes:
<svg version="1.1" width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="orbit" d="M 490 250 A 240 75 0 1 1 490 249.99" stroke="white" stroke-dasharray="50 500" stroke-dashoffset="250"/>
</svg>
And now you can probably see where this is going. We push the limit of the dash array to have one tiny dash, with the gap between dashes almost the entire circumference. Then we animate the offset through 0 to the length of the ellipse. We can calculate this using javascript, or equally we can just use the formula for an ellipse's circumference. In either case we end up with the following:
<svg version="1.1" width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="orbit" d="M 490 250 A 240 75 0 1 1 490 249.99" stroke="white" stroke-linecap="round" stroke-width="5" stroke-dasharray="0.1 1058.64">
<animate attributeName="stroke-dashoffset" values="0;1058.74" dur="10s" repeatCount="indefinite"/>
</path>
</svg>
We've added a few things to make this work, namely stroke width
and stroke-linecap
so that our tiny dash can actually be seen. On the whole though this seems to work well. We could look into making the orbit more accurate, as the orbit speed is constant rather than following Kepler's laws, but frankly I'm just not that much of a masochist, it's near enough.
I do want to add back in the full orbit paths though, and change the colour of the "electrons"
<svg version="1.1" width="500" height="500" viewBox="0 0 500 500" fill="none" stroke="red" xmlns="http://www.w3.org/2000/svg">
<g id="orbit">
<path d="M 490 250 A 240 75 0 1 1 490 249.99" stroke="white" stroke-width="10"/>
<path d="M 490 250 A 240 75 0 1 1 490 249.99" stroke-linecap="round" stroke-width="70" stroke-dasharray="0.1 1058.64">
<animate attributeName="stroke-dashoffset" values="0;1058.74" dur="1.5s" repeatCount="indefinite"/>
</path>
</g>
<use href="#orbit" transform="rotate(60 250 250)" stroke="lightblue"/>
<use href="#orbit" transform="rotate(120 250 250)" stroke="greenyellow"/>
</svg>
By setting the default stroke colour to red here I can get away without setting the electron colour in the first orbit, which then allows me to overwrite it when I duplicate it. I've specified the colour of the non animated path as white originally, so this does not get overwritten, only the animated "electron" as it has no stroke specified other than the svg default with the lowest priority.
I've also tweaked some of the values to make it work better as a favicon (the one for this page). If I was using it embeded in the page I would change them to something like this:
Have a play! Maybe you want the electrons all on top of the paths rather than passing under some of them. Maybe you want to shrink the size of the svg even more (it's sub 1kb currently). Maybe you want to fix the orbit speeds. Alternatively use the techniques above to come up with something completely novel.