Browse Source

Animation: implementation of the Player class.

pull/191/head
Vladimír Vondruš 8 years ago
parent
commit
2adc4e8f59
  1. 1
      doc/snippets/CMakeLists.txt
  2. 47
      doc/snippets/MagnumAnimation-custom.cpp
  3. 133
      doc/snippets/MagnumAnimation.cpp
  4. 2
      src/Magnum/Animation/Animation.h
  5. 2
      src/Magnum/Animation/CMakeLists.txt
  6. 57
      src/Magnum/Animation/Player.cpp
  7. 653
      src/Magnum/Animation/Player.h
  8. 188
      src/Magnum/Animation/Player.hpp
  9. 72
      src/Magnum/Animation/Test/Benchmark.cpp
  10. 4
      src/Magnum/Animation/Test/CMakeLists.txt
  11. 80
      src/Magnum/Animation/Test/PlayerCustomTest.cpp
  12. 774
      src/Magnum/Animation/Test/PlayerTest.cpp
  13. 3
      src/Magnum/Animation/Track.h
  14. 1
      src/Magnum/CMakeLists.txt

1
doc/snippets/CMakeLists.txt

@ -41,6 +41,7 @@ endif()
add_library(snippets-Magnum STATIC
Magnum.cpp
MagnumAnimation.cpp
MagnumAnimation-custom.cpp
MagnumMath.cpp)
target_link_libraries(snippets-Magnum PRIVATE Magnum)
set_target_properties(snippets-Magnum PROPERTIES FOLDER "Magnum/doc/snippets")

47
doc/snippets/MagnumAnimation-custom.cpp

@ -0,0 +1,47 @@
/*
This file is part of Magnum.
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018
Vladimír Vondruš <mosra@centrum.cz>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#include "Magnum/Magnum.h"
using namespace Magnum;
#ifndef CORRADE_TARGET_EMSCRIPTEN
/* [Player-usage-custom] */
#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);
}};
/* [Player-usage-custom] */
/* WARNING: Keep the above in sync with PlayerCustomTest */
#endif

133
doc/snippets/MagnumAnimation.cpp

@ -23,13 +23,144 @@
DEALINGS IN THE SOFTWARE.
*/
#include "Magnum/Timeline.h"
#include "Magnum/Math/Quaternion.h"
#include "Magnum/Animation/Track.h"
#include "Magnum/Animation/Player.h"
using namespace Magnum;
using namespace Magnum::Math::Literals;
int main() {
{
/* [Player-usage] */
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);
/* [Player-usage] */
}
{
const Animation::TrackView<Float, Vector3> translation;
const Animation::TrackView<Float, Quaternion> rotation;
const Animation::TrackView<Float, Vector3> scaling;
struct Object3D {
Object3D& setTranslation(const Vector3&) { return *this; }
Object3D& setRotation(const Quaternion&) { return *this; }
Object3D& setScaling(const Vector3&) { return *this; }
};
#ifdef __clang__
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wuninitialized"
#elif defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
#endif
/* [Player-usage-callback] */
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);
/* [Player-usage-callback] */
#if defined(__clang__) || defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
}
{
/* [Player-usage-playback] */
Animation::Player<Float> player;
Timeline timeline;
// during initialization
timeline.start();
player.play(timeline.previousFrameTime());
// every frame
player.advance(timeline.previousFrameTime());
/* [Player-usage-playback] */
}
{
/* [Player-usage-chrono] */
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());
/* [Player-usage-chrono] */
}
{
/* [Player-higher-order] */
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);
/* [Player-higher-order] */
}
{
Timeline timeline;
/* [Player-higher-order-animated-time] */
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());
/* [Player-higher-order-animated-time] */
}
{
/* [Track-usage] */
const Animation::Track<Float, Vector2> jump{{

2
src/Magnum/Animation/Animation.h

@ -43,6 +43,8 @@ template<class V> using ResultOf = typename Implementation::ResultTraits<V>::Typ
enum class Interpolation: UnsignedByte;
enum class Extrapolation: UnsignedByte;
template<class T, class K = T> class Player;
template<class K, class V, class R = ResultOf<V>> class Track;
template<class K> class TrackViewStorage;
template<class K, class V, class R = ResultOf<V>> class TrackView;

2
src/Magnum/Animation/CMakeLists.txt

@ -26,6 +26,8 @@
set(MagnumAnimation_HEADERS
Animation.h
Interpolation.h
Player.h
Player.hpp
Track.h)
# Force IDEs to display all header files in project view

57
src/Magnum/Animation/Player.cpp

@ -0,0 +1,57 @@
/*
This file is part of Magnum.
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018
Vladimír Vondruš <mosra@centrum.cz>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#include "Player.hpp"
namespace Magnum { namespace Animation {
Debug& operator<<(Debug& debug, const State value) {
switch(value) {
/* LCOV_EXCL_START */
#define _c(value) case State::value: return debug << "Animation::State::" #value;
_c(Stopped)
_c(Playing)
_c(Paused)
#undef _c
/* LCOV_EXCL_STOP */
}
return debug << "Animation::State(" << Debug::nospace << reinterpret_cast<void*>(UnsignedByte(value)) << Debug::nospace << ")";
}
/* On non-MinGW Windows the instantiations are already marked with extern
template */
#if !defined(CORRADE_TARGET_WINDOWS) || defined(__MINGW32__)
#define MAGNUM_EXPORT_HPP MAGNUM_EXPORT
#else
#define MAGNUM_EXPORT_HPP
#endif
#ifndef DOXYGEN_GENERATING_OUTPUT
template class MAGNUM_EXPORT_HPP Player<Float, Float>;
template class MAGNUM_EXPORT_HPP Player<std::chrono::nanoseconds, Float>;
#endif
}}

653
src/Magnum/Animation/Player.h

@ -0,0 +1,653 @@
#ifndef Magnum_Animation_Player_h
#define Magnum_Animation_Player_h
/*
This file is part of Magnum.
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018
Vladimír Vondruš <mosra@centrum.cz>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
/** @file
* @brief Class @ref Magnum::Animation::Player, enum @ref Magnum::Animation::State
*/
#include <chrono>
#include <vector>
#include "Magnum/Animation/Track.h"
#include "Magnum/Math/Range.h"
namespace Magnum { namespace Animation {
/**
@brief Player state
@see @ref Player
@experimental
*/
enum class State: UnsignedByte {
/**
* The animation clip is currently playing. Setting the state to
* @ref State::Playing does nothing.
*/
Playing,
/**
* The animation clip is currently paused. Setting the state to
* @ref State::Playing starts playing from where it left, setting the state
* to @ref State::Stopped stops the animation, setting the state to
* @ref State::Paused does nothing.
*/
Paused,
/**
* The animation clip is currently stopped. Setting the state to
* @ref State::Playing starts playing from the beginning, attempting to set
* the state to @ref State::Paused will retain the @ref State::Stopped
* state, setting the state to @ref State::Stopped does nothing.
*/
Stopped
};
/** @debugoperatorenum{State} */
MAGNUM_EXPORT Debug& operator<<(Debug& debug, State value);
namespace Implementation {
template<class, class> struct DefaultScaler;
}
/**
@brief Animation player
@tparam T Time type
@tparam 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
their running state.
@section Animation-Player-setup Setting up
The @ref 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 @ref add() --- that'll mean
you get a fresh set of animated values at your disposal after every iteration:
@snippet MagnumAnimation.cpp Player-usage
The Player stores just @ref TrackView instances, for every @ref 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 @ref addWithCallback()
function has also a typeless version taking just @cpp void* @ce user pointer
instead of a reference to a concrete type. Below is an example of animating
@ref SceneGraph object transformations using the
@ref SceneGraph::TranslationRotationScalingTransformation3D transformation
implementation:
@snippet MagnumAnimation.cpp Player-usage-callback
The @ref addWithCallbackOnChange() variant will fire the callback only if the
interpolated value changes, which is useful for triggering other events. See
@ref Animation-Player-higher-order "below" for an example.
By default, the @ref duration() of an animation is calculated implicitly from
all added tracks. You can use @ref setDuration() to specify a custom duration
--- if it extends beyond the keyframe values, values of begin/end keyframes
will be extrapolated according to @ref Extrapolation specified for every track;
if it will be shorter, only a slice of the animation will be played. The
animation is implicitly played only once, use @ref setPlayCount() to set a
number of repeats or make it repeat indefinitely.
@section Animation-Player-playback Animation playback
The @ref Player class doesn't access any global timer functionality, but
instead requires you to call its APIs with explicit time values. That allows
for greater flexibility and control over animation playback, among other
things.
By default, the player is in a @ref State::Stopped state. Call @ref play() with
a time value denoting the moment at which the animation should start. After
that, the @ref advance() function is meant to be called every frame with
a current time value. As long as the animation is playing, the @ref 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 @ref duration() of
all @ref playCount() iterations), the @ref advance() will update the
destination locations and/or fire user-defined callbacks with values that
correspond to @ref 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 @ref advance() would get called later, the result values
will never be calculated from a key value that's outside @ref duration().
Calling @ref stop() immediately transfers @ref state() to @ref State::Stopped
and the next @ref advance() iteration will give out interpolated values
corresponding to the begin time of @ref 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 @ref pause() while the animation is running immediately transfers the
animation state to @ref State::Paused and the next @ref advance() iteration
will give out interpolated values corresponding to a time that was passed to
the @ref pause() function. After that, no more updates are done until the
animation is resumed again with @ref play() or stopped with @ref stop().
The callbacks are only ever fired from within the @ref advance() function,
never from @ref pause(), @ref stop() or any other API.
For managing global application you can use @ref Timeline, @ref 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
@ref Animation-Player-time-type "below" for more information about using
different time types.
@snippet MagnumAnimation.cpp Player-usage-playback
@section Animation-Player-time-type Using custom time/key types
In long-running apps it's not desirable to use @ref Magnum::Float "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
@ref Magnum::Float "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 @ref std::chrono::nanoseconds as a time type and keeping
track key values as @ref Magnum::Float "Float" seconds:
@snippet MagnumAnimation.cpp Player-usage-chrono
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
@ref Player implementation is in a separate @ref Player.hpp file that you need
to include to get all needed template function definitions. See also
@ref compilation-speedup-hpp for more information.
@snippet MagnumAnimation-custom.cpp Player-usage-custom
@section Animation-Player-higher-order 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.
@snippet MagnumAnimation.cpp Player-higher-order
Besides state, you can also animate @ref setDuration() and @ref 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 @ref Player instance to speed it up or slow it down for
a particular animation:
@snippet MagnumAnimation.cpp Player-higher-order-animated-time
@section Animation-Player-explicit-specializations Explicit template specializations
The following specializations are explicitly compiled into the @ref Animation
library. For other specializations (e.g. using an integer key type) you have to
use the @ref Player.hpp implementation file to avoid linker errors. See also
@ref compilation-speedup-hpp for more information.
- @ref Player "Player<Float, Float>"
- @ref Player "Player<std::chrono::nanoseconds, Float>"
@experimental
*/
template<class T, class K
#ifdef DOXYGEN_GENERATING_OUTPUT
= T
#endif
> class Player {
public:
/** @brief Time type */
typedef T TimeType;
/** @brief Key type */
typedef K KeyType;
/**
* @brief 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.
*/
typedef std::pair<UnsignedInt, K>(*Scaler)(T, K);
/** @brief Constructor */
explicit Player();
/**
* @brief Construct with a custom scaler function
* @param scaler Scaler function
*/
explicit Player(Scaler scaler);
/** @brief Copying is not allowed */
Player(const Player<T, K>&) = delete;
/** @brief Move constructor */
Player(Player<T, K>&&) noexcept;
~Player();
/** @brief Copying is not allowed */
Player<T, K>& operator=(const Player<T, K>&) = delete;
/** @brief Move assignment */
Player<T, K>& operator=(Player<T, K>&&) noexcept;
/** @brief Time-to-key scaler */
Scaler scaler() const { return _scaler; }
/**
* @brief Duration
*
* If the duration was not set explicitly using @ref setDuration(),
* returns value calculated implicitly from all added tracks. If no
* tracks are added, returns default-constructed value.
*/
Math::Range1D<K> duration() const { return _duration; }
/**
* @brief 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
* @ref Extrapolation specified for given track. Setting a shorter
* duration will cause only a slice of all tracks to be played.
*
* Modifying this value while @ref state() is @ref State::Playing may
* cause the animation to jump or abruptly stop after next call to
* @ref advance().
* @see @ref TrackView::duration()
*/
Player<T, K>& setDuration(const Math::Range1D<K>& duration) {
_duration = duration;
return *this;
}
/** @brief Play count */
UnsignedInt playCount() const { return _playCount; }
/**
* @brief Set play count
*
* By default, play count is set to @cpp 1 @ce, meaning the animation
* @ref duration() is played once. Value of @cpp 0 @ce means the
* animation is repeated indefinitely.
*
* Modifying this value while @ref state() is @ref State::Playing may
* cause the animation to jump or abruptly stop after next call to
* @ref advance().
*/
Player<T, K>& setPlayCount(UnsignedInt count) {
_playCount = count;
return *this;
}
/**
* @brief Whether the player is empty
*
* @see @ref size(), @ref add(), @ref addWithCallback(),
* @ref addWithCallbackOnChange()
*/
bool isEmpty() const;
/**
* @brief Count of tracks managed by this player
*
* @see @ref isEmpty(), @ref add(), @ref addWithCallback(),
* @ref addWithCallbackOnChange()
*/
std::size_t size() const;
/**
* @brief Track at given position
*
* Due to the type-erased nature of the player implementation, it's not
* possible to know the exact track type.
*/
const TrackViewStorage<K>& track(std::size_t i) const;
/**
* @brief Add a track with a result destination
*
* The @p destination is updated with new value after each call to
* @ref advance() as long as the animation is playing.
*/
template<class V, class R> Player<T, K>& add(const TrackView<K, V, R>& track, R& destination);
/** @overload
*
* Note that track ownership is *not* transferred to the @ref Player
* and you have to ensure that it's kept in scope for the whole
* lifetime of the @ref Player instance.
*/
template<class V, class R> Player<T, K>& add(const Track<K, V, R>& track, R& destination) {
return add(TrackView<K, V, R>{track}, destination);
}
/**
* @brief Add a track with a result callback
*
* The @p callback is called with current key value, interpolated
* result value and the @p userData pointer after each call to
* @ref advance() as long as the animation is playing. The key value is
* guaranteed to never be outside of the @ref 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.
*/
#ifdef DOXYGEN_GENERATING_OUTPUT
template<class V, class R> Player<T, K>& addWithCallback(const TrackView<K, V, R>& track, void(*callback)(const K&, const R&, void*), void* userData = nullptr);
#else
/* Otherwise the user would be forced to use the + operator to convert
a lambda to a function pointer and (besides being weird and
annoying) it's also not portable because it doesn't work on MSVC
2015 and older versions of MSVC 2017. OTOH, putting this in the docs
wouldn say nothing about how the callback signature should look. */
template<class V, class R, class Callback> Player<T, K>& addWithCallback(const TrackView<K, V, R>& track, Callback callback, void* userData = nullptr);
#endif
/** @overload
*
* Note that the track ownership is *not* transferred to the
* @ref Player and you have to ensure that it's kept in scope for the
* whole lifetime of the @ref Player instance.
*/
#ifdef DOXYGEN_GENERATING_OUTPUT
template<class V, class R> Player<T, K>& addWithCallback(const Track<K, V, R>& track, void(*callback)(const K&, const R&, void*), void* userData = nullptr);
#else /* See above why */
template<class V, class R, class Callback> Player<T, K>& addWithCallback(const Track<K, V, R>& track, Callback callback, void* userData = nullptr) {
return addWithCallback(TrackView<K, V, R>{track}, callback, userData);
}
#endif
/**
* @brief Add a track with a result callback
*
* Equivalent to calling the above with a lambda wrapper that casts
* @cpp void* @ce back to @cpp T* @ce and dereferences it in order to
* pass it to @p callback. There is no additional overhead compared to
* the overload taking the @cpp void* @ce pointer.
*/
#ifdef DOXYGEN_GENERATING_OUTPUT
template<class V, class R, class U> Player<T, K>& addWithCallback(const TrackView<K, V, R>& track, void(*callback)(const K&, const R&, U&), U& userData);
#else /* See above why */
template<class V, class R, class U, class Callback> Player<T, K>& addWithCallback(const TrackView<K, V, R>& track, Callback callback, U& userData);
#endif
/** @overload
*
* Note that the track ownership is *not* transferred to the
* @ref Player and you have to ensure that it's kept in scope for the
* whole lifetime of the @ref Player instance.
*/
#ifdef DOXYGEN_GENERATING_OUTPUT
template<class V, class R, class U> Player<T, K>& addWithCallback(const Track<K, V, R>& track, void(*callback)(const K&, const R&, U&), U& userData);
#else /* See above why */
template<class V, class R, class U, class Callback> Player<T, K>& addWithCallback(const Track<K, V, R>& track, Callback callback, U& userData) {
return addWithCallback(TrackView<K, V, R>{track}, callback, userData);
}
#endif
/**
* @brief Add a track with a result callback that's called on change
*
* A combination of @ref add() and @ref addWithCallback() --- during
* each call to @ref advance(), as long as the animation is playing,
* the new value is compared to @p destination. If the new value is
* different from the stored one, @p callback is called and
* @p 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 @p destination location.
*
* See the overload below for a more convenient type-safe way to pass
* user data.
*/
#ifdef DOXYGEN_GENERATING_OUTPUT
template<class V, class R> Player<T, K>& addWithCallbackOnChange(const TrackView<K, V, R>& track, void(*callback)(const K&, const R&, void*), R& destination, void* userData = nullptr);
#else /* See above why */
template<class V, class R, class Callback> Player<T, K>& addWithCallbackOnChange(const TrackView<K, V, R>& track, Callback callback, R& destination, void* userData = nullptr);
#endif
/** @overload
*
* Note that the track ownership is *not* transferred to the
* @ref Player and you have to ensure that it's kept in scope for the
* whole lifetime of the @ref Player instance.
*/
#ifdef DOXYGEN_GENERATING_OUTPUT
template<class V, class R> Player<T, K>& addWithCallbackOnChange(const Track<K, V, R>& track, void(*callback)(const K&, const R&, void*), R& destination, void* userData = nullptr);
#else /* See above why */
template<class V, class R, class Callback> Player<T, K>& addWithCallbackOnChange(const Track<K, V, R>& track, Callback callback, R& destination, void* userData = nullptr) {
return addWithCallbackOnChange(TrackView<K, V, R>{track}, callback, destination, userData);
}
#endif
/**
* @brief Add a track with a result callback that's called on change
*
* Equivalent to calling the above with a lambda wrapper that casts
* @cpp void* @ce back to @cpp T* @ce and dereferences it in order to
* pass it to @p callback. There is no additional overhead compared to
* the overload taking the @cpp void* @ce pointer.
*/
#ifdef DOXYGEN_GENERATING_OUTPUT
template<class V, class R, class U> Player<T, K>& addWithCallbackOnChange(const TrackView<K, V, R>& track, void(*callback)(const K&, const R&, void*), R& destination, U& userData);
#else /* See above why */
template<class V, class R, class U, class Callback> Player<T, K>& addWithCallbackOnChange(const TrackView<K, V, R>& track, Callback callback, R& destination, U& userData);
#endif
/** @overload
*
* Note that the track ownership is *not* transferred to the
* @ref Player and you have to ensure that it's kept in scope for the
* whole lifetime of the @ref Player instance.
*/
#ifdef DOXYGEN_GENERATING_OUTPUT
template<class V, class R, class U> Player<T, K>& addWithCallbackOnChange(const Track<K, V, R>& track, void(*callback)(const K&, const R&, void*), R& destination, U& userData);
#else
template<class V, class R, class U, class Callback> Player<T, K>& addWithCallbackOnChange(const Track<K, V, R>& track, Callback callback, R& destination, U& userData) {
return addWithCallbackOnChange(TrackView<K, V, R>{track}, callback, destination, userData);
}
#endif
/**
* @brief State
*
* The player is @ref State::Stopped by default.
* @see @ref play(), @ref pause(), @ref stop(), @ref setState()
*/
State state() const { return _state; }
/**
* @brief Play
*
* Starts playing all tracks added to the player at given @p startTime.
* If @ref state() is already @ref State::Playing, the animation is
* restarted from the beginning at @p startTiime. If @ref state() is
* @ref State::Paused, the animation continues from the time that was
* passed to @ref pause().
*
* If @p startTime is in the future (that is, time passed to the next
* @ref advance() iteration will be less than @p startTime),
* @ref 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.
* @see @ref setState()
*/
Player<T, K>& play(T startTime);
/**
* @brief Pause
*
* Pauses the currently playing animation at given @p pauseTime. If
* @ref state() is not @ref State::Playing, the function does nothing.
* See @ref advance() for a detailed description of behavior when the
* animation gets paused.
* @see @ref setState()
*/
Player<T, K>& pause(T pauseTime);
/**
* @brief Stop
*
* Stops the currently playing animation. If @ref state() is
* @ref State::Paused, discard the pause information. If @ref state()
* is already @ref State::Stopped, the function does nothing. See
* @ref advance() for a detailed description of behavior when the
* animation gets stopped.
* @see @ref setState()
*/
Player<T, K>& stop();
/**
* @brief Set state
*
* Convenience function that calls @ref play(), @ref pause() or
* @ref stop() based on @p state. See documentation of these functions
* for detailed description. The @p time parameter is used only when
* @p state is @ref State::Playing or @ref State::Paused, it's ignored
* for @ref State::Stopped.
*/
Player<T, K>& setState(State state, T time);
/**
* @brief Advance the animation
*
* As long as @ref state() is @ref State::Playing, goes through all
* tracks added with @ref add(), @ref addWithCallback() or
* @ref addWithCallbackOnChange() in order they were added and updates
* the destination locations and/or fires the callbacks with
* interpolation results.
*
* If @ref state() is @ref State::Paused or @ref State::Stopped, the
* function does nothing. If @p time is less than time that was passed
* to @ref play(), the function does nothing. If @p time is large
* enough that @ref duration() times @ref 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 @ref duration() in order to correctly "park" the
* animation. The state then becomes @ref State::Stopped and no more
* updates are done until the animation is started again.
*
* If @ref pause() was called right before a particular @ref 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 @ref pause() call before to correctly
* "park" the animation. After that, no more updates are done until the
* animation is started again.
*
* If @ref stop() was called right before a particular @ref 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 @ref duration() to correctly "park" the
* animation back to its initial state. After that, no more updates are
* done until the animation is started again.
*/
Player<T, K>& advance(T time);
private:
struct Track;
Player<T, K>& addInternal(const TrackViewStorage<K>& track, void (*advancer)(const TrackViewStorage<K>&, K, std::size_t&, void*, void(*)(), void*), void* destination, void(*userCallback)(), void* userCallbackData);
std::vector<Track> _tracks;
Math::Range1D<K> _duration;
UnsignedInt _playCount{1};
State _state{State::Stopped};
T _startTime{}, _pauseTime{};
Scaler _scaler;
};
template<class T, class K> template<class V, class R> Player<T, K>& Player<T, K>::add(const TrackView<K, V, R>& track, R& destination) {
return addInternal(track,
[](const TrackViewStorage<K>& track, K key, std::size_t& hint, void* destination, void(*)(), void*) {
*static_cast<R*>(destination) = static_cast<const TrackView<K, V, R>&>(track).at(key, hint);
}, &destination, nullptr, nullptr);
}
#ifndef DOXYGEN_GENERATING_OUTPUT
template<class T, class K> template<class V, class R, class Callback> Player<T, K>& Player<T, K>::addWithCallback(const TrackView<K, V, R>& track, Callback callback, void* userData) {
auto callbackPtr = static_cast<void(*)(const K&, const R&, void*)>(callback);
return addInternal(track,
[](const TrackViewStorage<K>& track, K key, std::size_t& hint, void*, void(*callback)(), void* userData) {
reinterpret_cast<void(*)(const K&, const R&, void*)>(callback)(key, static_cast<const TrackView<K, V, R>&>(track).at(key, hint), userData);
}, nullptr, reinterpret_cast<void(*)()>(callbackPtr), userData);
}
template<class T, class K> template<class V, class R, class U, class Callback> Player<T, K>& Player<T, K>::addWithCallback(const TrackView<K, V, R>& track, Callback callback, U& userData) {
auto callbackPtr = static_cast<void(*)(const K&, const R&, U&)>(callback);
return addInternal(track,
[](const TrackViewStorage<K>& track, K key, std::size_t& hint, void*, void(*callback)(), void* userData) {
reinterpret_cast<void(*)(const K&, const R&, U&)>(callback)(key, static_cast<const TrackView<K, V, R>&>(track).at(key, hint), *static_cast<U*>(userData));
}, nullptr, reinterpret_cast<void(*)()>(callbackPtr), &userData);
}
template<class T, class K> template<class V, class R, class Callback> Player<T, K>& Player<T, K>::addWithCallbackOnChange(const TrackView<K, V, R>& track, Callback callback, R& destination, void* userData) {
auto callbackPtr = static_cast<void(*)(const K&, const R&, void*)>(callback);
return addInternal(track,
[](const TrackViewStorage<K>& track, K key, std::size_t& hint, void* destination, void(*callback)(), void* userData) {
R result = static_cast<const TrackView<K, V, R>&>(track).at(key, hint);
if(result == *static_cast<R*>(destination)) return;
reinterpret_cast<void(*)(const K&, const R&, void*)>(callback)(key, result, userData);
*static_cast<R*>(destination) = result;
}, &destination, reinterpret_cast<void(*)()>(callbackPtr), userData);
}
template<class T, class K> template<class V, class R, class U, class Callback> Player<T, K>& Player<T, K>::addWithCallbackOnChange(const TrackView<K, V, R>& track, Callback callback, R& destination, U& userData) {
auto callbackPtr = static_cast<void(*)(const K&, const R&, U&)>(callback);
return addInternal(track,
[](const TrackViewStorage<K>& track, K key, std::size_t& hint, void* destination, void(*callback)(), void* userData) {
R result = static_cast<const TrackView<K, V, R>&>(track).at(key, hint);
if(result == *static_cast<R*>(destination)) return;
reinterpret_cast<void(*)(const K&, const R&, U&)>(callback)(key, result, *static_cast<U*>(userData));
*static_cast<R*>(destination) = result;
}, &destination, reinterpret_cast<void(*)()>(callbackPtr), &userData);
}
#endif
#if defined(CORRADE_TARGET_WINDOWS) && !defined(__MINGW32__)
extern template class MAGNUM_EXPORT Player<Float, Float>;
extern template class MAGNUM_EXPORT Player<std::chrono::nanoseconds, Float>;
#endif
}}
#endif

188
src/Magnum/Animation/Player.hpp

@ -0,0 +1,188 @@
#ifndef Magnum_Animation_Player_hpp
#define Magnum_Animation_Player_hpp
/*
This file is part of Magnum.
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018
Vladimír Vondruš <mosra@centrum.cz>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
/** @file
* @brief @ref compilation-speedup-hpp "Template implementation" for @ref Animable.h and @ref AnimableGroup.h
*/
#include "Player.h"
namespace Magnum { namespace Animation {
namespace Implementation {
template<class T, class K> struct DefaultScaler {
static std::pair<UnsignedInt, K> scale(T time, K duration) {
const UnsignedInt playCount = time/duration;
const K factor = std::fmod(Double(time), Double(duration));
return {playCount, factor};
}
};
template<> struct DefaultScaler<std::chrono::nanoseconds, Float> {
static std::pair<UnsignedInt, Float> scale(std::chrono::nanoseconds time, Float duration) {
/* I hope I'm not having any catastrophic cancellation here :/ */
const std::chrono::nanoseconds durationNs{std::chrono::nanoseconds::rep(Double(duration)*1000000000.0)};
const UnsignedInt playCount = time/durationNs;
const Float factor = Float((time - playCount*durationNs).count()/1000000000.0);
return {playCount, factor};
}
};
}
#ifndef DOXYGEN_GENERATING_OUTPUT
template<class T, class K> struct Player<T, K>::Track {
/* Not sure why is this still needed for emplace_back(). It's 2018,
COME ON ¯\_()_/¯ */
/*implicit*/ Track(const TrackViewStorage<K>& track, void (*advancer)(const TrackViewStorage<K>&, K, std::size_t&, void*, void(*)(), void*), void* destination, void(*userCallback)(), void* userCallbackData, std::size_t hint) noexcept: track{track}, advancer{advancer}, destination{destination}, userCallback{userCallback}, userCallbackData{userCallbackData}, hint{hint} {}
TrackViewStorage<K> track;
void (*advancer)(const TrackViewStorage<K>&, K, std::size_t&, void*, void(*)(), void*);
void* destination;
void(*userCallback)();
void* userCallbackData;
std::size_t hint;
};
#endif
template<class T, class K> Player<T, K>::Player(Player<T, K>&&) noexcept = default;
template<class T, class K> Player<T, K>& Player<T, K>::operator=(Player<T, K>&&) noexcept = default;
template<class T, class K> Player<T, K>::Player(): Player<T, K>{Implementation::DefaultScaler<T, K>::scale} {}
template<class T, class K> Player<T, K>::Player(Scaler scaler): _scaler{scaler} {}
template<class T, class K> Player<T, K>::~Player() = default;
template<class T, class K> bool Player<T, K>::isEmpty() const {
return _tracks.empty();
}
template<class T, class K> std::size_t Player<T, K>::size() const {
return _tracks.size();
}
template<class T, class K> const TrackViewStorage<K>& Player<T, K>::track(std::size_t i) const {
CORRADE_ASSERT(i < _tracks.size(),
"Animation::Player::track(): index out of range", _tracks[i].track);
return _tracks[i].track;
}
template<class T, class K> Player<T, K>& Player<T, K>::addInternal(const TrackViewStorage<K>& track, void(*const advancer)(const TrackViewStorage<K>&, K, std::size_t&, void*, void(*)(), void*), void* const destination, void(*const userCallback)(), void* const userCallbackData) {
if(_tracks.empty() && _duration == Math::Range1D<K>{})
_duration = track.duration();
else
_duration = Math::join(track.duration(), _duration);
_tracks.emplace_back(track, advancer, destination, userCallback, userCallbackData, 0);
return *this;
}
template<class T, class K> Player<T, K>& Player<T, K>::play(T startTime) {
/* In case we were paused, move start time backwards by the duration that
was already played back */
if(_state == State::Paused) {
_startTime = startTime - _startTime;
_state = State::Playing;
return *this;
}
_state = State::Playing;
_startTime = startTime;
return *this;
}
template<class T, class K> Player<T, K>& Player<T, K>::pause(T pauseTime) {
/* Avoid breaking the pause state when not playing */
if(_state != State::Playing) return *this;
_state = State::Paused;
_pauseTime = pauseTime;
return *this;
}
template<class T, class K> Player<T, K>& Player<T, K>::stop() {
_state = State::Stopped;
return *this;
}
template<class T, class K> Player<T, K>& Player<T, K>::setState(State state, T time) {
switch(state) {
case State::Playing: return play(time);
case State::Paused: return pause(time);
case State::Stopped: return stop();
}
CORRADE_ASSERT_UNREACHABLE(); /* LCOV_EXCL_LINE */
}
template<class T, class K> Player<T, K>& Player<T, K>::advance(T time) {
/* Time to use for advancing the animation */
T timeToUse = time;
/* The animation was paused right before this iteration, "park" the
animation to the pause time. This time will be used by play() to offset
the playback when the animation is resumed.
std::chrono::duration doesn't have operator bool, so I need to compare
to default-constructed value. Ugh. */
if(_state == State::Paused && (_pauseTime != T{})) {
timeToUse = _pauseTime;
_startTime = _pauseTime - _startTime;
_pauseTime = {};
/* The animation was stopped by the user right before this iteration,
"park" the animation to the initial time */
} else if(_state == State::Stopped && (_startTime != T{})) {
_startTime = {};
timeToUse = {};
/* Otherwise, if the player is not playing or scheduled to start playing in
the future, do nothing */
} else if(_state != State::Playing || time < _startTime) return *this;
/* Calculate current play iteration and key value in that iteration. If we
exceeded play count, stop the animation and give out value at duration
end. */
UnsignedInt playCount;
K key;
std::tie(playCount, key) = _scaler(timeToUse - _startTime, _duration.size()[0]);
if(_playCount && playCount >= _playCount) {
_state = State::Stopped;
_startTime = {};
key = _duration.size()[0];
}
/* Advance all tracks. Properly handle durations that don't start at 0. */
for(Track& t: _tracks)
t.advancer(t.track, _duration.min()[0] + key, t.hint, t.destination, t.userCallback, t.userCallbackData);
return *this;
}
}}
#endif

72
src/Magnum/Animation/Test/Benchmark.cpp

@ -25,7 +25,7 @@
#include <Corrade/TestSuite/Tester.h>
#include "Magnum/Animation/Track.h"
#include "Magnum/Animation/Player.h"
namespace Magnum { namespace Animation { namespace Test {
@ -43,6 +43,11 @@ struct Benchmark: TestSuite::Tester {
void atStrictInterleaved();
void atStrictInterleavedDirectInterpolator();
void playerAdvanceEmpty();
void playerAdvanceEmptyTrack();
void playerAdvance();
void playerAdvanceCallback();
Containers::Array<Float> _keys;
Containers::Array<Int> _values;
Containers::Array<std::pair<Float, Int>> _interleaved;
@ -66,7 +71,12 @@ Benchmark::Benchmark() {
&Benchmark::atHint,
&Benchmark::atStrict,
&Benchmark::atStrictInterleaved,
&Benchmark::atStrictInterleavedDirectInterpolator}, 10);
&Benchmark::atStrictInterleavedDirectInterpolator,
&Benchmark::playerAdvanceEmpty,
&Benchmark::playerAdvanceEmptyTrack,
&Benchmark::playerAdvance,
&Benchmark::playerAdvanceCallback}, 10);
_keys = Containers::Array<Float>{DataSize};
_values = Containers::Array<Int>{Containers::DirectInit, DataSize, 1};
@ -160,6 +170,64 @@ void Benchmark::atStrictInterleaved() {
CORRADE_COMPARE(result, 125000);
}
void Benchmark::atStrictInterleavedDirectInterpolator() {
Int result{};
CORRADE_BENCHMARK(250) {
std::size_t hint{};
for(Float i = 0.0f; i < 500.0f; i += 1.0f)
result += _trackInterleaved.atStrict(Math::select, i, hint);
}
CORRADE_COMPARE(result, 125000);
}
void Benchmark::playerAdvanceEmpty() {
Player<Float> player;
player.play(0.0f);
CORRADE_BENCHMARK(250) {
for(Float i = 0.0f; i < 500.0f; i += 1.0f)
player.advance(i);
}
}
void Benchmark::playerAdvanceEmptyTrack() {
TrackView<Float, Int> empty{nullptr, nullptr, Math::select};
Int result{};
Player<Float> player;
player.add(empty, result)
.play({});
CORRADE_BENCHMARK(250) {
for(Float i = 0.0f; i < 500.0f; i += 1.0f)
player.advance(i);
}
CORRADE_COMPARE(result, 0);
}
void Benchmark::playerAdvance() {
Int result{};
Player<Float> player;
player.add(_track, result)
.play({});
CORRADE_BENCHMARK(250) {
for(Float i = 0.0f; i < 500.0f; i += 1.0f)
player.advance(i);
}
CORRADE_COMPARE(result, 1);
}
void Benchmark::playerAdvanceCallback() {
Int result{};
Player<Float> player;
player.addWithCallback(_track, [](const Float&, const Int& value, Int& result) {
result += value;
}, result)
.play({});
CORRADE_BENCHMARK(250) {
for(Float i = 0.0f; i < 500.0f; i += 1.0f)
player.advance(i);
}
CORRADE_COMPARE(result, 125000);
}
}}}
CORRADE_TEST_MAIN(Magnum::Animation::Test::Benchmark)

4
src/Magnum/Animation/Test/CMakeLists.txt

@ -25,6 +25,8 @@
corrade_add_test(AnimationBenchmark Benchmark.cpp LIBRARIES Magnum)
corrade_add_test(AnimationInterpolationTest InterpolationTest.cpp LIBRARIES MagnumTestLib)
corrade_add_test(AnimationPlayerTest PlayerTest.cpp LIBRARIES MagnumTestLib)
corrade_add_test(AnimationPlayerCustomTest PlayerCustomTest.cpp LIBRARIES MagnumTestLib)
corrade_add_test(AnimationTrackTest TrackTest.cpp LIBRARIES Magnum)
corrade_add_test(AnimationTrackViewTest TrackViewTest.cpp LIBRARIES Magnum)
@ -35,6 +37,8 @@ set_property(TARGET
set_target_properties(
AnimationBenchmark
AnimationInterpolationTest
AnimationPlayerTest
AnimationPlayerCustomTest
AnimationTrackTest
AnimationTrackViewTest
PROPERTIES FOLDER "Magnum/Animation/Test")

80
src/Magnum/Animation/Test/PlayerCustomTest.cpp

@ -0,0 +1,80 @@
/*
This file is part of Magnum.
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018
Vladimír Vondruš <mosra@centrum.cz>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#include <Corrade/TestSuite/Tester.h>
#include "Magnum/Animation/Player.hpp"
namespace Magnum { namespace Animation { namespace Test {
struct PlayerCustomTest: TestSuite::Tester {
explicit PlayerCustomTest();
void test();
};
PlayerCustomTest::PlayerCustomTest() {
addTests({&PlayerCustomTest::test});
}
#ifdef CORRADE_TARGET_EMSCRIPTEN
typedef std::uint64_t UnsignedLong; /** @todo what about this? */
#endif
namespace {
const Animation::Track<UnsignedShort, Float> Track{{
{24, 1.5f}, /* 1.0 sec */
{60, 3.0f}, /* 2.5 sec */
{72, 5.0f}, /* 3.0 sec */
{96, 2.0f} /* 4.0 sec */
}, Math::lerp};
}
void PlayerCustomTest::test() {
Player<UnsignedLong, UnsignedShort> player{
/* Keep this in sync with PlayerCustomTest */
[](UnsignedLong time, UnsignedShort duration) {
/* One frame is 1/24 second */
const UnsignedLong durationNs = UnsignedLong(duration)*1000000ull/24;
const UnsignedInt playCount = time/durationNs;
const UnsignedShort factor = (time - playCount*durationNs)*24/1000000ull;
return std::make_pair(playCount, factor);
}};
Float value = -1.0f;
player.add(Track, value)
.play(2000000ull);
CORRADE_COMPARE(player.duration().size()[0], 24*3);
/* 1.75 secs in */
player.advance(3750000ull);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, 4.0f);
}
}}}
CORRADE_TEST_MAIN(Magnum::Animation::Test::PlayerCustomTest)

774
src/Magnum/Animation/Test/PlayerTest.cpp

@ -0,0 +1,774 @@
/*
This file is part of Magnum.
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018
Vladimír Vondruš <mosra@centrum.cz>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#include <sstream>
#include <Corrade/TestSuite/Tester.h>
#include <Corrade/TestSuite/Compare/Numeric.h>
#include "Magnum/Animation/Player.h"
namespace Magnum { namespace Animation { namespace Test {
struct PlayerTest: TestSuite::Tester {
explicit PlayerTest();
void constructEmpty();
void construct();
void constructChrono();
void constructCopy();
void constructMove();
void setDurationExtend();
void setDurationReplace();
void trackInvalidIndex();
void advanceNotRunning();
void advancePlaying();
void advanceRestart();
void advanceStop();
void advancePauseResume();
void advancePauseStop();
void advancePlayCount();
void advancePlayCountInfinite();
void advanceChrono();
void setState();
void add();
void addWithCallback();
void addWithCallbackTemplate();
void addWithCallbackOnChange();
void addWithCallbackOnChangeTemplate();
void runFor100YearsFloat();
void runFor100YearsChrono();
void debugState();
};
namespace {
const struct {
const char* name;
Float offsetFloat;
std::chrono::nanoseconds offsetChrono;
bool failsFloat, failsFuzzyFloat;
} RunFor100YearsData[]{
{"0", {}, {}, false, false},
{"1 minute", 60.0f, std::chrono::minutes{1}, false, false},
{"5 minutes", 5.0f*60.0f, std::chrono::minutes{5}, true, false},
{"30 minutes", 30.0f*60.0f, std::chrono::minutes{30}, true, false},
{"1 hour", 60.0f*60.0f, std::chrono::hours{1}, true, false},
{"1 day", 24.0f*60.0f*60.0f, std::chrono::hours{24}, true, true},
{"100 days", 100.0f*24.0f*60.0f*60.0f, std::chrono::hours{100*24}, true, true},
{"100 years", 100.0f*365.0f*24.0f*60.0f*60.0f, std::chrono::hours{100*365*24}, true, true},
};
}
PlayerTest::PlayerTest() {
addTests({&PlayerTest::constructEmpty,
&PlayerTest::construct,
&PlayerTest::constructCopy,
&PlayerTest::constructMove,
&PlayerTest::setDurationExtend,
&PlayerTest::setDurationReplace,
&PlayerTest::trackInvalidIndex,
&PlayerTest::advanceNotRunning,
&PlayerTest::advancePlaying,
&PlayerTest::advanceRestart,
&PlayerTest::advanceStop,
&PlayerTest::advancePauseResume,
&PlayerTest::advancePauseStop,
&PlayerTest::advancePlayCount,
&PlayerTest::advancePlayCountInfinite,
&PlayerTest::advanceChrono,
&PlayerTest::setState,
&PlayerTest::add,
&PlayerTest::addWithCallback,
&PlayerTest::addWithCallbackTemplate,
&PlayerTest::addWithCallbackOnChange,
&PlayerTest::addWithCallbackOnChangeTemplate});
addInstancedTests({
&PlayerTest::runFor100YearsFloat,
&PlayerTest::runFor100YearsChrono},
Containers::arraySize(RunFor100YearsData));
addTests({&PlayerTest::debugState});
}
void PlayerTest::constructEmpty() {
Player<Float> player;
CORRADE_VERIFY(player.scaler());
CORRADE_COMPARE(player.duration(), Range1D{});
CORRADE_COMPARE(player.playCount(), 1);
CORRADE_COMPARE(player.state(), State::Stopped);
CORRADE_VERIFY(player.isEmpty());
CORRADE_COMPARE(player.size(), 0);
}
namespace {
const Animation::Track<Float, Float> Track{{
{1.0f, 1.5f},
{2.5f, 3.0f},
{3.0f, 5.0f},
{4.0f, 2.0f}
}, Math::lerp};
}
void PlayerTest::construct() {
Animation::Track<Float, Int> track2{{
{0.5f, 42},
{3.0f, 1337},
{3.5f, -17}
}, Math::select};
Float value = -1.0f;
Int value2 = -1;
Player<Float> player;
player.add(Track, value)
.add(track2, value2)
.setPlayCount(37);
CORRADE_VERIFY(player.scaler());
CORRADE_COMPARE(player.duration(), (Range1D{0.5f, 4.0f}));
CORRADE_COMPARE(player.playCount(), 37);
CORRADE_COMPARE(player.state(), State::Stopped);
CORRADE_VERIFY(!player.isEmpty());
CORRADE_COMPARE(player.size(), 2);
CORRADE_COMPARE(player.track(0).keys().data(), Track.keys().data());
CORRADE_COMPARE(player.track(1).keys().data(), track2.keys().data());
}
void PlayerTest::constructChrono() {
Animation::Track<Float, Int> track2{{
{0.5f, 42},
{3.0f, 1337},
{3.5f, -17}
}, Math::select};
Float value = -1.0f;
Int value2 = -1;
Player<std::chrono::nanoseconds, Float> player;
player.add(Track, value)
.add(track2, value2)
.setPlayCount(37);
CORRADE_VERIFY(player.scaler());
CORRADE_COMPARE(player.duration(), (Range1D{0.5f, 4.0f}));
CORRADE_COMPARE(player.playCount(), 37);
CORRADE_COMPARE(player.state(), State::Stopped);
CORRADE_VERIFY(!player.isEmpty());
CORRADE_COMPARE(player.size(), 2);
CORRADE_COMPARE(player.track(0).keys().data(), Track.keys().data());
CORRADE_COMPARE(player.track(1).keys().data(), track2.keys().data());
}
void PlayerTest::constructCopy() {
CORRADE_VERIFY(!(std::is_constructible<Player<Float>, const Player<Float>&>{}));
CORRADE_VERIFY(!(std::is_assignable<Player<Float>, const Player<Float>&>{}));
}
void PlayerTest::constructMove() {
Animation::Track<Float, Int> track2{{
{0.5f, 42},
{3.0f, 1337},
{3.5f, -17}
}, Math::select};
Float value = -1.0f;
Int value2 = -1;
Player<Float> a;
a.add(Track, value)
.add(track2, value2)
.setPlayCount(37)
.play({});
CORRADE_COMPARE(a.duration(), (Range1D{0.5f, 4.0f}));
CORRADE_COMPARE(a.playCount(), 37);
CORRADE_COMPARE(a.state(), State::Playing);
CORRADE_VERIFY(!a.isEmpty());
CORRADE_COMPARE(a.size(), 2);
CORRADE_COMPARE(a.track(0).keys().data(), Track.keys().data());
CORRADE_COMPARE(a.track(1).keys().data(), track2.keys().data());
Player<Float> b{std::move(a)};
CORRADE_COMPARE(b.duration(), (Range1D{0.5f, 4.0f}));
CORRADE_COMPARE(b.playCount(), 37);
CORRADE_COMPARE(b.state(), State::Playing);
CORRADE_VERIFY(!b.isEmpty());
CORRADE_COMPARE(b.size(), 2);
CORRADE_COMPARE(b.track(0).keys().data(), Track.keys().data());
CORRADE_COMPARE(b.track(1).keys().data(), track2.keys().data());
Player<Float> c;
c.setDuration({1.2f, 1.3f});
c = std::move(b);
CORRADE_COMPARE(c.duration(), (Range1D{0.5f, 4.0f}));
CORRADE_COMPARE(c.playCount(), 37);
CORRADE_COMPARE(c.state(), State::Playing);
CORRADE_VERIFY(!c.isEmpty());
CORRADE_COMPARE(c.size(), 2);
CORRADE_COMPARE(c.track(0).keys().data(), Track.keys().data());
CORRADE_COMPARE(c.track(1).keys().data(), track2.keys().data());
}
void PlayerTest::setDurationExtend() {
Float value;
Player<Float> player;
player.setDuration({-1.0f, 2.0f});
CORRADE_COMPARE(player.duration(), (Range1D{-1.0f, 2.0f}));
player.add(Track, value);
CORRADE_COMPARE(player.duration(), (Range1D{-1.0f, 4.0f}));
}
void PlayerTest::setDurationReplace() {
Float value;
Player<Float> player;
player.add(Track, value);
CORRADE_COMPARE(player.duration(), (Range1D{1.0f, 4.0f}));
player.setDuration({-1.0f, 2.0f});
CORRADE_COMPARE(player.duration(), (Range1D{-1.0f, 2.0f}));
}
void PlayerTest::trackInvalidIndex() {
std::ostringstream out;
Error redirectError{&out};
Player<Float> player;
player.track(0);
CORRADE_COMPARE(out.str(), "Animation::Player::track(): index out of range\n");
}
void PlayerTest::advanceNotRunning() {
Float value = -1.0f;
Player<Float> player;
player.add(Track, value);
CORRADE_COMPARE(player.state(), State::Stopped);
CORRADE_COMPARE(value, -1.0f);
player.advance(1.75f);
CORRADE_COMPARE(player.state(), State::Stopped);
CORRADE_COMPARE(value, -1.0f);
}
void PlayerTest::advancePlaying() {
Float value = -1.0f;
Player<Float> player;
player.add(Track, value)
.play(2.0f);
CORRADE_COMPARE(player.duration().size(), 3.0f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, -1.0f);
/* Still before starting time, nothing is done */
player.advance(1.75f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, -1.0f);
/* 1.75 secs in */
player.advance(3.75f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, 4.0f);
/* 2.67 secs in */
player.advance(4.6666667f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, 3.0f);
/* When the player gets stopped, the value at the stop time is written */
player.advance(5.5f);
CORRADE_COMPARE(player.state(), State::Stopped);
CORRADE_COMPARE(value, 2.0f);
/* But further advancing will not write anything */
value = -1.0f;
player.advance(100.0f);
CORRADE_COMPARE(player.state(), State::Stopped);
CORRADE_COMPARE(value, -1.0f);
}
void PlayerTest::advanceRestart() {
Float value = -1.0f;
Player<Float> player;
player.add(Track, value)
.play(2.0f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, -1.0f);
/* Still before starting time, nothing is done */
player.advance(1.75f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, -1.0f);
/* 1.75 secs in */
player.advance(3.75f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, 4.0f);
/* Call play again, will restart from the beginning... */
value = -1.0f;
player.play(4.0f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, -1.0f);
/* ... but only after calling advance() again. Now at 1 sec in. */
player.advance(5.0f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, 2.5f);
}
void PlayerTest::advanceStop() {
Float value = -1.0f;
Player<Float> player;
player.add(Track, value)
.play(2.0f);
player.advance(3.75f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, 4.0f);
/* Stop, should not update anything */
value = -1.0;
player.stop();
CORRADE_COMPARE(player.state(), State::Stopped);
CORRADE_COMPARE(value, -1.0f);
/* Advancing will update with a value from beginning of the duration */
player.advance(5.0f);
CORRADE_COMPARE(player.state(), State::Stopped);
CORRADE_COMPARE(value, 1.5f);
/* But further advancing will not write anything */
value = -1.0f;
player.advance(100.0f);
CORRADE_COMPARE(player.state(), State::Stopped);
CORRADE_COMPARE(value, -1.0f);
}
void PlayerTest::advancePauseResume() {
Float value = -1.0f;
Player<Float> player;
player.add(Track, value)
.play(2.0f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, -1.0f);
player.advance(3.75f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, 4.0f);
/* Pausing should not update anything */
value = -1.0f;
player.pause(4.0f);
CORRADE_COMPARE(player.state(), State::Paused);
CORRADE_COMPARE(value, -1.0f);
/* Pausing again should be a no-op */
player.pause(4.1f);
CORRADE_COMPARE(player.state(), State::Paused);
CORRADE_COMPARE(value, -1.0f);
/* But advance() after should. No matter what time is passed to it, it
should update with time of pause. */
player.advance(4.5f);
CORRADE_COMPARE(player.state(), State::Paused);
CORRADE_COMPARE(value, 5.0f); /* value at 2.0f, not 2.5f */
/* Advancing further should do nothing */
value = -1.0f;
player.advance(50.0f);
CORRADE_COMPARE(player.state(), State::Paused);
CORRADE_COMPARE(value, -1.0f);
/* Resuming the animation, again should not update anything */
player.play(100.0f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, -1.0f);
/* Advancing the animation should update again. It was paused after two
seconds, so continuing at 2.5 seconds now. */
player.advance(100.5f);
CORRADE_COMPARE(value, 3.5f);
}
void PlayerTest::advancePauseStop() {
Float value = -1.0f;
Player<Float> player;
player.add(Track, value)
.play(2.0f);
player.advance(3.75f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, 4.0f);
/* Pause, get value from the pause time */
player.pause(4.0f);
player.advance(4.5f);
CORRADE_COMPARE(player.state(), State::Paused);
CORRADE_COMPARE(value, 5.0f);
/* Stop, should not update anything */
value = -1.0;
player.stop();
CORRADE_COMPARE(player.state(), State::Stopped);
CORRADE_COMPARE(value, -1.0f);
/* Advancing will update with a value from beginning of the duration */
player.advance(5.0f);
CORRADE_COMPARE(player.state(), State::Stopped);
CORRADE_COMPARE(value, 1.5f);
/* But further advancing will not write anything */
value = -1.0f;
player.advance(100.0f);
CORRADE_COMPARE(player.state(), State::Stopped);
CORRADE_COMPARE(value, -1.0f);
/* Pause while stopped is a no-op */
player.pause(101.0f);
player.advance(101.0f);
CORRADE_COMPARE(player.state(), State::Stopped);
CORRADE_COMPARE(value, -1.0f);
}
void PlayerTest::advancePlayCount() {
Float value = -1.0f;
Player<Float> player;
player.add(Track, value)
.setPlayCount(3)
.play(2.0f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, -1.0f);
/* Still before starting time, nothing is done */
player.advance(1.75f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, -1.0f);
/* 1.75 secs in */
player.advance(3.75f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, 4.0f);
/* 2 secs in, second round */
player.advance(7.0f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, 5.0f);
/* 1.75 secs in, third round */
player.advance(9.75f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, 4.0f);
/* When the player gets stopped, the value at the stop time is written */
player.advance(11.5f);
CORRADE_COMPARE(player.state(), State::Stopped);
CORRADE_COMPARE(value, 2.0f);
/* But further advancing will not write anything */
value = -1.0f;
player.advance(100.0f);
CORRADE_COMPARE(player.state(), State::Stopped);
CORRADE_COMPARE(value, -1.0f);
}
void PlayerTest::advancePlayCountInfinite() {
Float value = -1.0f;
Player<Float> player;
player.add(Track, value)
.setPlayCount(0)
.play(2.0f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, -1.0f);
/* Still before starting time, nothing is done */
player.advance(1.75f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, -1.0f);
/* 1.75 secs in */
player.advance(3.75f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, 4.0f);
/* 2 secs in, second round */
player.advance(7.0f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, 5.0f);
/* 1.75 secs in, 10th round */
player.advance(33.75f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, 4.0f);
}
void PlayerTest::advanceChrono() {
Float value = -1.0f;
Player<std::chrono::nanoseconds, Float> player;
player.add(Track, value)
.play(std::chrono::seconds{2});
CORRADE_COMPARE(player.duration().size(), 3.0f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, -1.0f);
/* Still before starting time, nothing is done */
player.advance(std::chrono::milliseconds{1750});
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, -1.0f);
/* 1.75 secs in */
player.advance(std::chrono::milliseconds{3750});
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, 4.0f);
}
void PlayerTest::setState() {
Player<Float> player;
CORRADE_COMPARE(player.state(), State::Stopped);
player.setState(State::Playing, {});
CORRADE_COMPARE(player.state(), State::Playing);
player.setState(State::Paused, {});
CORRADE_COMPARE(player.state(), State::Paused);
player.setState(State::Stopped, {});
CORRADE_COMPARE(player.state(), State::Stopped);
}
void PlayerTest::add() {
Float value = -1.0f;
Player<Float> player;
player.add(Track, value)
.play(2.0f);
CORRADE_COMPARE(player.duration().size(), 3.0f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, -1.0f);
/* 1.75 secs in */
player.advance(3.75f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, 4.0f);
}
void PlayerTest::addWithCallback() {
struct Data {
Float value = -1.0f;
Int called = 0;
} data;
Player<Float> player;
player.addWithCallback(Track, [](const Float&, const Float& value, void* userData) {
static_cast<Data*>(userData)->value = value;
++static_cast<Data*>(userData)->called;
}, &data)
.play(2.0f);
CORRADE_COMPARE(player.duration().size(), 3.0f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(data.value, -1.0f);
CORRADE_COMPARE(data.called, 0);
/* 1.75 secs in */
player.advance(3.75f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(data.value, 4.0f);
CORRADE_COMPARE(data.called, 1);
}
void PlayerTest::addWithCallbackTemplate() {
struct Data {
Float value = -1.0f;
Int called = 0;
} data;
Player<Float> player;
player.addWithCallback(Track, [](const Float&, const Float& value, Data& userData) {
userData.value = value;
++userData.called;
}, data)
.play(2.0f);
CORRADE_COMPARE(player.duration().size(), 3.0f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(data.value, -1.0f);
CORRADE_COMPARE(data.called, 0);
/* 1.75 secs in */
player.advance(3.75f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(data.value, 4.0f);
CORRADE_COMPARE(data.called, 1);
}
void PlayerTest::addWithCallbackOnChange() {
struct Data {
Float value = -1.0f;
Int called = 0;
} data;
Player<Float> player;
player.addWithCallbackOnChange(Track, [](const Float&, const Float& value, void* userData) {
static_cast<Data*>(userData)->value = value;
++static_cast<Data*>(userData)->called;
}, data.value, &data)
.play(2.0f);
CORRADE_COMPARE(player.duration().size(), 3.0f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(data.value, -1.0f);
CORRADE_COMPARE(data.called, 0);
/* 1.75 secs in */
player.advance(3.75f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(data.value, 4.0f);
CORRADE_COMPARE(data.called, 1);
/* At the same time, same value, should not be called again */
player.advance(3.75f);
CORRADE_COMPARE(data.value, 4.0f);
CORRADE_COMPARE(data.called, 1);
/* Different time, different value, called again */
player.advance(4.0f);
CORRADE_COMPARE(data.value, 5.0f);
CORRADE_COMPARE(data.called, 2);
}
void PlayerTest::addWithCallbackOnChangeTemplate() {
struct Data {
Float value = -1.0f;
Int called = 0;
} data;
Player<Float> player;
player.addWithCallbackOnChange(Track, [](const Float&, const Float& value, Data& userData) {
userData.value = value;
++userData.called;
}, data.value, data)
.play(2.0f);
CORRADE_COMPARE(player.duration().size(), 3.0f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(data.value, -1.0f);
CORRADE_COMPARE(data.called, 0);
/* 1.75 secs in */
player.advance(3.75f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(data.value, 4.0f);
CORRADE_COMPARE(data.called, 1);
/* At the same time, same value, should not be called again */
player.advance(3.75f);
CORRADE_COMPARE(data.value, 4.0f);
CORRADE_COMPARE(data.called, 1);
/* Different time, different value, called again */
player.advance(4.0f);
CORRADE_COMPARE(data.value, 5.0f);
CORRADE_COMPARE(data.called, 2);
}
void PlayerTest::runFor100YearsFloat() {
auto&& data = RunFor100YearsData[testCaseInstanceId()];
setTestCaseDescription(data.name);
Float value = -1.0f;
Player<Float> player;
player.add(Track, value)
.setPlayCount(0)
.play({});
/* The track must fit an integer number of times into the day for this test
to work (3 seconds do fit) */
CORRADE_COMPARE(player.duration().size()[0], 3.0f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, -1.0f);
/* 2.67 secs in given iteration. Comparing with a slightly larger epsilon,
because it fails right after five minutes otherwise. */
player.advance(data.offsetFloat + 2.6666666666667f);
if(data.failsFloat || data.failsFuzzyFloat)
Debug{} << "Calculated value:" << value;
CORRADE_COMPARE(player.state(), State::Playing);
{
CORRADE_EXPECT_FAIL_IF(data.failsFuzzyFloat, "Imprecision larger than 2.5e-4f.");
CORRADE_COMPARE_WITH(value, 3.0f, TestSuite::Compare::around(0.00025f));
}
if(!data.failsFuzzyFloat) {
CORRADE_EXPECT_FAIL_IF(data.failsFloat, "Imprecision larger than 1e-6f.");
CORRADE_COMPARE(value, 3.0f);
}
}
void PlayerTest::runFor100YearsChrono() {
auto&& data = RunFor100YearsData[testCaseInstanceId()];
setTestCaseDescription(data.name);
Float value = -1.0f;
Player<std::chrono::nanoseconds, Float> player;
player.add(Track, value)
.setPlayCount(0)
.play({});
/* The track must fit an integer number of times into the day for this test
to work (3 seconds do fit) */
CORRADE_COMPARE(player.duration().size()[0], 3.0f);
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, -1.0f);
/* 2.67 secs in */
player.advance(data.offsetChrono + std::chrono::nanoseconds{2666666667ull});
CORRADE_COMPARE(player.state(), State::Playing);
CORRADE_COMPARE(value, 3.0f);
}
void PlayerTest::debugState() {
std::ostringstream out;
Debug{&out} << State::Playing << State(0xde);
CORRADE_COMPARE(out.str(), "Animation::State::Playing Animation::State(0xde)\n");
}
}}}
CORRADE_TEST_MAIN(Magnum::Animation::Test::PlayerTest)

3
src/Magnum/Animation/Track.h

@ -43,7 +43,8 @@ namespace Magnum { namespace Animation {
@tparam V Value type
@tparam R Result type
Immutable storage of keyframe + value pairs.
Immutable storage of keyframe + value pairs. Usually used in combination with
the @ref Player class, but it's possible to use it separately as well.
@section Animation-Track-usage Basic usage

1
src/Magnum/CMakeLists.txt

@ -39,6 +39,7 @@ set(Magnum_GracefulAssert_SRCS
ImageView.cpp
PixelFormat.cpp
Animation/Player.cpp
Animation/Interpolation.cpp)
set(Magnum_HEADERS

Loading…
Cancel
Save