Creating SVG animations with Snap.svg

Animations can go a long way in making your application feel more attactive, friendly and immersive. Using SVG as the foundation of them, you'll get resolution independent, responsive animations, that might otherwise be difficult to build. Let's see how we can create one...

Creating SVG animations with Snap.svg

Animations can go a long way in making your application feel more attractive, friendly and immersive.
Using SVG as the foundation of them, you'll get resolution independent, responsive animations, that might otherwise be difficult to build.
Let's see how we can create one...

Snap.svg

Snap.svg is a Javascript library that makes it really simple to build, animate and interact with SVG graphics. Snap.svg is open source and free to use under Apache License 2.0.
The library works on any major modern browser (IE9+, Chrome, Safari, Firefox, and Opera).
Weighting less than a regular image (~60Kb minified and compressed) it brings a lot of power with it.

Here are some examples of what's possible:

The animation

Through the rest of this article we'll see how this animation works.

Try resizing your browser to see how the animation resizes too.
Most of what you see here is drawn and controlled by Snap.svg, although it doesn't necessarily have to be like that.
Snap.svg can work with SVG elements that are already present on the page, or it can also load remote SVGs. This way you can draw them using your favorite tool (Illustrator, Inkscape, etc...) and control them through code using selectors to get all the different references.

new Snap();

The first thing we need to do is create a snap SVG element that will hold the whole animation.

Snap can either create a new DOM element or use an existing one if we pass a selector.

const s = new Snap("#jump-animation").attr({viewBox: "0 0 330 200"});

Setting a viewBox is very important for scalable SVGs.

Let's draw our pine.
We we'll build our pine out of 10 different triangles.
There's no <triangle> in SVG that we can use, so we'll have to create each of them by hand as a polygon.
We just have to repeat the following 10 times, saving a reference for the triangle on top, to animate it later on.

const triangle = s.polygon(x0, y0, x1, y1, x2, y2).attr({fill: color});

You can find the math behind figuring out the coordinates and the color of each of the different triangles in this CodePen.

The plank

const plank = s.line(0,0, plankLength, 0);
const plankBox = plank.getBBox();
const plankMatrix = new Snap.Matrix();
plankMatrix.translate(plankOffsetX, plankOffsetY);
plankMatrix.rotate(startingTriangleAngle, plankBox.cx, plankBox.cy);
plank.attr({strokeWidth: 5, stroke: "brown", transform: plankMatrix});

First we draw a straight line with the desired length.
After that we use a Matrix to set the plank in position.
Matrixes are very important and we'll use them a lot. They are what allows us to easily scale, translate, skew and rotate elements.
Be careful with the order on which you apply matrix operations: rotate + translate != translate + rotate.

Rotation needs a point where to be applied (rotating 5° at the center is not the same as rotating at the edge).
Our plank has to be rotated at its center. Snap has an easy way to get the center point of any element through its bounding box (getBBox()).
We finally apply some styling and the matrix transformation.

The plank's support works pretty much the same way but uses a s.rect instead of a line.

The weight
For the JS logo, we're not going to draw it. We're going to load it from an existing SVG file instead.

const weightScaleFactor = 0.15;
const weightMatrix = new Snap.Matrix();
weightMatrix.translate(640, 264);
weightMatrix.scale(weightScaleFactor);
const jsLogo = await new Promise((resolve) => Snap.load("https://anilogo-gcucwsckgj.now.sh/img/js.svg", ( fragment ) => {
    const logo = fragment.select("g");
    logo.attr({transform: weightMatrix});
    s.append(fragment);
    resolve(logo);
}));

Snap.load loads the resource asynchronously and takes a callback as a response. A good way to work around this complexity is to wrap the call in a Promise and use async/await to make the code more readable.

Once the fragment has been loaded, we have to scale and position it using another Matrix transformation.
Note that we can't transform the <svg> directly. We have to get a reference to its contents and transform it instead. An easy way of doing that, is to group everything that's inside the SVG in a <g> element and apply the transformation to it.

The jump path
The last thing we have to create is the path that the triangle will follow once it jumps.

const path = s.path("M10,44Q38,-40,89,167")
path.attr({fill: "none"})
const curveLength = path.getTotalLength();

We use the s.path to create a Quadratic curve and make it invisible with fill: "none". Its position is not importante since we're only interested on its shape.
If you want to see it, you can add some extra styling path.attr({fill: "none", strokeWidth: 1, stroke: "black"});

We also get the lenght of the curve for later use.

Top triangle start position

const topTriBox = topTriangle.getBBox();
const moveToPoint = path.getPointAtLength(curveLength);
const topTriDownMtx = new Snap.Matrix();
topTriDownMtx.translate(moveToPoint.x - 10, moveToPoint.y - 44);
topTriDownMtx.rotate(plankAngle, topTriBox.cx, topTriBox.cy);
topTriangle.attr({transform: topTriDownMtx});

To position the triangle at the bottom, we use path.getPointAtLength to get the coordinates of the furthest point of the curve.
We then translate and rotate the triangle using another Matrix.

So far we have the initial status of the animation:

Adding some movement

It's time to staring making things move.

If we decompose the animation, we see 3 things moving:

  • The weight coming down
  • The plank rotating
  • The triangle jumping to the top

Snap.svg only provides two low level methods for animating elements.
The Element.animate method that we'll use for the first 2 cases and Snap.animate for the triangle.
There's no built-in way of definiting a timeline of animations, so we'll have to take care of the timing ourselves with the use of timers.

Weight animation

const weightDownMtx = weightUpMtx.clone();
weightDownMtx.translate(0, 205/weightScaleFactor);
jsLogo.animate({transform: weightDownMtx}, duration*0.5, mina.bounce);

Here we are using the weight's animate method that takes 4 parameters:

  • The set of properties that we want to animate. In this case, we just want to apply a new transform.
  • The duration
  • The easeing the animation will use. There're a lot of options here (easin, easeout, linear, bounce, etc...). We'll use a bounce easing in this case.
  • An optional callback function to execute when the animation ends.

Plank rotation and jumping

setTimeout(() => {
  plank.animate({transform: plankDownMtx}, duration*0.01);

  Snap.animate(curveLength, 0, (step) => {
      const moveToPoint = path.getPointAtLength(step);

      const topTrgMtx = new Snap.Matrix();
      topTrgMtx.translate(moveToPoint.x - 10, moveToPoint.y - 44);

      const angle = (360 + plankAngle)*step/curveLength;
      topTrgMtx.rotate(angle, topTriBox.cx, topTriBox.cy);

      topTriangle.attr({transform: topTrgMtx});
  }, duration, mina.easein);
}, duration*0.20);

The plank rotation and thus the jumping has to start before the weight animation ends (the weight animation ends at the bottom position, but the point of contact with the plank happens earlier). That means we can't simply use the callback function of the weight animation to trigger the other animations.

Instead, at the same time we fire the weight animation, we set a timer that will trigger the plank and the jump. Unfortunately the duration of the timer has to be manually calculated since the bouncing makes things complicated.

Animating the plank is very similar to animating the weight.
We calculate a new matrix for the final position and call animate to apply the transformation.

For the triangle though, we use something different.
Snap.svg doesn't have a built in way to animate an element along a path.
Using Snap.animate(curveLength, 0, (step) => {...}, duration, mina.easein), Snap will call our callback at every point of the animation with values starting at curveLength all the way down to 0.

The same way we positioned the triangle at the bottom of the path, we can get the position the triangle should be at every interval using const moveToPoint = path.getPointAtLength(step);.
Then we create a new matrix and translate the triangle to that point.
We also do some math to figure out how much the triangle should be rotated based on the progress of the animation.
Finally we apply the new transformation directly and not animating, since the animation is already driven by the continuous call of the callback.
We use an easein easing to make it look like there's some gravity involved.

And that's it... we've got our jumping triangle!!

There're some extra things that you have to do if you want your animation to run over and over, but they're very similar to what we have already seen. Look at the CodePen for these small details.

Event handlers

Something that we haven't used on this animation but Snap.svg is capable of doing is adding event handlers to different elements.
It's pretty simple, just register a callback on the element using for example el.click(() => {...}).

With few lines of code, you can do something like this. Tap the circle!!

Now you can go and add some movement to your site!