Skip to main content

ffcine_core/anim/
mod.rs

1//! Param tracks (`docs/compositing.md#param-tracks`): one type for every numeric, vector and
2//! colour parameter, a constant, keyframes with easing, or an [expression](expr).
3//!
4//! A [`Track`] is parsed once from its JSON form against a [`Clock`] (what a key's `t` means)
5//! and a [`Scope`] (the names an expression may use), then evaluated per frame on a [`Frame`]
6//! or sampled over a range of frames with an [`Env`]. Everything here is a pure function of the
7//! frame: no state, no GPU, no libav.
8
9pub mod expr;
10mod vars;
11
12use std::f64::consts::PI;
13use std::ops::Range;
14
15use serde_json::{Map, Value};
16
17use crate::{Error, Result};
18pub use expr::{Compiled, ExprError};
19pub use vars::Vars;
20
21/// The shape of a move over its progress `u` (0..1): a key's `interp` (alias `ease`).
22#[derive(Debug, Clone, Copy, PartialEq, Default)]
23pub enum Ease {
24    #[default]
25    Linear,
26    /// Keeps this key's value until the next key.
27    Hold,
28    /// `0.5 - 0.5 cos(pi u)` (reframe's `ease`).
29    Ease,
30    /// Cubic, accelerating.
31    In,
32    /// Cubic, decelerating.
33    Out,
34    /// Cubic ease in and out.
35    InOut,
36    /// Decelerates past the end and settles back (overshoot).
37    Back,
38    /// `3u² − 2u³` (the audio schemas' `ease`).
39    Smoothstep,
40    /// CSS `cubic-bezier(x1, y1, x2, y2)`.
41    Bezier([f64; 4]),
42}
43
44impl Ease {
45    /// A name: `linear`, `hold`, `ease`, `in`, `out`, `in_out`, `back`, `smoothstep` (and the
46    /// aliases `ease_in`, `ease_out`, `ease_in_out`, `overshoot`).
47    pub fn from_name(s: &str) -> Option<Self> {
48        Some(match s {
49            "linear" => Self::Linear,
50            "hold" => Self::Hold,
51            "ease" => Self::Ease,
52            "in" | "ease_in" => Self::In,
53            "out" | "ease_out" => Self::Out,
54            "in_out" | "ease_in_out" => Self::InOut,
55            "back" | "overshoot" => Self::Back,
56            "smoothstep" => Self::Smoothstep,
57            _ => return None,
58        })
59    }
60
61    /// A name or `[x1, y1, x2, y2]` (`x1` and `x2` in 0..1, as in CSS).
62    pub fn parse(v: &Value) -> Option<Self> {
63        if let Some(a) = v.as_array() {
64            let c: Vec<f64> = a.iter().filter_map(Value::as_f64).collect();
65            let unit = |x: f64| (0.0..=1.0).contains(&x);
66            return match c[..] {
67                [x1, y1, x2, y2] if c.len() == a.len() && unit(x1) && unit(x2) => {
68                    Some(Self::Bezier([x1, y1, x2, y2]))
69                }
70                _ => None,
71            };
72        }
73        Self::from_name(v.as_str()?)
74    }
75
76    /// Progress at `u`, clamped to 0..1 (`Back` overshoots 1 on the way).
77    pub fn at(self, u: f64) -> f64 {
78        let u = u.clamp(0.0, 1.0);
79        match self {
80            Self::Linear => u,
81            Self::Hold => {
82                if u >= 1.0 {
83                    1.0
84                } else {
85                    0.0
86                }
87            }
88            Self::Ease => 0.5 - 0.5 * (PI * u).cos(),
89            Self::In => u * u * u,
90            Self::Out => 1.0 - (1.0 - u).powi(3),
91            Self::InOut if u < 0.5 => 4.0 * u * u * u,
92            Self::InOut => 1.0 - (-2.0 * u + 2.0).powi(3) / 2.0,
93            Self::Back => {
94                let (c1, c3) = (1.701_58, 2.701_58);
95                1.0 + c3 * (u - 1.0).powi(3) + c1 * (u - 1.0).powi(2)
96            }
97            Self::Smoothstep => u * u * (3.0 - 2.0 * u),
98            Self::Bezier(c) => bezier(c, u),
99        }
100    }
101}
102
103/// `y` of a CSS cubic bezier at `x` (bisection on the curve's `x`). `x1` and `x2` are clamped
104/// to 0..1, where the curve's `x` is monotonic.
105fn bezier([x1, y1, x2, y2]: [f64; 4], x: f64) -> f64 {
106    let (x1, x2) = (x1.clamp(0.0, 1.0), x2.clamp(0.0, 1.0));
107    let curve = |a: f64, b: f64, t: f64| {
108        3.0 * a * t * (1.0 - t).powi(2) + 3.0 * b * t * t * (1.0 - t) + t.powi(3)
109    };
110    let (mut lo, mut hi, mut t) = (0.0, 1.0, x);
111    for _ in 0..32 {
112        let v = curve(x1, x2, t) - x;
113        if v.abs() < 1e-7 {
114            break;
115        }
116        if v > 0.0 {
117            hi = t;
118        } else {
119            lo = t;
120        }
121        t = f64::midpoint(lo, hi);
122    }
123    curve(y1, y2, t)
124}
125
126/// A value that can be keyframed: `f64`, `[f64; N]` (vectors, colours).
127pub trait Animatable: Copy + std::fmt::Debug + PartialEq {
128    /// Components.
129    const N: usize;
130    fn lerp(a: Self, b: Self, k: f64) -> Self;
131    fn component(self, i: usize) -> f64;
132    fn from_fn(f: impl FnMut(usize) -> f64) -> Self;
133
134    /// A number (`N` = 1), an array of `N` numbers, or (`N` = 3 or 4) a colour name.
135    fn from_json(v: &Value) -> Option<Self> {
136        match v {
137            Value::Number(n) if Self::N == 1 => n.as_f64().map(|x| Self::from_fn(|_| x)),
138            Value::Array(a) if a.len() == Self::N => {
139                let c: Vec<f64> = a.iter().map(Value::as_f64).collect::<Option<_>>()?;
140                Some(Self::from_fn(|i| c[i]))
141            }
142            Value::String(s) if Self::N == 3 || Self::N == 4 => {
143                colour(s).map(|c| Self::from_fn(|i| c[i]))
144            }
145            _ => None,
146        }
147    }
148}
149
150impl Animatable for f64 {
151    const N: usize = 1;
152    fn lerp(a: Self, b: Self, k: f64) -> Self {
153        a + (b - a) * k
154    }
155    fn component(self, _: usize) -> f64 {
156        self
157    }
158    fn from_fn(mut f: impl FnMut(usize) -> f64) -> Self {
159        f(0)
160    }
161}
162
163impl<const N: usize> Animatable for [f64; N] {
164    const N: usize = N;
165    fn lerp(a: Self, b: Self, k: f64) -> Self {
166        std::array::from_fn(|i| a[i] + (b[i] - a[i]) * k)
167    }
168    fn component(self, i: usize) -> f64 {
169        self[i]
170    }
171    fn from_fn(f: impl FnMut(usize) -> f64) -> Self {
172        std::array::from_fn(f)
173    }
174}
175
176/// `black`, `white` or `#rrggbb[aa]` as sRGB 0..1 with alpha.
177pub fn colour(s: &str) -> Option<[f64; 4]> {
178    match s {
179        "black" => return Some([0.0, 0.0, 0.0, 1.0]),
180        "white" => return Some([1.0; 4]),
181        _ => {}
182    }
183    let h = s.strip_prefix('#')?;
184    if !(h.len() == 6 || h.len() == 8) || !h.is_ascii() {
185        return None;
186    }
187    let byte = |i: usize| {
188        u8::from_str_radix(&h[i..i + 2], 16)
189            .ok()
190            .map(|b| f64::from(b) / 255.0)
191    };
192    Some([
193        byte(0)?,
194        byte(2)?,
195        byte(4)?,
196        if h.len() == 8 { byte(6)? } else { 1.0 },
197    ])
198}
199
200/// A keyframe: value `v` at `t` on the track's [`Clock`], `ease` shaping the segment to the
201/// next key.
202#[derive(Debug, Clone, Copy, PartialEq)]
203pub struct Key<T> {
204    pub t: f64,
205    pub v: T,
206    pub ease: Ease,
207}
208
209/// The value of sorted keys at `t`: held before the first and after the last.
210pub fn keys_at<T: Animatable>(keys: &[Key<T>], t: f64) -> Option<T> {
211    let first = keys.first()?;
212    if t.is_nan() || t <= first.t {
213        return Some(first.v);
214    }
215    // The last key at or before t.
216    let i = keys.partition_point(|k| k.t <= t) - 1;
217    let a = &keys[i];
218    let Some(b) = keys.get(i + 1) else {
219        return Some(a.v);
220    };
221    let span = b.t - a.t;
222    if span <= 0.0 {
223        return Some(b.v);
224    }
225    Some(T::lerp(a.v, b.v, a.ease.at((t - a.t) / span)))
226}
227
228/// What a key's `t` means, and which variables an expression sees.
229#[derive(Debug, Clone, Copy, PartialEq, Eq)]
230pub enum Clock {
231    /// The clip's source time, s (timeline reframe, transform, crop, shader and audio keys).
232    /// Expressions see `st`.
233    Source,
234    /// Seconds from the clip's start, negative from its end (timeline text keys). Expressions
235    /// see `lt` and `dur`.
236    Clip,
237    /// Seconds from a comp layer's `in`, negative from its `out`. Expressions see every
238    /// variable.
239    Layer,
240}
241
242macro_rules! builtins {
243    ($($v:ident $name:literal,)*) => {
244        /// The variables every frame defines (`docs/compositing.md#expressions`).
245        #[derive(Debug, Clone, Copy, PartialEq, Eq)]
246        #[repr(u16)]
247        pub enum Builtin { $($v,)* }
248        impl Builtin {
249            pub const ALL: &[Builtin] = &[$(Self::$v,)*];
250            pub const COUNT: usize = Self::ALL.len();
251            pub fn name(self) -> &'static str {
252                match self { $(Self::$v => $name,)* }
253            }
254            pub fn from_name(s: &str) -> Option<Self> {
255                match s { $($name => Some(Self::$v),)* _ => None }
256            }
257        }
258    };
259}
260
261builtins! {
262    T "t", Lt "lt", St "st", N "n", Ln "ln", Fps "fps", CanvasW "W", CanvasH "H",
263    W "w", H "h", Sw "sw", Sh "sh", In "in", Out "out", Dur "dur", Total "T", Value "value",
264}
265
266impl Builtin {
267    /// Whether an expression on `clock` may read it.
268    fn on(self, clock: Clock) -> bool {
269        match clock {
270            Clock::Layer => true,
271            Clock::Source => self == Self::St,
272            Clock::Clip => matches!(self, Self::Lt | Self::Dur),
273        }
274    }
275}
276
277/// The names an expression compiles against: the clock's built-ins, `value` in the
278/// `{keys, expr}` form, and [`Vars`].
279#[derive(Debug, Clone, Copy)]
280pub struct Scope<'a> {
281    pub clock: Clock,
282    pub value: bool,
283    pub vars: Option<&'a Vars>,
284}
285
286impl<'a> Scope<'a> {
287    pub fn new(clock: Clock) -> Self {
288        Self {
289            clock,
290            value: false,
291            vars: None,
292        }
293    }
294
295    pub fn with_vars(clock: Clock, vars: &'a Vars) -> Self {
296        Self {
297            clock,
298            value: false,
299            vars: Some(vars),
300        }
301    }
302
303    /// The slot of `name`: a built-in's index, or `Builtin::COUNT` + a var's index.
304    pub fn slot(&self, name: &str) -> Option<u16> {
305        #[allow(clippy::cast_possible_truncation)]
306        if let Some(b) = Builtin::from_name(name) {
307            let ok = if b == Builtin::Value {
308                self.value
309            } else {
310                b.on(self.clock)
311            };
312            return ok.then_some(b as u16);
313        }
314        #[allow(clippy::cast_possible_truncation)]
315        self.vars?.slot(name).map(|i| (Builtin::COUNT + i) as u16)
316    }
317
318    pub(crate) fn unknown(&self, name: &str) -> String {
319        match Builtin::from_name(name) {
320            Some(Builtin::Value) => {
321                "`value` is only defined in the {\"keys\", \"expr\"} form".into()
322            }
323            Some(_) => format!(
324                "`{name}` isn't known on this parameter's clock ({:?})",
325                self.clock
326            ),
327            None => format!("unknown variable `{name}`"),
328        }
329    }
330}
331
332/// One frame's variables: the built-ins (by [`Builtin`] index) and the var values.
333#[derive(Debug, Clone)]
334pub struct Frame<'a> {
335    pub b: [f64; Builtin::COUNT],
336    pub vars: &'a [f64],
337}
338
339impl Frame<'_> {
340    /// A frame with only the clock's time known: `st` for [`Clock::Source`], `lt` (and `dur`)
341    /// otherwise.
342    pub fn at(clock: Clock, t: f64, dur: f64) -> Frame<'static> {
343        let mut b = [0.0; Builtin::COUNT];
344        match clock {
345            Clock::Source => b[Builtin::St as usize] = t,
346            Clock::Clip | Clock::Layer => b[Builtin::Lt as usize] = t,
347        }
348        b[Builtin::Dur as usize] = dur;
349        Frame { b, vars: &[] }
350    }
351
352    pub fn get(&self, slot: u16) -> f64 {
353        let s = usize::from(slot);
354        match self.b.get(s) {
355            Some(v) => *v,
356            None => self
357                .vars
358                .get(s - Builtin::COUNT)
359                .copied()
360                .unwrap_or(f64::NAN),
361        }
362    }
363
364    pub fn var(&self, b: Builtin) -> f64 {
365        self.b[b as usize]
366    }
367
368    /// The time a key's `t` is compared with.
369    fn key_time(&self, clock: Clock) -> f64 {
370        match clock {
371            Clock::Source => self.var(Builtin::St),
372            Clock::Clip | Clock::Layer => self.var(Builtin::Lt),
373        }
374    }
375}
376
377/// What a range of frames is sampled in: the frame `n` (on the comp's or timeline's rate) maps
378/// to `t = n / fps`, `lt = t − in`, `st = source_in + lt`.
379#[derive(Debug, Clone, Copy, Default)]
380pub struct Env<'a> {
381    pub fps: f64,
382    /// Canvas size, px.
383    pub canvas: [f64; 2],
384    /// Layer box, px.
385    pub size: [f64; 2],
386    /// Source display size, px.
387    pub source: [f64; 2],
388    pub in_: f64,
389    pub out: f64,
390    /// Comp duration, s.
391    pub total: f64,
392    pub source_in: f64,
393    pub vars: Option<&'a Vars>,
394}
395
396impl Env<'_> {
397    /// The built-ins on frame `n`.
398    #[allow(clippy::cast_precision_loss)]
399    pub fn builtins(&self, n: i64) -> [f64; Builtin::COUNT] {
400        let fps = self.rate();
401        self.fill(n as f64 / fps, n as f64)
402    }
403
404    /// The built-ins at `t` comp seconds, between frames too (`n` is then fractional): for
405    /// integrating a track over time.
406    pub fn builtins_at(&self, t: f64) -> [f64; Builtin::COUNT] {
407        self.fill(t, t * self.rate())
408    }
409
410    fn rate(&self) -> f64 {
411        if self.fps > 0.0 { self.fps } else { 24.0 }
412    }
413
414    fn fill(&self, t: f64, n: f64) -> [f64; Builtin::COUNT] {
415        let fps = self.rate();
416        let lt = t - self.in_;
417        let mut b = [0.0; Builtin::COUNT];
418        for (k, v) in [
419            (Builtin::T, t),
420            (Builtin::Lt, lt),
421            (Builtin::St, self.source_in + lt),
422            (Builtin::N, n),
423            (Builtin::Ln, n - (self.in_ * fps).round()),
424            (Builtin::Fps, fps),
425            (Builtin::CanvasW, self.canvas[0]),
426            (Builtin::CanvasH, self.canvas[1]),
427            (Builtin::W, self.size[0]),
428            (Builtin::H, self.size[1]),
429            (Builtin::Sw, self.source[0]),
430            (Builtin::Sh, self.source[1]),
431            (Builtin::In, self.in_),
432            (Builtin::Out, self.out),
433            (Builtin::Dur, self.out - self.in_),
434            (Builtin::Total, self.total),
435        ] {
436            b[k as usize] = v;
437        }
438        b
439    }
440}
441
442/// A parameter: a constant, keyframes, an expression per component, or an expression over its
443/// own keys (`value`).
444#[derive(Debug, Clone, PartialEq)]
445pub enum Track<T> {
446    Const(T),
447    /// Sorted by `t`; held before the first and after the last.
448    Keys(Vec<Key<T>>),
449    /// One per component.
450    Expr(Vec<Compiled>),
451    /// An expression per component over its own keys (`value`).
452    KeysExpr(Vec<Key<T>>, Vec<Compiled>),
453    /// Per frame, from the range's first frame (internal: exact lowering).
454    Sampled(Vec<T>),
455}
456
457impl<T: Default> Default for Track<T> {
458    fn default() -> Self {
459        Self::Const(T::default())
460    }
461}
462
463/// Whether `v` is the keyframe form (an array of `{t, v}` objects).
464pub fn is_keys(v: &Value) -> bool {
465    v.as_array()
466        .and_then(|a| a.first())
467        .is_some_and(|k| k.get("t").is_some() && k.get("v").is_some())
468}
469
470/// Seconds from a `RationalTime` (`{rate, value}`) or a plain number.
471pub fn seconds(v: &Value) -> Option<f64> {
472    if let Some(s) = v.as_f64() {
473        return Some(s);
474    }
475    let rate = v.get("rate")?.as_f64()?;
476    let value = v.get("value")?.as_f64()?;
477    (rate > 0.0).then(|| value / rate)
478}
479
480fn invalid(what: &str, v: &Value) -> Error {
481    Error::Track {
482        path: what.to_owned(),
483        msg: format!("invalid value {v}"),
484    }
485}
486
487fn compile(src: &str, scope: &Scope, what: &str) -> Result<Compiled> {
488    Compiled::new(src, scope).map_err(|e| Error::Expr {
489        path: what.to_owned(),
490        col: e.col,
491        msg: e.msg,
492        src: src.to_owned(),
493    })
494}
495
496/// Sorts keys by `t` (stable for equal times).
497fn sorted<T>(mut keys: Vec<Key<T>>) -> Vec<Key<T>> {
498    keys.sort_by(|a, b| a.t.total_cmp(&b.t));
499    keys
500}
501
502impl<T: Animatable> Track<T> {
503    /// A constant, keys, an expression string (`N` = 1), an array of `N` constants or
504    /// expressions, or `{"keys": [...], "expr": ...}`. `what` names the parameter in errors.
505    pub fn parse(v: &Value, clock: Clock, what: &str) -> Result<Self> {
506        Self::parse_in(v, &Scope::new(clock), what)
507    }
508
509    /// [`Track::parse`] against `scope` (its clock and vars).
510    pub fn parse_in(v: &Value, scope: &Scope, what: &str) -> Result<Self> {
511        if let Some(c) = T::from_json(v) {
512            return Ok(Self::Const(c));
513        }
514        if is_keys(v) {
515            return Ok(Self::Keys(Self::parse_keys(v, what)?));
516        }
517        let exprs = match v {
518            Value::String(s) if T::N == 1 => vec![compile(s, scope, what)?],
519            Value::Array(a) if a.len() == T::N => a
520                .iter()
521                .enumerate()
522                .map(|(i, c)| match c {
523                    Value::Number(n) => n
524                        .as_f64()
525                        .map(Compiled::constant)
526                        .ok_or_else(|| invalid(what, v)),
527                    Value::String(s) => compile(s, scope, &format!("{what}[{i}]")),
528                    _ => Err(invalid(what, v)),
529                })
530                .collect::<Result<_>>()?,
531            Value::Object(o) if o.contains_key("keys") && o.contains_key("expr") => {
532                return Self::parse_keys_expr(o, scope, what);
533            }
534            _ => return Err(invalid(what, v)),
535        };
536        Ok(Self::from_exprs(exprs))
537    }
538
539    fn from_exprs(exprs: Vec<Compiled>) -> Self {
540        match exprs
541            .iter()
542            .map(Compiled::as_const)
543            .collect::<Option<Vec<_>>>()
544        {
545            Some(c) => Self::Const(T::from_fn(|i| c[i])),
546            None => Self::Expr(exprs),
547        }
548    }
549
550    fn parse_keys_expr(o: &Map<String, Value>, scope: &Scope, what: &str) -> Result<Self> {
551        let kv = &o["keys"];
552        if !is_keys(kv) {
553            return Err(invalid(&format!("{what}.keys"), kv));
554        }
555        let keys = Self::parse_keys(kv, what)?;
556        let s = Scope {
557            value: true,
558            ..*scope
559        };
560        let what = format!("{what}.expr");
561        let exprs = match &o["expr"] {
562            Value::String(e) => {
563                let c = compile(e, &s, &what)?;
564                vec![c; T::N]
565            }
566            Value::Array(a) if a.len() == T::N => a
567                .iter()
568                .enumerate()
569                .map(|(i, e)| match e {
570                    Value::String(e) => compile(e, &s, &format!("{what}[{i}]")),
571                    Value::Number(n) => n
572                        .as_f64()
573                        .map(Compiled::constant)
574                        .ok_or_else(|| invalid(&what, e)),
575                    _ => Err(invalid(&what, e)),
576                })
577                .collect::<Result<_>>()?,
578            e => return Err(invalid(&what, e)),
579        };
580        Ok(Self::KeysExpr(keys, exprs))
581    }
582
583    fn parse_keys(v: &Value, what: &str) -> Result<Vec<Key<T>>> {
584        let mut keys = Vec::new();
585        for k in v.as_array().into_iter().flatten() {
586            let ease = match k.get("interp").or_else(|| k.get("ease")) {
587                None => Ease::Linear,
588                Some(e) => Ease::parse(e).ok_or_else(|| invalid(what, v))?,
589            };
590            keys.push(Key {
591                t: k.get("t")
592                    .and_then(seconds)
593                    .ok_or_else(|| invalid(what, v))?,
594                v: k.get("v")
595                    .and_then(T::from_json)
596                    .ok_or_else(|| invalid(what, v))?,
597                ease,
598            });
599        }
600        Ok(sorted(keys))
601    }
602
603    /// Keys in any order (sorted here, stable for equal times).
604    pub fn from_keys(keys: Vec<Key<T>>) -> Self {
605        Self::Keys(sorted(keys))
606    }
607
608    /// Whether the value can change from frame to frame.
609    pub fn is_animated(&self) -> bool {
610        match self {
611            Self::Const(_) => false,
612            Self::Keys(k) => k.len() > 1,
613            Self::Sampled(s) => s.windows(2).any(|w| w[0] != w[1]),
614            Self::Expr(..) | Self::KeysExpr(..) => true,
615        }
616    }
617
618    /// The keys, if keyframed.
619    pub fn keys(&self) -> Option<&[Key<T>]> {
620        match self {
621            Self::Keys(k) | Self::KeysExpr(k, ..) => Some(k),
622            _ => None,
623        }
624    }
625
626    fn keyed(keys: &[Key<T>], clock: Clock, f: &Frame) -> Option<T> {
627        let t = f.key_time(clock);
628        if clock == Clock::Source || keys.iter().all(|k| k.t >= 0.0) {
629            return keys_at(keys, t);
630        }
631        // Negative times count from the end.
632        let dur = f.var(Builtin::Dur);
633        let k: Vec<Key<T>> = keys
634            .iter()
635            .map(|k| Key {
636                t: if k.t < 0.0 { dur + k.t } else { k.t },
637                ..*k
638            })
639            .collect();
640        keys_at(&sorted(k), t)
641    }
642
643    /// The value on `frame`, `None` for empty keys; an error if an expression isn't finite.
644    /// `Sampled` reads `frame`'s `n`.
645    pub fn try_eval(&self, clock: Clock, frame: &Frame) -> Result<Option<T>> {
646        let finite = |v: f64, c: &Compiled| {
647            if v.is_finite() {
648                Ok(v)
649            } else {
650                Err(Error::Track {
651                    path: c.source().to_owned(),
652                    msg: format!(
653                        "not finite ({v}) at t = {}, lt = {}, st = {}, n = {}",
654                        frame.var(Builtin::T),
655                        frame.var(Builtin::Lt),
656                        frame.var(Builtin::St),
657                        frame.var(Builtin::N)
658                    ),
659                })
660            }
661        };
662        const { assert!(T::N <= 4, "tracks have at most 4 components") };
663        let comps = |e: &[Compiled], f: &Frame| -> Result<T> {
664            let mut out = [0.0; 4];
665            for (i, c) in e.iter().enumerate().take(4) {
666                out[i] = finite(c.eval(f), c)?;
667            }
668            Ok(T::from_fn(|i| out[i]))
669        };
670        Ok(match self {
671            Self::Const(v) => Some(*v),
672            Self::Keys(k) => Self::keyed(k, clock, frame),
673            Self::Expr(e) => Some(comps(e, frame)?),
674            Self::KeysExpr(k, e) => {
675                let Some(v) = Self::keyed(k, clock, frame) else {
676                    return Ok(None);
677                };
678                let mut out = [0.0; 4];
679                for (i, c) in e.iter().enumerate().take(4) {
680                    let mut f = frame.clone();
681                    f.b[Builtin::Value as usize] = v.component(i);
682                    out[i] = finite(c.eval(&f), c)?;
683                }
684                Some(T::from_fn(|i| out[i]))
685            }
686            #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
687            Self::Sampled(s) => {
688                let n = frame.var(Builtin::N).max(0.0) as usize;
689                s.get(n).or(s.last()).copied()
690            }
691        })
692    }
693
694    /// The value on `frame`, keys on [`Clock::Layer`] (`lt`).
695    pub fn eval(&self, frame: &Frame) -> Result<T> {
696        self.try_eval(Clock::Layer, frame)?
697            .ok_or_else(|| Error::Track {
698                path: String::new(),
699                msg: "no keys".into(),
700            })
701    }
702
703    /// The value at `t` on `clock` when only that time is known (timeline effects), `None`
704    /// for empty keys or a non-finite expression.
705    pub fn at(&self, clock: Clock, t: f64, dur: f64) -> Option<T> {
706        self.try_eval(clock, &Frame::at(clock, t, dur))
707            .ok()
708            .flatten()
709    }
710
711    /// The values on `frames` in `env` (vars evaluated per frame). `Sampled` tracks index from
712    /// the range's start.
713    pub fn sample(&self, clock: Clock, frames: Range<i64>, env: &Env) -> Result<Vec<T>> {
714        let start = frames.start;
715        frames
716            .map(|n| {
717                let b = env.builtins(n);
718                let vars = match env.vars {
719                    Some(v) => v.eval(&b)?,
720                    None => Vec::new(),
721                };
722                let mut frame = Frame { b, vars: &vars };
723                #[allow(clippy::cast_precision_loss)]
724                if let Self::Sampled(_) = self {
725                    frame.b[Builtin::N as usize] = (n - start) as f64;
726                }
727                self.try_eval(clock, &frame)?.ok_or_else(|| Error::Track {
728                    path: String::new(),
729                    msg: "no keys".into(),
730                })
731            })
732            .collect()
733    }
734}
735
736#[cfg(test)]
737mod tests;