diff --git a/doc/snippets/CMakeLists.txt b/doc/snippets/CMakeLists.txt index 7d1044408..4f025a56a 100644 --- a/doc/snippets/CMakeLists.txt +++ b/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") diff --git a/doc/snippets/MagnumAnimation-custom.cpp b/doc/snippets/MagnumAnimation-custom.cpp new file mode 100644 index 000000000..2497eab22 --- /dev/null +++ b/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š + + 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 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 diff --git a/doc/snippets/MagnumAnimation.cpp b/doc/snippets/MagnumAnimation.cpp index e1761108d..b778a22df 100644 --- a/doc/snippets/MagnumAnimation.cpp +++ b/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 translation; +const Animation::TrackView rotation; +const Animation::TrackView scaling; + +Vector3 objectScaling; +Quaternion objectRotation; +Vector3 objectTranslation; + +Animation::Player player; +player.add(scaling, objectScaling) + .add(rotation, objectRotation) + .add(translation, objectTranslation); +/* [Player-usage] */ +} + +{ +const Animation::TrackView translation; +const Animation::TrackView rotation; +const Animation::TrackView 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 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 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 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 player; // player we want to control + Timeline timeline; +} data; + +Animation::Track 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 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 player; // player we want to control + +Animation::Track 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 timer; +timer.addWithCallback(timeTrack, + [](const Float&, const Float& time, Animation::Player& 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 jump{{ diff --git a/src/Magnum/Animation/Animation.h b/src/Magnum/Animation/Animation.h index d2792b545..eee5cdf6b 100644 --- a/src/Magnum/Animation/Animation.h +++ b/src/Magnum/Animation/Animation.h @@ -43,6 +43,8 @@ template using ResultOf = typename Implementation::ResultTraits::Typ enum class Interpolation: UnsignedByte; enum class Extrapolation: UnsignedByte; +template class Player; + template> class Track; template class TrackViewStorage; template> class TrackView; diff --git a/src/Magnum/Animation/CMakeLists.txt b/src/Magnum/Animation/CMakeLists.txt index 1bd1d535a..0b8a316d6 100644 --- a/src/Magnum/Animation/CMakeLists.txt +++ b/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 diff --git a/src/Magnum/Animation/Player.cpp b/src/Magnum/Animation/Player.cpp new file mode 100644 index 000000000..d245a4348 --- /dev/null +++ b/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š + + 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(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; +template class MAGNUM_EXPORT_HPP Player; +#endif + +}} diff --git a/src/Magnum/Animation/Player.h b/src/Magnum/Animation/Player.h new file mode 100644 index 000000000..64165c5b6 --- /dev/null +++ b/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š + + 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 +#include + +#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 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" +- @ref Player "Player" + +@experimental +*/ +template 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(*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&) = delete; + + /** @brief Move constructor */ + Player(Player&&) noexcept; + + ~Player(); + + /** @brief Copying is not allowed */ + Player& operator=(const Player&) = delete; + + /** @brief Move assignment */ + Player& operator=(Player&&) 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 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& setDuration(const Math::Range1D& 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& 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& 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 Player& add(const TrackView& 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 Player& add(const Track& track, R& destination) { + return add(TrackView{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 Player& addWithCallback(const TrackView& 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 Player& addWithCallback(const TrackView& 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 Player& addWithCallback(const Track& track, void(*callback)(const K&, const R&, void*), void* userData = nullptr); + #else /* See above why */ + template Player& addWithCallback(const Track& track, Callback callback, void* userData = nullptr) { + return addWithCallback(TrackView{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 Player& addWithCallback(const TrackView& track, void(*callback)(const K&, const R&, U&), U& userData); + #else /* See above why */ + template Player& addWithCallback(const TrackView& 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 Player& addWithCallback(const Track& track, void(*callback)(const K&, const R&, U&), U& userData); + #else /* See above why */ + template Player& addWithCallback(const Track& track, Callback callback, U& userData) { + return addWithCallback(TrackView{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 Player& addWithCallbackOnChange(const TrackView& track, void(*callback)(const K&, const R&, void*), R& destination, void* userData = nullptr); + #else /* See above why */ + template Player& addWithCallbackOnChange(const TrackView& 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 Player& addWithCallbackOnChange(const Track& track, void(*callback)(const K&, const R&, void*), R& destination, void* userData = nullptr); + #else /* See above why */ + template Player& addWithCallbackOnChange(const Track& track, Callback callback, R& destination, void* userData = nullptr) { + return addWithCallbackOnChange(TrackView{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 Player& addWithCallbackOnChange(const TrackView& track, void(*callback)(const K&, const R&, void*), R& destination, U& userData); + #else /* See above why */ + template Player& addWithCallbackOnChange(const TrackView& 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 Player& addWithCallbackOnChange(const Track& track, void(*callback)(const K&, const R&, void*), R& destination, U& userData); + #else + template Player& addWithCallbackOnChange(const Track& track, Callback callback, R& destination, U& userData) { + return addWithCallbackOnChange(TrackView{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& 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& 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& 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& 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& advance(T time); + + private: + struct Track; + + Player& addInternal(const TrackViewStorage& track, void (*advancer)(const TrackViewStorage&, K, std::size_t&, void*, void(*)(), void*), void* destination, void(*userCallback)(), void* userCallbackData); + + std::vector _tracks; + Math::Range1D _duration; + UnsignedInt _playCount{1}; + State _state{State::Stopped}; + T _startTime{}, _pauseTime{}; + Scaler _scaler; +}; + +template template Player& Player::add(const TrackView& track, R& destination) { + return addInternal(track, + [](const TrackViewStorage& track, K key, std::size_t& hint, void* destination, void(*)(), void*) { + *static_cast(destination) = static_cast&>(track).at(key, hint); + }, &destination, nullptr, nullptr); +} + +#ifndef DOXYGEN_GENERATING_OUTPUT +template template Player& Player::addWithCallback(const TrackView& track, Callback callback, void* userData) { + auto callbackPtr = static_cast(callback); + return addInternal(track, + [](const TrackViewStorage& track, K key, std::size_t& hint, void*, void(*callback)(), void* userData) { + reinterpret_cast(callback)(key, static_cast&>(track).at(key, hint), userData); + }, nullptr, reinterpret_cast(callbackPtr), userData); +} + +template template Player& Player::addWithCallback(const TrackView& track, Callback callback, U& userData) { + auto callbackPtr = static_cast(callback); + return addInternal(track, + [](const TrackViewStorage& track, K key, std::size_t& hint, void*, void(*callback)(), void* userData) { + reinterpret_cast(callback)(key, static_cast&>(track).at(key, hint), *static_cast(userData)); + }, nullptr, reinterpret_cast(callbackPtr), &userData); +} + +template template Player& Player::addWithCallbackOnChange(const TrackView& track, Callback callback, R& destination, void* userData) { + auto callbackPtr = static_cast(callback); + return addInternal(track, + [](const TrackViewStorage& track, K key, std::size_t& hint, void* destination, void(*callback)(), void* userData) { + R result = static_cast&>(track).at(key, hint); + if(result == *static_cast(destination)) return; + reinterpret_cast(callback)(key, result, userData); + *static_cast(destination) = result; + }, &destination, reinterpret_cast(callbackPtr), userData); +} + +template template Player& Player::addWithCallbackOnChange(const TrackView& track, Callback callback, R& destination, U& userData) { + auto callbackPtr = static_cast(callback); + return addInternal(track, + [](const TrackViewStorage& track, K key, std::size_t& hint, void* destination, void(*callback)(), void* userData) { + R result = static_cast&>(track).at(key, hint); + if(result == *static_cast(destination)) return; + reinterpret_cast(callback)(key, result, *static_cast(userData)); + *static_cast(destination) = result; + }, &destination, reinterpret_cast(callbackPtr), &userData); +} +#endif + +#if defined(CORRADE_TARGET_WINDOWS) && !defined(__MINGW32__) +extern template class MAGNUM_EXPORT Player; +extern template class MAGNUM_EXPORT Player; +#endif + +}} + +#endif diff --git a/src/Magnum/Animation/Player.hpp b/src/Magnum/Animation/Player.hpp new file mode 100644 index 000000000..fcd17e1f8 --- /dev/null +++ b/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š + + 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 struct DefaultScaler { + static std::pair 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 { + static std::pair 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 struct Player::Track { + /* Not sure why is this still needed for emplace_back(). It's 2018, + COME ON ¯\_(ツ)_/¯ */ + /*implicit*/ Track(const TrackViewStorage& track, void (*advancer)(const TrackViewStorage&, 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 track; + void (*advancer)(const TrackViewStorage&, K, std::size_t&, void*, void(*)(), void*); + void* destination; + void(*userCallback)(); + void* userCallbackData; + std::size_t hint; +}; +#endif + +template Player::Player(Player&&) noexcept = default; + +template Player& Player::operator=(Player&&) noexcept = default; + +template Player::Player(): Player{Implementation::DefaultScaler::scale} {} + +template Player::Player(Scaler scaler): _scaler{scaler} {} + +template Player::~Player() = default; + +template bool Player::isEmpty() const { + return _tracks.empty(); +} + +template std::size_t Player::size() const { + return _tracks.size(); +} + +template const TrackViewStorage& Player::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 Player& Player::addInternal(const TrackViewStorage& track, void(*const advancer)(const TrackViewStorage&, K, std::size_t&, void*, void(*)(), void*), void* const destination, void(*const userCallback)(), void* const userCallbackData) { + if(_tracks.empty() && _duration == Math::Range1D{}) + _duration = track.duration(); + else + _duration = Math::join(track.duration(), _duration); + _tracks.emplace_back(track, advancer, destination, userCallback, userCallbackData, 0); + return *this; +} + +template Player& Player::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 Player& Player::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 Player& Player::stop() { + _state = State::Stopped; + return *this; +} + +template Player& Player::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 Player& Player::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 diff --git a/src/Magnum/Animation/Test/Benchmark.cpp b/src/Magnum/Animation/Test/Benchmark.cpp index 66e69db61..6ef9180ed 100644 --- a/src/Magnum/Animation/Test/Benchmark.cpp +++ b/src/Magnum/Animation/Test/Benchmark.cpp @@ -25,7 +25,7 @@ #include -#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 _keys; Containers::Array _values; Containers::Array> _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{DataSize}; _values = Containers::Array{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 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 empty{nullptr, nullptr, Math::select}; + Int result{}; + Player 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 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 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) diff --git a/src/Magnum/Animation/Test/CMakeLists.txt b/src/Magnum/Animation/Test/CMakeLists.txt index 8813397d0..d6c58d1a1 100644 --- a/src/Magnum/Animation/Test/CMakeLists.txt +++ b/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") diff --git a/src/Magnum/Animation/Test/PlayerCustomTest.cpp b/src/Magnum/Animation/Test/PlayerCustomTest.cpp new file mode 100644 index 000000000..38c19b27c --- /dev/null +++ b/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š + + 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 + +#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 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 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) diff --git a/src/Magnum/Animation/Test/PlayerTest.cpp b/src/Magnum/Animation/Test/PlayerTest.cpp new file mode 100644 index 000000000..e90b16f5f --- /dev/null +++ b/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š + + 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 +#include +#include + +#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 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 Track{{ + {1.0f, 1.5f}, + {2.5f, 3.0f}, + {3.0f, 5.0f}, + {4.0f, 2.0f} + }, Math::lerp}; +} + +void PlayerTest::construct() { + Animation::Track track2{{ + {0.5f, 42}, + {3.0f, 1337}, + {3.5f, -17} + }, Math::select}; + + Float value = -1.0f; + Int value2 = -1; + Player 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 track2{{ + {0.5f, 42}, + {3.0f, 1337}, + {3.5f, -17} + }, Math::select}; + + Float value = -1.0f; + Int value2 = -1; + Player 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, const Player&>{})); + CORRADE_VERIFY(!(std::is_assignable, const Player&>{})); +} + +void PlayerTest::constructMove() { + Animation::Track track2{{ + {0.5f, 42}, + {3.0f, 1337}, + {3.5f, -17} + }, Math::select}; + + Float value = -1.0f; + Int value2 = -1; + Player 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 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 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 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 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 player; + player.track(0); + + CORRADE_COMPARE(out.str(), "Animation::Player::track(): index out of range\n"); +} + +void PlayerTest::advanceNotRunning() { + Float value = -1.0f; + Player 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 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 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 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 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 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 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 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 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 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 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 player; + player.addWithCallback(Track, [](const Float&, const Float& value, void* userData) { + static_cast(userData)->value = value; + ++static_cast(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 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 player; + player.addWithCallbackOnChange(Track, [](const Float&, const Float& value, void* userData) { + static_cast(userData)->value = value; + ++static_cast(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 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 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 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) diff --git a/src/Magnum/Animation/Track.h b/src/Magnum/Animation/Track.h index 5a7f7e098..ad118938e 100644 --- a/src/Magnum/Animation/Track.h +++ b/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 diff --git a/src/Magnum/CMakeLists.txt b/src/Magnum/CMakeLists.txt index 10e23f106..944da0a00 100644 --- a/src/Magnum/CMakeLists.txt +++ b/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