Blog

JUCE 8 Feature Overview: Animation Module

What's the feature?

JUCE 8 includes a new juce_animation module. It's a feature-rich way to compose and run smooth animations with your JUCE UI elements.

Deprecations

The following have been deprecated in JUCE 8:

  • The existing ComponentAnimator is superseded by the new animation module.
  • The old AnimationDemo (which used ComponentAnimator) has been removed.

What's new?

The new juce_animation module contains the following:

  • An Animator class which describes how to animate something. You can call start, complete, and reset on instances of this class.
  • A ValueAnimatorBuilder class is how you will describe and build an Animator. It's how you specify easings and callbacks.
  • An AnimatorSetBuilder lets you compose and coordinate Animators to create sophisticated animations.
  • A VBlankAnimatorUpdater which you add Animators to, syncing them to hardware refreshes, ensuring smooth animations without frame drops.
  • A set of default easings that align with CSS, such as ease, easeIn and easeOut.
  • A way to specify custom easings.

In addition, there are 2 new demos:

  • AnimatorsDemo
  • AnimationEasingDemo

Understanding Easings

When you roll a ball across a floor, friction will slow it down over time. It eases to a stop. It would be jarring if instead the ball instantly stopped after traveling at a constant velocity.

It's the same for animation in our UIs. We want to provide natural feeling movements, whether we are animating a modal window's opacity or the position of a line.

This is why easings exist. By simulating rudimentary real world physics, we can give animations a more natural feeling.

In code, an easing describes how a value's rate of change behaves over time. It's often visualized with time on the x-axis and the value used for animating on the y-axis, like so:

Default Easings

A common way to create easings with code is by constructing a cubic bezier curve.

JUCE 8 is no exception, and builds easings on top of Chromium's cubic bezier algorithm, optimized for animation usage.

To help you get started, JUCE 8 includes a convenient set of default easings that align with CSS standards, such as getEase(), getEaseIn(), getEaseOut(), getEaseInOut(). A description of them and the values being fed to the cubic bezier function can be found on the MDN website.

JUCE 8 includes a few other easings such as easeOutBack, easeOutCubic, as well as some options that go beyond cubic bezier: a spring builder and Easings::createEaseOutBounce.

You can also manually create easings with Easings::createCubicBezier. Check out easings.net to find values for more types of easings.

Easings Demo

To help you select and get a feel for easings, JUCE 8 comes with an Easings Demo. It allows you to preview and compare animations of an elements position, size or opacity across all of JUCE's easing options.

A basic animation example

Let's look at a minimal example of how animation works in JUCE.

Let's take an example of a bouncing ball. When we click, the ball will fall and bounce:

The code to reproduce this minimal example is below. Note that we only animate the yPosition of the blue ball.

class BouncingBall : public juce::Component
{
public:
    BouncingBall()
    {
        updater.addAnimator (bounce);
    }
    void paint (juce::Graphics& g) override
    {
        g.setColour (juce::Colours::blue);
        g.fillEllipse (150, yPosition, 200, 200);
    }
    void mouseDown (const juce::MouseEvent& event) override
    {
        bounce.start();
    }
private:
    float yPosition = 50;
    juce::VBlankAnimatorUpdater updater { this };
    juce::Animator bounce = juce::ValueAnimatorBuilder {}
                                .withEasing (juce::Easings::createEaseOutBounce (4))
                                .withDurationMs (2000)
                                .withValueChangedCallback ([this] (auto value) {
                                    yPosition = 50 + (float) value * 200;
                                    repaint();
                                })
                                .build();
};

There are a few important things to note:

We want our component to sync and repaint with the hardware refresh rate, so we make it own a VBlankAnimatorUpdater. Our Animator is then added to this updater. This ensures the valueChangedCallback will be called in sync with display refreshes. (Each component that has animation will own its own instance of VBlankAnimatorUpdater).

In the withValueChangedCallback, we calculate our new yPosition. The value in our callback will be between 0 and 1. This is the normal range for an easing value, although it's possible to create easings that go outside that range, for example with spring style animation, or a cubic bezier with y values that exceed 0 or 1.

We then call repaint(), marking the component as dirty. Because this callback happens inline in the paint cycle, right before JUCE begins painting components, it ensures our paint() function will be called and the next frame drawn in sync.

If you need to capture some additional state when an animation is starting (for example, a component's current bounds) you can use the withOnStartReturningValueChangedCallback instead of withValueChangedCallback.

When to call repaint?

In our simple example, we just call repaint inside the Animator callback. For the duration of the animation, the component will be repainted.

Sometimes you will have overlapping Animators that you've composed with AnimatorSetBuilder. To avoid calling repaint separately in each, you can create an Animator that always runs and calls repaint, like so:

const auto repaintAnimator = ValueAnimatorBuilder{}
    .withValueChangedCallback ([this] (auto) { repaint(); })
    .runningInfinitely()
    .build();

See the Animators Demo for more inspiration.

makeAnimationLimits

When animating values, the value you are animating typically has a lower and upper bound.

In our simple example, we did the following in our withValueChangedCallback:

yPosition = 50 + (float) value * 200;

This ensures our yPosition will always be least 50 and the maximum yPosition will be 250 (as the maximum value is 1.0)

JUCE includes a helper called makeAnimationLimits to make it even easier to interpolate the normalized animator callback value to a meaningful value in your UI. For example, we can replace the logic above with a more explicit:

yPosition = makeAnimationLimits (50, 250).lerp(value);

makeAnimationLimits can also take a std::tuple, which allows you to do fun things like so:

const auto [x, y, w, h] = limits.lerp (v);
component.setBounds (x, y, w, h);

Note: When animating component bounds, avoid setBounds. It takes integer values and will produce stair-stepped movement. Instead, use AffineTransforms for smoother (floating point) animations.

AnimatorsDemo

JUCE 8 comes with a demo that shows how to construct and coordinate more advanced animations using classes such as AnimatorSetBuilder.

More

linkedin facebook pinterest youtube rss twitter instagram facebook-blank rss-blank linkedin-blank pinterest youtube twitter instagram