Files
Tween/Tween.h
2025-05-13 17:23:59 +03:00

310 lines
8.9 KiB
C++
Raw 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"
//----------------------------------------------------------------------
// EASING HELPERS
//----------------------------------------------------------------------
namespace EasingFunc {
// typedef float (*eFunc)(float); // тип easing-функции, как у Вас
// Статические данные для обёртки
static const TimeWarp* g_warp = nullptr;
static eFunc g_baseEasing = nullptr;
// Обёртка с модификатором времени (вызывается Tween'ом)
inline float _easingWithWarp(float t) {
if (!g_warp || !g_baseEasing) return t;
float warpedT = g_warp->apply(t);
return g_baseEasing(warpedT);
}
// Установить текущий модификатор времени
inline eFunc withTimeWarp(eFunc easing, const TimeWarp& warp) {
g_warp = &warp;
g_baseEasing = easing;
return _easingWithWarp;
}
}; // 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;
static inline uint32_t frameRateTimer;
Tween* next;
TweenListener* listener = nullptr;
public:
Tween(uint16_t fps = 30) {
setFps(fps);
if (Tween::head == nullptr) {
Tween::head = this;
}
if (last != nullptr) {
last->next = this;
}
last = this;
}
static void tick() {
Tween* current = Tween::head;
while (current != nullptr) {
current->update();
current = current->next;
}
}
void setListener(TweenListener* l) { listener = l; }
TweenListener* getListener() const { return listener; }
///////
private:
uint16_t frameTime;
float dt;
uint32_t oldMillis;
float from;
float to;
float duration;
EasingFunc::eFunc easing = 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() {
if (millis() - frameRateTimer > frameTime) {
uint32_t now = millis();
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);
current = Tween::lerp(from, to, easing(normProgress));
if (progress >= duration) {
current = to;
triggerLastTick = true;
isPlayingF = false;
}
if (listener != nullptr) listener->onTweenUpdate(*this);
frameRateTimer = millis();
}
}
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;
from = from_;
to = to_;
duration = durationMs / 1000.0f;
progress = 0;
current = from;
oldMillis = millis();
isPlayingF = true;
triggerLastTick = false;
}
void resetToStart() {
progress = 0;
}
void stop() {
isPlayingF = false;
current = to;
triggerLastTick = true;
}
bool isPlaying() const {
return isPlayingF || triggerLastTick; // Учитываем последний тик
}
void setFps(uint16_t fps){
frameTime = 1000 / fps;
}
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;
}
private:
// int clamp(int value, int a, int b) {
// if (a > b) {
// a ^= b;
// b ^= a;
// a ^= b;
// }
// if (value < a) return a;
// else if (value > b) return b;
// else return value;
// }
};
//----------------------------------------------------------------------
// 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;
};
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];
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});
}
void addAnim(FromFunc fromF, float to, uint16_t duration, EasingFunc::eFunc easing = nullptr) {
animations.push_back({0, fromF, to, duration, easing});
}
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;
}
//------------------------------------------------------------------
// 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;
}
}
};