DEV Community

Cover image for Keyframe animation for UI with navigation
edA‑qa mort‑ora‑y
edA‑qa mort‑ora‑y

Posted on • Originally published at mortoray.com

Keyframe animation for UI with navigation

When I saw the below demo I thought "neat, how'd they do that"? That may sound surprising if you knew that I wrote the API for this. In this article I'll describe how three orthogonal features, generic navigation, event triggers and keyframe tracks come together to produce this pleasing animation.

[Video](https://videopress.com/v/ziEUG5qo) - [The Fuse Example](https://www.fusetools.com/examples/angled-navigation)

Keyframe animation

Creating the jagged path is perhaps the easiest part. Keyframe animation is something I added to the API a long time ago -- our users felt understandably constrained with only simple easings.

Let's take a look at one-half of the animation, the movement to the left of center.

<Move RelativeTo="Size">
    <Keyframe Time="0.25" X="-0.7" Y="0.7"/>
    <Keyframe Time="0.5" X="-1.4" Y="0"/>
    <Keyframe Time="0.75" X="-2.1" Y="-0.7"/>
    <Keyframe Time="1" X="-3.8" Y="-1.4"/>
</Move>
Enter fullscreen mode Exit fullscreen mode

Our animation engine is built around moving things away from where they normally are. In this case, each card has a position on the screen determined by the layout engine. This Move says how to move it away from that position. The RelativeTo="Size" also uses the layout size: the fractional values for X and Y are multiples of the element's size. You can visualize the path in your head, or just look at the animation again.

Spline track

A jagged path works well in this example, but it's not always desired. We could instead put <Move KeyframeInterpolation="Smooth"> to create a path with rounded corners -- it creates a spline curve from the values.

The animators uses dynamic composition to implement the path. If a Keyframe is used it uses the SplineTrack provider, otherwise the EasingTrack provider is used. There are a couple more for dealing with discrete values.

How does it create the smooth curve? In short, I'm using the Kochanek-Bartels equation to get tangents, which are turned into a cubic Hermite spline. I ended up using the exact same approach (same code even) to create a smooth curve in our vector drawing. You can read more about this in my spline article.

Animation unaware navigation

Move specifies the complete path, but the cards are moving only part of the distance on each transition. This is where the linear navigation comes in.

The tricky part in Fuse was to define a navigation system that didn't have prebaked animations. We wanted to give designers free reign in what navigation looks like on the screen. Getting this "right" took a lot of iterations in the code. This history is still apparent in the interfaces for navigation -- I have an outstanding issue to clean this up a bit. Okay, but how does it work?

Each card is a page in a LinearNavigation, giving them a specific order in the navigation (you can see the left-right order in the demo). The active page is given progress 0 for navigation. The page just "in front" of it is given progress 1, the page just "behind" it has -1. All pages are given a progress based on their distance to the active page. The page two behind the active one has -2, three behind, -3, etc.

These values are continuous. When the navigation switches pages it gradually alters the values; for example, from 0 to 1 for a page moving forward. In this particular example the navigation is configured to use a "CircularOut" easing for the transition.

The key point here is that the navigation only deals with these progress values. It's blissfully unaware of the visual representation of the pages. How then is the animation achieved?

Scaled animators

The Move I showed above is set inside an EnteringAnimation trigger.

<EnteringAnimation Scale="0.25">
    <Move RelativeTo="Size">
Enter fullscreen mode Exit fullscreen mode

EnteringAnimation subscribes to progress update events on the page (this uses a C#/Uno event). It converts the page progress value into a progress value for the Move timeline -- it seeks to this new value. By default it maps progress 0 to 0%, and progress 1 to 100%.

The Keyframe items specify a Time parameter, which is given in seconds. When driven by something like EnteringAnimation the actual seconds part is ignored -- the timeline is normalized from 0% to 100% progress and driven by the progress value of the page.

We don't want to animate over the entire Move timeline. We only want to navigate a bit of the way. This is what the Scale="0.25" does. The page progress value is multiplied by this amount. A page with progress 1 will thus only seek to 25% of the animation. A page at progress 2, two away from the active one, will seek to 50% in the timeline. These scaled values match the Time values given in the Keyframe.

The counterpart to EnteringAnimation is ExitingAnimation. We saw that the page progress values can be positive or negative. EnteringAnimation deals only with position values. ExitingAnimation deals with the negative values -- converting -1 to 100%. This is how we can provide different animations for pages "in front of" and "behind" the active page.

The names "Entering" and "Exiting" were chosen based on early use-cases where pages were visually entering and leaving the screen. This was actually done prior to coming up the page progress mechanism. We've debated often about these names, yet never managed to come up with something clearer. Nobody seemed to like my PositivePageProgressAnimation suggestion.

Other triggers

This demo is a good example of the value in an orthogonal API:

  • The navigation system cares only about modifying a page progress value. Whether it's a timed transition, or the user swiping on the screen, it's all mapped back to this simple progress value.
  • The trigger system, like EnteringAnimation, cares only about converting this value into an animation progress. This makes it easy to create other triggers, like ExitingAnimation, or ActivatingAnimation and DeactivatingAnimation if the direction isn't relevant. It also allows for WhileActive or WhileInactive, which can both specify a Treshhold for how active they need to be.
  • The Move timeline is just a generic animator and doesn't care what's driving it. A Rotate or Scale could be plugged in here just as easily. The Keyframe applies equally to any of these animators.

It's nice to see it work out so well in this demo.

Top comments (0)