Home

Awesome

Friz

A generic UI animation controller for JUCE projects.

For a fuller discussion of these classes, see my blog post at https://artandlogic.com/friz-and-the-illusion-of-life/.

API Docs

API documentation is available here.

Overview

The friz project is a set of C++ classes that can be used in projects built with the JUCE application framework to add animation effects to user interface elements. Using this library lets you easily specify a stream of timed values that can be used for any purpose in an application, but most frequently for moving or otherwise changing the visual appearance of UI elements in the application. It comes with a rich set of "easing curves" that control how the values are generated over time, and adding your own control curves is simple and straightforward.

It's named after Friz Freling, animator and director of Looney Tunes/Merrie Melodies shorts in the golden age of the Warner Bros. animation studio.

Crash Course

Let's imagine a simple use of this: A JUCE applicaton with a main component that has nothing in it. Every time you click inside the window, the background color will smoothly animate to a new, random color. This component needs two member variables, a juce::Colour and a friz::Animator.

class MyContentComponent : public juce::Component 
{
public:
    MyContentComponent (); 

    // called whenever the mouse is clicked in the component
    void mouseDown (const juce::MouseEvent& e) override;

    // flood-fill the component with the active color
    void paint (juce::Graphics& g) override { g.fillAll (bgColor); }
private:
    juce::Colour bgColor { juce::Colours::white };
    friz::Animator animator;   
};

We'll hand-wave past all of the "getting a JUCE application that has a main window that can host this as a content component" business -- you should already know how to do that part.

Without doing anything more, we have a component that is ready to generate and handle animation events. Instantiating a friz::Animator with no arguments creates an animation controller that's driven by a juce::Timer and will execute at a rate of 30 frames per second when an animation is running.

To get the color change animation running, we'll implement the mouseDown handler that JUCE provides for us:

void MyContentComponent::mouseDown (const juce::MouseEvent& e)
{
    // arbitrary ID value; we use to prevent multiple animations at once. 
    const int animationId { 1 };

    // check to see if our animation is currently running -- if so, just exit:
    if (animator->getAnimation (animationId) != nullptr)
        return;
    // we'll animate the color through HSB color space, so we need some random numbers to go toward: 
    auto& random { Random::getSystemRandom () };
    const auto nextHue { random.nextFloat () };
    const auto nextSat { random.nextFloat () };
    const auto nextBright { random.nextFloat () };
    // a random number of milliseconds for the transition, between 300 and 2000
    const auto duration { random.nextInt { 300, 2000 } };

    // create the animation, using 3 parametric curves with the same settings:
    auto effect { friz::makeAnimation <friz::Parametric, 3> (
        // ID of the animation
        animationId, 
        //  The values to start from, Hue/Saturation/Brightness
        { bgColor.getHue (), bgColor.getSaturation (), bgColor.getBrightness ()}, 
        // The values to animate toward
        { nextHue, nextSat, nextBright}, 
        // # of millseconds for the transition
        duration, 
        // additional args -- here, we specify the shape of the curve. 
        friz::Parametric::kCubic)
        };

    // The animator will execute this callback lambda on every frame:
    effect->updateFn = [this] (int id, const auto& vals)
    {
        // vals is an std::array<float, 3> containing the current in-between state of the 
        // animation. Update the bg color variable and repaint. 
        bgColor = juce::Colour (vals[0], vals[1], vals[2], 1.f);
        repaint ();
    }

    // pass the animation object to the animator, which will start running it immediately. 
    animator.addAnimation (std::move (effect));
}

...and that's it. Clicking inside the component will start the animation, run it to completion, and clean up all the objects that it needed when it completes.

This animation at 30 frames/second is pretty nice, but JUCE timers aren't rock-solid (by design; they really weren't designed for this level of precision). If your app targets versions of JUCE after 7.0.0, you can add a single line of code to make your animations sync exactly to the refresh rate of the monitor, which will make them execute as smoothly as they possibly can:

MyContentComponent::MyContentComponent ()
:   animator { std::make_unique<friz::DisplaySyncController> (this) }
{

}

This instantiates the animator so that it uses the refresh interval of whatever display the component is being displayed on as its timing source; if you drag your app window from a monitor with a 60Hz refresh rate to a different monitor that updates at 120 Hz, everything adapts automatically for the best performance.

Design Goals

Classes

friz::Animator

Animator docs

Top level animator object, typically owned by a JUCE Component object that needs to animate some aspect of one or more child components.

friz::Animation

Animation docs

An individual instance of a set of animation data. Each animation can provide one or more sets of animation curve data that will be sent back to your code on each frame. A derived class friz::Sequence is used to chain multiple animations together as a single logical unit.

friz::AnimatedValue

AnimatedValue docs

Base class for a set of animation curve types that can be instantiated with a start/end value and the definition of when the end value has been reached, either a time duration, or a floating point tolerance to the ending value.

Currently Defined Curves

Demo application

The demonstration application (located in the bgporter/frizDemo repository performs simple animations that

The control panel on the right can be hidden and shown by clicking on its gray border (and is itself animated). All of the animation parameters can be tweaked and played with using the sliders in the control panel.

Additionally, a 'show breadcrumbs' checkbox controls the display of a point on the screen indicating the position of the last square on each frame of the animation—very helpful for visualizing how each of the curves actually behaves.

Other Examples

(Courtesy of Sudara): This tween animates 2 values at once, the opacity and the position of the component. It uses an AffineTransform for smooth float positioning accuracy. You could also use juce::Component's setBounds methods with integers.

auto fadeIn = std::make_unique<friz::Parametric> (friz::Parametric::CurveType::kEaseOutSine, 0.0f, 1.0f, 0.3f * 60);
auto dropIn = std::make_unique<friz::Parametric> (friz::Parametric::CurveType::kEaseOutSine, -10.f, 0.f, 0.5f * 60);
auto animation = std::make_unique<friz::Animation<2>> (friz::Animation<2>::SourceList { std::move (fadeIn), std::move (dropIn) }, 0);
animation->onUpdate ([&] (int id, const auto& val) {
    juce::AffineTransform t;
    this->setTransform (t.translated (0, val[1]));
    this->setAlpha (val[0]);
});
animator.addAnimation (std::move (animation));

Release History

2.1.1 Feb 12, 2023

Non-breaking Changes

2.1.0 Feb 11, 2023

Non-breaking Changes

Chain animation type

Add a new Chain animation type that lets you bundle together a series of individual animations that will be triggered sequentially. Animations added to a Chain each retain their own update and completion callback functions, and do not need to have the same number of Values.

The existing Sequence animation type has been updated so that it's a specialization of the Chain type, where all the contained animations must contain the same number of Values, and a single UpdateFn callback is used for the entire series of effects.

Comceptually, a Chain lets you say "I want this to happen, and when that's done, I want this other thing to happen." A Sequence is used when you want to build a single complex animation out of several simpler effects that happen in order.

In the demo app, we use a Sequence for the 'pop out' of the sidebar—we first click the sidebar to the right, then run another animation to pop it out and make it visible. A single callback updates the position of the sidebar component.

We use a Chain when handling the demo boxes; the first animation moves a box from its creation point to some other (x,y) position on screen, and when that's done we start a second animation that performs a linear (1-dimensional) fade of the box's fill color.

makeAnimation Factory Function

Add a new free function makeAnimation to make the creation of many effects much simpler. In practice, most of the effects that I use are either 1-dimensional, or multi-dimensional, but all of the values being generated are using the same curve type and parameters.

Code that before might have been written like:

auto fade = std::make_unique<friz::Animation<1>> (
    friz::Animation<1>::SourceList {
        std::make_unique<friz::Linear> (startValue, endValue, duration) },
                effectId);

can be cleaned up into:

auto fade = friz::makeAnimation<friz::Linear>(effectId, startValue, endValue, duration);

The benefits become apparent when making multi-dimensional effects, as you can pass all the keyframe values at once:

auto moveEffect { friz::makeAnimation<friz::Parametric, 2> (
    effectId, {0.f, 0.f}, {100.f, 100.f}, 200, friz::Parametric::kCubic)};

-- creating an animation object that contains two AnimatedValue obiects, ready to run by passing to an Animator object.

2.0.0 February 5, 2023

Breaking Changes

This is a major version bump, and as such, there are breaking changes that will require updates in existing code:

Non-breaking Changes

1.6.0 January 2023

1.5.0 March 2022

1.4.0 November 3, 2021

1.3.0 November 6, 2020

Restructured the Animation::Update() logic.

Originally, on each frame, we got the next value from each of the value sources in the animation, and if all of them are in a state of completion, after call the OnUpdate() function, we'd immediately call the OnCompletion() function.

I've rewritten this so that the OnCompletion function is not called until the next frame's Update() call -- I encountered code where calling the completion function too soon after the update function created weird behavior because model code was being updated in the completion callback before the message loop had a chance to process the value set in the preceding Update call.

1.2.0: November 4, 2020

Corrected bug where the EaseIn, EaseOut, and Spring curve classes all used integer parameters in their constructors for the startValue and endValue.

1.1.0: October 2, 2020: Added the new Parametric animated value, which supports the following common easing curves: