This article is a tutorial in creating animations with SVG + SMIL. I start with the SVG from part 1 and part 2 of our algorithmic metamorphant article series and create a vectorised loading animation. You can find the final code example in the algorithmic-metamorphant-logo github repo tag third-blog-article.
The goal: A vectorized loading animation for the web
In part 1 and part 2 of this series we refactored the metamorphant logo to a simplified SVG version based on algorithmic construction rules. This SVG will be our starting point. We will not change its geometry any more in this article.
We will create a loading animation similar to the first metamorphant loading animation created in a sloppy, hackish way with Synfig. Our animation will be optimised for use on the web by using native web technology. This time we will not compromise on exactness. As our animation description is algorithmic as well, the loading animation can be easily reproduced when the details of the logo are changed.
Animating SVG
In order to achieve our goal of animating the logo directly, we need a way to animate the SVG vector art. In former times, people would not have used SVG for that purpose, but rather some plugin-based technology like the proprietary Adobe Flash.
As of 2020, it is possible to create vector animations for the web using web native technology. Unfortunately, there is more than one canonical way to do it. To be honest, it’s a mess.
SMIL was originally intended by W3C to become the main animation method for SVG, but according to SVG working group’s github issues this is a point of debate.
CSS Animations
With the last changes in CSS Specifications, CSS got more powerful in terms of animating elements on a web page. Since the CSS standard was modularized, the available bits and pieces live in different specification modules.
CSS Transitions is used for animating between CSS property changes. It is specific to this purpose.
CSS Animations Level 1 gives the CSS author more control over the animation details than CSS Transitions. It provides a language to describe complete animations of CSS properties.
Web Animations focuses more on the timing and interaction between a web page and an animation. It is more general and agnostic of the concrete SVG animation technology used.
All CSS Animation standards focus on animation of CSS properties only. This is logical, as the whole idea behind CSS is the separation of concerns between content and presentation. In practise, this limits CSS animations to SVG presentation attributes. This is the subset of SVG element attributes, that are exposed as CSS properties.
Javascript
Another approach for animating SVG vector graphics is using the browser’s JavaScript Engine to manipulate the SVG DOM. In particular this becomes simpler when using a reactive JavaScript framework and modeling time as events. If you do not want to use vanilla JavaScript a.k.a. modern days assembly language, you can resort to more convenient transpiled languages like Elm or ClojureScript + re-frame. Maybe you can achieve the same using the WebAssembly compilation features of other languages. For example Go supports WebAssembly as compilation target. However, I am not sure how far you get when it comes to SVG DOM manipulation.
This is a common concern e.g. in browser games. There, animation will not be limited to time as event source, but will have to react to other events like button presses, mouse movement or even network interaction with other players. In this case using a full-fledged language is really natural. When it comes to simple time-based animations, it feels like overkill and less declarative than e.g. abstract keyframe descriptions.
SVG + SMIL
The SVG 1.1 Standard includes some animation properties itself, hinting to SMIL and SMIL Animation as the standard way of describing SVG animations. SVG serves as a host language for SMIL in that case.
This is the approach I will take in this article.
SVG + SMIL Building blocks
I will first explore all bits and pieces you need to create our animation. In the end you will put them together to a complete and working animation.
SVG animations work by describing the temporal behaviour of our elements’ attributes. So for example if you want a circle to move from the left to the right, you can write
Neat! You see how the attribute cx changes over the duration of 3 seconds from cx="50" to cx="350". The end="click" helps you stop the animation while reading this article. Just click the circle. Now let us learn about using keyTimes and values for more complex animations by making the circle bounce back and forth
You will have noticed that the <animate ... /> element is a child of the <circle ... /> element. Using it this way would force you to couple animation details with the geometry of your drawing. You can decouple the 2 concerns by writing
Great! Having this trick up our sleeves, we can describe our animation separately from the geometry of the original logo. We just need to reference its elements.
Before jumping into our little project of animating the metamorphant logo, I want to show you another feature we will need: composition of multiple animation elements. In our Synfig animation of the metamorphant logo we needed to describe the complete timeline up front. SVG+SMIL provides you with a way to decouple the timing by using relative references between different <animate ... /> elements.
As an example let me write our bounce animation in a different way:
Of course, now you can not use the end="click" trick any more. Also you have to watch out for proper id naming: In case of embedding SVG like it’s done in this blog article, id attributes have to be globally unique. Uargh.
Line drawing effect
As a next step towards a metamorphant loading animation, I need a way to create a line drawing effect. This can be easily done using the SVG attributes stroke-dasharray and stroke-dashoffset. I will first demonstrate the effect of these attributes using a simple example path
The pathLength attribute serves as a normalization scale for the other attributes. stroke-dasharray enables creation of dashed paths. The extreme value of stroke-dasharray="1000" with a pathLength="1000" means no dashing at all. Finally, stroke-dashoffset controls the offset of the first dash. I will visualize that by some examples:
stroke-dasharray="20"
stroke-dasharray="200", stroke-dashoffset="0"
stroke-dasharray="200", stroke-dashoffset="200" (the opposite dash pattern)
stroke-dasharray="200", stroke-dashoffset="50" (a slight offset)
stroke-dasharray="200", stroke-dashoffset="450" (the offset is periodic with a period of 2x the dasharray)
You are now ready to create a dash array animation:
By using a stroke-dasharray="1000" for a pathLength="1000" you can achieve the desired drawing effect:
This is enough to create a “connecting the dots” line animation. The dots will not be animated, yet. They will just be present all the time. The resulting Clojure code:
The XML can be generated by lein run:
In this example I use relative timing and animation element dependencies between the animation steps. You can find more detailed information about timing of animation steps in the SMIL standard sections SMIL 3.0 Timing and Synchronization and SMIL 3.0 Time Manipulations. The other “new” feature you will see in this example, is the ill-named fill="freeze" attribute, which ensures that the state of the animated element will keep the same even after an animation step finished.
Animating opacity should be a simple exercise with your newly-acquired knowledge:
Animating scale
The second part is letting the eye grow. This is a bit harder to achieve and will help me to introduce another SMIL animation feature: <animateTransform>
Let me do a first naive experiment:
You will see, that the transform also affects the reference coordinate system and therefore the positioning of the circle. We want it to grow from the center. I found one way to avoid this effect on Stackoverflow. Change from positioning by cx="50" cy="50" to positioning by transform="translate(50 50)".
Combining them yields the following updated eye definition and animation script:
The resuling XML:
And the resulting animation:
Please note, that in the animateTransform elements, the fill="freeze" attributes have not been included, as it breaks the additive logic of transformations.
Animating the start and end dots
Until now, the start and end dots of hulk and ear were static. The next challenge is to animate them. Of course, we could cheat and switch to the same approach as in Synfig: do not directly animate the path, but overlay it with a hiding path and animate that. In fact, it would be easy to achieve. We would just have to duplicate the path, extend it a bit and increase its width to cover the logo path.
Instead I will try two different effects here. You got me: I also do it to introduce the missing <animateMotion> element.
Letting markers appear and vanish
To achieve a simple appear/vanish effect, it is enough to just animate the marker-start/marker-end attributes with the right timing. The animation script starts to grow complex and I would love to have some better abstractions…
In XML:
The animation then looks like this:
I think this is already a pretty convincing result, isn’t it? Can we improve on it?
More complex marker animations
One way to improve the appearance of the animation would be to smoothen the appear/vanish effect of the dots.
SVG Marker elements can be animated like any other element. It does not help me, though: We wish to animate the marker at the point of use, i.e. the path, and not its definition. Otherwise it would apply to all its instances. Of course, we could generate a definition per instance like in this JavaScript example of styling SVG markers. To be honest, I do not think it is an elegant solution. In that case I would rather prefer to model the dots not as markers, at all.
Long story short: Animating markers is not one of the strengths of SVG+SMIL. I would even go so far to say that markers are not a strength of SVG, in general.
Animating dot movement
So, I want to think about an even more creative and complex way to improve upon our animation: I want to animate the dot as if it would be the end marker of the revealed line, leading the revelation!
Again we hit the limitations of the SVG marker concept: We can neither animate the revelation of the path and end the part revealed so far with a marker, nor can we place a marker-mid in a proper position to achieve the same effect.
In order to work around this issue, I will consider the dots as first class citizens and not model them as markers. In particular, they will be positioned precisely where they should be. The animateMotion feature of SVG+SMIL will help me. Using <mpath> it should be possible to move an object along a path.
A naive implementation of the effect looks like this:
How can I handle that for the situation I have? There are given paths (hulk and ear), that are already described and should just be reused. Handcrafting separate animation paths is not an option. Possible reactions: Either
create the animation path at generation time by duplicating the given path and calculating the path starting at (0, 0) or
set the circle to (0, 0) and reveal it only at start time of the animation to avoid flicker.
I decide in favour of the second choice.
Applying the effect to the logo animation yields:
Resp. in XML:
It is worth pointing out the keyTimes="0 ; 1" keyPoints="1 ; 0" trick for reversing the path animation direction resp. the keyTimes="0 ; 1" keyPoints="1 ; 1" trick for setting the position to the end of the path. I took it from a Stackoverflow thread about reversing the direction of SVG mpath animations.
Working with SVG+SMIL is fun. I definitely enjoyed describing this animation as code.
For me, it also was a nostalgic journey back to the roots, reminiscent of one of my first more complex computer graphics experiences: a little family project together with my father and my brother (yes, I confess, that’s kinda geeky); based on model calculation data from an actual simulation, we created a 3D animation of the fusion of two atomic nuclei. That must have been 1996. I remember our brand new Pentium Pro 200 Mhz machine just arrived at home. It ran the brand new SuSE Linux operating system. So much room for exploration! Our tool of choice back then was PovRay and its rudimentary animation capabilities.
At the same time, this experience showed me the limitations of SVG and SMIL. The languages are fine as a target format for animations, but normally you will want to use some tooling based on them. They just don’t seem to be intended for direct ‘animations as code’ development, which poses the interesting question: What would a good DSL for animations look like?