The Inner Workings of Magic MotionThe Inner Workings of Magic Motion

A behind-the-scenes look at our new smart animation tool that enables designers to create their own custom animations and transitions.

avatar

Matt Perry

June 11, 2020

Magic Motion is a powerful new feature in Framer that allows designers to draw a line between any two screens and create a prototype that will smoothly animate between them.

It’s built on the new auto-animate and shared layout features in Framer Motion 2. As a developer, handoff is no longer a pit-of-the-stomach experience, a hasty “you can’t do that on the web!” Implementing a Magic Motion prototype as a production-ready, URL-driven animation between completely different views is just a few lines of React markup.

Crucially, Framer Motion performs all these layout animations at 60fps. In this post, we’re going to learn how.

The problem

CSS offers a number of different layout systems that can all interoperate with each other. A flexbox can be placed within an absolutely positioned grid that sits within a static float: right. With all these possibilities, calculating how a webpage should look is expensive. At 60fps, a browser has just 16.6 milliseconds to update the screen before the next frame. It’s unlikely that a browser will be able to update a layout within that frame budget, so there’s no real API that lets you try.

Take this switch, which is laid out using a simple flexbox. Even with transition: all, changing justify-content has an instant effect.

.switch {
justify-content: align-start;
transition: all;
}
.switch.on {
justify-content: align-end;
};
COPY

So developers are limited in their options. Ideally, animations should be performed using cheap compositable properties like transform and opacity. Properties like background-color and box-shadow trigger paint, which is a little more expensive, but sometimes it’s unavoidable. What we ideally want then, is a way of animating layout using only transforms.

FLIP

The technique at the core of performant layout transitions in the browser was first described by Paul Lewis. It’s called FLIP, which stands for First, Last, Invert, Play.

It works like this:

  1. Measure the first layout
  2. Update the CSS and measure the last layout
  3. Apply the inverted delta as a transform to make the last layout look like the first
  4. Play the animation

So we do the expensive thing (layout) at the start of the animation, where we have a window of time where the user won’t notice heavy work. Then we do the cheap thing (animating transform) once per frame.

For very simple use-cases, this is enough to smoothly animate layout.

But there’s a drawback to FLIP that can instantly wreck the illusion: scale distortion.

By replacing the animation of width and height with scaleX and scaleY, every style that was bound to that width and height is visibly broken. This includes box-shadow, border-radius, and the size and styles of any children too. Notice the distortions in this example as it switches between the two visual states.

Magic Motion’s key innovation is the ability to correct all of this visual distortion, throughout an infinitely deep tree.

This is scale correction. We apply it to CSS properties that can be corrected without triggering layout, like box-shadow and border-radius.

It’s applied throughout a tree on any component that a user has set to automatically animate.

<motion.div animate />
COPY

Or has included in a shared element transition.

<motion.div layoutId="header" />
COPY

In the future, we may bring scale correction to all motion components.

Correcting CSS styles

Correcting the appearance of border-radius and box-shadow is a three-step process. First, if we’re animating between two different values, we interpolate between those.

const borderRadius = mix(origin, target, time);
COPY

Second, we keep a record of the “actual”, pre-correction value. If the animation is interrupted, the next animation will start from this rather than the final scale-corrected value (which might have no relevance in a future scale context).

this.current.borderRadius = borderRadius;
COPY

Finally, we apply the scale correction.

The border-radius style can be set per corner with styles like border-top-left-radius. Each corner can accept two values, one for each axis. So to correct for each axis, we divide the current border-radius once by the scale of each.

const x = borderRadius / scaleX;
const y = borderRadius / scaleY;
element.style.borderTopLeftRadius = '${x}px ${y}px';
COPY

box-shadow has an x and y setting that be corrected in the same way, but it also has blur and spread that don’t have single-axis controls. To correct these values, we take an average scale and apply that to both instead:

const averageScale = mix(scaleX, scaleY, 0.5);
blur = blur / averageScale;
spread = spread / averageScale;
COPY

But generally the ratios we animate from/to are similar enough that the blur and spread scale correction looks pretty good. In the future, there may be some weighting we can do to stop the more extreme distortions.

Correcting these two styles fixes most of the visual distortion on a component. But we’re still left with the distortion of children.

Correcting child components

Without child correction, the shape of this round ball becomes distorted as its parent changes scaleX.

In addition, it would be impossible to also try and reliably animate this ball’s x position, because the space which it travels through would itself be stretching and squashing. This would lead to a very uneven motion like it was sat upon lapping waves.

Correcting child distortion is where we start to pull away from the literal technique of FLIP and adhere to it more in principle.

The first step is to loop through every animating component and remove any currently animating styles. Then, we snapshot their layout in a second pass.

children.forEach((child) => child.reset());
children.forEach((child) => child.snapshot());
COPY

By batching the reads and writes in this way we prevent layout thrashing. In Framer, a prototype might have hundreds of animating components, so optimizing this can have a profound effect on performance.

We also ensure we’re snapshotting every component as it will exist on the screen in its final state, unaffected by the transform of its parent(s). This is important because it means we can then track, within a tree, all of the transforms we’ve applied to each component, then use this to correct the appearance of its children.

Because we want to play the animation of every component independently of this tree transform (to avoid the lapping waves), each component has a shadow bounding box. This gets interpolated from its visual origin to its target once per frame.

const latest = mix(origin, target, time);
COPY

The target is usually the same as the measured last layout (the L in FLIP), but for some effects like AnimateSharedLayout’s crossfade, it might be somewhere else on screen. Either way, we now know where on the screen we want our component to appear visually. We use this information to calculate the delta between where we want the component to appear, and where it actually is.

const delta = calcDelta(latest, actualPosition);
COPY

The component saves this delta to a context that all of its children have access to. The component itself might also have some parent deltas that it has to correct for. So before calculating delta we first apply all the latest parent deltas to the actual measured position.

const latest = mix(origin, target, t);
const transformedPosition = applyParentDeltas(actualPosition, parentDeltas);
const delta = calcDelta(latest, transformedPosition);
COPY

This is how the scale correction is performed. By applying parentDeltas to the actual position, we are then left with figuring out how to go from there to our desired visual position.

As a final step, we also use parentDeltas to calculate the combined scale of the tree. We can use this on the CSS style corrections from before, so they correct parent distortions too.

const x = borderRadius / scaleX / treeScaleX;
COPY

Our ball now stays the correct size, and can even animate through scaled space even as it distorts around it.

Wrapping up

Animating layout in the browser is hard, but Framer and Framer Motion are presenting it in an accessible way, removing all the friction from handoff.

Thanks to the foundations of the FLIP technique, we can do this at 60fps. By accounting for tree transformations, its possible to correct scale distortions throughout an infinitely deep tree, on size, position, and even CSS styles like box-shadow and border-radius.

See Magic Motion in action and create your own animations by signing up for Framer Web. It’s available for free! If you’re a developer, you can also try out Framer Motion 2’s new auto-animate and shared layout features.


This blog post originally appeared on inventingwithmonster.io.

Like this article? Spread the word.

Sign up for our newsletter

Join our newsletter and get resources, curated content, and design inspiration delivered straight to your inbox.