friz and the Illusion of Life

friz—a Flexible Animation Controller for JUCE

As is often the case, I found myself working on a personal project and had some UI elements that really wanted to have some life to them on the screen.

I tucked into the JUCE documentation expecting to see something that I could use to easily add some personality to the interface, and…didn’t find what I was looking for. There’s a ComponentAnimator class that supports moving a single component from one set of coordinates/bounds to another linearly, and also modify the component’s alpha value to have it fade in or out.

I was looking for something…more expressive.

Earlier this year, I read Ge Wang’s book "Artful Design" (see my review here) and it’s occupied a larger than expected chunk of my brain for a long time, especially some of the topics covered there in Chapter 3 on Visual Design.

The ideas presented there on animation and design and interactivity resonated so strongly with me at least partly because I’ve always been a student and fan of animation as an art form, whether classic-era hand drawn or eventually computer-created. I have vivid memories from my time in college studying to become a composer of the look of shock and disappointment on my professor’s face when I told him that most of my thinking about musical structure had its roots in the Warner Bros. cartoons of Chuck Jones.

Somewhere in my house I still have an old copy of the book "The Illusion of Life," written by Frank Thomas and Ollie Johnston, two of Disney’s "Nine Old Men," the animators who led the creation of Disney’s post-war feature films. In that book they call out some principles that I wanted to be able to express at least in some small ways in my software.

This video is a quick look at them:

Not all of them are directly applicable to software that’s restricting itself to living in a 2D plane, but let’s take them as an inspiration, and something to aspire to.

I sat down to sketch some things out and ended up making a small framework for generating dynamic data that can be used to animate pretty much any aspect of the user interface of a program written using the JUCE application framework (which I’ve written about here many times before), and has the goals of being:

  • Lightweight—if there aren’t any animations in progress, there’s no runtime overhead.
  • Flexible—it’s easy to add new types of animation curves if you want to, or to chain multiple curves together into a more complex sequence.
  • Decoupled—it doesn’t need or want to know anything about your application. When your code creates an animation effect, you pass it a pair of std::function objects; one to handle updates for each frame, and another one to handle the completion of the effect.
  • Modern—written using current C++ techniques (defined for our purposes as C++11 and later). I spent some time away from C++ and have come back to find the language has undergone some serious changes that require me to consciously update my habits, making me feel like the programmer version of SNL’s Unfrozen Caveman Lawyer.

As the code began to take shape, I decided to name it after the great Warner Bros. animator/director Friz Freling. I would like to have called it ‘Chuck’ after his colleague Chuck Jones, but "ChucK" already means something else in the electronic music software world.

Orbital View

At the outermost level, all we want to be able to do is:

  • define one or more values that will change from one value to another value…
  • …over some period of time…
  • …using some set of curves that describe how the values change over time.

It’s just a question of how to create those pieces and plug them together. We’ll start looking at the pieces from the bottom up, with a look at the curves we support, using a demo application that lets us play with things.

animator demo app

The Animator is mostly a blank window that you can click inside to create randomly colored squares that will animate away from the mouse using one of the pre-defined curves. After the animation of a square completes, the square will animate its color to fade away, until it disappears.

The control panel on the right lets you tweak parameters for each of the animation types to explore the different behaviors. The control panel is itself animated:

Available Curves

All of the classes that are used to generate animated data are derived from the friz::AnimatedValue class (which you can also derive classes from to define any behavior makes sense for your application). Right now, the following are available as part of the friz module:

Constant

Generates a single value for the duration of the effect. Perhaps it’s not obviously useful to be able to generate unchanging values in an animation, this can be useful when building up sequences of multiple effects or when controlling the regeneration of cyclical effects.

Linear

Performs a simple linear interpolation between two values over a specified amount of time.

EaseIn

Accelerates quickly away from the start value, decelerates smoothly into the end value. The amount of time for the animation is not specified, instead your code specifies a tolerance of how close to the end value it needs to be before the effect is complete.

In his Artful Design book, Dr Wang refers to this as "Zeno’s Interpolator" because it keeps moving some fraction of the way between the current value and the end value, never quite getting there. Our tolerance argument keeps us from calculating ever smaller slices.

EaseOut

Accelerates slowly away from the start value, accelerates into the end value. Also tolerance-based.

Spring

Accelerates away from the start value, and may go past the end value; if so, it does a simplified model of the oscillation of a dampened spring (…and if I wasn’t worried that there might be physics professors reading this I’d insert some text here about Young’s Modulus and how that affects the behavior here but instead will point out that this is in no way intended as a realistic physics modeling engine).

A few examples of different damping values:

Sinusoid

Generates sin/cos wave values between any two arbitrary phase points. The data generated is always in the natural (-1.0..+1.0) range of those functions.

Inheritance Diagram for the value types

The Animation class

In code, the basic process is to

  1. decide how many curves your animation needs — perhaps one for a control panel opening/closing or a color/alpha change, two for moving a component around, more for changing location/size of a component

  2. Create AnimatedValue objects of the appropriate types and parameters

  3. Add those value objects to a new instance of the Animation class.

  4. Configure the Animation appropriately — usually, this will require you to at least provide a function to call on each frame of the animation.

  5. Add that Animation object to the appropriate Animator object, which will start executing the animation.

  6. When all of the values in the animation are complete, your completion function (if any) will be called, and the Animation will be deleted.

An example—this is the code inside the animator demo application that closes the control panel when the mouse is clicked:

void MainComponent::ClosePanel()
{
   // 1. get parameters in place 
   jassert(PanelState::kOpen == fPanelState);
   int width = this->getWidth();

   int startX = fControls->getX();
   int endX = width - kClosedPanelWidth;

   float accel = 1.4f;
   float dampen = 0.4f;

   // 2. Create a Spring value
   auto curve = std::make_unique<friz::Spring>(startX, endX, 0.5, accel, dampen);

   // 3. Create an animation and add the curve to it. 
   auto animation = std::make_unique<friz::Animation<1>>(
      friz::Animation<1>::SourceList{std::move(curve)}, 0);

   // 4. Add a lambda to the animation that will move the panel
   animation->OnUpdate([=] (int id, const friz::Animation<1>::ValueList& val){
      fControls->setTopLeftPosition(val[0], 0);
   });

   // 5. Add a lambda to call when the effect is complete. 
   animation->OnCompletion([=] (int id) {
      fPanelState = PanelState::kClosed;
   });

   fPanelState = PanelState::kClosing;
   // 6. Add the animation to the animator, starting it.  
   fPanelAnimator.AddAnimation(std::move(animation));

}

Each animation may also be given an optional id value.

This id will be passed as an argument to the Update and Completion functions registered with the animation, and may also be used to cancel an animation that’s in progress.

Sequence

There’s also a separate friz::Sequence class that can hold a series of Animation objects and execute them in order as if they were a single long animation. Here’s an example of EaseIn and EaseOut combined into a single effect:

Inheritance Diagram for the animation types:

About Those lambda Callbacks

You will want to provide at least one (a per-frame callback) and possibly two (an animation complete callback) lambda(s) to each animation you create. The example code in the demo app gives several models for how these functions should be declared and used. If you, like me, have a bit of Unfrozen Caveman Programmer in you, there are tons of references out there on using lambdas in modern C++; my favorite in terms of depth can be found in Meyers’ Effective Modern C++.

The per-frame update callback will always have two arguments:

  • The ID for the animation being performed
  • an std::array of float values for this frame.

Your callback should do whatever it needs to do to implement the effect as quickly as it can—remember that another callback will be coming when the timer elapses again.

The completion callback will have a single argument, the ID of the animation being completed.

Note that if you are combining multiple Animation objects together into a single Sequence, your callbacks will receive updates using the ID assigned to the sequence, not to the individual animation objects.

You should also be very careful to watch the lifetimes of variables carried inside of the lambda objects via capture. It’s tempting to think that "hey, we’re using smart pointers because we’re all modern here so object lifetime is handled for us!"

That’s not always true with constructs like these — make sure that you puzzle out all the ownership and lifetime issues (and test, test, test!). A good pattern to follow is to make sure that the object that owns the Animator object driving an animation also owns any Component objects that will be controlled by an animation, and that in the destructor of that object, you CancelAllAnimations() to prevent stray timer updates from trying to update a component that’s about to be deleted.

The Animator

Your app will need one or more instances of the Animator object. The easiest approach is likely to be having any component that wants to control some aspect of one or more child components own an Animator.

To start an animation, pass it to an Animator, which will take ownership of it and control its lifecycle from there. Ordinarily, the animation will run to its completion and be destroyed.

You may, however, want to explicitly cancel a single animation that’s in progress (or perhaps a group of animations). The Animator exposes two methods to cancel in-progress animations:

bool Animator::CancelAllAnimations(bool moveToEndPosition)

will cancel all animations that are currently executing, optionally advancing them all to their final positions.

bool CancelAnimation(int id, bool moveToEndPosition);

cancels any animations with an id value that matches the argument. Since there’s no requirement that animations use unique id values (or any id at all), you can use this knowledge to group animations together with a shared id value if that is useful to you.

Technical Details

You can grab the demo application that includes the friz code from GitHub at https://github.com/bgporter/animator.

If you have Doxygen installed, you can use the included doxygen/Doxyfile to generate a local copy of HTML documentation that should prove useful.

It’s licensed under the terms of the MIT license—if it’s useful to you, use it as you like. Open source, closed source, commercial, free, whatever, as long as the terms of that license are amenable to you (and perhaps to whoever writes the checks funding your project).

At some point I will package it up as a JUCE user module to make it easier to work with.

Aspirations

Part of me felt a strange obligation to say something about how animation is like a seasoning where a little goes a long way, and about the value and importance of subtlety. Part of me actually believes those things.

A different part of me walks around the show floor at the NAMM show every January, checking out new music software releases and dies a little inside from the sameness of everything. That part of me is hoping that someone will use this and create something that’s way outside the lines.

Make cool things.

Brett g Porter

Brett g Porter

Lead Engineer, Audio+Music at Art+Logic
Lead Engineer, Audio+Music development at Art+Logic. Always looking for excuses to write code. Tweets at both @artandlogic and @bgporter.
Brett g Porter

@bgporter

Music+Software+Music Software+Ice Cream. Relapsing composer/trombonist. Day job @artandlogic. Creator of @tmbotg.
Seeing Kavanaugh back in the news makes me think about my childhood friend ‘Squi'. Little known fact, all US babies… https://t.co/ZlTEsXyACM - 7 hours ago
Brett g Porter
Brett g Porter

Latest posts by Brett g Porter (see all)

Tags:

Creative Commons License

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

Submit a Comment

Your email address will not be published. Required fields are marked *