mirror of https://github.com/mosra/magnum.git
14 changed files with 2013 additions and 4 deletions
@ -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 |
||||||
@ -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 |
||||||
|
|
||||||
|
}} |
||||||
@ -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 |
||||||
@ -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 |
||||||
@ -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) |
||||||
@ -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) |
||||||
Loading…
Reference in new issue