zaplib 0.0.3

⚡ Zaplib is a fast, cross-platform (web+native), GPU-based application framework, written in Rust.
Documentation
//! An animation system for transitioning between various kinds of values over time.

use crate::*;
use std::f64::consts::PI;

/// Manages animations. Assumes that you always pass it with [`Anim`] objects with
/// the same types of [`Track`]s. For example, if track 0 represents represents a
/// color Vec4, and track 1 represents a Float value, then make sure to use that
/// consistently throughout the lifetime of an Animator.
///
/// TODO(JP): Look into enforcing this through the type system, instead of at
/// runtime.
///
/// Also note that the [`Animator`] always contains the "source of truth" for the
/// values it manages, so whenever necessary you should copy the values kept
/// here to the actual "draw objects".
#[derive(Debug, Default)]
pub struct Animator {
    /// The current source of truth of values that this [`Animator`] manages.
    /// Initially undefined, until [`Animator::draw`] initializes the Animator.
    ///
    /// Call [`Animator::draw`] or [`Animator::handle`] to update these
    /// values based on the [`Animator::current`] animation and the current time.
    values: Option<Vec<AnimValue>>,

    /// The current [`Anim`] that is being played. Is [`None`] when there is no
    /// active animation.
    current: Option<Anim>,

    /// The [`Anim`] that will be played next when [`Animator::current`] is done playing. It
    /// should never be possible that [`Animator::next`] is set but [`Animator::current`] isn't. There
    /// can only be one animation queued up.
    next: Option<Anim>,

    /// The time that the [`Animator::current`] animation started playing.
    current_start_time: f64,

    /// The last timestamp we updated our animation, used to avoid computing the
    /// animation values multiple times for the same timestamp.
    last_processed_time: f64,
}

impl Animator {
    /// Play an animation. If an animation is already playing, it's either cut
    /// off, or remains playing with the new animation queued up, if [`Anim::chain`]
    /// is set in the new animation. If there was already another animation
    /// queued up, then it's kicked from the queue.
    pub fn play_anim(&mut self, cx: &mut Cx, anim: Anim) {
        if self.current.is_none() || !anim.chain {
            // If there is no current animation or if we're not chaining, just
            // overwrite the current animation.
            self.current = Some(anim);
            self.next = None;
            self.current_start_time = cx.last_event_time;
            // Make sure that we request a new frame to play our animation in.
            cx.request_next_frame();
        } else {
            // Otherwise, queue it, kicking out any previous animation in the
            // queue.
            self.next = Some(anim);
        }
    }

    /// Process animations from a "draw" function. This must be called before reading any values.
    ///
    /// The `anim_default` will initialze the Animator if it's currently uninitialized.
    pub fn draw(&mut self, cx: &mut Cx, anim_default: Anim) {
        if self.values.is_none() {
            self.values = Some(anim_default.get_last_values());
        }
        self.run_animator(cx);
    }

    /// Convenient function for only calling [`Animator::run_animator`] if the event is
    /// an [`Event::NextFrame`]. Returns true if we processed the animation so you
    /// can update your "draw objects".
    pub fn handle(&mut self, cx: &mut Cx, event: &Event) -> bool {
        match event {
            Event::NextFrame => self.values.is_some() && self.run_animator(cx),
            _ => false,
        }
    }

    /// Process any playing animations based on the current time from [`Cx`].
    /// Returns whether or not [`Animator::values`] have been updated, so you can update
    /// your "draw objects" accordingly. Note that [`Animator::values`] are the "source of
    /// truth", so when in doubt it's always safe to just update your objects
    /// based on [`Animator::values`] regardless of the return value of [`Animator::draw`].
    fn run_animator(&mut self, cx: &mut Cx) -> bool {
        // Skip if time hasn't changed, unless this is the initial call.
        if cx.last_event_time == self.last_processed_time {
            return false;
        }
        self.last_processed_time = cx.last_event_time;

        // First check if the current animation has expired, in which case we need to either stop
        // animating, or start with the queued up animation.
        if let Some(current_anim) = &self.current {
            if self.current_start_time + current_anim.duration <= cx.last_event_time {
                // Make sure that `values` actually reflects the "end state" of the animation, since
                // at the previous rendering step we were probably a little bit before the actual
                // end.
                self.values = Some(current_anim.get_last_values());

                if self.next.is_none() {
                    // If there was no animation queued up, just bail out, but still return `true`
                    // since we've changed `values`.
                    self.current = None;
                    return true;
                } else {
                    // Don't just set the new current_start_time to the current time, since most
                    // likely we have overshot a little and are actually a tiny bit into the next
                    // animation already.
                    self.current_start_time += current_anim.duration;
                    std::mem::swap(&mut self.current, &mut self.next);
                    self.next = None;
                    // Fall through, so we compute the current values based on the animation that
                    // was queued up (and which is now `current`).
                }
            }
        }

        // If we still have an active animation, compute `values`.
        if let Some(current_anim) = &self.current {
            // First, make sure that we will get a next frame for our animation.
            cx.request_next_frame();

            // Compute the fraction between 0 and 1 of how far we are into the current animation.
            let time_fraction = (cx.last_event_time - self.current_start_time) / current_anim.duration;

            let values = self.values.as_mut().unwrap();

            // Update all the individual values based on how far we are into the current animation.
            for (index, track) in current_anim.tracks.iter().enumerate() {
                match track {
                    Track::Float { key_frames, ease } => {
                        values[index] = AnimValue::Float(Track::compute_track_float(
                            time_fraction,
                            key_frames,
                            values[index].unwrap_float(),
                            ease,
                        ));
                    }
                    Track::Vec2 { key_frames, ease } => {
                        values[index] = AnimValue::Vec2(Track::compute_track_vec2(
                            time_fraction,
                            key_frames,
                            values[index].unwrap_vec2(),
                            ease,
                        ));
                    }
                    Track::Vec3 { key_frames, ease } => {
                        values[index] = AnimValue::Vec3(Track::compute_track_vec3(
                            time_fraction,
                            key_frames,
                            values[index].unwrap_vec3(),
                            ease,
                        ));
                    }
                    Track::Vec4 { key_frames, ease } => {
                        values[index] = AnimValue::Vec4(Track::compute_track_vec4(
                            time_fraction,
                            key_frames,
                            values[index].unwrap_vec4(),
                            ease,
                        ));
                    }
                }
            }
            return true;
        }
        false
    }

    /// Get the value of the given track as a float. Be sure to call this only if the given track is
    /// indeed always a float in the [`Anim`]s you pass into this [`Animator`].
    /// TODO(JP): Instead of having multiple functions here, perhaps we can use [`Into`?
    pub fn get_float(&self, track_index: usize) -> f32 {
        self.values.as_ref().unwrap()[track_index].unwrap_float()
    }

    /// Get the value of the given track as a [`Vec2`]. Be sure to call this only if the given track is
    /// indeed always a [`Vec2`] in the [`Anim`]s you pass into this [`Animator`].
    /// TODO(JP): Instead of having multiple functions here, perhaps we can use [`Into`?
    pub fn get_vec2(&self, track_index: usize) -> Vec2 {
        self.values.as_ref().unwrap()[track_index].unwrap_vec2()
    }

    /// Get the value of the given track as a [`Vec3`]. Be sure to call this only if the given track is
    /// indeed always a [`Vec3`] in the [`Anim`]s you pass into this [`Animator`].
    /// TODO(JP): Instead of having multiple functions here, perhaps we can use [`Into`?
    pub fn get_vec3(&self, track_index: usize) -> Vec3 {
        self.values.as_ref().unwrap()[track_index].unwrap_vec3()
    }

    /// Get the value of the given track as a [`Vec4`]. Be sure to call this only if the given track is
    /// indeed always a [`Vec4`] in the [`Anim`]s you pass into this [`Animator`].
    /// TODO(JP): Instead of having multiple functions here, perhaps we can use [`Into`?
    pub fn get_vec4(&self, track_index: usize) -> Vec4 {
        self.values.as_ref().unwrap()[track_index].unwrap_vec4()
    }
}

/// Represents an actual value in an [`Animator`], which can be of a few
/// different types, but should remain consistent in its type (for a
/// given [`Track`]) for the lifetime of an [`Animator`].
#[derive(Clone, Debug)]
pub enum AnimValue {
    Float(f32),
    Vec2(Vec2),
    Vec3(Vec3),
    Vec4(Vec4),
}

impl AnimValue {
    /// Get the value as a float. Be sure to call this only if the given track is
    /// indeed always a float in the [`Anim`]s you pass into the [`Animator`].
    /// TODO(JP): Instead of having multiple functions here, perhaps we can use [`Into`?
    fn unwrap_float(&self) -> f32 {
        match self {
            AnimValue::Float(cur_val) => *cur_val,
            _ => panic!("Unexpected AnimValue type"),
        }
    }

    /// Get the value as a [`Vec2`]. Be sure to call this only if the given track is
    /// indeed always a [`Vec2`] in the [`Anim`]s you pass into the [`Animator`].
    /// TODO(JP): Instead of having multiple functions here, perhaps we can use [`Into`?
    fn unwrap_vec2(&self) -> Vec2 {
        match self {
            AnimValue::Vec2(cur_val) => *cur_val,
            _ => panic!("Unexpected AnimValue type"),
        }
    }

    /// Get the value as a [`Vec3`]. Be sure to call this only if the given track is
    /// indeed always a [`Vec3`] in the [`Anim`]s you pass into the [`Animator`].
    /// TODO(JP): Instead of having multiple functions here, perhaps we can use [`Into`?
    fn unwrap_vec3(&self) -> Vec3 {
        match self {
            AnimValue::Vec3(cur_val) => *cur_val,
            _ => panic!("Unexpected AnimValue type"),
        }
    }

    /// Get the value as a [`Vec4`]. Be sure to call this only if the given track is
    /// indeed always a [`Vec4`] in the [`Anim`]s you pass into the [`Animator`].
    /// TODO(JP): Instead of having multiple functions here, perhaps we can use [`Into`?
    fn unwrap_vec4(&self) -> Vec4 {
        match self {
            AnimValue::Vec4(cur_val) => *cur_val,
            _ => panic!("Unexpected AnimValue type"),
        }
    }
}

/// An actual animation that can be played.
#[derive(Clone, Debug, PartialEq)]
pub struct Anim {
    /// The time it should take for this animation to complete, in seconds.
    pub duration: f64,

    /// If set, this animation will get queued up if there is an existing
    /// animation playing.
    pub chain: bool,

    /// The actual tracks of values that will change during this animation.
    /// Should remain consistent between the different animations that you pass
    /// into a single [`Animator`].
    pub tracks: &'static [Track],
}

impl Anim {
    /// TODO(JP): Replace these with Anim::default() when
    /// <https://github.com/rust-lang/rust/issues/67792> gets done
    pub const DEFAULT: Anim = Anim { duration: 0., chain: false, tracks: &[] };

    /// Get the values for the "end state" of an animation, ie. the values for
    /// when the animation is done.
    fn get_last_values(&self) -> Vec<AnimValue> {
        self.tracks
            .iter()
            .map(|track| match track {
                Track::Vec4 { key_frames, .. } => AnimValue::Vec4(key_frames.last().unwrap().1),
                Track::Vec3 { key_frames, .. } => AnimValue::Vec3(key_frames.last().unwrap().1),
                Track::Vec2 { key_frames, .. } => AnimValue::Vec2(key_frames.last().unwrap().1),
                Track::Float { key_frames, .. } => AnimValue::Float(key_frames.last().unwrap().1),
            })
            .collect()
    }
}
impl Default for Anim {
    fn default() -> Self {
        Anim::DEFAULT
    }
}

/// Describes how output values of a [`Track`] get mapped for fractions in between
/// keyframes. See these pages for more explanations:
/// * <https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function>
/// * <https://easings.net>
#[derive(Clone, Debug, PartialEq)]
pub enum Ease {
    Lin,
    InQuad,
    OutQuad,
    InOutQuad,
    InCubic,
    OutCubic,
    InOutCubic,
    InQuart,
    OutQuart,
    InOutQuart,
    InQuint,
    OutQuint,
    InOutQuint,
    InSine,
    OutSine,
    InOutSine,
    InExp,
    OutExp,
    InOutExp,
    InCirc,
    OutCirc,
    InOutCirc,
    InElastic,
    OutElastic,
    InOutElastic,
    InBack,
    OutBack,
    InOutBack,
    InBounce,
    OutBounce,
    InOutBounce,
    Pow { begin: f64, end: f64 },
    Bezier { cp0: f64, cp1: f64, cp2: f64, cp3: f64 },
}
impl Ease {
    /// TODO(JP): Replace these with Ease::default() when
    /// <https://github.com/rust-lang/rust/issues/67792> gets done
    pub const DEFAULT: Ease = Ease::InOutCubic;
}
impl Default for Ease {
    fn default() -> Self {
        Ease::DEFAULT
    }
}

impl Ease {
    // Clippy TODO
    #[warn(clippy::many_single_char_names)]
    fn map(&self, t: f64) -> f64 {
        match self {
            Ease::Lin => t.max(0.0).min(1.0),
            Ease::Pow { begin, end } => {
                if t < 0. {
                    return 0.;
                }
                if t > 1. {
                    return 1.;
                }
                let a = -1. / (begin * begin).max(1.0);
                let b = 1. + 1. / (end * end).max(1.0);
                let t2 = (((a - 1.) * -b) / (a * (1. - b))).powf(t);
                (-a * b + b * a * t2) / (a * t2 - b)
            }

            Ease::InQuad => t * t,
            Ease::OutQuad => t * (2.0 - t),
            Ease::InOutQuad => {
                let t = t * 2.0;
                if t < 1. {
                    0.5 * t * t
                } else {
                    let t = t - 1.;
                    -0.5 * (t * (t - 2.) - 1.)
                }
            }
            Ease::InCubic => t * t * t,
            Ease::OutCubic => {
                let t2 = t - 1.0;
                t2 * t2 * t2 + 1.0
            }
            Ease::InOutCubic => {
                let t = t * 2.0;
                if t < 1. {
                    0.5 * t * t * t
                } else {
                    let t = t - 2.;
                    1. / 2. * (t * t * t + 2.)
                }
            }
            Ease::InQuart => t * t * t * t,
            Ease::OutQuart => {
                let t = t - 1.;
                -(t * t * t * t - 1.)
            }
            Ease::InOutQuart => {
                let t = t * 2.0;
                if t < 1. {
                    0.5 * t * t * t * t
                } else {
                    let t = t - 2.;
                    -0.5 * (t * t * t * t - 2.)
                }
            }
            Ease::InQuint => t * t * t * t * t,
            Ease::OutQuint => {
                let t = t - 1.;
                t * t * t * t * t + 1.
            }
            Ease::InOutQuint => {
                let t = t * 2.0;
                if t < 1. {
                    0.5 * t * t * t * t * t
                } else {
                    let t = t - 2.;
                    0.5 * (t * t * t * t * t + 2.)
                }
            }
            Ease::InSine => -(t * PI * 0.5).cos() + 1.,
            Ease::OutSine => (t * PI * 0.5).sin(),
            Ease::InOutSine => -0.5 * ((t * PI).cos() - 1.),
            Ease::InExp => {
                if t < 0.001 {
                    0.
                } else {
                    2.0f64.powf(10. * (t - 1.))
                }
            }
            Ease::OutExp => {
                if t > 0.999 {
                    1.
                } else {
                    -(2.0f64.powf(-10. * t)) + 1.
                }
            }
            Ease::InOutExp => {
                if t < 0.001 {
                    return 0.;
                }
                if t > 0.999 {
                    return 1.;
                }
                let t = t * 2.0;
                if t < 1. {
                    0.5 * 2.0f64.powf(10. * (t - 1.))
                } else {
                    let t = t - 1.;
                    0.5 * (-(2.0f64.powf(-10. * t)) + 2.)
                }
            }
            Ease::InCirc => -((1. - t * t).sqrt() - 1.),
            Ease::OutCirc => {
                let t = t - 1.;
                (1. - t * t).sqrt()
            }
            Ease::InOutCirc => {
                let t = t * 2.;
                if t < 1. {
                    -0.5 * ((1. - t * t).sqrt() - 1.)
                } else {
                    let t = t - 2.;
                    0.5 * ((1. - t * t).sqrt() + 1.)
                }
            }
            Ease::InElastic => {
                let p = 0.3;
                let s = p / 4.0; // c = 1.0, b = 0.0, d = 1.0
                if t < 0.001 {
                    return 0.;
                }
                if t > 0.999 {
                    return 1.;
                }
                let t = t - 1.0;
                -(2.0f64.powf(10.0 * t) * ((t - s) * (2.0 * PI) / p).sin())
            }
            Ease::OutElastic => {
                let p = 0.3;
                let s = p / 4.0; // c = 1.0, b = 0.0, d = 1.0

                if t < 0.001 {
                    return 0.;
                }
                if t > 0.999 {
                    return 1.;
                }
                2.0f64.powf(-10.0 * t) * ((t - s) * (2.0 * PI) / p).sin() + 1.0
            }
            Ease::InOutElastic => {
                let p = 0.3;
                let s = p / 4.0; // c = 1.0, b = 0.0, d = 1.0
                if t < 0.001 {
                    return 0.;
                }
                if t > 0.999 {
                    return 1.;
                }
                let t = t * 2.0;
                if t < 1. {
                    let t = t - 1.0;
                    -0.5 * (2.0f64.powf(10.0 * t) * ((t - s) * (2.0 * PI) / p).sin())
                } else {
                    let t = t - 1.0;
                    0.5 * 2.0f64.powf(-10.0 * t) * ((t - s) * (2.0 * PI) / p).sin() + 1.0
                }
            }
            Ease::InBack => {
                let s = 1.70158;
                t * t * ((s + 1.) * t - s)
            }
            Ease::OutBack => {
                let s = 1.70158;
                let t = t - 1.;
                t * t * ((s + 1.) * t + s) + 1.
            }
            Ease::InOutBack => {
                let s = 1.70158;
                let t = t * 2.0;
                if t < 1. {
                    let s = s * 1.525;
                    0.5 * (t * t * ((s + 1.) * t - s))
                } else {
                    let t = t - 2.;
                    0.5 * (t * t * ((s + 1.) * t + s) + 2.)
                }
            }
            Ease::InBounce => 1.0 - Ease::OutBounce.map(1.0 - t),
            Ease::OutBounce => {
                if t < (1. / 2.75) {
                    return 7.5625 * t * t;
                }
                if t < (2. / 2.75) {
                    let t = t - (1.5 / 2.75);
                    return 7.5625 * t * t + 0.75;
                }
                if t < (2.5 / 2.75) {
                    let t = t - (2.25 / 2.75);
                    return 7.5625 * t * t + 0.9375;
                }
                let t = t - (2.625 / 2.75);
                7.5625 * t * t + 0.984375
            }
            Ease::InOutBounce => {
                if t < 0.5 {
                    Ease::InBounce.map(t * 2.) * 0.5
                } else {
                    Ease::OutBounce.map(t * 2. - 1.) * 0.5 + 0.5
                }
            }
            Ease::Bezier { cp0, cp1, cp2, cp3 } => {
                if t < 0. {
                    return 0.;
                }
                if t > 1. {
                    return 1.;
                }

                if (cp0 - cp1).abs() < 0.001 && (cp2 - cp3).abs() < 0.001 {
                    return t;
                }

                let epsilon = 1.0 / 200.0 * t;
                let cx = 3.0 * cp0;
                let bx = 3.0 * (cp2 - cp0) - cx;
                let ax = 1.0 - cx - bx;
                let cy = 3.0 * cp1;
                let by = 3.0 * (cp3 - cp1) - cy;
                let ay = 1.0 - cy - by;
                let mut u = t;

                for _i in 0..6 {
                    let x = ((ax * u + bx) * u + cx) * u - t;
                    if x.abs() < epsilon {
                        return ((ay * u + by) * u + cy) * u;
                    }
                    let d = (3.0 * ax * u + 2.0 * bx) * u + cx;
                    if d.abs() < 1e-6 {
                        break;
                    }
                    u -= x / d;
                }

                if t > 1. {
                    return (ay + by) + cy;
                }
                if t < 0. {
                    return 0.0;
                }

                let mut w = 0.0;
                let mut v = 1.0;
                u = t;
                for _i in 0..8 {
                    let x = ((ax * u + bx) * u + cx) * u;
                    if (x - t).abs() < epsilon {
                        return ((ay * u + by) * u + cy) * u;
                    }

                    if t > x {
                        w = u;
                    } else {
                        v = u;
                    }
                    u = (v - w) * 0.5 + w;
                }

                ((ay * u + by) * u + cy) * u
            }
        }
    }
}

/// Represents a single value that changes during the course of an animation.
/// Should remain consistent in its type and what it represents between the
/// different animations that you pass into a single [`Animator`].
///
/// `key_frames` are tuples, where the first value is the fraction between 0 and
/// 1 that represents how much of the animation has been played so far, and the
/// second value is the actual value that this track should take on at that time.
#[derive(Clone, Debug, PartialEq)]
pub enum Track {
    Float { ease: Ease, key_frames: &'static [(f64, f32)] },
    Vec2 { ease: Ease, key_frames: &'static [(f64, Vec2)] },
    Vec3 { ease: Ease, key_frames: &'static [(f64, Vec3)] },
    Vec4 { ease: Ease, key_frames: &'static [(f64, Vec4)] },
}

impl Track {
    fn compute_track_float(time: f64, track: &[(f64, f32)], init: f32, ease: &Ease) -> f32 {
        if track.is_empty() {
            return init;
        }
        fn lerp(a: f32, b: f32, f: f32) -> f32 {
            a * (1.0 - f) + b * f
        }
        // find the 2 keys we want
        for i in 0..track.len() {
            if time >= track[i].0 {
                // we found the left key
                let val1 = &track[i];
                if i == track.len() - 1 {
                    // last key
                    return val1.1;
                }
                let val2 = &track[i + 1];
                // lerp it
                let f = ease.map((time - val1.0) / (val2.0 - val1.0)) as f32;
                return lerp(val1.1, val2.1, f);
            }
        }
        let val2 = &track[0];
        let f = ease.map(time / val2.0) as f32;
        lerp(init, val2.1, f)
    }

    fn compute_track_vec2(time: f64, track: &[(f64, Vec2)], init: Vec2, ease: &Ease) -> Vec2 {
        if track.is_empty() {
            return init;
        }
        fn lerp(a: Vec2, b: Vec2, f: f32) -> Vec2 {
            let nf = 1.0 - f;
            Vec2 { x: a.x * nf + b.x * f, y: a.y * nf + b.y * f }
        }
        // find the 2 keys we want
        for i in 0..track.len() {
            if time >= track[i].0 {
                // we found the left key
                let val1 = &track[i];
                if i == track.len() - 1 {
                    // last key
                    return val1.1;
                }
                let val2 = &track[i + 1];
                // lerp it
                let f = ease.map((time - val1.0) / (val2.0 - val1.0)) as f32;
                return lerp(val1.1, val2.1, f);
            }
        }
        let val2 = &track[0];
        let f = ease.map(time / val2.0) as f32;
        lerp(init, val2.1, f)
    }

    fn compute_track_vec3(time: f64, track: &[(f64, Vec3)], init: Vec3, ease: &Ease) -> Vec3 {
        if track.is_empty() {
            return init;
        }
        fn lerp(a: Vec3, b: Vec3, f: f32) -> Vec3 {
            let nf = 1.0 - f;
            Vec3 { x: a.x * nf + b.x * f, y: a.y * nf + b.y * f, z: a.z * nf + b.z * f }
        }
        // find the 2 keys we want
        for i in 0..track.len() {
            if time >= track[i].0 {
                // we found the left key
                let val1 = &track[i];
                if i == track.len() - 1 {
                    // last key
                    return val1.1;
                }
                let val2 = &track[i + 1];
                // lerp it
                let f = ease.map((time - val1.0) / (val2.0 - val1.0)) as f32;
                return lerp(val1.1, val2.1, f);
            }
        }
        let val2 = &track[0];
        let f = ease.map(time / val2.0) as f32;
        lerp(init, val2.1, f)
    }

    fn compute_track_vec4(time: f64, track: &[(f64, Vec4)], init: Vec4, ease: &Ease) -> Vec4 {
        if track.is_empty() {
            return init;
        }
        fn lerp(a: Vec4, b: Vec4, f: f32) -> Vec4 {
            let nf = 1.0 - f;
            Vec4 { x: a.x * nf + b.x * f, y: a.y * nf + b.y * f, z: a.z * nf + b.z * f, w: a.w * nf + b.w * f }
        }
        // find the 2 keys we want
        for i in 0..track.len() {
            if time >= track[i].0 {
                // we found the left key
                let val1 = &track[i];
                if i == track.len() - 1 {
                    // last key
                    return val1.1;
                }
                let val2 = &track[i + 1];
                // lerp it
                let f = ease.map((time - val1.0) / (val2.0 - val1.0)) as f32;
                return lerp(val1.1, val2.1, f);
            }
        }
        let val2 = &track[0];
        let f = ease.map(time / val2.0) as f32;
        lerp(init, val2.1, f)
    }
}