Rolling Your Own Framer Animations

How to animate a layer along a motion path.

Framer makes it easy to animate many aspects of a layer, but from time to time you may want to reach beyond the default animatable properties and do something the designers didn’t account for. It’s possible to roll your own animations, with your own animation curves. We’ll look at how to do that using the example of animating a layer along a path.

First, we need a motion path drawn with SVG. You can use any app you like to make this, but I like Boxy SVG for exposing both canvas and code at the same time.

SVGs and Framer aren’t the most natural fit, unfortunately. SVGs introduce their own coordinate systems into a framework that also has its own coordinate system, easily resulting in confusion. There are modules that can help marry the two, but for now, let me give you two simple rules for Framer SVG sanity:

  • Do not specify the SVG’s width, height, or viewbox
  • Set the SVG’s overflow to visible

These adjustments have the effect of preventing the SVG from scaling with the layer, which is not always what you want. It will still move with the layer, however. For our needs, this is sufficient.

The Motion Path

Create a new Framer prototype, switch to the Code tab, and add the following variable:

Note that the path must include an ID or we won’t be able to reference it later. Here, our ID is “motionPath.”

Create a new layer and insert the motionPathSVG into its HTML content. The layer can be of any size or position.

You should now be able to see a light blue sine wave on your canvas.

The motion path.

The Moving Layer

Next, we’ll create a layer for traveling along our motion path. The specifics aren’t important. Make this look however you’d like.

Rocket by janjf93.

The Animation Curve

Since we’re building our own animation from scratch, we won’t have access to conveniences like Framer’s easing curves. We’ll have to supply the math in a custom function. Add this now:

easeOutQuad() takes a number between 0 and 1 and then returns an adjusted value in the same range. That value will be "eased" toward the larger end of the spectrum.

Getting the Motion Path

If you correctly assigned your motion path an ID, we can use getElementByID() to grab the path. Rather than run the function every time we want the element, let's store it in a variable for quick reference.

The Animation Function

Now we have the parts we need to build an animation. We’re going to write a function that takes three parameters: the layer to be animated, the motion path it should follow, and the length of time the animation should last. Create a function with those parameters, something like:

We’ll need to know the whole length of the motion path in order to make sure the moving layer arrives at its endpoint on time. getTotalLength() makes this straightforward.

It’s important to note the time the animation begins, so we know when to stop. We can do this using Framer’s Utils.getTime() function. It simply returns the current time in seconds.

Utils.getTime() is really a stand-in for the JavaScript function, though getTime() converts from milliseconds. This is helpful since we'll be supplying duration in seconds too.

Loop With Animation Frame

The plan now is to create a looping function within this function that repeatedly compares the current time to startTime and moves the traveling layer along the motion path according to the difference between them. To make sure our animation happens smoothly, we're going to use a little trick known as window.requestAnimationFrame.

There are two neat things about window.requestAnimationFrame: it helps ensure smooth 60-frames-per-second animation; and it only fires once, on the browser's next animation frame. Framer's events can be triggered multiple times, over and over again. window.requestAnimationFrame is a one-time thing. That's why we have to loop back and call it again.

The overall structure of the function will look like this:

The reason for including do is that it allows us to both define the animate() function and immediately run it. animate() will perform window.requestAnimationFrame, which will in turn call animate() again. Clearly, we'll need a way to stop this loop when the animation is finished or it'll run forever.

Working pathLength and startTime back in, the current function looks like this:

Elapsed Time

The first thing to do within animate() is get the current elapsed time, which we'll regard as the difference between now (Utils.getTime()) and startTime.

If you want to be safe about it, we can add a check that keeps elapsedTime from ever running over the animation's duration.

Motion Path Length at Time

We’re going to treat elapsedTime as a percentage of the animation's duration and translate that into a subsection of the motion path's total length. To get our percentage we could just do:

But keep in mind we’ve got our easing curve to consider. We want to pass that raw percentage into our easing function to get the eased amount relative to the current moment in the animation. That looks like so:

We then multiply our total motion path length by this eased percentage to get the length up to the current moment.

Adding to the animateAlongPath() function:

Move the Layer

With those elements in place, we’re ready to actually animate the layer in window.requestAnimationFrame.

We want the midpoint of our traveling layer to keep in sync with the endpoint of our motion path (at currentLength). We can make this pretty easy with an SVG feature, getPointAtLength(). Our animateAlongPath() function takes path as a parameter, so we're going to assume it's an SVG path and use that method on it.

If this assumption worries you, you could introduce a check, like if path not instance of SVGPathElement then return, to have the function bail when an SVG path isn't correctly supplied.

getPointAtLength() returns a point with both x and y values, so this is very close to what we need:

However, if you’ve moved your motionPathLayer around, you could see unexpected effects. Since that’s the layer containing our motion path, it’s a good idea to account for its position:

Those lines get added under window.requestAnimationFrame:

The only thing left to do is make sure the animation stops running when it should. We’ll wrap the second call to animation() in a conditional:

The final function looks like this:

Make It Happen

To trigger the animation, we’ll just call animateAlongPath() and supply the layer, motion path, and duration. We saved our motion path as pathByID, so this is pretty easy:

Preview the animation or download the prototype. If you’d like to experiment with other animation curves, here’s the whole set.

Black Pixel is a creative digital products agency. Subscribe to BPXL Craft and follow us on Twitter.