Anyone using juce::Animator?

Who is playing with juce::Animator?

Here are some thoughts and questions, if so.

  1. How are you checking if an animation is in progress? I have been manually juggling that by adding an isAnimating member variable and updating it on valueChanged and completeCallback. Feels like this should be a part of the API, did I miss something?

  2. How to best deal with debounces? It’s common for a user to click repeatedly, which tends to result in visual chaos unless debounces are handled. I’m just refusing to create a new animation until the old one is still in progress, but there’s situations where animations need to “undo from current position” and so on (be reversible). Example of no debounce in demo:

  1. The API is verbose, especially when you just want to sprinkle some easings around. Are people building wrappers for it?

I’ve only built this very simple one so far. The goal was to have an API with 3 LOC, easy to “sprinkle” into a component:

  • Hold an Animation object
  • Call animation.springTo (nextLocation, 250) where nextLocation is a pixel value and 250 is the duration.
  • Access animation.currentPosition in paint.

The idea is to build a set of Animators that you’ll just reuse as needed. However, this is currently only optimized for only one animation per component (and other assumptions). Works well, but curious what others are doing.

3 Likes

The way CSS handles this is, when you change the target value of a transition/animation, the start value is updated to whatever the current value is.
So if you’re easing linearly from 0 to 100 over 10s, then after 5s update the target value to say 200, it will then take a further 10s to transition from 50 to 200.

How you’d do that with juce::Animator IDK, but that’s how I’d expect an API to behave when you change the target mid-transition.
This is exactly how JIVE works, FWIW:

Screen Recording 2024-10-22 at 21.33.16.mov [video-to-gif output image]

1 Like

No, but I’d be tempted to create some templated types that wrap the animator, the source & target values, etc. so you can just have a Animated<juce::Colour> or whatever

1 Like

Awesome! Somehow didn’t realize JIVE has a full fledged animation api!

if you’re easing linearly from 0 to 100 over 10s, then after 5s update the target value to say 200, it will then take a further 10s to transition from 50 to 200.

Agreed, this is a default I’m also used to…

Back to JUCE — It looks like runningIndefinitely isn’t available on AnimatorSetBuilder — I’m looking to fade something in, followedBy fading something out — and have that loop.

@Attila would this be a difficult addition, or am I missing a way to do that?

It would also really be convenient if followedBy and friends were methods on juce::ValueAnimatorBuilder so one could chain them like so (the set wrapping would be under the hood).

const auto pulseIn = defaultAnimator (1, durationMs).withEasing (juce::Easings::createEaseOut());
const auto pulseOut = defaultAnimator (0, durationMs).withEasing (juce::Easings::createEaseOut());
animator = pulseIn.followedBy (pulseOut).runningIndefinitely().build();

I have a related question which I wanted to ask about. If I remember correctly, why you set runningIndefinitely as true, the float value in the callback ramps to 1 in the allotted time and then stays there. How would you get around this, like if you wanted to pulse the brightness of something up and down?

The callback value is only determined by the easing function and can go beyond 1.0. From the ValueChangedCallback doc:

[…] unless the Animator is infinitely running, in which case the progress will go beyond 1.0 […]

You could create a pulsing effect by supplying an easing function e.g. [] (auto progress) { return std::sin (progress); } and use it with an infinitely running animator.

I’m still thinking about the solution for runningInfinitely with AnimatorSetBuilder, and starting to think you make a good point for adding it.

As for followedBy how do you feel about

using ASB = AnimatorSetBuilder;
animator = ASB { pulseIn }.followedBy (pulseOut).runningIndefinitely().build();

At this point I’d gravitate towards keeping the API clean with functions separated unless it’s a major inconvenience.

Edit: :man_facepalming: Oh, right, there’s no runningInfinitely on ASB. I see your point.

This is pretty clean!

Yeah, not sure what to do without runningIndefinitely on SetBuilder. For my specific simple case, 2 separate Animators that trigger each other onComplete seemed like a possibility, but would remove some composability…

My plan was to have 2 Animators in a Set, one that eased the value from 0 to 1 and one that eased the value from 1 to 0.

The problem with this route is it eliminates being able to any of the built-in and classic cubic easings. Chaining 2 Animators in a Set seems more friendly. Adding a bool reverse to start () would also open up some options.

You could also combine easings.

[ramp = createOnOffRamp(), cubic = createEaseIn()] (auto p)
{
    const auto periodicZeroToOne = p - std::floor (p);
    const auto onOff = ramp (periodicZeroToOne);
    return cubic (onOff);
}
1 Like

Ah yikes, somehow I totally missed this! I’ll give that a go, thanks!

You’re absolutely right, sorry for the stupid question.

Just dropping a note to say this got me 90% of the way there — the limitation is that the same easing has to be used for both fade in and out.

My ideal path would still be 2 Animators in a Set (assuming runningIdefinitely can get added to Set) — that would let me easily tune timing/easings, make animations symmetrical, etc.

Here’s something else I’m trying to figure out:

I want 3 elements to have the exact same animation, but I want them staggered by 100ms.

Something like gsap’s stagger.

I thought maybe I could supply a delay to AnimatorSetBuilder::togetherWith (double delayMs) but actually I’m confused because that constructor doesn’t actually take an animator.

Example use case is animating 3 rects into existence:

for (auto i = 0; i < rects.size(); ++i)
{
    g.fillRect (rects[i].withWidth (animation.currentPosition * width));
}

I’ve been building things like the above with a wrapper (handles the boilerplate, only exposes the eased value to my component) — I was thinking I’d create a wrapper function like .stagger (numberOfElements, msToStagger) function or something that spawns n animators in a set and provides n currentPositions to my component… but not quite sure how to wrangle AnimationSetBuilder

In short, is there a way to do this with Set, or is the only way to have 3 separate animations that I manually start based on the previous animator’s value?

Hello folks! Not sure how to answer the above questions, but thought I’d share what I’m currently trying to do with these resources.

The plugin I’m working on shows a sort of map, you could say, and a section of it is lit up. The lit section can be moved by the user, and I’m shifting the map to follow the highlighted section around. Doing it naively felt extremely jerky, so the easings are super helpful here.

The first problem I’m running into is the debounce thing you mentioned Sudara. I’ve set up my animator so that it reacts the way ImJimmi described above if interrupted and restarted. Which would work well enough if linear, but I’m using createEaseInOut(), which looks much better generally for my purposes. But if interrupted by user input, the easing obviously resets, which feels really weird.

I’m wondering if it may be possible to solve it using createCubicBezier() instead, starting with the InOut numbers. Then before calling start() while already running, set the values to the Bezier function so the current rate of change is maintained, and ease out (or maybe in out) from there). I’m not sure what values those should be though. Anyone has any ideas I’m all ears.

I build a wrapper around VBankAnimatorUpdater which other people might find useful. I was trying to solve two problems:

  1. The problem of multiple animations (i.e. debouncing).
  2. Lifetime issues for juce::Animator (you have to keep them alive somewhere after you have added them, and I wanted a general solution for this).

There are three mehods which offer different options for animator sequences:

  • runAnimatorInSeries(): wait for the last Animator to finish before starting this one
  • runAnimatorInParallel() : run this animator now, regardless of what other animators are doing
  • runBlockingAnimator() : run this animator only if the last one has finished.

All of these methods keep the animators alive, and the last one solves the debouncing issue mentioned above. Note that these three methods do not interact with each other (i.e. runBlockingAnimator() won’t check to see if parallel or series animations are running, etc.). It’s really just three separate systems combined in one class.

AnimatorQueue.h

#pragma once

#include <JuceHeader.h>
#include <list>
#include <vector>

class AnimatorQueue
{
public:
    using Ptr = juce::WeakReference<AnimatorQueue>;

    AnimatorQueue(juce::Component&);

    void runAnimatorInSeries(const juce::Animator);
    void runAnimatorInParallel(const juce::Animator);
    bool runBlockingAnimator(const juce::Animator);

private:
    void triggerNextSeries();
    void startFirstSeries();
    void decrementParallel();

    juce::VBlankAnimatorUpdater updater;
    std::list<juce::Animator> seriesAnimators;
    std::vector<juce::Animator> parallelAnimators;
    std::unique_ptr<juce::Animator> blockingAnimator;
    bool seriesIsRunning = false;
    int parallelCount = 0;

    JUCE_DECLARE_WEAK_REFERENCEABLE(AnimatorQueue)
};

AnimatorQueue.cpp

#include "AnimatorQueue.h"

AnimatorQueue::AnimatorQueue(juce::Component& c) : updater(&c)
{
}

void AnimatorQueue::runAnimatorInSeries(const juce::Animator b)
{
    JUCE_ASSERT_MESSAGE_THREAD;

    seriesAnimators.push_back(juce::AnimatorSetBuilder(0)
        .followedBy(b)
        .followedBy([ths = Ptr(this)]()
            {
                if (ths) ths->triggerNextSeries();
            })
        .build());

    if (!seriesIsRunning)
        startFirstSeries();
}

void AnimatorQueue::runAnimatorInParallel(juce::Animator a)
{
    JUCE_ASSERT_MESSAGE_THREAD;

    ++parallelCount;

    auto set = juce::AnimatorSetBuilder(0)
        .followedBy(a)
        .followedBy([ths = Ptr(this)]()
            {
                if (ths)
                    ths->decrementParallel();
            })
        .build();

    updater.addAnimator(set);
    parallelAnimators.push_back(set);
    set.start();
}

void AnimatorQueue::triggerNextSeries()
{
    seriesAnimators.pop_front();

    if (!seriesAnimators.empty())
        startFirstSeries();
    else
        seriesIsRunning = false;
}

bool AnimatorQueue::runBlockingAnimator(const juce::Animator a)
{
    JUCE_ASSERT_MESSAGE_THREAD;

    if (blockingAnimator) return false;

    blockingAnimator = std::make_unique<juce::Animator>(juce::AnimatorSetBuilder(0)
        .followedBy(a)
        .followedBy([ths = Ptr(this)]()
            {
                if (ths) ths->blockingAnimator.reset();
            })
        .build());

    updater.addAnimator(*blockingAnimator);
    blockingAnimator->start();

    return true;
}

void AnimatorQueue::startFirstSeries()
{
    jassert(!seriesAnimators.empty());
    seriesIsRunning = true;
    updater.addAnimator(seriesAnimators.front());
    seriesAnimators.front().start();
}

void AnimatorQueue::decrementParallel()
{
    --parallelCount;

    if (parallelCount <= 0)
        parallelAnimators.clear();
}
2 Likes