use super::{Tween, TweenState, Lerp};
use super::easing::Easing;
use glam::{Vec2, Vec3, Vec4};
pub struct SequenceStep<T: Lerp + std::fmt::Debug> {
pub tween: TweenState<T>,
pub overlap: f32,
pub(crate) start_t: f32,
}
pub struct TweenSequence<T: Lerp + Clone + std::fmt::Debug> {
pub steps: Vec<SequenceStep<T>>,
pub elapsed: f32,
pub looping: bool,
pub duration: f32,
pub done: bool,
default_val: T,
}
impl<T: Lerp + Clone + std::fmt::Debug> TweenSequence<T> {
pub fn new(steps: Vec<(Tween<T>, f32)>, default_val: T, looping: bool) -> Self {
let mut seq_steps: Vec<SequenceStep<T>> = Vec::with_capacity(steps.len());
let mut cursor = 0.0_f32;
for (tween, overlap) in steps {
let start_t = cursor - overlap;
cursor = start_t + tween.duration;
seq_steps.push(SequenceStep {
tween: TweenState::new(tween),
overlap,
start_t,
});
}
let duration = cursor;
Self { steps: seq_steps, elapsed: 0.0, looping, duration, done: false, default_val }
}
pub fn tick(&mut self, dt: f32) -> T {
self.elapsed += dt;
if self.looping && self.elapsed >= self.duration {
self.elapsed -= self.duration;
}
self.done = !self.looping && self.elapsed >= self.duration;
self.current_value()
}
pub fn current_value(&self) -> T {
let t = if self.looping {
self.elapsed % self.duration.max(f32::EPSILON)
} else {
self.elapsed.min(self.duration)
};
let mut active: Option<usize> = None;
for (i, step) in self.steps.iter().enumerate() {
if t >= step.start_t {
active = Some(i);
}
}
if let Some(idx) = active {
let step = &self.steps[idx];
let local_t = (t - step.start_t).max(0.0);
step.tween.tween.sample(local_t)
} else {
self.default_val.clone()
}
}
pub fn reset(&mut self) {
self.elapsed = 0.0;
self.done = false;
}
pub fn progress(&self) -> f32 {
(self.elapsed / self.duration.max(f32::EPSILON)).clamp(0.0, 1.0)
}
}
pub struct SequenceBuilder<T: Lerp + Clone + std::fmt::Debug> {
steps: Vec<(Tween<T>, f32)>,
default_val: T,
looping: bool,
next_overlap: f32,
}
impl<T: Lerp + Clone + std::fmt::Debug> SequenceBuilder<T> {
pub fn new(default_val: T) -> Self {
Self { steps: Vec::new(), default_val, looping: false, next_overlap: 0.0 }
}
pub fn then(mut self, from: T, to: T, duration: f32, easing: Easing) -> Self {
let overlap = self.next_overlap;
self.next_overlap = 0.0;
self.steps.push((Tween::new(from, to, duration, easing), overlap));
self
}
pub fn overlap(mut self, seconds: f32) -> Self {
self.next_overlap = seconds;
self
}
pub fn wait(mut self, seconds: f32) -> Self {
self.next_overlap = -seconds;
self
}
pub fn looping(mut self, looping: bool) -> Self {
self.looping = looping;
self
}
pub fn build(self) -> TweenSequence<T> {
TweenSequence::new(self.steps, self.default_val, self.looping)
}
}
pub struct TweenTimeline {
pub tracks: std::collections::HashMap<String, TweenSequence<f32>>,
pub elapsed: f32,
pub looping: bool,
duration: f32,
pub done: bool,
}
impl TweenTimeline {
pub fn new(looping: bool) -> Self {
Self {
tracks: std::collections::HashMap::new(),
elapsed: 0.0,
looping,
duration: 0.0,
done: false,
}
}
pub fn add_track(&mut self, name: impl Into<String>, seq: TweenSequence<f32>) {
self.duration = self.duration.max(seq.duration);
self.tracks.insert(name.into(), seq);
}
pub fn tick(&mut self, dt: f32) {
self.elapsed += dt;
if self.looping && self.elapsed >= self.duration {
self.elapsed -= self.duration;
for track in self.tracks.values_mut() { track.reset(); }
}
self.done = !self.looping && self.elapsed >= self.duration;
for track in self.tracks.values_mut() {
track.tick(dt);
}
}
pub fn get(&self, name: &str) -> f32 {
self.tracks.get(name).map(|t| t.current_value()).unwrap_or(0.0)
}
pub fn reset(&mut self) {
self.elapsed = 0.0;
self.done = false;
for track in self.tracks.values_mut() { track.reset(); }
}
pub fn progress(&self) -> f32 {
(self.elapsed / self.duration.max(f32::EPSILON)).clamp(0.0, 1.0)
}
}
impl TweenTimeline {
pub fn damage_flash(intensity: f32) -> Self {
let mut tl = Self::new(false);
let flash_seq = SequenceBuilder::new(0.0f32)
.then(intensity, 0.0, 0.3, Easing::EaseOutExpo)
.build();
tl.add_track("flash", flash_seq);
let scale_seq = SequenceBuilder::new(1.0f32)
.then(1.0 + intensity * 0.1, 1.0, 0.25, Easing::EaseOutBack)
.build();
tl.add_track("scale", scale_seq);
tl
}
pub fn level_up() -> Self {
let mut tl = Self::new(false);
let flash = SequenceBuilder::new(0.0f32)
.then(1.5, 1.0, 0.15, Easing::EaseOutExpo)
.then(1.0, 1.0, 1.5, Easing::Linear)
.then(1.0, 0.0, 0.5, Easing::EaseInQuad)
.build();
tl.add_track("brightness", flash);
let hue_shift = SequenceBuilder::new(0.0f32)
.then(0.0, 360.0, 2.0, Easing::Linear)
.build();
tl.add_track("hue_shift", hue_shift);
let bloom = SequenceBuilder::new(1.0f32)
.then(3.0, 1.0, 0.8, Easing::EaseOutCubic)
.build();
tl.add_track("bloom", bloom);
tl
}
pub fn boss_entrance() -> Self {
let mut tl = Self::new(false);
let vignette = SequenceBuilder::new(0.15f32)
.then(0.15, 0.9, 0.8, Easing::EaseInCubic)
.then(0.9, 0.9, 1.2, Easing::Linear)
.then(0.9, 0.2, 0.6, Easing::EaseOutExpo)
.build();
tl.add_track("vignette", vignette);
let chromatic = SequenceBuilder::new(0.002f32)
.then(0.002, 0.02, 0.8, Easing::EaseInExpo)
.then(0.02, 0.001, 0.4, Easing::EaseOutCubic)
.build();
tl.add_track("chromatic", chromatic);
let saturation = SequenceBuilder::new(1.0f32)
.then(1.0, 0.0, 0.8, Easing::EaseInCubic)
.then(0.0, 1.2, 0.6, Easing::EaseOutBack)
.build();
tl.add_track("saturation", saturation);
tl
}
pub fn death_sequence() -> Self {
let mut tl = Self::new(false);
let saturation = SequenceBuilder::new(1.0f32)
.then(1.0, 0.0, 2.5, Easing::EaseInCubic)
.build();
tl.add_track("saturation", saturation);
let brightness = SequenceBuilder::new(0.0f32)
.then(0.0, -0.8, 3.0, Easing::EaseInQuart)
.build();
tl.add_track("brightness", brightness);
let vignette = SequenceBuilder::new(0.15f32)
.then(0.15, 1.0, 3.0, Easing::EaseInCubic)
.build();
tl.add_track("vignette", vignette);
let chromatic = SequenceBuilder::new(0.002f32)
.then(0.002, 0.015, 1.5, Easing::EaseInQuad)
.build();
tl.add_track("chromatic", chromatic);
tl
}
pub fn heal_pulse(amount_fraction: f32) -> Self {
let mut tl = Self::new(false);
let green = SequenceBuilder::new(1.0f32)
.then(1.0, 1.0 + amount_fraction * 0.4, 0.15, Easing::EaseOutExpo)
.then(1.0 + amount_fraction * 0.4, 1.0, 0.4, Easing::EaseInQuad)
.build();
tl.add_track("green_tint", green);
let bloom = SequenceBuilder::new(1.0f32)
.then(1.0, 1.8, 0.15, Easing::EaseOutExpo)
.then(1.8, 1.0, 0.5, Easing::EaseOutCubic)
.build();
tl.add_track("bloom", bloom);
tl
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sequence_basic() {
let mut seq = SequenceBuilder::new(0.0f32)
.then(0.0, 1.0, 1.0, Easing::Linear)
.then(1.0, 2.0, 1.0, Easing::Linear)
.build();
let v0 = seq.tick(0.0);
assert!((v0 - 0.0).abs() < 1e-4);
seq.elapsed = 0.5;
let v_mid = seq.current_value();
assert!((v_mid - 0.5).abs() < 1e-4, "expected 0.5 got {v_mid}");
seq.elapsed = 1.5;
let v2 = seq.current_value();
assert!((v2 - 1.5).abs() < 1e-4, "expected 1.5 got {v2}");
}
#[test]
fn timeline_damage_flash() {
let mut tl = TweenTimeline::damage_flash(1.0);
let flash_start = tl.get("flash");
assert!((flash_start - 1.0).abs() < 0.01, "flash starts at intensity");
tl.tick(0.3);
let flash_end = tl.get("flash");
assert!(flash_end < 0.2, "flash should decay quickly");
}
#[test]
fn builder_wait() {
let seq = SequenceBuilder::new(0.0f32)
.then(0.0, 1.0, 0.5, Easing::Linear)
.wait(0.5)
.then(1.0, 2.0, 0.5, Easing::Linear)
.build();
assert!((seq.steps[1].start_t - 1.0).abs() < 1e-4, "second step at t=1.0");
}
}