Recently I needed to morph one SVG icon into another, in an effort to provide more visual feedback for end users – ideally without adding complex JavaScript logic (e.g. via GSAP).
I’d started out by picking two simple shapes from
iconsvg.xyz:
Here’s what, eventually, I ended up with: (Direct link; view source for the implementation.)
Initially I’d figured using
CSS transforms
should suffice and would be fairly straightforward – but that quickly (and
quite literally) went awry: translate
and rotate
didn’t suffice because line
lengths differ between the original and the target icon, yet applying scale
to
fix that resulted in a skewed projection. After learning that
- the order of transforms is significant (which makes perfect sense in hindsight… )
- CSS
px
translate to user units when applied to SVG (as a pixel denialist, I was very wary of this)
I managed to create something a little closer to what I had in mind – but it still looked awkward and relied on magic numbers. This being me, eyeballing it was not an option. Neither was Pythagoras: Figuring out the math required to accurately transform the original shapes proved sufficiently annoying (also with respect to posterity) that eventually I went looking for a different approach – though not before descending down a rabbit hole of arduously refreshing my long neglected understanding of vector matrices.
Thus I arrived at
SMIL.
I had believed this to be deprecated, but turns out Google
have since relented.
And indeed, SMIL is about as straightforward as I had imagined: <animate>
allows specifying a before and after state with the same attributes used in the
actual shapes, the engine takes care of calculating the transition.[1] Thus
both shapes are combined within a single <svg>
element:
(Yes, that from
redundancy is slightly irritating.)
In order to avoid excessive redundancies animating
<line>
attributes (x1
/y1
and x2
/y2
, each of which would have required an
<animate>
declaration), I manually converted them to
<path>
s, thus
reducing the shape definition to a single d
attribute. Where with CSS before
I’d used JavaScript to toggle a class name, here I needed to
invoke #beginElement
on
each <animate>
element to kick off the animation on demand.
However, that still leaves reversing the animation and providing a fallback for browsers that don’t support SMIL. Both proved similarly straightforward, e.g. using a custom element:
It’s rather pleasing that our code here only has to know about <animate>
’s
declarative instructions; note how the fallback reads the desired target state
from <animate>
and applies it directly to the corresponding shape element. So
this implementation isn’t tied to our particular icons.
Thanks to the various friends and colleagues who patiently assisted in figuring this out. (The actual, err, path was even more convoluted than described here.)
-
It appears this doesn't work for arbitrary shapes though: There needs to be some correlation between
to
andfrom
(e.g. equal number of points within the respective shape definition) for the animation to be calculated correctly. ↩