Spirited sparkles

I tend to shy away from frame-by-frame animation. It tends to be time consuming as you need to draw a subject over and over, changing appearances ever so slightly to create the illusion of movement. But the result can be quite intoxicating.

Perhaps you are working on a redesign for a website that in hindsight is exceedingly static; perhaps you want to spruce up a landing page with a spark of life.

You can certainly animate shapes in position and scale, but the risk is that the effect is often smooth to a fault. Perfect, paced, but also robotic, predictable.

In this direction, animating frames drawn with care and a bit of asymmetry, of unpolished mess is all the more welcome. A great way to add character and deviate from the tried and true.

In theory

There are two concepts coming together to set up the animation: the SVG viewBox and a CSS timing function.

First, the viewBox. In the opening tag of any svg element you have the option to initialize the attribute with a string of four numbers.

<svg viewBox="0 0 100 100">
	<!-- ... -->
</svg>

These describe the visible area with a rectangle, in position — x and y — and dimensions — width and height. You see, the SVG canvas is virtually infinite. You can draw with additional elements virtually anywhere.

<svg viewBox="0 0 100 100">
	<circle cx="50" cy="50" r="10" />
</svg>

But the shapes will appear only if the elements fall in the designated box.

viewBox

The important fact: even if unseen, the shapes are still present. With this in mind you can draw something “off screen”, off to the side.

<svg viewBox="0 0 100 100">
	<circle cx="150" cy="50" r="10" />
</svg>

And have the graphic reappear translating the element back.

<svg viewBox="0 0 100 100">
	<circle transform="translate(-100 0)" cx="150" cy="50" r="10" />
</svg>

The element or a convenient group element wrapping around the drawing node.

<g transform="translate(-100 0)">
	<circle cx="150" cy="50" r="10" />
</g>

With more than a single frame, you need to draw the sprites at increasing offsets.

<g transform="translate(100 0)">
	<!-- frame #1 -->
</g>
<!-- ...frames 2, 3 -->
<g transform="translate(400 0)">
	<!-- frame #4 -->
</g>

And translate them into relevance, one after the other.

<g transform="translate(-200 0)">
	<!-- ...frames -->
</g>

That being said, the change needs to be immediate, not smooth.

#0#1#2#3#4continuous#0#1#2#3#4discrete

And this is where CSS and a specific type of timing function help out. For discrete updates there are technically three step functions, but two are a particular implementation of the root function steps.

steps(4)

In the first argument you argue for the number of steps, dividing the larger update in equal intervals. If you were to select four steps, for instance, these would break the progress in four equivalent stretches, from 0 to 25%, 25% to 50% and so forth, until you reach completion at 100%.

With each interval the change is regular and happens in jumps. But which value is assumed when is up to debate.

Continuing with the example of four steps, if you were to translate an object by 100 pixels, the distance would map out to a predictable array of lengths: 0, 25, 50, 75, and finally 100.

0 0.25 0.50 0.75 1 25 50 75 100

By default, the steps function implies that the values change at the end of each interval. You would therefore maintain the original offset, 0, in the first stretch, from 0 to 25%. The second offset, 25, will become relevant only with the second interval, and end at 50%.

You can change the default behavior with a second argument.

steps(4, end)

This one, detailing the step position, can help you set a different behavior. But as mentioned, it defaults to end. The jumps do happen at the end of each segment and with the made-up example, the shape reaches 100 pixels, but only at the very end.

In practice

Enough fuzzy theory, however. You took the time to draw five whole frames, each different from the next, and offset each sprite to the side.

<!-- ...frames 1 to 4 -->
<g transform="translate(500 0)">
	<!-- frame #5 -->
</g>

Proof that you can animate frame-by-frame with the described tech, you need only to target the common group element and set up the animation. In a stylesheet, this is achieved in two equally important steps.

On one side, a @keframes declaration, detailing what should happen and when.

@keyframes sparkle {
	from {
		transform: translate(0px, 0px);
	}

	to {
		transform: translate(-600px, 0px);
	}
}

With the five sprites, we want to translate the group in the direction opposite to that of the offset, until you move the element to completely negate the distance. And then some.

It will not have escaped your keen sense of observation, but the final keyframe has indeed a larger offset than that of the final frame, moving to a nonexistent picture. But the reason should become apparent looking back at the chart mapping the progress of the steps function and the default step position.

0 0.2 0.4 0.6 0.8 1 100 200 300 400 500

When the last step describes the last frame, you don’t see this sprite until the very end. If the animation were to repeat, or revert to the original state, this means the sprite would disappear without a moment’s notice, and without a chance to make an impression.

Translating the group by an additional step guarantees that even this frame will be displayed in full.

On one end you elaborate the animation with the keyframes. Pay attention that in CSS, you do need to suffix the unit of pixels, and can’t get away with unitless numbers.

On the opposing end, you then trigger the change with the animation property.

svg > g {
	animation: sparkle 0.5s steps(6);
}

Pointing to the animation by name, you can set the duration in seconds, or any number of milliseconds large enough to suffice. And it is finally here that the steps function completes the effect, with the number of frames plus one — can’t very well forget that last extra step.

In the end, I did use JavaScript for the widget, but only to have the animation follow your input. The refreshing change happens with CSS.

Of course if you are using JavaScript, it is possible to lean in on the language to have the animation run after a small interaction. And with the Web Animation API, the transition is close to seamless. Once you isolate the element you want to animate, the parent group.

const group = document.querySelector('svg g');

As with any HTML element you gain access to the animate method. This one accepts two arguments, which may sound familiar.

group.animate(keyframes, options);

Immediately, you have an array detailing the keyframes.

const keyframes = [
	{
		transform: 'translate(0px, 0px)'
	},
	{
		transform: 'translate(-600px, 0px)'
	}
];

The objects reflect the changes in CSS properties, repeating the structure of the @keyframes declaration.

Past the collection, you then have an object setting a few options.

const options = {
	duration: 500,
	easing: 'steps(6)'
};

The properties mirror those of the animation property, so that you can match the duration and the custom step function.

That’s it. You can trigger the animation following whichever event you may want. Perhaps the click of an enticing button?