mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-13 22:02:15 +00:00
539 lines
20 KiB
Rust
539 lines
20 KiB
Rust
//! Animated values with After Effects keyframe semantics.
|
||
//!
|
||
//! * **Temporal** interpolation between two keys is linear, Bezier or hold. Bezier eases are given
|
||
//! per dimension as *speed* (value units per second) and *influence* (fraction of the segment's
|
||
//! duration, 0.1 %–100 %), exactly the numbers the Keyframe Velocity dialog shows.
|
||
//! * **Spatial** properties (Position, Anchor Point, Point effect params) move along a Bezier motion
|
||
//! path defined by spatial tangents; their temporal ease is a single speed along the path
|
||
//! (pixels/second), evaluated through an arc-length table.
|
||
//! * Auto-Bezier and continuous-Bezier keys derive their tangents from their neighbours.
|
||
|
||
pub mod value;
|
||
|
||
use effectcraft_time::{TICKS_PER_SECOND, Tick};
|
||
use serde::{Deserialize, Serialize};
|
||
pub use value::{Gradient, Justify, ShapePath, TextDoc, Value};
|
||
|
||
/// Temporal interpolation on one side of a keyframe.
|
||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||
pub enum Interp {
|
||
#[default]
|
||
Linear,
|
||
Bezier,
|
||
Hold,
|
||
}
|
||
|
||
impl Interp {
|
||
pub fn label(self) -> &'static str {
|
||
match self {
|
||
Interp::Linear => "Linear",
|
||
Interp::Bezier => "Bezier",
|
||
Interp::Hold => "Hold",
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Temporal ease on one side of a key: speed in units/s and influence 0..1 (fraction of the
|
||
/// adjacent segment duration).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
||
pub struct Ease {
|
||
pub speed: f64,
|
||
pub influence: f64,
|
||
}
|
||
|
||
impl Default for Ease {
|
||
fn default() -> Self {
|
||
Ease { speed: 0.0, influence: 1.0 / 6.0 }
|
||
}
|
||
}
|
||
|
||
impl Ease {
|
||
/// Easy Ease: zero speed, 33.33 % influence.
|
||
pub const EASY: Ease = Ease { speed: 0.0, influence: 1.0 / 3.0 };
|
||
}
|
||
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||
pub struct Keyframe {
|
||
pub time: Tick,
|
||
pub value: Value,
|
||
#[serde(default)]
|
||
pub in_interp: Interp,
|
||
#[serde(default)]
|
||
pub out_interp: Interp,
|
||
/// Per-dimension (or one entry for spatial / single-dimension values).
|
||
#[serde(default)]
|
||
pub in_ease: Vec<Ease>,
|
||
#[serde(default)]
|
||
pub out_ease: Vec<Ease>,
|
||
/// Temporal continuous / auto Bezier flags.
|
||
#[serde(default)]
|
||
pub continuous: bool,
|
||
#[serde(default)]
|
||
pub auto_bezier: bool,
|
||
/// Spatial tangents relative to the key value (spatial properties only).
|
||
#[serde(default)]
|
||
pub spatial_in: [f64; 3],
|
||
#[serde(default)]
|
||
pub spatial_out: [f64; 3],
|
||
/// Spatial auto-Bezier: tangents computed from neighbours.
|
||
#[serde(default = "yes")]
|
||
pub spatial_auto: bool,
|
||
#[serde(default)]
|
||
pub spatial_continuous: bool,
|
||
#[serde(default)]
|
||
pub roving: bool,
|
||
}
|
||
|
||
fn yes() -> bool {
|
||
true
|
||
}
|
||
|
||
impl Keyframe {
|
||
pub fn new(time: Tick, value: Value) -> Keyframe {
|
||
Keyframe {
|
||
time,
|
||
value,
|
||
in_interp: Interp::Linear,
|
||
out_interp: Interp::Linear,
|
||
in_ease: vec![],
|
||
out_ease: vec![],
|
||
continuous: false,
|
||
auto_bezier: false,
|
||
spatial_in: [0.0; 3],
|
||
spatial_out: [0.0; 3],
|
||
spatial_auto: true,
|
||
spatial_continuous: false,
|
||
roving: false,
|
||
}
|
||
}
|
||
pub fn hold(mut self) -> Keyframe {
|
||
self.in_interp = Interp::Hold;
|
||
self.out_interp = Interp::Hold;
|
||
self
|
||
}
|
||
/// Easy Ease on both sides.
|
||
pub fn eased(mut self) -> Keyframe {
|
||
self.in_interp = Interp::Bezier;
|
||
self.out_interp = Interp::Bezier;
|
||
let n = self.value.dims().max(1);
|
||
self.in_ease = vec![Ease::EASY; n];
|
||
self.out_ease = vec![Ease::EASY; n];
|
||
self
|
||
}
|
||
/// Shape of the keyframe icon in the timeline.
|
||
pub fn icon(&self) -> KeyIcon {
|
||
let side = |i: Interp, auto: bool| match i {
|
||
Interp::Hold => KeyHalf::Hold,
|
||
Interp::Linear => KeyHalf::Linear,
|
||
Interp::Bezier if auto => KeyHalf::Auto,
|
||
Interp::Bezier => KeyHalf::Bezier,
|
||
};
|
||
KeyIcon { left: side(self.in_interp, self.auto_bezier), right: side(self.out_interp, self.auto_bezier) }
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub enum KeyHalf {
|
||
Linear,
|
||
Bezier,
|
||
Auto,
|
||
Hold,
|
||
}
|
||
|
||
/// Keyframe icon halves (diamond = linear, hourglass = bezier/ease, circle = auto, square = hold).
|
||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||
pub struct KeyIcon {
|
||
pub left: KeyHalf,
|
||
pub right: KeyHalf,
|
||
}
|
||
|
||
fn secs(t: Tick) -> f64 {
|
||
t.0 as f64 / TICKS_PER_SECOND as f64
|
||
}
|
||
|
||
/// Cubic Bezier in 1D.
|
||
fn bez(p0: f64, p1: f64, p2: f64, p3: f64, u: f64) -> f64 {
|
||
let v = 1.0 - u;
|
||
v * v * v * p0 + 3.0 * v * v * u * p1 + 3.0 * v * u * u * p2 + u * u * u * p3
|
||
}
|
||
fn bez_d(p0: f64, p1: f64, p2: f64, p3: f64, u: f64) -> f64 {
|
||
let v = 1.0 - u;
|
||
3.0 * v * v * (p1 - p0) + 6.0 * v * u * (p2 - p1) + 3.0 * u * u * (p3 - p2)
|
||
}
|
||
|
||
/// Solve `bez(x0..x3, u) = x` for u in 0..1 (x curve monotone).
|
||
fn solve_u(x0: f64, x1: f64, x2: f64, x3: f64, x: f64) -> f64 {
|
||
if x <= x0 {
|
||
return 0.0;
|
||
}
|
||
if x >= x3 {
|
||
return 1.0;
|
||
}
|
||
let mut u = (x - x0) / (x3 - x0);
|
||
for _ in 0..8 {
|
||
let f = bez(x0, x1, x2, x3, u) - x;
|
||
if f.abs() < 1e-12 {
|
||
return u;
|
||
}
|
||
let d = bez_d(x0, x1, x2, x3, u);
|
||
if d.abs() < 1e-12 {
|
||
break;
|
||
}
|
||
let n = u - f / d;
|
||
if !(0.0..=1.0).contains(&n) {
|
||
break;
|
||
}
|
||
u = n;
|
||
}
|
||
let (mut lo, mut hi) = (0.0, 1.0);
|
||
for _ in 0..60 {
|
||
let mid = (lo + hi) * 0.5;
|
||
if bez(x0, x1, x2, x3, mid) < x {
|
||
lo = mid;
|
||
} else {
|
||
hi = mid;
|
||
}
|
||
}
|
||
(lo + hi) * 0.5
|
||
}
|
||
|
||
/// Fraction of progress 0..1 through a 1D Bezier temporal segment whose value goes from 0 to 1,
|
||
/// with normalised out/in speeds (speed × duration / Δvalue) and influences.
|
||
pub fn ease_progress(out_speed_n: f64, out_inf: f64, in_speed_n: f64, in_inf: f64, x: f64) -> f64 {
|
||
let (mut i0, mut i1) = (out_inf.clamp(0.0, 1.0), in_inf.clamp(0.0, 1.0));
|
||
if i0 + i1 > 1.0 {
|
||
let k = 1.0 / (i0 + i1);
|
||
i0 *= k;
|
||
i1 *= k;
|
||
}
|
||
let u = solve_u(0.0, i0, 1.0 - i1, 1.0, x);
|
||
bez(0.0, out_speed_n * i0, 1.0 - in_speed_n * i1, 1.0, u)
|
||
}
|
||
|
||
/// Spatial cubic segment with an arc-length table.
|
||
struct SpatialSeg {
|
||
p: [[f64; 3]; 4],
|
||
/// Cumulative length at u = i / N.
|
||
table: Vec<f64>,
|
||
}
|
||
|
||
const ARC_N: usize = 96;
|
||
|
||
impl SpatialSeg {
|
||
fn new(p: [[f64; 3]; 4]) -> SpatialSeg {
|
||
let mut table = Vec::with_capacity(ARC_N + 1);
|
||
table.push(0.0);
|
||
let mut prev = Self::point(&p, 0.0);
|
||
let mut acc = 0.0;
|
||
for i in 1..=ARC_N {
|
||
let q = Self::point(&p, i as f64 / ARC_N as f64);
|
||
acc += ((q[0] - prev[0]).powi(2) + (q[1] - prev[1]).powi(2) + (q[2] - prev[2]).powi(2)).sqrt();
|
||
table.push(acc);
|
||
prev = q;
|
||
}
|
||
SpatialSeg { p, table }
|
||
}
|
||
fn point(p: &[[f64; 3]; 4], u: f64) -> [f64; 3] {
|
||
[bez(p[0][0], p[1][0], p[2][0], p[3][0], u), bez(p[0][1], p[1][1], p[2][1], p[3][1], u), bez(p[0][2], p[1][2], p[2][2], p[3][2], u)]
|
||
}
|
||
fn length(&self) -> f64 {
|
||
*self.table.last().unwrap_or(&0.0)
|
||
}
|
||
fn at_length(&self, s: f64) -> [f64; 3] {
|
||
let len = self.length();
|
||
if len <= 0.0 {
|
||
return self.p[0];
|
||
}
|
||
let s = s.clamp(0.0, len);
|
||
let i = self.table.partition_point(|&v| v < s).clamp(1, ARC_N);
|
||
let (a, b) = (self.table[i - 1], self.table[i]);
|
||
let f = if b > a { (s - a) / (b - a) } else { 0.0 };
|
||
Self::point(&self.p, (i as f64 - 1.0 + f) / ARC_N as f64)
|
||
}
|
||
}
|
||
|
||
fn v3(v: &Value) -> [f64; 3] {
|
||
v.as_vec3()
|
||
}
|
||
|
||
/// Auto-Bezier spatial tangents (Catmull-Rom style) for key `i`.
|
||
fn auto_spatial(keys: &[Keyframe], i: usize) -> ([f64; 3], [f64; 3]) {
|
||
if i == 0 || i + 1 >= keys.len() {
|
||
return ([0.0; 3], [0.0; 3]);
|
||
}
|
||
let a = v3(&keys[i - 1].value);
|
||
let b = v3(&keys[i].value);
|
||
let c = v3(&keys[i + 1].value);
|
||
let d = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
||
let dl = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
|
||
if dl < 1e-12 {
|
||
return ([0.0; 3], [0.0; 3]);
|
||
}
|
||
let lin = ((b[0] - a[0]).powi(2) + (b[1] - a[1]).powi(2) + (b[2] - a[2]).powi(2)).sqrt();
|
||
let lout = ((c[0] - b[0]).powi(2) + (c[1] - b[1]).powi(2) + (c[2] - b[2]).powi(2)).sqrt();
|
||
let n = [d[0] / dl, d[1] / dl, d[2] / dl];
|
||
let tin = [-n[0] * lin / 3.0, -n[1] * lin / 3.0, -n[2] * lin / 3.0];
|
||
let tout = [n[0] * lout / 3.0, n[1] * lout / 3.0, n[2] * lout / 3.0];
|
||
(tin, tout)
|
||
}
|
||
|
||
/// Effective spatial tangents of key `i` (auto or stored).
|
||
pub fn spatial_tangents(keys: &[Keyframe], i: usize) -> ([f64; 3], [f64; 3]) {
|
||
if keys[i].spatial_auto { auto_spatial(keys, i) } else { (keys[i].spatial_in, keys[i].spatial_out) }
|
||
}
|
||
|
||
/// Effective temporal ease of key `i` for dimension `d` (auto-Bezier keys get the neighbour slope).
|
||
fn effective_ease(keys: &[Keyframe], i: usize, d: usize, spatial: bool, out: bool) -> Ease {
|
||
let k = &keys[i];
|
||
let list = if out { &k.out_ease } else { &k.in_ease };
|
||
if k.auto_bezier && i > 0 && i + 1 < keys.len() {
|
||
let dt = secs(keys[i + 1].time) - secs(keys[i - 1].time);
|
||
let slope = if spatial {
|
||
let a = v3(&keys[i - 1].value);
|
||
let c = v3(&keys[i + 1].value);
|
||
((c[0] - a[0]).powi(2) + (c[1] - a[1]).powi(2) + (c[2] - a[2]).powi(2)).sqrt() / dt.max(1e-12)
|
||
} else {
|
||
let a = keys[i - 1].value.components();
|
||
let c = keys[i + 1].value.components();
|
||
(c.get(d).unwrap_or(&0.0) - a.get(d).unwrap_or(&0.0)) / dt.max(1e-12)
|
||
};
|
||
return Ease { speed: slope, influence: 1.0 / 6.0 };
|
||
}
|
||
list.get(d).or(list.first()).copied().unwrap_or_default()
|
||
}
|
||
|
||
/// Evaluate a keyframed property at `t`. `spatial` selects motion-path semantics for 2D/3D values.
|
||
pub fn evaluate(keys: &[Keyframe], t: Tick, spatial: bool) -> Option<Value> {
|
||
let first = keys.first()?;
|
||
if keys.len() == 1 || t <= first.time {
|
||
return Some(first.value.clone());
|
||
}
|
||
let last = keys.last()?;
|
||
if t >= last.time {
|
||
return Some(last.value.clone());
|
||
}
|
||
let i = keys.partition_point(|k| k.time <= t) - 1;
|
||
Some(segment_value(keys, i, t, spatial))
|
||
}
|
||
|
||
/// Value inside segment `i` → `i+1` at time `t`.
|
||
pub fn segment_value(keys: &[Keyframe], i: usize, t: Tick, spatial: bool) -> Value {
|
||
let a = &keys[i];
|
||
let b = &keys[i + 1];
|
||
if a.out_interp == Interp::Hold || !a.value.interpolates() {
|
||
return a.value.clone();
|
||
}
|
||
let (t0, t1) = (secs(a.time), secs(b.time));
|
||
let dur = (t1 - t0).max(1e-12);
|
||
let x = ((secs(t) - t0) / dur).clamp(0.0, 1.0);
|
||
let lin_out = a.out_interp == Interp::Linear;
|
||
let lin_in = b.in_interp == Interp::Linear;
|
||
let is_spatial = spatial && matches!(a.value, Value::Vec2(_) | Value::Vec3(_));
|
||
if is_spatial {
|
||
let p0 = v3(&a.value);
|
||
let p3 = v3(&b.value);
|
||
let (_, out_t) = spatial_tangents(keys, i);
|
||
let (in_t, _) = spatial_tangents(keys, i + 1);
|
||
let seg = SpatialSeg::new([p0, [p0[0] + out_t[0], p0[1] + out_t[1], p0[2] + out_t[2]], [p3[0] + in_t[0], p3[1] + in_t[1], p3[2] + in_t[2]], p3]);
|
||
let len = seg.length();
|
||
let f = if lin_out && lin_in || len <= 1e-12 {
|
||
x
|
||
} else {
|
||
let eo = if lin_out { Ease { speed: len / dur, influence: 1.0 / 3.0 } } else { effective_ease(keys, i, 0, true, true) };
|
||
let ei = if lin_in { Ease { speed: len / dur, influence: 1.0 / 3.0 } } else { effective_ease(keys, i + 1, 0, true, false) };
|
||
ease_progress(eo.speed * dur / len, eo.influence, ei.speed * dur / len, ei.influence, x)
|
||
};
|
||
let p = seg.at_length(f * len);
|
||
return a.value.with_components(&p);
|
||
}
|
||
if lin_out && lin_in {
|
||
return a.value.lerp(&b.value, x);
|
||
}
|
||
let ca = a.value.components();
|
||
let cb = b.value.components();
|
||
if ca.is_empty() || ca.len() != cb.len() {
|
||
// Paths, gradients: ease on dimension 0 as overall progress.
|
||
let eo = if lin_out { Ease { speed: 1.0 / dur, influence: 1.0 / 3.0 } } else { effective_ease(keys, i, 0, false, true) };
|
||
let ei = if lin_in { Ease { speed: 1.0 / dur, influence: 1.0 / 3.0 } } else { effective_ease(keys, i + 1, 0, false, false) };
|
||
let f = ease_progress(eo.speed * dur, eo.influence, ei.speed * dur, ei.influence, x);
|
||
return a.value.lerp(&b.value, f);
|
||
}
|
||
let mut out = Vec::with_capacity(ca.len());
|
||
for d in 0..ca.len() {
|
||
let dv = cb[d] - ca[d];
|
||
let eo = if lin_out { Ease { speed: dv / dur, influence: 1.0 / 3.0 } } else { effective_ease(keys, i, d, false, true) };
|
||
let ei = if lin_in { Ease { speed: dv / dur, influence: 1.0 / 3.0 } } else { effective_ease(keys, i + 1, d, false, false) };
|
||
// Bezier in (time, value) space directly: P1 = (i0, v0 + s0*i0*dur), P2 = (1-i1, v1 - s1*i1*dur).
|
||
let (mut i0, mut i1) = (eo.influence.clamp(0.0, 1.0), ei.influence.clamp(0.0, 1.0));
|
||
if i0 + i1 > 1.0 {
|
||
let k = 1.0 / (i0 + i1);
|
||
i0 *= k;
|
||
i1 *= k;
|
||
}
|
||
let u = solve_u(0.0, i0, 1.0 - i1, 1.0, x);
|
||
let v = bez(ca[d], ca[d] + eo.speed * i0 * dur, cb[d] - ei.speed * i1 * dur, cb[d], u);
|
||
out.push(v);
|
||
}
|
||
a.value.with_components(&out)
|
||
}
|
||
|
||
/// Numeric velocity (units/s per dimension) at `t` by central difference.
|
||
pub fn velocity(keys: &[Keyframe], t: Tick, spatial: bool) -> Vec<f64> {
|
||
let h = Tick(TICKS_PER_SECOND / 1000);
|
||
let (Some(a), Some(b)) = (evaluate(keys, t - h, spatial), evaluate(keys, t + h, spatial)) else { return vec![] };
|
||
let dt = 2.0 * secs(h);
|
||
a.components().iter().zip(b.components()).map(|(x, y)| (y - x) / dt).collect()
|
||
}
|
||
|
||
/// Speed (magnitude of velocity).
|
||
pub fn speed(keys: &[Keyframe], t: Tick, spatial: bool) -> f64 {
|
||
velocity(keys, t, spatial).iter().map(|v| v * v).sum::<f64>().sqrt()
|
||
}
|
||
|
||
/// Insert or replace (same time) a key; keeps keys sorted. Returns its index.
|
||
pub fn set_key(keys: &mut Vec<Keyframe>, key: Keyframe) -> usize {
|
||
match keys.binary_search_by(|k| k.time.cmp(&key.time)) {
|
||
Ok(i) => {
|
||
let mut k = key;
|
||
// Keep the existing interpolation when only the value changes.
|
||
let old = &keys[i];
|
||
k.in_interp = old.in_interp;
|
||
k.out_interp = old.out_interp;
|
||
k.in_ease = old.in_ease.clone();
|
||
k.out_ease = old.out_ease.clone();
|
||
k.auto_bezier = old.auto_bezier;
|
||
k.continuous = old.continuous;
|
||
k.spatial_in = old.spatial_in;
|
||
k.spatial_out = old.spatial_out;
|
||
k.spatial_auto = old.spatial_auto;
|
||
keys[i] = k;
|
||
i
|
||
}
|
||
Err(i) => {
|
||
keys.insert(i, key);
|
||
i
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Index of the key exactly at `t`.
|
||
pub fn key_at(keys: &[Keyframe], t: Tick) -> Option<usize> {
|
||
keys.binary_search_by(|k| k.time.cmp(&t)).ok()
|
||
}
|
||
|
||
/// Apply Easy Ease (in, out or both) to key `i`.
|
||
pub fn easy_ease(k: &mut Keyframe, ease_in: bool, ease_out: bool) {
|
||
let n = k.value.dims().max(1);
|
||
if ease_in {
|
||
k.in_interp = Interp::Bezier;
|
||
k.in_ease = vec![Ease::EASY; n];
|
||
}
|
||
if ease_out {
|
||
k.out_interp = Interp::Bezier;
|
||
k.out_ease = vec![Ease::EASY; n];
|
||
}
|
||
k.auto_bezier = false;
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn s(x: f64) -> Tick {
|
||
Tick::from_seconds_f64(x)
|
||
}
|
||
|
||
#[test]
|
||
fn linear_and_hold() {
|
||
let keys = vec![Keyframe::new(s(0.0), Value::Scalar(0.0)), Keyframe::new(s(2.0), Value::Scalar(100.0))];
|
||
assert_eq!(evaluate(&keys, s(1.0), false), Some(Value::Scalar(50.0)));
|
||
assert_eq!(evaluate(&keys, s(-1.0), false), Some(Value::Scalar(0.0)));
|
||
assert_eq!(evaluate(&keys, s(3.0), false), Some(Value::Scalar(100.0)));
|
||
let held = vec![Keyframe::new(s(0.0), Value::Scalar(0.0)).hold(), Keyframe::new(s(2.0), Value::Scalar(100.0))];
|
||
assert_eq!(evaluate(&held, s(1.999), false), Some(Value::Scalar(0.0)));
|
||
}
|
||
|
||
#[test]
|
||
fn easy_ease_is_symmetric_and_slow_at_ends() {
|
||
let keys = vec![Keyframe::new(s(0.0), Value::Scalar(0.0)).eased(), Keyframe::new(s(1.0), Value::Scalar(100.0)).eased()];
|
||
let mid = evaluate(&keys, s(0.5), false).unwrap().as_f64();
|
||
assert!((mid - 50.0).abs() < 1e-6, "{mid}");
|
||
let early = evaluate(&keys, s(0.1), false).unwrap().as_f64();
|
||
assert!(early < 10.0 && early > 0.0, "{early}");
|
||
let v0 = velocity(&keys, s(0.01), false)[0];
|
||
let vm = velocity(&keys, s(0.5), false)[0];
|
||
assert!(v0 < vm);
|
||
}
|
||
|
||
#[test]
|
||
fn bezier_with_linear_speed_matches_linear() {
|
||
let mut a = Keyframe::new(s(0.0), Value::Scalar(0.0));
|
||
let mut b = Keyframe::new(s(2.0), Value::Scalar(100.0));
|
||
a.out_interp = Interp::Bezier;
|
||
b.in_interp = Interp::Bezier;
|
||
a.out_ease = vec![Ease { speed: 50.0, influence: 1.0 / 3.0 }];
|
||
b.in_ease = vec![Ease { speed: 50.0, influence: 1.0 / 3.0 }];
|
||
let keys = vec![a, b];
|
||
for i in 0..=20 {
|
||
let t = i as f64 * 0.1;
|
||
let v = evaluate(&keys, s(t), false).unwrap().as_f64();
|
||
assert!((v - 50.0 * t).abs() < 1e-6, "{t} {v}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn spatial_linear_constant_speed() {
|
||
let keys = vec![Keyframe::new(s(0.0), Value::Vec2([0.0, 0.0])), Keyframe::new(s(1.0), Value::Vec2([300.0, 400.0]))];
|
||
let p = evaluate(&keys, s(0.5), true).unwrap().as_vec2();
|
||
assert!((p[0] - 150.0).abs() < 0.5 && (p[1] - 200.0).abs() < 0.5, "{p:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn spatial_auto_bezier_passes_through_keys() {
|
||
let keys = vec![
|
||
Keyframe::new(s(0.0), Value::Vec2([0.0, 0.0])),
|
||
Keyframe::new(s(1.0), Value::Vec2([100.0, 100.0])),
|
||
Keyframe::new(s(2.0), Value::Vec2([200.0, 0.0])),
|
||
];
|
||
let p = evaluate(&keys, s(1.0), true).unwrap().as_vec2();
|
||
assert_eq!(p, [100.0, 100.0]);
|
||
// Curved: the midpoint of the first segment lies off the straight line.
|
||
let m = evaluate(&keys, s(0.5), true).unwrap().as_vec2();
|
||
assert!((m[0] - m[1]).abs() > 1.0, "{m:?}");
|
||
}
|
||
|
||
#[test]
|
||
fn set_key_replaces_same_time() {
|
||
let mut keys = vec![];
|
||
set_key(&mut keys, Keyframe::new(s(1.0), Value::Scalar(1.0)).eased());
|
||
set_key(&mut keys, Keyframe::new(s(0.0), Value::Scalar(0.0)));
|
||
set_key(&mut keys, Keyframe::new(s(1.0), Value::Scalar(5.0)));
|
||
assert_eq!(keys.len(), 2);
|
||
assert_eq!(keys[1].value, Value::Scalar(5.0));
|
||
assert_eq!(keys[1].in_interp, Interp::Bezier);
|
||
}
|
||
|
||
#[test]
|
||
fn colors_and_paths_interpolate() {
|
||
let keys = vec![Keyframe::new(s(0.0), Value::Color([0.0, 0.0, 0.0, 1.0])), Keyframe::new(s(1.0), Value::Color([1.0, 0.5, 0.0, 1.0]))];
|
||
assert_eq!(evaluate(&keys, s(0.5), false).unwrap().as_color(), [0.5, 0.25, 0.0, 1.0]);
|
||
let a = ShapePath::rect([0.0, 0.0], 10.0, 10.0);
|
||
let b = ShapePath::rect([0.0, 0.0], 20.0, 20.0);
|
||
let keys = vec![Keyframe::new(s(0.0), Value::Path(a)), Keyframe::new(s(1.0), Value::Path(b))];
|
||
let v = evaluate(&keys, s(0.5), false).unwrap();
|
||
assert_eq!(v.as_path().unwrap().vertices[0], [-7.5, -7.5]);
|
||
}
|
||
|
||
proptest::proptest! {
|
||
#[test]
|
||
fn eased_values_stay_in_range(t in 0.0f64..1.0, i0 in 0.01f64..1.0, i1 in 0.01f64..1.0) {
|
||
let mut a = Keyframe::new(s(0.0), Value::Scalar(0.0)).eased();
|
||
let mut b = Keyframe::new(s(1.0), Value::Scalar(10.0)).eased();
|
||
a.out_ease = vec![Ease { speed: 0.0, influence: i0 }];
|
||
b.in_ease = vec![Ease { speed: 0.0, influence: i1 }];
|
||
let v = evaluate(&[a, b], s(t), false).unwrap().as_f64();
|
||
proptest::prop_assert!((-1e-9..=10.0 + 1e-9).contains(&v));
|
||
}
|
||
}
|
||
}
|