What started out as an attempt to test performance ended up in a nice looking CodePen.

To give you a little background, I challenged myself a week ago to build a lightweight SVG morph library that fit into about 5kb. I wanted to make sure this library was compatible with Just Animate and other animation libraries. That effort resulted in the Polymorph library.

Getting Started

A few days ago, I needed to test how well the library did under stress, so I found two free to use images that had very complex paths. When drawing SVG paths, you can pick up the pen with the M (move) command and start drawing from another location without the lines being attached. Effectively, you can draw most monochrome images with a single SVG element this way. Both the Leo and the Skull image were built this way, so they were great specimens for the test.

In the spirit of Halloween, I used Permanent Marker for the font and decided to morph to the Skull rather than from it.

Pen Composition

Here is a breakdown of important parts of the CodePen.

First, I had to get my SVGs into the CodePen. Because of their size and complexity, I had to put them in a separate pen to keep the editor responsive. After adding them to a separate pen, I embedded that pen in my HTML:

  https://codepen.io/notoriousb1t/pen/wPwjBz

Next, I created a renderer function.

  const renderMorph = polymorph.interpolate([ '#leo path', '#skull path']);

A renderer function accepts a number between 0 and 1 representing 0% to 100% of the distance between each value and returns the value in between. For example, if my starting value is 0 and my ending value is 100, passing 0.5 would return 50.

This is a convention best seen in easing/timing functions, but some animation engines work this way internally. polymorph.interpolate accepts an array of path data or CSS selectors where the path data is. It returns a render function that can find those in between values.

After creating a render function, I created a simple animation loop. This is achieved by creating a function and having it call requestAnimationFrame each time it finishes. That schedules it to run again in approximately 1/60 of a second.

  const MAX_FRAMES = 60;
let frame = 0;
let step = 1;
const target = document.querySelector('#target');

function animate() {
   frame += step;

   if (frame > MAX_FRAMES) {
      return
   }

   // get relative offset
   const offset = just.curves.easeInOut(frame / MAX_FRAMES);

   // render morphed svg
   target.setAttribute('d', renderMorph(offset))

   // request next frame
   requestAnimationFrame(animate);
}

animate();

To keep the time, I decided against tracking time (frame sync), and decided to count frames instead. I chose counting frames over syncing because frame counting guarantees all frames are rendered.

To get the offset (0 to 1) for the render function, I divided my current frame by the maximum number of frames. Then I passed it into an easing function from Just Curves to make it more life like. The easing function also accepts a number between 0 and 1 and modifies it slightly to make the animation more life-like.

After calculating the eased offset, I passed that to the render function to get the current value of the path at that point in time.
Finally, I set the d attribute on the target path with the new value.

Wrapping Up

After it was working, I added a little logic to replay the animation on click. Overall, this is one of my favorite pens to date!


1,129 0 5