Skip to main content

bevy_react/animations/
eval.rs

1//! Binding evaluation and interpolation primitives — the value math shared
2//! by the apply engine (`super::apply`) and `bevy-react`'s transition engine:
3//! [`Lerp`], the piecewise curves behind `interpolate`/`interpolateColor`
4//! bindings, and [`build_ui_transform`] (both `UiTransform` writers must
5//! agree on channel semantics).
6
7use bevy::prelude::*;
8use bevy::ui::UiTransform;
9
10use super::SharedValues;
11use super::protocol::Binding;
12
13/// Build a `UiTransform` from the six scalar transform channels (each `None`
14/// stays at identity: no translation, unit scale, no rotation). `scale` is
15/// uniform; `scale_x`/`scale_y` override a single axis. Shared by the animated
16/// node apply and `bevy-react`'s static/transition transform path so the channel
17/// semantics stay identical across both.
18pub fn build_ui_transform(
19    translate_x: Option<Val>,
20    translate_y: Option<Val>,
21    scale: Option<f32>,
22    scale_x: Option<f32>,
23    scale_y: Option<f32>,
24    rotate: Option<f32>,
25) -> UiTransform {
26    let mut t = UiTransform::IDENTITY;
27    if let Some(v) = translate_x {
28        t.translation.x = v;
29    }
30    if let Some(v) = translate_y {
31        t.translation.y = v;
32    }
33    let mut sx = 1.0;
34    let mut sy = 1.0;
35    if let Some(v) = scale {
36        sx = v;
37        sy = v;
38    }
39    if let Some(v) = scale_x {
40        sx = v;
41    }
42    if let Some(v) = scale_y {
43        sy = v;
44    }
45    t.scale = Vec2::new(sx, sy);
46    if let Some(v) = rotate {
47        t.rotation = Rot2::radians(v);
48    }
49    t
50}
51
52// --- Binding evaluation --------------------------------------------------------
53
54pub(super) fn eval_scalar(binding: &Binding, values: &SharedValues) -> Option<f32> {
55    match binding {
56        Binding::Shared { id } => values.get(*id),
57        Binding::Interpolate { id, input, output } => {
58            Some(piecewise(values.get(*id)?, input, output))
59        }
60        Binding::InterpolateColor { .. } => None,
61    }
62}
63
64pub(super) fn eval_color(binding: &Binding, values: &SharedValues) -> Option<[f32; 4]> {
65    match binding {
66        Binding::InterpolateColor { id, input, output } => {
67            Some(piecewise_color(values.get(*id)?, input, output))
68        }
69        _ => None,
70    }
71}
72
73/// Linear interpolation between two values of the same kind, `t` in `0.0..=1.0`.
74/// The one primitive every interpolated quantity shares — implemented here for
75/// the scalar and color bindings, and by `bevy-react`'s transition engine for its
76/// own channel types (hence public).
77pub trait Lerp: Copy {
78    /// `self + (other - self) * t`, component-wise where applicable.
79    fn lerp(self, other: Self, t: f32) -> Self;
80}
81
82impl Lerp for f32 {
83    fn lerp(self, other: Self, t: f32) -> Self {
84        self + (other - self) * t
85    }
86}
87
88impl Lerp for [f32; 4] {
89    fn lerp(self, other: Self, t: f32) -> Self {
90        // Qualified: `bevy::math::FloatExt::lerp` is also in scope for `f32`.
91        [
92            Lerp::lerp(self[0], other[0], t),
93            Lerp::lerp(self[1], other[1], t),
94            Lerp::lerp(self[2], other[2], t),
95            Lerp::lerp(self[3], other[3], t),
96        ]
97    }
98}
99
100/// Piecewise-linear interpolation, clamped at the ends. `input` must be ascending.
101pub(super) fn piecewise(x: f32, input: &[f32], output: &[f32]) -> f32 {
102    if input.is_empty() || output.is_empty() {
103        return x;
104    }
105    piecewise_impl(x, input, output)
106}
107
108/// Per-channel piecewise-linear color interpolation (rgba in `0.0..=1.0`).
109pub(super) fn piecewise_color(x: f32, input: &[f32], output: &[[f32; 4]]) -> [f32; 4] {
110    if input.is_empty() || output.is_empty() {
111        return [0.0, 0.0, 0.0, 1.0];
112    }
113    piecewise_impl(x, input, output)
114}
115
116/// The shared segment routine behind [`piecewise`]/[`piecewise_color`]: find the
117/// segment containing `x` and lerp within it, clamping at both ends. `input` must
118/// be ascending and both slices non-empty (the wrappers handle empty).
119fn piecewise_impl<T: Lerp>(x: f32, input: &[f32], output: &[T]) -> T {
120    let n = input.len().min(output.len());
121    if n == 1 || x <= input[0] {
122        return output[0];
123    }
124    if x >= input[n - 1] {
125        return output[n - 1];
126    }
127    for i in 0..n - 1 {
128        let (a, b) = (input[i], input[i + 1]);
129        if x >= a && x <= b {
130            let t = if (b - a).abs() < f32::EPSILON {
131                0.0
132            } else {
133                (x - a) / (b - a)
134            };
135            return output[i].lerp(output[i + 1], t);
136        }
137    }
138    output[n - 1]
139}