Files
Tween/Tween.h
2026-02-18 16:40:11 +03:00

525 lines
17 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#pragma once
#include "Easing.h"
#include "TimeWarp.h"
#include <functional>
//----------------------------------------------------------------------
// EASING HELPERS
//----------------------------------------------------------------------
namespace EasingFunc {
// typedef float (*eFunc)(float); // тип easing-функции, как у Вас
// Обёртка с модификатором времени (вызывается Tween'ом)
inline float _easingWithWarp(float t, const TimeWarp* warp, eFunc baseEasing) {
if (!warp || !baseEasing) return t;
float warpedT = warp->apply(t);
return baseEasing(warpedT);
}
// Создать обёртку с модификатором времени
inline std::function<float(float)> withTimeWarp(eFunc easing, const TimeWarp& warp) {
return [easing, &warp](float t) -> float {
return _easingWithWarp(t, &warp, easing);
};
}
}; // namespace EasingFunc
//----------------------------------------------------------------------
// LISTENERИНТЕРФЕЙС
//----------------------------------------------------------------------
class Tween; // forward
class TweenListener {
public:
virtual void onTweenFinished(Tween& tween) = 0;
virtual void onTweenUpdate(Tween& tween) = 0;
virtual ~TweenListener() = default;
};
//----------------------------------------------------------------------
// TWEEN
//----------------------------------------------------------------------
class Tween {
private:
static inline Tween* head = nullptr;
static inline Tween* last = nullptr;
Tween* next = nullptr;
TweenListener* listener = nullptr;
public:
Tween(const Tween&) = delete;
Tween& operator=(const Tween&) = delete;
Tween(Tween&& other) noexcept;
Tween& operator=(Tween&& other) noexcept;
Tween(uint16_t fps = 30) {
setFps(fps);
frameRateTimer = millis();
if (Tween::head == nullptr) {
Tween::head = this;
}
if (last != nullptr) {
last->next = this;
}
last = this;
}
static void tick() {
uint32_t now = millis();
Tween* current = Tween::head;
while (current != nullptr) {
current->update(now);
current = current->next;
}
}
void setListener(TweenListener* l) { listener = l; }
TweenListener* getListener() const { return listener; }
///////
private:
void unlinkFromList() {
if (Tween::head == nullptr) return;
if (this == Tween::head) {
Tween::head = this->next;
if (this == Tween::last) Tween::last = nullptr;
} else {
Tween* prev = Tween::head;
while (prev && prev->next != this) prev = prev->next;
if (prev) prev->next = this->next;
if (this == Tween::last) Tween::last = prev;
}
this->next = nullptr;
}
uint16_t frameTime;
float dt;
uint32_t oldMillis;
uint32_t frameRateTimer;
float from;
float to;
float duration;
EasingFunc::eFunc easing = nullptr;
std::function<float(float)> easingFunc = nullptr;
float progress;
bool isPlayingF;
bool triggerLastTick = false; // Флаг последнего тика
public:
float current;
void dtTick() {
uint32_t loopStartTime = millis();
dt = (loopStartTime - oldMillis) / 1000.0;
oldMillis = loopStartTime;
}
void update(uint32_t now) {
if (now - frameRateTimer > frameTime) {
dt = (now - oldMillis) / 1000.0;
oldMillis = now;
if (!isPlayingF && triggerLastTick) {
triggerLastTick = false;
current = to;
if (listener != nullptr) listener->onTweenFinished(*this);
return;
}
if (!isPlayingF) return;
progress = constrain(progress + dt, 0, duration);
float normProgress = constrain(progress / duration, 0, 1);
if (easingFunc) {
current = Tween::lerp(from, to, easingFunc(normProgress));
} else if (easing) {
current = Tween::lerp(from, to, easing(normProgress));
} else {
current = Tween::lerp(from, to, normProgress);
}
if (progress >= duration) {
current = to;
triggerLastTick = true;
isPlayingF = false;
}
if (listener != nullptr) listener->onTweenUpdate(*this);
frameRateTimer = now;
}
}
float getProgress() {
return constrain(progress / duration, 0, 1);
}
void start(float from_, float to_, uint16_t durationMs, EasingFunc::eFunc easing_ = nullptr) {
// если easing не задан, используем линейную
easing = (easing_ != nullptr) ? easing_ : EasingFunc::easeLinear;
easingFunc = nullptr; // сбрасываем функцию с TimeWarp
from = from_;
to = to_;
duration = durationMs / 1000.0f;
progress = 0;
current = from;
uint32_t now = millis();
oldMillis = now;
frameRateTimer = now;
isPlayingF = true;
triggerLastTick = false;
}
void start(float from_, float to_, uint16_t durationMs, std::function<float(float)> easingFunc_) {
easing = nullptr; // сбрасываем обычную функцию
easingFunc = easingFunc_;
from = from_;
to = to_;
duration = durationMs / 1000.0f;
progress = 0;
current = from;
uint32_t now = millis();
oldMillis = now;
frameRateTimer = now;
isPlayingF = true;
triggerLastTick = false;
}
void resetToStart() {
progress = 0;
}
void stop() {
isPlayingF = false;
current = to;
triggerLastTick = false;
}
// =========================================================
// Force Update API - принудительное обновление параметров
// =========================================================
// Принудительное обновление начальной точки (from) без сброса прогресса
// Пересчитывает current на основе нового from и текущего прогресса
void forceUpdateFrom(float newFrom) {
from = newFrom;
if (isPlayingF && duration > 0.0f) {
// Пересчитываем current на основе нового from и текущего прогресса
float normProgress = constrain(progress / duration, 0, 1);
if (easingFunc) {
current = Tween::lerp(from, to, easingFunc(normProgress));
} else if (easing) {
current = Tween::lerp(from, to, easing(normProgress));
} else {
current = Tween::lerp(from, to, normProgress);
}
} else {
// Если не играет или duration = 0, просто обновляем from
current = from;
}
}
// Принудительное обновление конечной точки (to) без сброса прогресса
// Пересчитывает current на основе нового to и текущего прогресса
void forceUpdateTo(float newTo) {
to = newTo;
if (isPlayingF && duration > 0.0f) {
// Пересчитываем current на основе нового to и текущего прогресса
float normProgress = constrain(progress / duration, 0, 1);
if (easingFunc) {
current = Tween::lerp(from, to, easingFunc(normProgress));
} else if (easing) {
current = Tween::lerp(from, to, easing(normProgress));
} else {
current = Tween::lerp(from, to, normProgress);
}
} else {
// Если не играет или duration = 0, просто обновляем to и current
current = to;
}
}
// Принудительное обновление обеих точек (from и to) без сброса прогресса
// Пересчитывает current на основе новых точек и текущего прогресса
void forceUpdateFromTo(float newFrom, float newTo) {
from = newFrom;
to = newTo;
if (isPlayingF && duration > 0.0f) {
// Пересчитываем current на основе новых точек и текущего прогресса
float normProgress = constrain(progress / duration, 0, 1);
if (easingFunc) {
current = Tween::lerp(from, to, easingFunc(normProgress));
} else if (easing) {
current = Tween::lerp(from, to, easing(normProgress));
} else {
current = Tween::lerp(from, to, normProgress);
}
} else {
// Если не играет или duration = 0, устанавливаем current в from
current = from;
}
}
// Принудительное обновление прогресса (progress) без сброса других параметров
// Пересчитывает current на основе нового прогресса
// newProgress - абсолютное значение прогресса (0..duration)
void forceUpdateProgress(float newProgress) {
progress = constrain(newProgress, 0, duration);
if (duration > 0.0f) {
// Пересчитываем current на основе нового прогресса
float normProgress = constrain(progress / duration, 0, 1);
if (easingFunc) {
current = Tween::lerp(from, to, easingFunc(normProgress));
} else if (easing) {
current = Tween::lerp(from, to, easing(normProgress));
} else {
current = Tween::lerp(from, to, normProgress);
}
} else {
// Если duration = 0, устанавливаем current в to
current = to;
}
}
bool isPlaying() const {
return isPlayingF || triggerLastTick; // Учитываем последний тик
}
void setFps(uint16_t fps){
frameTime = 1000 / fps;
}
uint16_t getFrameTime() const { return frameTime; }
float getFrom() const {
return from;
}
float getTo() const {
return to;
}
static float integrateEasing(EasingFunc::eFunc easing, int steps = 100) {
float sum = 0;
for (int i = 0; i <= steps; ++i) {
float t = i / (float)steps;
sum += easing(t);
}
return sum / (steps + 1); // Среднее значение функции
}
static float lerp(float a, float b, float t) {
return a + (b - a) * t;
}
};
//----------------------------------------------------------------------
// TWEEN MOVE SEMANTICS
//----------------------------------------------------------------------
inline Tween::Tween(Tween&& other) noexcept {
unlinkFromList();
next = other.next;
listener = other.listener;
frameTime = other.frameTime;
dt = other.dt;
oldMillis = other.oldMillis;
frameRateTimer = other.frameRateTimer;
from = other.from;
to = other.to;
duration = other.duration;
easing = other.easing;
easingFunc = std::move(other.easingFunc);
progress = other.progress;
isPlayingF = other.isPlayingF;
triggerLastTick = other.triggerLastTick;
current = other.current;
other.next = nullptr;
other.listener = nullptr;
other.easing = nullptr;
other.easingFunc = nullptr;
other.isPlayingF = false;
other.triggerLastTick = false;
if (Tween::head == &other) Tween::head = this;
else {
Tween* prev = Tween::head;
while (prev && prev->next != &other) prev = prev->next;
if (prev) prev->next = this;
}
if (Tween::last == &other) Tween::last = this;
}
inline Tween& Tween::operator=(Tween&& other) noexcept {
if (this != &other) {
unlinkFromList();
next = other.next;
listener = other.listener;
frameTime = other.frameTime;
dt = other.dt;
oldMillis = other.oldMillis;
frameRateTimer = other.frameRateTimer;
from = other.from;
to = other.to;
duration = other.duration;
easing = other.easing;
easingFunc = std::move(other.easingFunc);
progress = other.progress;
isPlayingF = other.isPlayingF;
triggerLastTick = other.triggerLastTick;
current = other.current;
other.next = nullptr;
other.listener = nullptr;
other.easing = nullptr;
other.easingFunc = nullptr;
other.isPlayingF = false;
other.triggerLastTick = false;
if (Tween::head == &other) Tween::head = this;
else {
Tween* prev = Tween::head;
while (prev && prev->next != &other) prev = prev->next;
if (prev) prev->next = this;
}
if (Tween::last == &other) Tween::last = this;
}
return *this;
}
//----------------------------------------------------------------------
// ANIMATION CHAIN
//----------------------------------------------------------------------
class AnimationChain : public TweenListener {
public:
using FromFunc = std::function<float()>;
struct Anim {
float from;
FromFunc fromFunc = nullptr;
float to;
uint16_t duration; // мс
EasingFunc::eFunc easing = nullptr;
std::function<float(float)> easingFunc = nullptr;
};
private:
std::vector<Anim> animations;
Tween tween; // единственный Tween
size_t currentIndex = 0;
bool chainPlaying = false;
float* currentUserPtr = nullptr;
void launchCurrent() {
const Anim& a = animations[currentIndex];
if (a.easingFunc) {
tween.start(a.fromFunc ? a.fromFunc() : a.from, a.to, a.duration, a.easingFunc);
} else {
tween.start(a.fromFunc ? a.fromFunc() : a.from, a.to, a.duration, a.easing);
}
}
public:
float current = 0; // актуальное значение наружу
explicit AnimationChain(uint16_t fps = 30) : tween(fps) {
tween.setListener(this); // подписываемся
}
//------------------------------------------------------------------
// API ЦЕПОЧКИ
//------------------------------------------------------------------
void addAnim(float from, float to, uint16_t duration, EasingFunc::eFunc easing = nullptr) {
animations.push_back({from, {}, to, duration, easing, nullptr});
}
void addAnim(FromFunc fromF, float to, uint16_t duration, EasingFunc::eFunc easing = nullptr) {
animations.push_back({0, fromF, to, duration, easing, nullptr});
}
void addAnim(float from, float to, uint16_t duration, std::function<float(float)> easingFunc) {
animations.push_back({from, {}, to, duration, nullptr, easingFunc});
}
void addAnim(FromFunc fromF, float to, uint16_t duration, std::function<float(float)> easingFunc) {
animations.push_back({0, fromF, to, duration, nullptr, easingFunc});
}
void clear() { animations.clear(); }
void start() {
if (animations.empty()) return;
currentIndex = 0;
chainPlaying = true;
launchCurrent();
// Serial.printf("Chain start [%d]\n", currentIndex);
}
void stop() {
chainPlaying = false;
tween.stop();
}
void setCustomParam(float* val) {
currentUserPtr = val;
}
void resetCustomParam() {
currentUserPtr = nullptr;
}
bool isStaticAnim() {
if(!isChainPlaying()) return true;
const Anim& anim = animations[currentIndex];
float from = anim.fromFunc ? anim.fromFunc() : anim.from;
float to = anim.to;
return from == to;
}
Tween& getTween() { return tween; }
//------------------------------------------------------------------
// STATE
//------------------------------------------------------------------
bool isChainPlaying() const { return chainPlaying; }
bool isAnimPlaying(size_t index) const {
return chainPlaying && index == currentIndex && tween.isPlaying();
}
size_t getCurrentIndex() const { return currentIndex; }
// void setCurrentIndex(size_t index){ currentIndex = index; }
private:
//------------------------------------------------------------------
// LISTENER CALLBACK
//------------------------------------------------------------------
void onTweenFinished(Tween& /*t*/) override {
++currentIndex;
if (currentIndex < animations.size()) {
// Serial.printf("Chain next [%d]\n", currentIndex);
launchCurrent();
} else {
// Serial.printf("Chain stop [x]\n", currentIndex);
chainPlaying = false; // вся цепочка закончилась
}
}
void onTweenUpdate(Tween& /*t*/) override {
if (chainPlaying) {
current = tween.current; // даём наружу актуальное значение
if(currentUserPtr != nullptr) *currentUserPtr = current;
} else {
current = *currentUserPtr;
}
}
};