template<class T, class K = T>
Magnum::Animation::Player class

Animation player.

Template parameters
T Time type
K Key type

Provides a generic way for querying interpolated results from multiple animation tracks of distinct types from a single place, together with managing the animation running state.

Similarly to Track / TrackView, the player is also partially statless — in particular, it neither accesses any global timer or keeps any notion of "current time". Instead, all time-dependent functions take absolute time as a parameter. This both simplifies the internal state management and adds additional flexibility on user side.

Setting up

The Player class is used by adding tracks to it and specifying what should be done with interpolation result values. The simplest option is specifying a destination location when adding the track using add() — that'll mean you get a fresh set of animated values at your disposal after every iteration:

const Animation::TrackView<Float, Vector3> translation;
const Animation::TrackView<Float, Quaternion> rotation;
const Animation::TrackView<Float, Vector3> scaling;

Vector3 objectScaling;
Quaternion objectRotation;
Vector3 objectTranslation;

Animation::Player<Float> player;
player.add(scaling, objectScaling)
      .add(rotation, objectRotation)
      .add(translation, objectTranslation);

The Player stores just TrackView instances, for every Track instance you have to ensure that it stays alive for the whole lifetime of the player instance.

In case you need to apply the animated values using a setter, it's possible to fire a callback every iteration. Note that the addWithCallback() function has also a typeless version taking just void* user pointer instead of a reference to a concrete type. Below is an example of animating SceneGraph object transformations using the SceneGraph::TranslationRotationScalingTransformation3D transformation implementation:

Object3D* object;

Animation::Player<Float> player;
player.addWithCallback(scaling,
    [](const Float&, const Vector3& scaling, Object3D& object) {
        object.setScaling(scaling);
    }, *object);
player.addWithCallback(rotation,
    [](const Float&, const Quaternion& rotation, Object3D& object) {
        object.setRotation(rotation);
    }, *object);
player.addWithCallback(translation,
    [](const Float&, const Vector3& translation, Object3D& object) {
        object.setTranslation(translation);
    }, *object);

The addWithCallbackOnChange() variant will fire the callback only if the interpolated value changes, which is useful for triggering other events. See below for an example. Lastly, there is addRawCallback() that allows for greater control and further performance optimizations. See its documentation for a usage example code snippet.

The animation is implicitly played only once, use setPlayCount() to set a number of repeats or make it repeat indefinitely. By default, the duration() of an animation is calculated implicitly from all added tracks. You can use setDuration() to specify a custom duration:

  • If it extends beyond the keyframe values, values of begin/end keyframes will be extrapolated according to Extrapolation specified for every track.
  • If it will be shorter, only a slice of the animation will be played.
  • If duration size is empty (min and and max se to the same value) and setPlayCount() is set to inifite, then the animator will indefinitely give out value from a key that's at the start of the duration. If play count is finite, the animation will get stopped right away.

Animation playback

By default, the player is in a State::Stopped state. Call play() with a time value denoting the moment at which the animation should start. After that, the advance() function is meant to be called every frame with a current time value. As long as the animation is playing, the advance() function will update track result destination locations with interpolated values and/or fire user-defined callbacks described above.

Once the animation playback is finished (exhausing the whole duration() of all playCount() iterations), the advance() will update the destination locations and/or fire user-defined callbacks with values that correspond to duration() end time. This is guaranteed to be always the case in order to correctly "park" the animations — even if your app would freeze for a while and advance() would get called later, the result values will never be calculated from a key value that's outside duration().

Calling stop() immediately transfers state() to State::Stopped and the next advance() iteration will give out interpolated values corresponding to the begin time of duration(), again to "park" the animation back to its initial state. After that, no more updates are done until the animation is started again. Compared to when the animation stops by itself, this will park it at the beginning, not at the end.

Calling pause() while the animation is running immediately transfers the animation state to State::Paused and the next advance() iteration will give out interpolated values corresponding to a time that was passed to the pause() function. After that, no more updates are done until the animation is resumed again with play() or stopped with stop().

The callbacks are only ever fired from within the advance() function, never from pause(), stop() or any other API.

For managing global application you can use Timeline, std::chrono APIs or any other type that supports basic arithmetic. The time doesn't have to be monotonic or have constant speed, but note that non-continuous and backward time jumps may have worse performance than going monotonically forward. See below for more information about using different time types.

Animation::Player<Float> player;
Timeline timeline;

// during initialization
timeline.start();
player.play(timeline.previousFrameTime());

// every frame
player.advance(timeline.previousFrameTime());

Using custom time/key types

In long-running apps it's not desirable to use Float for global application time, since its precision will deteriorate over time. Even after one hour the precision loss might start to get noticeable. To overcome this problem, it's possible to specify a type for time values that's different from type used for animation track keys. In contrast, using Float for animation track key values is usually good enough, as the tracks are never too long for this to become a problem — and if the tracks are long, you can always use a different key type for them as well. A good choice is std::chrono::nanoseconds as a time type and keeping track key values as Float seconds:

Animation::Player<std::chrono::nanoseconds, Float> player;
// add tracks…

// start the animation
player.play(std::chrono::system_clock::now().time_since_epoch());

// call every frame
player.advance(std::chrono::system_clock::now().time_since_epoch());

While there's a builtin support for the above, you are free to use any other type combination — for that you need to provide a scaler function that will take care of converting a time difference to play iteration index and key value inside given iteration. The types should be implicitly constructible, and have basic arithmetic and comparison operators. In order to reduce header size, the Player implementation is in a separate Player.hpp file that you need to include to get all needed template function definitions. See also Template headers and implementation files for more information.

#include "Magnum/Animation/Player.hpp"

// …

/* 64-bit integer global time (microseconds), 16-bit frame counter with 24 FPS */
Animation::Player<UnsignedLong, UnsignedShort> player{
    [](UnsignedLong time, UnsignedShort duration) {
        /* One frame is 1/24 second */
        const UnsignedLong durationNs = UnsignedLong(duration)*1000000/24;
        const UnsignedInt playCount = time/durationNs;
        const UnsignedShort factor = (time - playCount*durationNs)*24/1000000;
        return std::make_pair(playCount, factor);
    }};

Higher-order players, animating time

Sometimes you might want to control multiple players at the same time or animate player state. That's doable by creating specialized tracks that control given player via a state change callback. By adding more tracks you can control multiple players from a central location.

struct Data {
    Animation::Player<Float> player; // player we want to control
    Timeline timeline;
} data;

Animation::Track<Float, Animation::State> stateTrack{{
    {3.0f, Animation::State::Playing},
    {3.0f, Animation::State::Paused},
    {3.5f, Animation::State::Playing},
    {5.0f, Animation::State::Stopped}
}, Math::select};
Animation::State state;

Animation::Player<Float> controller;
controller.addWithCallbackOnChange(stateTrack,
    [](const Float&, const Animation::State& state, Data& data) {
        data.player.setState(state, data.timeline.previousFrameTime());
    }, state, data);

Besides state, you can also animate setDuration() and setPlayCount(), but be aware that setting those while the animation is playing might cause unwanted jumps and abrupt stops. Time is also completely in your control and you can employ another Player instance to speed it up or slow it down for a particular animation:

Animation::Player<Float> player; // player we want to control

Animation::Track<Float, Float> timeTrack{{
    {0.0f, 0.0f}, /* Start normal */
    {1.0f, 1.0f}, /* Then speed up */
    {2.0f, 3.0f}, /* Pause for a bit */
    {5.0f, 3.0f}, /* And normal again */
    {6.0f, 4.0f}
}, Animation::Interpolation::Linear};

Animation::Player<Float> timer;
timer.addWithCallback(timeTrack,
    [](const Float&, const Float& time, Animation::Player<Float>& player) {
        player.advance(time);
    }, player);

/* Calls player.advance() with the animated time */
timer.advance(timeline.previousFrameTime());

Explicit template specializations

The following specializations are explicitly compiled into the Animation library. For other specializations (e.g. using an integer key type) you have to use the Player.hpp implementation file to avoid linker errors. See also Template headers and implementation files for more information.

Public types

using TimeType = T
Time type.
using KeyType = K
Key type.
using Scaler = std::pair<UnsignedInt, K>(*)(T, K)
Scaler function type.

Constructors, destructors, conversion operators

Player() explicit
Constructor.
Player(Scaler scaler) explicit
Construct with a custom scaler function.
Player(const Player<T, K>&) deleted
Copying is not allowed.
Player(Player<T, K>&&)
Move constructor.

Public functions

auto operator=(const Player<T, K>&) -> Player<T, K>& deleted
Copying is not allowed.
auto operator=(Player<T, K>&&) -> Player<T, K>&
Move assignment.
auto scaler() const -> Scaler
Time-to-key scaler.
auto duration() const -> Math::Range1D<K>
Duration.
auto setDuration(const Math::Range1D<K>& duration) -> Player<T, K>&
Set duration.
auto playCount() const -> UnsignedInt
Play count.
auto setPlayCount(UnsignedInt count) -> Player<T, K>&
Set play count.
auto isEmpty() const -> bool
Whether the player is empty.
auto size() const -> std::size_t
Count of tracks managed by this player.
auto track(std::size_t i) const -> const TrackViewStorage<K>&
Track at given position.
template<class V, class R>
auto add(const TrackView<K, V, R>& track, R& destination) -> Player<T, K>&
Add a track with a result destination.
template<class V, class R>
auto add(const Track<K, V, R>& track, R& destination) -> Player<T, K>&
template<class V, class R>
auto addWithCallback(const TrackView<K, V, R>& track, void(*)(const K&, const R&, void*) callback, void* userData = nullptr) -> Player<T, K>&
Add a track with a result callback.
template<class V, class R>
auto addWithCallback(const Track<K, V, R>& track, void(*)(const K&, const R&, void*) callback, void* userData = nullptr) -> Player<T, K>&
template<class V, class R, class U>
auto addWithCallback(const TrackView<K, V, R>& track, void(*)(const K&, const R&, U&) callback, U& userData) -> Player<T, K>&
Add a track with a result callback.
template<class V, class R, class U>
auto addWithCallback(const Track<K, V, R>& track, void(*)(const K&, const R&, U&) callback, U& userData) -> Player<T, K>&
template<class V, class R>
auto addWithCallbackOnChange(const TrackView<K, V, R>& track, void(*)(const K&, const R&, void*) callback, R& destination, void* userData = nullptr) -> Player<T, K>&
Add a track with a result callback that's called on change.
template<class V, class R>
auto addWithCallbackOnChange(const Track<K, V, R>& track, void(*)(const K&, const R&, void*) callback, R& destination, void* userData = nullptr) -> Player<T, K>&
template<class V, class R, class U>
auto addWithCallbackOnChange(const TrackView<K, V, R>& track, void(*)(const K&, const R&, void*) callback, R& destination, U& userData) -> Player<T, K>&
Add a track with a result callback that's called on change.
template<class V, class R, class U>
auto addWithCallbackOnChange(const Track<K, V, R>& track, void(*)(const K&, const R&, void*) callback, R& destination, U& userData) -> Player<T, K>&
template<class V, class R, class Callback>
auto addRawCallback(const TrackView<K, V, R>& track, void(*)(const TrackViewStorage<K>&, K, std::size_t&, void*, void(*)(), void*) callback, void* destination, void(*)() userCallback, void* userData) -> Player<T, K>&
Add a track with a raw callback.
template<class V, class R>
auto addRawCallback(const Track<K, V, R>& track, void(*)(const TrackViewStorage<K>&, K, std::size_t&, void*, void(*)(), void*) callback, void* destination, void(*)() userCallback, void* userData) -> Player<T, K>&
auto state() const -> State
State.
auto elapsed(T time) const -> std::pair<UnsignedInt, K>
Elapsed animation iteration and keyframe.
auto play(T startTime) -> Player<T, K>&
Play.
auto pause(T pauseTime) -> Player<T, K>&
Pause.
auto stop() -> Player<T, K>&
Stop.
auto setState(State state, T time) -> Player<T, K>&
Set state.
auto advance(T time) -> Player<T, K>&
Advance the animation.

Typedef documentation

template<class T, class K>
typedef std::pair<UnsignedInt, K>(*Magnum::Animation::Player<T, K>::Scaler)(T, K)

Scaler function type.

The function gets time from when the animation started and combined duration of all tracks; returns play iteration index and key value inside given iteration. The combined duration is guaranteed to be always non-zero, zero durations are handled by the player itself.

Function documentation

template<class T, class K>
Magnum::Animation::Player<T, K>::Player(Scaler scaler) explicit

Construct with a custom scaler function.

Parameters
scaler Scaler function

template<class T, class K>
Math::Range1D<K> Magnum::Animation::Player<T, K>::duration() const

Duration.

If the duration was not set explicitly using setDuration(), returns value calculated implicitly from all added tracks. If no tracks are added, returns default-constructed value.

template<class T, class K>
Player<T, K>& Magnum::Animation::Player<T, K>::setDuration(const Math::Range1D<K>& duration)

Set duration.

The duration is initially a default-constructed value, then calculated implicitly from added tracks. Setting it explicitly will overwrite the implicitly calculated value. Adding a track after the duration was set explicitly will extend the duration to span all track durations.

Setting a duration that extends beyond the keyframe values will cause values of begin/end keyframes to be extrapolated according to Extrapolation specified for given track. Setting a shorter duration will cause only a slice of all tracks to be played.

Modifying this value while state() is State::Playing may cause the animation to jump or abruptly stop after next call to advance().

template<class T, class K>
Player<T, K>& Magnum::Animation::Player<T, K>::setPlayCount(UnsignedInt count)

Set play count.

By default, play count is set to 1, meaning the animation duration() is played once. Value of 0 means the animation is repeated indefinitely.

Modifying this value while state() is State::Playing may cause the animation to jump or abruptly stop after next call to advance().

template<class T, class K>
bool Magnum::Animation::Player<T, K>::isEmpty() const

Whether the player is empty.

template<class T, class K>
std::size_t Magnum::Animation::Player<T, K>::size() const

Count of tracks managed by this player.

template<class T, class K>
const TrackViewStorage<K>& Magnum::Animation::Player<T, K>::track(std::size_t i) const

Track at given position.

Due to the type-erased nature of the player implementation, it's not possible to know the exact track type.

template<class T, class K> template<class V, class R>
Player<T, K>& Magnum::Animation::Player<T, K>::add(const TrackView<K, V, R>& track, R& destination)

Add a track with a result destination.

The destination is updated with new value after each call to advance() as long as the animation is playing.

template<class T, class K> template<class V, class R>
Player<T, K>& Magnum::Animation::Player<T, K>::add(const Track<K, V, R>& track, R& destination)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

Note that track ownership is not transferred to the Player and you have to ensure that it's kept in scope for the whole lifetime of the Player instance.

template<class T, class K> template<class V, class R>
Player<T, K>& Magnum::Animation::Player<T, K>::addWithCallback(const TrackView<K, V, R>& track, void(*)(const K&, const R&, void*) callback, void* userData = nullptr)

Add a track with a result callback.

The callback is called with current key value, interpolated result value and the userData pointer after each call to advance() as long as the animation is playing. The key value is guaranteed to never be outside of the duration() ranage, with the interpolated result always corresponding to that key value.

See the overload below for a more convenient type-safe way to pass user data.

template<class T, class K> template<class V, class R>
Player<T, K>& Magnum::Animation::Player<T, K>::addWithCallback(const Track<K, V, R>& track, void(*)(const K&, const R&, void*) callback, void* userData = nullptr)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

Note that the track ownership is not transferred to the Player and you have to ensure that it's kept in scope for the whole lifetime of the Player instance.

template<class T, class K> template<class V, class R, class U>
Player<T, K>& Magnum::Animation::Player<T, K>::addWithCallback(const TrackView<K, V, R>& track, void(*)(const K&, const R&, U&) callback, U& userData)

Add a track with a result callback.

Equivalent to calling the above with a lambda wrapper that casts void* back to T* and dereferences it in order to pass it to callback. There is no additional overhead compared to the overload taking the void* pointer, however see addRawCallback() for optimization possibilities.

template<class T, class K> template<class V, class R, class U>
Player<T, K>& Magnum::Animation::Player<T, K>::addWithCallback(const Track<K, V, R>& track, void(*)(const K&, const R&, U&) callback, U& userData)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

Note that the track ownership is not transferred to the Player and you have to ensure that it's kept in scope for the whole lifetime of the Player instance.

template<class T, class K> template<class V, class R>
Player<T, K>& Magnum::Animation::Player<T, K>::addWithCallbackOnChange(const TrackView<K, V, R>& track, void(*)(const K&, const R&, void*) callback, R& destination, void* userData = nullptr)

Add a track with a result callback that's called on change.

A combination of add() and addWithCallback() — during each call to advance(), as long as the animation is playing, the new value is compared to destination. If the new value is different from the stored one, callback is called and destination is updated. Note that in order to keep the memory management inside the player class simple, the value can't be cached inside and you are required to provide the destination location.

See the overload below for a more convenient type-safe way to pass user data.

template<class T, class K> template<class V, class R>
Player<T, K>& Magnum::Animation::Player<T, K>::addWithCallbackOnChange(const Track<K, V, R>& track, void(*)(const K&, const R&, void*) callback, R& destination, void* userData = nullptr)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

Note that the track ownership is not transferred to the Player and you have to ensure that it's kept in scope for the whole lifetime of the Player instance.

template<class T, class K> template<class V, class R, class U>
Player<T, K>& Magnum::Animation::Player<T, K>::addWithCallbackOnChange(const TrackView<K, V, R>& track, void(*)(const K&, const R&, void*) callback, R& destination, U& userData)

Add a track with a result callback that's called on change.

Equivalent to calling the above with a lambda wrapper that casts void* back to T* and dereferences it in order to pass it to callback. There is no additional overhead compared to the overload taking the void* pointer, however see addRawCallback() for optimization possibilities.

template<class T, class K> template<class V, class R, class U>
Player<T, K>& Magnum::Animation::Player<T, K>::addWithCallbackOnChange(const Track<K, V, R>& track, void(*)(const K&, const R&, void*) callback, R& destination, U& userData)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

Note that the track ownership is not transferred to the Player and you have to ensure that it's kept in scope for the whole lifetime of the Player instance.

template<class T, class K> template<class V, class R, class Callback>
Player<T, K>& Magnum::Animation::Player<T, K>::addRawCallback(const TrackView<K, V, R>& track, void(*)(const TrackViewStorage<K>&, K, std::size_t&, void*, void(*)(), void*) callback, void* destination, void(*)() userCallback, void* userData)

Add a track with a raw callback.

This is a low-level function meant to be used if you want to avoid the extra overhead of an additional callback in addWithCallback() or addWithCallbackOnChange(), want more flexibility in the user callback or want to control the track interpolation directly — for example taking advantage of TrackView::atStrict() or passing an inlineable interpolator function instead of using the saved interpolator function pointer.

The callback takes the raw TrackViewStorage reference (which you need to cast to a correct type), the interpolated key and hint that's meant to be passed to TrackView::at(), the destination pointer (equivalent to the one passed to add()), user callback pointer (which again needs to be cast to a correct type) and user data pointer. The following code snippet shows implementation of the addWithCallbackOnChange() API using this function, using a custom callback to add a value to a vector if it changes:

Animation::Track<Float, Int> track;

Int result;
std::vector<Int> data;
auto callback = [](std::vector<Int>& data, Int value) {
    data.push_back(value);
};

Animation::Player<Float> player;
player.addRawCallback(track,
    [](const Animation::TrackViewStorage<Float>& track, Float key,
    std::size_t& hint, void* destination, void(*callback)(), void* userData) {
        Int value = static_cast<const Animation::TrackView<Float, Int>&>(track)
            .atStrict(key, hint);
        if(value == *static_cast<Int*>(destination)) return;
        *static_cast<Int*>(destination) = value;
        reinterpret_cast<void(*)(std::vector<Int>&, Int)>(callback)
            (*static_cast<std::vector<Int>*>(userData), value);
    }, &result, reinterpret_cast<void(*)()>(+callback), &data);

template<class T, class K> template<class V, class R>
Player<T, K>& Magnum::Animation::Player<T, K>::addRawCallback(const Track<K, V, R>& track, void(*)(const TrackViewStorage<K>&, K, std::size_t&, void*, void(*)(), void*) callback, void* destination, void(*)() userCallback, void* userData)

This is an overloaded member function, provided for convenience. It differs from the above function only in what argument(s) it accepts.

Note that the track ownership is not transferred to the Player and you have to ensure that it's kept in scope for the whole lifetime of the Player instance.

template<class T, class K>
State Magnum::Animation::Player<T, K>::state() const

State.

The player is State::Stopped by default.

template<class T, class K>
std::pair<UnsignedInt, K> Magnum::Animation::Player<T, K>::elapsed(T time) const

Elapsed animation iteration and keyframe.

Returns repeat iteration index and elapsed animation keyframe in given iteration corresponding to time. If state() is State::Stopped and the player was stopped explicitly, the function returns a default-constructed value (usually {0, 0.0f}). If state() is State::Stopped due to the animation running out, the function returns the iteration count and duration end keyframe. If state() is State::Paused, the function returns a time at which the animation was paused.

Unlike advance(), this function doesn't modify the animation state in any way, it's merely a query.

template<class T, class K>
Player<T, K>& Magnum::Animation::Player<T, K>::play(T startTime)

Play.

Starts playing all tracks added to the player at given startTime. If state() is already State::Playing, the animation is restarted from the beginning at startTime. If state() is State::Paused, the animation continues from the time that was passed to pause().

If startTime is in the future (that is, time passed to the next advance() iteration will be less than startTime), advance() will do nothing until given point in the future. Setting time to such a particular value can be used to synchronize playback of multiple independent animation clips.

template<class T, class K>
Player<T, K>& Magnum::Animation::Player<T, K>::pause(T pauseTime)

Pause.

Pauses the currently playing animation at given pauseTime. If state() is not State::Playing, the function does nothing. See advance() for a detailed description of behavior when the animation gets paused.

template<class T, class K>
Player<T, K>& Magnum::Animation::Player<T, K>::stop()

Stop.

Stops the currently playing animation. If state() is State::Paused, discard the pause information. If state() is already State::Stopped, the function does nothing. See advance() for a detailed description of behavior when the animation gets stopped.

template<class T, class K>
Player<T, K>& Magnum::Animation::Player<T, K>::setState(State state, T time)

Set state.

Convenience function that calls play(), pause() or stop() based on state. See documentation of these functions for detailed description. The time parameter is used only when state is State::Playing or State::Paused, it's ignored for State::Stopped.

template<class T, class K>
Player<T, K>& Magnum::Animation::Player<T, K>::advance(T time)

Advance the animation.

As long as state() is State::Playing, goes through all tracks added with add(), addWithCallback() or addWithCallbackOnChange() in order they were added and updates the destination locations and/or fires the callbacks with interpolation results.

If state() is State::Paused or State::Stopped, the function does nothing. If time is less than time that was passed to play(), the function does nothing. If time is large enough that duration() times playCount() got exhausted, the function will update destination locations and/or fire user-defined callback with key and result values corresponding to the end time of duration() in order to correctly "park" the animation. The state then becomes State::Stopped and no more updates are done until the animation is started again.

If pause() was called right before a particular advance() iteration, the function will update destination locations and/or fire user-defined callbacks with key and result values corresponding to the time passed to the pause() call before to correctly "park" the animation. After that, no more updates are done until the animation is started again.

If stop() was called right before a particular advance() iteration, the function will update destination locations and/or fire user-defined callbacks with key and result values corresponding to the begin time of duration() to correctly "park" the animation back to its initial state. After that, no more updates are done until the animation is started again.