Skip to main content

proof_engine/tween/
sequence.rs

1//! Tween sequences — chains and parallel groups of tweens.
2//!
3//! A `TweenSequence` runs tweens one after another with optional overlap.
4//! A `TweenTimeline` runs multiple named tracks in parallel.
5
6use super::{Tween, TweenState, Lerp};
7use super::easing::Easing;
8use glam::{Vec2, Vec3, Vec4};
9
10// ── SequenceStep ──────────────────────────────────────────────────────────────
11
12/// A single step in a sequence, which may overlap with the previous step.
13pub struct SequenceStep<T: Lerp + std::fmt::Debug> {
14    pub tween:          TweenState<T>,
15    /// Seconds of overlap with the previous step (negative = gap).
16    pub overlap:        f32,
17    /// Start time within the sequence (computed by SequenceBuilder).
18    pub(crate) start_t: f32,
19}
20
21// ── TweenSequence ─────────────────────────────────────────────────────────────
22
23/// A sequence of tweens played one after another with optional overlap.
24///
25/// The sequence tracks its own clock and drives one step at a time.
26/// The current value is the output of the currently active step.
27pub struct TweenSequence<T: Lerp + Clone + std::fmt::Debug> {
28    pub steps:    Vec<SequenceStep<T>>,
29    pub elapsed:  f32,
30    pub looping:  bool,
31    pub duration: f32,
32    pub done:     bool,
33    default_val:  T,
34}
35
36impl<T: Lerp + Clone + std::fmt::Debug> TweenSequence<T> {
37    /// Build a sequence from a list of (tween, overlap) pairs.
38    pub fn new(steps: Vec<(Tween<T>, f32)>, default_val: T, looping: bool) -> Self {
39        let mut seq_steps: Vec<SequenceStep<T>> = Vec::with_capacity(steps.len());
40        let mut cursor = 0.0_f32;
41        for (tween, overlap) in steps {
42            let start_t = cursor - overlap;
43            cursor = start_t + tween.duration;
44            seq_steps.push(SequenceStep {
45                tween: TweenState::new(tween),
46                overlap,
47                start_t,
48            });
49        }
50        let duration = cursor;
51        Self { steps: seq_steps, elapsed: 0.0, looping, duration, done: false, default_val }
52    }
53
54    /// Advance the sequence by `dt` and return the current interpolated value.
55    pub fn tick(&mut self, dt: f32) -> T {
56        self.elapsed += dt;
57        if self.looping && self.elapsed >= self.duration {
58            self.elapsed -= self.duration;
59        }
60        self.done = !self.looping && self.elapsed >= self.duration;
61        self.current_value()
62    }
63
64    /// Current value based on elapsed time, without advancing.
65    pub fn current_value(&self) -> T {
66        let t = if self.looping {
67            self.elapsed % self.duration.max(f32::EPSILON)
68        } else {
69            self.elapsed.min(self.duration)
70        };
71
72        // Find the active step — the last step that has started
73        let mut active: Option<usize> = None;
74        for (i, step) in self.steps.iter().enumerate() {
75            if t >= step.start_t {
76                active = Some(i);
77            }
78        }
79
80        if let Some(idx) = active {
81            let step = &self.steps[idx];
82            let local_t = (t - step.start_t).max(0.0);
83            step.tween.tween.sample(local_t)
84        } else {
85            self.default_val.clone()
86        }
87    }
88
89    pub fn reset(&mut self) {
90        self.elapsed = 0.0;
91        self.done = false;
92    }
93
94    pub fn progress(&self) -> f32 {
95        (self.elapsed / self.duration.max(f32::EPSILON)).clamp(0.0, 1.0)
96    }
97}
98
99// ── SequenceBuilder ───────────────────────────────────────────────────────────
100
101/// Fluent builder for TweenSequence.
102///
103/// ```rust,no_run
104/// use proof_engine::tween::sequence::SequenceBuilder;
105/// use proof_engine::tween::Easing;
106///
107/// let seq = SequenceBuilder::new(0.0f32)
108///     .then(0.0, 1.0, 0.5, Easing::EaseOutCubic)
109///     .then(1.0, 0.5, 0.3, Easing::EaseInQuad)
110///     .overlap(0.1)
111///     .looping(false)
112///     .build();
113/// ```
114pub struct SequenceBuilder<T: Lerp + Clone + std::fmt::Debug> {
115    steps:       Vec<(Tween<T>, f32)>,
116    default_val: T,
117    looping:     bool,
118    next_overlap: f32,
119}
120
121impl<T: Lerp + Clone + std::fmt::Debug> SequenceBuilder<T> {
122    pub fn new(default_val: T) -> Self {
123        Self { steps: Vec::new(), default_val, looping: false, next_overlap: 0.0 }
124    }
125
126    /// Add a tween step from `from` to `to` over `duration` seconds.
127    pub fn then(mut self, from: T, to: T, duration: f32, easing: Easing) -> Self {
128        let overlap = self.next_overlap;
129        self.next_overlap = 0.0;
130        self.steps.push((Tween::new(from, to, duration, easing), overlap));
131        self
132    }
133
134    /// Set overlap (in seconds) for the next step. Positive = overlap with previous.
135    pub fn overlap(mut self, seconds: f32) -> Self {
136        self.next_overlap = seconds;
137        self
138    }
139
140    /// Add a pause (gap) before the next step.
141    pub fn wait(mut self, seconds: f32) -> Self {
142        self.next_overlap = -seconds;
143        self
144    }
145
146    pub fn looping(mut self, looping: bool) -> Self {
147        self.looping = looping;
148        self
149    }
150
151    pub fn build(self) -> TweenSequence<T> {
152        TweenSequence::new(self.steps, self.default_val, self.looping)
153    }
154}
155
156// ── TweenTimeline ─────────────────────────────────────────────────────────────
157
158/// Multiple named f32 animation tracks running in parallel.
159///
160/// Each track is an independent `TweenSequence<f32>`.
161/// Access current values by name each frame.
162pub struct TweenTimeline {
163    pub tracks:  std::collections::HashMap<String, TweenSequence<f32>>,
164    pub elapsed: f32,
165    pub looping: bool,
166    duration:    f32,
167    pub done:    bool,
168}
169
170impl TweenTimeline {
171    pub fn new(looping: bool) -> Self {
172        Self {
173            tracks:  std::collections::HashMap::new(),
174            elapsed: 0.0,
175            looping,
176            duration: 0.0,
177            done:    false,
178        }
179    }
180
181    /// Add a named track.
182    pub fn add_track(&mut self, name: impl Into<String>, seq: TweenSequence<f32>) {
183        self.duration = self.duration.max(seq.duration);
184        self.tracks.insert(name.into(), seq);
185    }
186
187    /// Advance all tracks and return the current state.
188    pub fn tick(&mut self, dt: f32) {
189        self.elapsed += dt;
190        if self.looping && self.elapsed >= self.duration {
191            self.elapsed -= self.duration;
192            for track in self.tracks.values_mut() { track.reset(); }
193        }
194        self.done = !self.looping && self.elapsed >= self.duration;
195        for track in self.tracks.values_mut() {
196            track.tick(dt);
197        }
198    }
199
200    /// Get the current value of a named track. Returns 0.0 if not found.
201    pub fn get(&self, name: &str) -> f32 {
202        self.tracks.get(name).map(|t| t.current_value()).unwrap_or(0.0)
203    }
204
205    /// Reset all tracks to the beginning.
206    pub fn reset(&mut self) {
207        self.elapsed = 0.0;
208        self.done = false;
209        for track in self.tracks.values_mut() { track.reset(); }
210    }
211
212    pub fn progress(&self) -> f32 {
213        (self.elapsed / self.duration.max(f32::EPSILON)).clamp(0.0, 1.0)
214    }
215}
216
217// ── Predefined game-useful timelines ──────────────────────────────────────────
218
219impl TweenTimeline {
220    /// Build a damage flash timeline: screen red flash + slight scale pop.
221    pub fn damage_flash(intensity: f32) -> Self {
222        let mut tl = Self::new(false);
223
224        let flash_seq = SequenceBuilder::new(0.0f32)
225            .then(intensity, 0.0, 0.3, Easing::EaseOutExpo)
226            .build();
227        tl.add_track("flash", flash_seq);
228
229        let scale_seq = SequenceBuilder::new(1.0f32)
230            .then(1.0 + intensity * 0.1, 1.0, 0.25, Easing::EaseOutBack)
231            .build();
232        tl.add_track("scale", scale_seq);
233
234        tl
235    }
236
237    /// Build a level-up timeline: brightness flash + long glow fade.
238    pub fn level_up() -> Self {
239        let mut tl = Self::new(false);
240
241        let flash = SequenceBuilder::new(0.0f32)
242            .then(1.5, 1.0, 0.15, Easing::EaseOutExpo)
243            .then(1.0, 1.0, 1.5, Easing::Linear)
244            .then(1.0, 0.0, 0.5, Easing::EaseInQuad)
245            .build();
246        tl.add_track("brightness", flash);
247
248        let hue_shift = SequenceBuilder::new(0.0f32)
249            .then(0.0, 360.0, 2.0, Easing::Linear)
250            .build();
251        tl.add_track("hue_shift", hue_shift);
252
253        let bloom = SequenceBuilder::new(1.0f32)
254            .then(3.0, 1.0, 0.8, Easing::EaseOutCubic)
255            .build();
256        tl.add_track("bloom", bloom);
257
258        tl
259    }
260
261    /// Build a boss entrance timeline: dark vignette crunch → reveal.
262    pub fn boss_entrance() -> Self {
263        let mut tl = Self::new(false);
264
265        let vignette = SequenceBuilder::new(0.15f32)
266            .then(0.15, 0.9, 0.8, Easing::EaseInCubic)
267            .then(0.9, 0.9, 1.2, Easing::Linear)
268            .then(0.9, 0.2, 0.6, Easing::EaseOutExpo)
269            .build();
270        tl.add_track("vignette", vignette);
271
272        let chromatic = SequenceBuilder::new(0.002f32)
273            .then(0.002, 0.02, 0.8, Easing::EaseInExpo)
274            .then(0.02, 0.001, 0.4, Easing::EaseOutCubic)
275            .build();
276        tl.add_track("chromatic", chromatic);
277
278        let saturation = SequenceBuilder::new(1.0f32)
279            .then(1.0, 0.0, 0.8, Easing::EaseInCubic)
280            .then(0.0, 1.2, 0.6, Easing::EaseOutBack)
281            .build();
282        tl.add_track("saturation", saturation);
283
284        tl
285    }
286
287    /// Build a death sequence: drain color, crush vignette, fade to black.
288    pub fn death_sequence() -> Self {
289        let mut tl = Self::new(false);
290
291        let saturation = SequenceBuilder::new(1.0f32)
292            .then(1.0, 0.0, 2.5, Easing::EaseInCubic)
293            .build();
294        tl.add_track("saturation", saturation);
295
296        let brightness = SequenceBuilder::new(0.0f32)
297            .then(0.0, -0.8, 3.0, Easing::EaseInQuart)
298            .build();
299        tl.add_track("brightness", brightness);
300
301        let vignette = SequenceBuilder::new(0.15f32)
302            .then(0.15, 1.0, 3.0, Easing::EaseInCubic)
303            .build();
304        tl.add_track("vignette", vignette);
305
306        let chromatic = SequenceBuilder::new(0.002f32)
307            .then(0.002, 0.015, 1.5, Easing::EaseInQuad)
308            .build();
309        tl.add_track("chromatic", chromatic);
310
311        tl
312    }
313
314    /// Build a healing pulse: green tint flash + brightness glow.
315    pub fn heal_pulse(amount_fraction: f32) -> Self {
316        let mut tl = Self::new(false);
317
318        let green = SequenceBuilder::new(1.0f32)
319            .then(1.0, 1.0 + amount_fraction * 0.4, 0.15, Easing::EaseOutExpo)
320            .then(1.0 + amount_fraction * 0.4, 1.0, 0.4, Easing::EaseInQuad)
321            .build();
322        tl.add_track("green_tint", green);
323
324        let bloom = SequenceBuilder::new(1.0f32)
325            .then(1.0, 1.8, 0.15, Easing::EaseOutExpo)
326            .then(1.8, 1.0, 0.5, Easing::EaseOutCubic)
327            .build();
328        tl.add_track("bloom", bloom);
329
330        tl
331    }
332}
333
334#[cfg(test)]
335mod tests {
336    use super::*;
337
338    #[test]
339    fn sequence_basic() {
340        let mut seq = SequenceBuilder::new(0.0f32)
341            .then(0.0, 1.0, 1.0, Easing::Linear)
342            .then(1.0, 2.0, 1.0, Easing::Linear)
343            .build();
344        let v0 = seq.tick(0.0);
345        assert!((v0 - 0.0).abs() < 1e-4);
346        seq.elapsed = 0.5;
347        let v_mid = seq.current_value();
348        assert!((v_mid - 0.5).abs() < 1e-4, "expected 0.5 got {v_mid}");
349        seq.elapsed = 1.5;
350        let v2 = seq.current_value();
351        assert!((v2 - 1.5).abs() < 1e-4, "expected 1.5 got {v2}");
352    }
353
354    #[test]
355    fn timeline_damage_flash() {
356        let mut tl = TweenTimeline::damage_flash(1.0);
357        let flash_start = tl.get("flash");
358        assert!((flash_start - 1.0).abs() < 0.01, "flash starts at intensity");
359        tl.tick(0.3);
360        let flash_end = tl.get("flash");
361        assert!(flash_end < 0.2, "flash should decay quickly");
362    }
363
364    #[test]
365    fn builder_wait() {
366        let seq = SequenceBuilder::new(0.0f32)
367            .then(0.0, 1.0, 0.5, Easing::Linear)
368            .wait(0.5)
369            .then(1.0, 2.0, 0.5, Easing::Linear)
370            .build();
371        // Second step starts at 0.5 (first step) + 0.5 (gap) = 1.0
372        assert!((seq.steps[1].start_t - 1.0).abs() < 1e-4, "second step at t=1.0");
373    }
374}