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
- Set the SVG’s
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 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.
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.
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
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.
startTime back in, the current function looks like this:
The first thing to do within
animate() is get the current elapsed time, which we'll regard as the difference between now (
If you want to be safe about it, we can add a check that keeps
elapsedTime from ever running over the animation's
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
Move the Layer
With those elements in place, we’re ready to actually animate the layer in
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,
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
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: