Skip to main content

fastanim_core/
anim.rs

1//! Animations (SPEC §4.4): planned once at bake time, then sampled as a pure function of progress.
2
3use std::f64::consts::{FRAC_PI_2, TAU};
4
5use kurbo::{Affine, CubicBez, Point, Vec2};
6
7use crate::Interpolate;
8use crate::color::{Color, YELLOW};
9use crate::geom::{VPath, align};
10use crate::mobject::{MobjectId, SceneState, Stroke, VState};
11use crate::timeline::Scene;
12use crate::transform_diff::center;
13
14/// Maps linear progress `0..=1` to eased progress.
15#[derive(Debug, Clone, Copy)]
16pub enum RateFn {
17    /// No easing.
18    Linear,
19    /// Smootherstep; close to manim's default `smooth`.
20    Smooth,
21    /// Smooth out to 1 at the midpoint and back to 0.
22    ThereAndBack,
23    /// Starts slow.
24    EaseIn(Ease),
25    /// Ends slow.
26    EaseOut(Ease),
27    /// Starts and ends slow.
28    EaseInOut(Ease),
29    /// Damped oscillation that overshoots and settles at 1: `stiffness` is the angular
30    /// frequency and `damping` the decay rate, both per clip (e.g. 20 and 6).
31    Spring {
32        /// Angular frequency in radians per clip.
33        stiffness: f32,
34        /// Exponential decay rate per clip.
35        damping: f32,
36    },
37    /// Any function with `f(0) == 0`.
38    Custom(fn(f32) -> f32),
39}
40
41/// Curve shape for [`RateFn::EaseIn`] and friends.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Ease {
44    /// `t²`.
45    Quad,
46    /// `t³`.
47    Cubic,
48    /// `2^(10t − 10)`.
49    Expo,
50    /// Pulls back slightly before going.
51    Back,
52}
53
54impl Ease {
55    /// The ease-in curve, exact at both ends.
56    fn ease_in(self, t: f32) -> f32 {
57        if t <= 0.0 {
58            return 0.0;
59        }
60        if t >= 1.0 {
61            return 1.0;
62        }
63        match self {
64            Ease::Quad => t * t,
65            Ease::Cubic => t * t * t,
66            Ease::Expo => 2f32.powf(10.0 * t - 10.0),
67            Ease::Back => {
68                const C1: f32 = 1.70158;
69                (C1 + 1.0) * t * t * t - C1 * t * t
70            }
71        }
72    }
73}
74
75impl RateFn {
76    /// Eases `t` (clamped to `0..=1`).
77    pub fn apply(self, t: f32) -> f32 {
78        let t = t.clamp(0.0, 1.0);
79        let smooth = |t: f32| t * t * t * (t * (6.0 * t - 15.0) + 10.0);
80        match self {
81            RateFn::Linear => t,
82            RateFn::Smooth => smooth(t),
83            RateFn::ThereAndBack => smooth(if t < 0.5 { 2.0 * t } else { 2.0 - 2.0 * t }),
84            RateFn::EaseIn(e) => e.ease_in(t),
85            RateFn::EaseOut(e) => 1.0 - e.ease_in(1.0 - t),
86            RateFn::EaseInOut(e) if t < 0.5 => e.ease_in(2.0 * t) / 2.0,
87            RateFn::EaseInOut(e) => 1.0 - e.ease_in(2.0 - 2.0 * t) / 2.0,
88            RateFn::Spring { .. } if t >= 1.0 => 1.0,
89            RateFn::Spring { stiffness, damping } => {
90                let f = |t: f32| 1.0 - (-damping * t).exp() * (stiffness * t).cos();
91                // Normalized so it lands on exactly 1.
92                f(t) / f(1.0)
93            }
94            RateFn::Custom(f) => f(t),
95        }
96    }
97}
98
99/// Something that changes the scene over a clip.
100pub trait Animation: Send + Sync {
101    /// Called once when played, with the scene state at the start of the clip. Do the expensive
102    /// work (alignment, diffing) here.
103    fn plan(&mut self, state: &SceneState);
104
105    /// Writes the targets' state at eased progress `alpha` into `state`, which holds the scene
106    /// as it was at the start of the clip. Must be cheap and pure.
107    fn sample(&self, alpha: f32, state: &mut SceneState);
108
109    /// Length in seconds.
110    fn duration(&self) -> f32 {
111        1.0
112    }
113
114    /// Easing applied to progress before [`sample`](Animation::sample).
115    fn rate_fn(&self) -> RateFn {
116        RateFn::Smooth
117    }
118}
119
120impl Animation for Box<dyn Animation> {
121    fn plan(&mut self, state: &SceneState) {
122        (**self).plan(state);
123    }
124    fn sample(&self, alpha: f32, state: &mut SceneState) {
125        (**self).sample(alpha, state);
126    }
127    fn duration(&self) -> f32 {
128        (**self).duration()
129    }
130    fn rate_fn(&self) -> RateFn {
131        (**self).rate_fn()
132    }
133}
134
135/// Builder methods available on every animation.
136pub trait AnimationExt: Animation + Sized {
137    /// Overrides the length in seconds.
138    fn run_time(self, secs: f32) -> Timed<Self> {
139        let rate = self.rate_fn();
140        Timed {
141            anim: self,
142            secs,
143            rate,
144        }
145    }
146
147    /// Overrides the easing.
148    fn rate(self, rate: RateFn) -> Timed<Self> {
149        let secs = self.duration();
150        Timed {
151            anim: self,
152            secs,
153            rate,
154        }
155    }
156}
157
158impl<A: Animation> AnimationExt for A {}
159
160/// An animation with overridden timing; see [`AnimationExt`].
161pub struct Timed<A> {
162    anim: A,
163    secs: f32,
164    rate: RateFn,
165}
166
167impl<A: Animation> Timed<A> {
168    /// Overrides the length in seconds.
169    pub fn run_time(self, secs: f32) -> Self {
170        Self { secs, ..self }
171    }
172
173    /// Overrides the easing.
174    pub fn rate(self, rate: RateFn) -> Self {
175        Self { rate, ..self }
176    }
177}
178
179impl<A: Animation> Animation for Timed<A> {
180    fn plan(&mut self, state: &SceneState) {
181        self.anim.plan(state);
182    }
183    fn sample(&self, alpha: f32, state: &mut SceneState) {
184        self.anim.sample(alpha, state);
185    }
186    fn duration(&self) -> f32 {
187        self.secs
188    }
189    fn rate_fn(&self) -> RateFn {
190        self.rate
191    }
192}
193
194fn get(state: &SceneState, id: MobjectId) -> &VState {
195    state
196        .get(&id)
197        .unwrap_or_else(|| panic!("{id:?} is not in the scene; `add` it first"))
198}
199
200type UpdateFn = Box<dyn Fn(&VState, f32) -> VState + Send + Sync>;
201
202/// Rewrites one mobject from its start-of-clip state and progress: the building block for the
203/// simple animations below.
204pub struct Update {
205    id: MobjectId,
206    start: Option<VState>,
207    f: UpdateFn,
208}
209
210impl Update {
211    /// `f(start_state, alpha)` gives the mobject's state at `alpha`.
212    pub fn new(id: MobjectId, f: impl Fn(&VState, f32) -> VState + Send + Sync + 'static) -> Self {
213        Self {
214            id,
215            start: None,
216            f: Box::new(f),
217        }
218    }
219}
220
221impl Animation for Update {
222    fn plan(&mut self, state: &SceneState) {
223        self.start = Some(get(state, self.id).clone());
224    }
225    fn sample(&self, alpha: f32, state: &mut SceneState) {
226        let start = self.start.as_ref().expect("sample before plan");
227        state.insert(self.id, (self.f)(start, alpha));
228    }
229}
230
231/// Draws the outline in along its arc length.
232pub fn create(id: MobjectId) -> Update {
233    Update::new(id, |s, a| VState {
234        draw_range: 0.0..a,
235        ..s.clone()
236    })
237}
238
239/// Fades from transparent to the current opacity.
240pub fn fade_in(id: MobjectId) -> Update {
241    Update::new(id, |s, a| VState {
242        opacity: s.opacity * a,
243        ..s.clone()
244    })
245}
246
247/// Fades to transparent. The mobject stays in the scene until removed.
248pub fn fade_out(id: MobjectId) -> Update {
249    Update::new(id, |s, a| VState {
250        opacity: s.opacity * (1.0 - a),
251        ..s.clone()
252    })
253}
254
255/// Translates by `v`.
256pub fn shift(id: MobjectId, v: Vec2) -> Update {
257    Update::new(id, move |s, a| {
258        s.clone().transform(Affine::translate(v * f64::from(a)))
259    })
260}
261
262/// Rotates counter-clockwise by `angle` radians about the start bounding-box center.
263pub fn rotate(id: MobjectId, angle: f64) -> Update {
264    Update::new(id, move |s, a| {
265        let c = s.path.center();
266        s.clone()
267            .transform(Affine::rotate_about(angle * f64::from(a), c))
268    })
269}
270
271/// Scales by `factor` about the start bounding-box center.
272pub fn scale(id: MobjectId, factor: f64) -> Update {
273    Update::new(id, move |s, a| {
274        let c = s.path.center();
275        s.clone()
276            .transform(Affine::scale_about(1.0 + (factor - 1.0) * f64::from(a), c))
277    })
278}
279
280/// Grows from a point at its center, manim's `GrowFromCenter`.
281pub fn grow_from_center(id: MobjectId) -> Update {
282    Update::new(id, |s, a| s.clone().scale(f64::from(a)))
283}
284
285/// Grows from a point while turning a quarter turn into place, manim's `SpinInFromNothing`.
286pub fn spin_in(id: MobjectId) -> Update {
287    Update::new(id, |s, a| {
288        let c = s.path.center();
289        let a = f64::from(a);
290        s.clone()
291            .transform(Affine::rotate_about(FRAC_PI_2 * (a - 1.0), c) * Affine::scale_about(a, c))
292    })
293}
294
295/// Shrinks to a point at its center. The mobject stays in the scene until removed.
296pub fn shrink_to_center(id: MobjectId) -> Update {
297    Update::new(id, |s, a| s.clone().scale(f64::from(1.0 - a)))
298}
299
300/// Erases the outline backwards along its arc length; the reverse of [`create`]. The mobject
301/// stays in the scene until removed.
302pub fn uncreate(id: MobjectId) -> Update {
303    Update::new(id, |s, a| VState {
304        draw_range: 0.0..1.0 - a,
305        ..s.clone()
306    })
307}
308
309/// Moves so the bounding-box center ends at `p`.
310pub fn move_to(id: MobjectId, p: Point) -> Update {
311    Update::new(id, move |s, a| {
312        s.clone().shift((p - s.path.center()) * f64::from(a))
313    })
314}
315
316/// Morphs every control point `q` towards `f(q)`, manim's `ApplyFunction` / `ApplyPointwise`.
317pub fn apply_function(id: MobjectId, f: impl Fn(Point) -> Point + Send + Sync + 'static) -> Update {
318    Update::new(id, move |s, a| {
319        let t = f64::from(a);
320        let mut m = s.clone();
321        for seg in m.path.subpaths.iter_mut().flat_map(|sp| &mut sp.segments) {
322            let g = |q: Point| q.lerp(f(q), t);
323            *seg = CubicBez::new(g(seg.p0), g(seg.p1), g(seg.p2), g(seg.p3));
324        }
325        m
326    })
327}
328
329type GroupFn = Box<dyn Fn(&VState, Point, f32) -> VState + Send + Sync>;
330
331/// Rewrites several mobjects together from each one's start state, their joint bounding-box
332/// center and progress: the building block for the emphasis animations below.
333pub struct UpdateGroup {
334    ids: Vec<MobjectId>,
335    starts: Vec<VState>,
336    center: Point,
337    f: GroupFn,
338}
339
340impl UpdateGroup {
341    /// `f(start_state, group_center, alpha)` gives each mobject's state at `alpha`.
342    pub fn new(
343        ids: &[MobjectId],
344        f: impl Fn(&VState, Point, f32) -> VState + Send + Sync + 'static,
345    ) -> Self {
346        Self {
347            ids: ids.to_vec(),
348            starts: Vec::new(),
349            center: Point::ORIGIN,
350            f: Box::new(f),
351        }
352    }
353}
354
355impl Animation for UpdateGroup {
356    fn plan(&mut self, state: &SceneState) {
357        self.starts = self.ids.iter().map(|&id| get(state, id).clone()).collect();
358        self.center = center(self.starts.iter());
359    }
360    fn sample(&self, alpha: f32, state: &mut SceneState) {
361        for (&id, s) in self.ids.iter().zip(&self.starts) {
362            state.insert(id, (self.f)(s, self.center, alpha));
363        }
364    }
365}
366
367/// Briefly scales up by 1.2 and tints yellow, then settles back, manim's `Indicate`.
368pub fn indicate(ids: &[MobjectId]) -> Timed<UpdateGroup> {
369    UpdateGroup::new(ids, |s, c, a| {
370        if a <= 0.0 {
371            return s.clone();
372        }
373        let tint = |col: Color| Color::lerp(&col, &YELLOW.with_alpha(col.a), a);
374        VState {
375            fill: tint(s.fill),
376            stroke: Stroke {
377                color: tint(s.stroke.color),
378                ..s.stroke
379            },
380            ..s.clone()
381        }
382        .transform(Affine::scale_about(1.0 + 0.2 * f64::from(a), c))
383    })
384    .rate(RateFn::ThereAndBack)
385}
386
387/// Wobbles in place while swelling slightly, manim's `Wiggle`; lasts 2 s.
388pub fn wiggle(ids: &[MobjectId]) -> Timed<UpdateGroup> {
389    UpdateGroup::new(ids, |s, c, a| {
390        if a <= 0.0 || a >= 1.0 {
391            return s.clone();
392        }
393        let a = f64::from(a);
394        let swell = RateFn::ThereAndBack.apply(a as f32) as f64;
395        let angle = 0.1 * swell * (3.0 * TAU * a).sin();
396        s.clone()
397            .transform(Affine::rotate_about(angle, c) * Affine::scale_about(1.0 + 0.1 * swell, c))
398    })
399    .run_time(2.0)
400    .rate(RateFn::Linear)
401}
402
403/// Helper shapes added to the scene now, invisible, animated by `f` from their added state, and
404/// removed when the clip ends: e.g. [`circumscribe`] and [`flash`].
405pub struct Overlay {
406    ids: Vec<MobjectId>,
407    starts: Vec<VState>,
408    f: UpdateFn,
409}
410
411impl Overlay {
412    /// Adds `shapes` to `s`, hidden (`draw_range` empty); `f(shape, alpha)` gives each one's
413    /// state during the clip.
414    pub fn new(
415        s: &mut Scene,
416        shapes: Vec<VState>,
417        f: impl Fn(&VState, f32) -> VState + Send + Sync + 'static,
418    ) -> Self {
419        let ids = (shapes.iter())
420            .map(|m| {
421                s.add(VState {
422                    draw_range: 0.0..0.0,
423                    ..m.clone()
424                })
425            })
426            .collect();
427        Self {
428            ids,
429            starts: shapes,
430            f: Box::new(f),
431        }
432    }
433}
434
435impl Animation for Overlay {
436    fn plan(&mut self, _: &SceneState) {}
437    fn sample(&self, alpha: f32, state: &mut SceneState) {
438        for (&id, m) in self.ids.iter().zip(&self.starts) {
439            if alpha >= 1.0 {
440                state.remove(&id);
441            } else {
442                state.insert(id, (self.f)(m, alpha));
443            }
444        }
445    }
446}
447
448/// Draws a yellow rectangle around `ids` and erases it again, manim's `Circumscribe`.
449pub fn circumscribe(s: &mut Scene, ids: &[MobjectId]) -> Overlay {
450    const BUFF: f64 = 0.2;
451    let st = s.state();
452    let bbox = (ids.iter())
453        .filter_map(|id| get(st, *id).path.bbox())
454        .reduce(|a, b| a.union(b))
455        .unwrap_or_default()
456        .inflate(BUFF, BUFF);
457    let rect = VState::rectangle(bbox.width(), bbox.height())
458        .move_to(bbox.center())
459        .stroke(YELLOW, 0.06);
460    Overlay::new(s, vec![rect], |m, a| VState {
461        // Traces in over the first half and out over the second.
462        draw_range: (2.0 * a - 1.0).max(0.0)..(2.0 * a).min(1.0),
463        ..m.clone()
464    })
465}
466
467/// Twelve short yellow rays burst outwards from `p`, manim's `Flash`.
468pub fn flash(s: &mut Scene, p: Point) -> Overlay {
469    let rays = (0..12)
470        .map(|i| {
471            let d = Vec2::from_angle(TAU * f64::from(i) / 12.0);
472            VState::line(p + d * 0.3, p + d * 0.8).stroke(YELLOW, 0.04)
473        })
474        .collect();
475    Overlay::new(s, rays, |m, a| VState {
476        draw_range: (2.0 * a - 1.0).max(0.0)..(2.0 * a).min(1.0),
477        ..m.clone()
478    })
479}
480
481/// `s` part drawn at progress `p`: over the first half its outline traces in (in its fill color
482/// if it has no stroke), over the second its fill fades up while the outline settles back.
483fn border_then_fill(s: &VState, p: f32) -> VState {
484    if p >= 1.0 {
485        return s.clone();
486    }
487    let outline = if s.stroke.width > 0.0 && s.stroke.color.a > 0.0 {
488        s.stroke
489    } else {
490        Stroke {
491            color: s.fill.with_alpha(1.0),
492            width: 0.02,
493        }
494    };
495    if p < 0.5 {
496        VState {
497            stroke: outline,
498            fill: s.fill.with_alpha(0.0),
499            draw_range: 0.0..p * 2.0,
500            ..s.clone()
501        }
502    } else {
503        let q = p * 2.0 - 1.0;
504        VState {
505            stroke: Stroke {
506                color: Color::lerp(&outline.color, &s.stroke.color, q),
507                width: outline.width + (s.stroke.width - outline.width) * f64::from(q),
508            },
509            fill: s.fill.with_alpha(s.fill.a * q),
510            ..s.clone()
511        }
512    }
513}
514
515/// Traces the outline in, then fills it, manim's `DrawBorderThenFill`; lasts 2 s. Like one
516/// mobject of [`write`].
517pub fn draw_border_then_fill(id: MobjectId) -> Timed<Update> {
518    Update::new(id, border_then_fill).run_time(2.0)
519}
520
521/// Draws mobjects in one after another, manim's `Write`: each outline traces in, then its fill
522/// fades up while the outline fades back to its own style. See [`write`].
523pub struct Write {
524    ids: Vec<MobjectId>,
525    starts: Vec<VState>,
526}
527
528/// Writes `ids` in order (e.g. the glyphs of a text), staggered like manim's `Write`.
529pub fn write(ids: &[MobjectId]) -> Write {
530    Write {
531        ids: ids.to_vec(),
532        starts: Vec::new(),
533    }
534}
535
536impl Animation for Write {
537    fn plan(&mut self, state: &SceneState) {
538        self.starts = self.ids.iter().map(|&id| get(state, id).clone()).collect();
539    }
540    fn sample(&self, alpha: f32, state: &mut SceneState) {
541        let n = self.ids.len() as f32;
542        // manim's lag ratio: each item starts `lag` of an item's duration after the previous.
543        let lag = (4.0 / n.max(1.0)).min(0.2);
544        let w = 1.0 / (1.0 + (n - 1.0).max(0.0) * lag);
545        for (i, (&id, s)) in self.ids.iter().zip(&self.starts).enumerate() {
546            // `alpha >= 1` must give back the exact start; the division can land just short.
547            let p = if alpha >= 1.0 {
548                1.0
549            } else {
550                ((alpha - i as f32 * lag * w) / w).clamp(0.0, 1.0)
551            };
552            state.insert(id, border_then_fill(s, p));
553        }
554    }
555    fn duration(&self) -> f32 {
556        // manim: 1 s for short texts, up to 2 s for long ones.
557        (self.ids.len() as f32 / 15.0).clamp(1.0, 2.0)
558    }
559}
560
561/// Erases mobjects last to first, manim's `Unwrite`: [`write`] played backwards. The mobjects
562/// stay in the scene until removed.
563pub struct Unwrite(Write);
564
565/// Unwrites `ids` (e.g. the glyphs of a text); the reverse of [`write`].
566pub fn unwrite(ids: &[MobjectId]) -> Unwrite {
567    Unwrite(write(ids))
568}
569
570impl Animation for Unwrite {
571    fn plan(&mut self, state: &SceneState) {
572        self.0.plan(state);
573    }
574    fn sample(&self, alpha: f32, state: &mut SceneState) {
575        self.0.sample(1.0 - alpha, state);
576    }
577    fn duration(&self) -> f32 {
578        self.0.duration()
579    }
580}
581
582/// Morphs a mobject into `target` (path, style and all).
583pub struct Transform {
584    id: MobjectId,
585    target: VState,
586    ends: Option<(VState, VState)>,
587}
588
589/// Morphs mobject `id` into `target`.
590pub fn transform(id: MobjectId, target: VState) -> Transform {
591    Transform {
592        id,
593        target,
594        ends: None,
595    }
596}
597
598impl Animation for Transform {
599    fn plan(&mut self, state: &SceneState) {
600        let from = get(state, self.id);
601        let (a, b) = align(&from.path, &self.target.path);
602        self.ends = Some((
603            VState {
604                path: a,
605                ..from.clone()
606            },
607            VState {
608                path: b,
609                ..self.target.clone()
610            },
611        ));
612    }
613    fn sample(&self, alpha: f32, state: &mut SceneState) {
614        let (a, b) = self.ends.as_ref().expect("sample before plan");
615        // Exact endpoints: alignment re-segments both paths. At 0, `state` already holds the start.
616        if alpha >= 1.0 {
617            state.insert(self.id, self.target.clone());
618        } else if alpha > 0.0 {
619            state.insert(self.id, VState::lerp(a, b, alpha));
620        }
621    }
622}
623
624/// Morphs one mobject into another already in the scene, then swaps them, manim's
625/// `ReplacementTransform`: `target` is hidden until the end, when it takes over and `id` is
626/// removed. See [`replacement_transform`].
627pub struct ReplacementTransform {
628    id: MobjectId,
629    target: MobjectId,
630    morph: Transform,
631}
632
633/// Morphs mobject `id` into mobject `target` (e.g. just `add`ed), leaving only `target`.
634pub fn replacement_transform(id: MobjectId, target: MobjectId) -> ReplacementTransform {
635    ReplacementTransform {
636        id,
637        target,
638        morph: transform(id, VState::new(VPath::default())),
639    }
640}
641
642impl Animation for ReplacementTransform {
643    fn plan(&mut self, state: &SceneState) {
644        self.morph.target = get(state, self.target).clone();
645        self.morph.plan(state);
646    }
647    fn sample(&self, alpha: f32, state: &mut SceneState) {
648        if alpha >= 1.0 {
649            // `target` is already in `state` as it was added.
650            state.remove(&self.id);
651        } else {
652            state.remove(&self.target);
653            self.morph.sample(alpha, state);
654        }
655    }
656}
657
658/// Runs animations together; lasts as long as the longest. Each child keeps its own easing.
659/// Children targeting the same mobject: the last one wins.
660pub struct Parallel(pub Vec<Box<dyn Animation>>);
661
662impl Animation for Parallel {
663    fn plan(&mut self, state: &SceneState) {
664        self.0.iter_mut().for_each(|a| a.plan(state));
665    }
666    fn sample(&self, alpha: f32, state: &mut SceneState) {
667        let t = alpha * self.duration();
668        for a in &self.0 {
669            let p = if a.duration() > 0.0 {
670                t / a.duration()
671            } else {
672                1.0
673            };
674            a.sample(a.rate_fn().apply(p), state);
675        }
676    }
677    fn duration(&self) -> f32 {
678        self.0.iter().map(|a| a.duration()).fold(0.0, f32::max)
679    }
680    fn rate_fn(&self) -> RateFn {
681        RateFn::Linear
682    }
683}
684
685/// Plays animations one after another, manim's `Succession`; each plans against the scene as
686/// the previous one left it.
687pub struct Sequence(pub Vec<Box<dyn Animation>>);
688
689impl Animation for Sequence {
690    fn plan(&mut self, state: &SceneState) {
691        let mut st = state.clone();
692        for a in &mut self.0 {
693            a.plan(&st);
694            a.sample(a.rate_fn().apply(1.0), &mut st);
695        }
696    }
697    fn sample(&self, alpha: f32, state: &mut SceneState) {
698        let mut t = alpha * self.duration();
699        for a in &self.0 {
700            if t < 0.0 {
701                break;
702            }
703            let d = a.duration();
704            // `alpha >= 1` finishes everything exactly; the subtraction can land just short.
705            let p = if alpha >= 1.0 || d <= 0.0 { 1.0 } else { t / d };
706            a.sample(a.rate_fn().apply(p), state);
707            t -= d;
708        }
709    }
710    fn duration(&self) -> f32 {
711        self.0.iter().map(|a| a.duration()).sum()
712    }
713    fn rate_fn(&self) -> RateFn {
714        RateFn::Linear
715    }
716}
717
718/// Runs animations together, each starting `lag_ratio` of the previous one's duration after
719/// it, manim's `LaggedStart`. Like [`Parallel`], all plan against the same start state.
720pub struct LaggedStart {
721    anims: Vec<Box<dyn Animation>>,
722    starts: Vec<f32>,
723}
724
725/// Staggers `anims` by `lag_ratio` (0 is [`Parallel`], 1 is back to back).
726pub fn lagged_start(lag_ratio: f32, anims: Vec<Box<dyn Animation>>) -> LaggedStart {
727    let starts = (anims.iter())
728        .scan(0.0, |t, a| {
729            let s = *t;
730            *t += a.duration() * lag_ratio;
731            Some(s)
732        })
733        .collect();
734    LaggedStart { anims, starts }
735}
736
737impl Animation for LaggedStart {
738    fn plan(&mut self, state: &SceneState) {
739        self.anims.iter_mut().for_each(|a| a.plan(state));
740    }
741    fn sample(&self, alpha: f32, state: &mut SceneState) {
742        let t = alpha * self.duration();
743        for (a, s) in self.anims.iter().zip(&self.starts) {
744            let d = a.duration();
745            let p = if alpha >= 1.0 || d <= 0.0 {
746                1.0
747            } else {
748                (t - s) / d
749            };
750            a.sample(a.rate_fn().apply(p), state);
751        }
752    }
753    fn duration(&self) -> f32 {
754        (self.anims.iter().zip(&self.starts))
755            .map(|(a, s)| s + a.duration())
756            .fold(0.0, f32::max)
757    }
758    fn rate_fn(&self) -> RateFn {
759        RateFn::Linear
760    }
761}