Dawn/archive/animation/SimpleAnimation.hpp

207 lines
5.8 KiB
C++

// Copyright (c) 2022 Dominic Masters
//
// This software is released under the MIT License.
// https://opensource.org/licenses/MIT
#pragma once
#include "Animation.hpp"
#include "easing.hpp"
#include "util/mathutils.hpp"
namespace Dawn {
template<typename T>
struct SimpleKeyframe {
float_t time;
T value;
};
template<typename T>
struct SimpleAnimation : public Animation {
protected:
/**
* Function for subclasses to be "notified" when the value has been
* modified.
*/
virtual void onValueModified() {
}
/**
* Sorts internal keyframes by their time to make them conform correctly.
*/
void sortKeyframes() {
std::sort(
this->keyframes.begin(),
this->keyframes.end(),
[](struct SimpleKeyframe<T> &a, struct SimpleKeyframe<T> &b) {
return a.time < b.time;
}
);
}
public:
std::function<float_t(const float_t)> easing = easeLinear;
T *modifies;
std::vector<struct SimpleKeyframe<T>> keyframes;
/**
* Constructs a new Simple Animation instance.
*
* @param modifies Pointer to the value that will be modified.
*/
SimpleAnimation(T *modifies) {
assertNotNull(modifies, "Modifies cannot be null");
this->modifies = modifies;
}
/**
* Adds a keyframe that will be slerped to at a given time.
*
* @param time Time the keyframe occurs.
* @param value Value at this given time.
*/
void addKeyframe(float_t time, T value) {
assertTrue(time >= 0, "Time must be >= 0");
struct SimpleKeyframe<T> keyframe;
keyframe.time = time;
keyframe.value = value;
this->duration = mathMax<float_t>(this->duration, time);
this->finished = false;
this->keyframes.push_back(keyframe);
if(time < this->duration) this->sortKeyframes();
}
/**
* Quickly add a series of keyframes. For example, if you want to have a
* keyframe series of;
* [ 1, 3, 5, 7, 9 ]
*
* And occur at times
* [ 0, 2, 4, 6, 8 ]
*
* You would pass frameTime as 2, and step as 2.
*
* @param startTime When the first keyframe occurs.
* @param frameTime At what rate do keyframes occur.
* @param start Initial value (the value at startTime).
* @param end The end value (for the last keyframe).
* @param step How to step the value.
*/
void addSequentialKeyframes(
float_t startTime,
float_t frameTime,
T start,
T end,
T step
) {
T v = start;
float_t n = startTime;
while(v != end) {
this->addKeyframe(n, v);
n += frameTime;
v += step;
}
}
/**
* Shorthand addSequentialKeyframes, assumes a step of 1 and a startTime
* of 0.
*
* @param frameTime Time between frames.
* @param start Initial value.
* @param end End value.
*/
void addSequentialKeyframes(float_t frameTime, T start, T end) {
this->addSequentialKeyframes(0, frameTime, start, end, 1);
}
/**
* Immediately sets the value, bypassing keyframes and ticks. Useful for
* setting an initial value.
*
* @param value Value to set.
*/
void setValue(T value) {
*modifies = value;
this->onValueModified();
}
void tick(float_t delta) override {
if(this->finished) return;
float_t newTime = this->time + delta;
struct SimpleKeyframe<T> *keyframeNext = nullptr;
struct SimpleKeyframe<T> *keyframeCurrent = nullptr;
// Find current and next keyframe(s)
auto itKey = this->keyframes.begin();
while(itKey != this->keyframes.end()) {
if(itKey->time > newTime) {
keyframeNext = &(*itKey);
break;
}
keyframeCurrent = &(*itKey);
++itKey;
}
// Update values
if(keyframeCurrent != nullptr && keyframeNext == nullptr) {
// "End of animation"
*this->modifies = keyframeCurrent->value;
this->onValueModified();
} else if(keyframeNext != nullptr) {
T oldValue;
float_t oldTime;
if(keyframeCurrent == nullptr) {
// "Start of animation"
oldValue = keyframeCurrent->value;
oldTime = keyframeCurrent->time;
} else {
// "Mid animation"
oldTime = this->time;
oldValue = *this->modifies;
}
// Slerp between keyframes
float_t keyframeDelta = this->easing(
(newTime - oldTime) / (keyframeNext->time - oldTime)
);
*this->modifies = oldValue + (
(keyframeNext->value - oldValue) * keyframeDelta
);
this->onValueModified();
}
// First possible frame? I think this can be done cleaner
if(this->time == 0 && keyframeCurrent->time == 0) {
*this->modifies = keyframeCurrent->value;
this->onValueModified();
}
// Update time.
this->time = newTime;
// Has the animation finished?
if(newTime < this->duration) return;
// Do we need to loop?
if(this->loop) {
this->time = 0;
return;
}
// Animation end.
this->finished = true;
this->eventAnimationEnd.invoke();
}
void clear() override {
Animation::clear();
this->keyframes.clear();
}
};
}