Skip to main content

bevy_react/transition/
channels.rs

1//! The transition engine's channel runtime: the shared ease machinery
2//! ([`Interp`] / [`EasedChannel`]), the value-space [`Channel`] (springs
3//! integrate in value space), the whole-value [`FilterChannel`], and the
4//! per-entity [`TransitionState`] they roll up into.
5
6use bevy::prelude::*;
7
8use crate::animations::{Lerp, Runner, build_runner};
9use crate::protocol::units::Length;
10
11use super::spec::ChannelTransition;
12use super::{shape_channel, transform3d};
13
14/// The transition engine's plain scalar/length channels — one row per
15/// [`TransitionState`] channel whose target rides a same-named
16/// [`TransitionInput`] field: `(channel, (identity default), group)`.
17/// Consumed by the mount-seed block (every row seeds `state.<channel>` from
18/// `input.<channel>` at its identity default) and by the size drive block
19/// (`size` rows, which also name the written `Node` field). The
20/// color/filter/backdrop/transform3d/scroll/shape channels have their own
21/// target shapes and stay explicit.
22macro_rules! with_input_channels {
23    ($cb:ident) => {
24        $cb! {
25            (translate_x, (Length::Px(0.0)), transform),
26            (translate_y, (Length::Px(0.0)), transform),
27            (scale, (1.0), transform),
28            (scale_x, (1.0), transform),
29            (scale_y, (1.0), transform),
30            (rotate, (0.0), transform),
31            (opacity, (1.0), value),
32            (width, (Length::Auto), size),
33            (height, (Length::Auto), size),
34            (max_width, (Length::Auto), size),
35            (max_height, (Length::Auto), size),
36        }
37    };
38}
39pub(super) use with_input_channels;
40
41/// Per-entity transition runtime: one [`Runner`]-backed channel per animatable
42/// property. Persists across re-renders (the engine owns it); created lazily by
43/// [`apply_transition`]. `#[require(UiTransform)]` so the drive query always
44/// matches even for an opacity/color-only transition.
45#[derive(Component, Default)]
46#[require(UiTransform)]
47pub struct TransitionState {
48    pub(super) translate_x: ProgressChannel<Length>,
49    pub(super) translate_y: ProgressChannel<Length>,
50    pub(super) scale: Channel,
51    pub(super) scale_x: Channel,
52    pub(super) scale_y: Channel,
53    pub(super) rotate: Channel,
54    pub(super) opacity: Channel,
55    pub(super) color: ProgressChannel<[f32; 4]>,
56    pub(super) width: ProgressChannel<Length>,
57    pub(super) height: ProgressChannel<Length>,
58    pub(super) max_width: ProgressChannel<Length>,
59    pub(super) max_height: ProgressChannel<Length>,
60    pub(super) filter: FilterChannel,
61    pub(super) backdrop_filter: FilterChannel,
62    pub(super) transform3d: transform3d::Transform3dChannels,
63    /// SVG shape-attr easing (spec + targets both ride `SvgShape.attrs` —
64    /// shapes have no style). Self-seeding per attr, so it doesn't
65    /// participate in the `initialized` block.
66    pub(super) shape: shape_channel::ShapeChannel,
67    pub(super) initialized: bool,
68}
69
70/// The whole-value `filter` channel: eases a promoted root's
71/// [`crate::filters::ResolvedFilterChain`] packed params between wire targets
72/// (see [`crate::filters::plan_filter_ease`] for the strategy). Unlike the
73/// scalar channels, its current reading cannot be re-read from the component —
74/// [`crate::filters::resolve_chains`] snaps the component to the new
75/// target on the retarget frame, before this system runs — so the state owns
76/// the last-written pass list (the `ProgressChannel` state-owned-current
77/// pattern, list-shaped).
78#[derive(Default)]
79pub(super) struct FilterChannel {
80    /// The last wire chain seen (retarget detection). Empty = no filter.
81    pub(super) wire: crate::filters::FilterChain,
82    /// The shared ease machinery: `channel.current` is the pass list this
83    /// channel last wrote (or adopted from the resolver) — the next ease's
84    /// start; the interp holds the armed plan. Both are (re)set together at
85    /// retarget, so the runner and its plan can't go out of sync.
86    pub(super) channel: EasedChannel<Vec<crate::filters::ResolvedFilterPass>, FilterEaseInterp>,
87}
88
89/// The filter-ease [`Interp`]: samples a planned pass-list ease
90/// ([`crate::filters::FilterEase`] — strategy + endpoints planned at retarget
91/// by [`crate::filters::plan_filter_ease`], delegated to verbatim, never
92/// reimplemented). The wrapper injects the plan at arm time; `arm` itself is
93/// a no-op because planning needs registry/assets context the trait seam
94/// doesn't carry.
95#[derive(Default)]
96pub(super) struct FilterEaseInterp {
97    plan: Option<crate::filters::FilterEase>,
98}
99
100impl Interp<Vec<crate::filters::ResolvedFilterPass>> for FilterEaseInterp {
101    fn arm(
102        &mut self,
103        _from: &Vec<crate::filters::ResolvedFilterPass>,
104        _to: &Vec<crate::filters::ResolvedFilterPass>,
105    ) {
106    }
107    fn sample(
108        &self,
109        p: f32,
110        _target: &Vec<crate::filters::ResolvedFilterPass>,
111    ) -> Vec<crate::filters::ResolvedFilterPass> {
112        self.plan.as_ref().map(|e| e.sample(p)).unwrap_or_default()
113    }
114    /// Completion writes the plan's own settle list — the resolver's snapped
115    /// output, bit-exact, so the two writers agree and stop churning (the
116    /// stage-interplay rule: bake the final value, don't approximate it).
117    fn settle(
118        &self,
119        _target: &Vec<crate::filters::ResolvedFilterPass>,
120    ) -> Vec<crate::filters::ResolvedFilterPass> {
121        self.plan
122            .as_ref()
123            .map(|e| e.settle().to_vec())
124            .unwrap_or_default()
125    }
126}
127
128impl FilterChannel {
129    /// Advance the filter chain toward the wire target in `input`, writing the
130    /// eased packed params into `resolved`. Returns `true` when it wrote —
131    /// the caller pushes composite-only dirt (filter output never dirties the
132    /// capture, which holds unfiltered content).
133    ///
134    /// Three writers touch [`crate::filters::ResolvedFilterChain`]; precedence
135    /// runs resolver → transition → bindings. On the retarget frame
136    /// [`crate::filters::resolve_chains`] (ordered before
137    /// [`drive_transitions`]) *snaps* the component to the new target; this
138    /// method *eases* over that snap — starting from the state-owned
139    /// `current`, the last value this channel wrote, never the
140    /// already-snapped component; and per-param animation bindings
141    /// (`filter[<i>].<param>`) *re-assert* individual params on top, winning
142    /// by gating this channel out via `skip_filter` (the imperative-wins
143    /// pattern of the scalar channels, coarse: any filter binding parks the
144    /// whole channel).
145    ///
146    /// The target rides the wire-chain component (`FilterInput` /
147    /// `BackdropInput` — the caller projects to the inner [`FilterChain`]),
148    /// NOT [`TransitionInput`] — a chain-only delta dirties the
149    /// FILTER/BACKDROP|LAYER groups, never TRANSITION, so a target stamped
150    /// into the input would go stale; the chain component is re-stamped by
151    /// that same delta. Both channel instances (filter, backdropFilter) run
152    /// this same code over their own component pair.
153    pub(super) fn drive(
154        &mut self,
155        input: Option<&crate::filters::FilterChain>,
156        mut resolved: Option<Mut<crate::filters::ResolvedFilterChain>>,
157        spec: Option<&ChannelTransition>,
158        registry: Option<&crate::filters::FilterRegistry>,
159        assets: Option<&AssetServer>,
160        dt: f32,
161    ) -> bool {
162        let retargeted = match input {
163            Some(fi) => *fi != self.wire,
164            None => !self.wire.0.is_empty(),
165        };
166        if retargeted {
167            let to_wire = input.cloned().unwrap_or_default();
168            let from_wire = std::mem::replace(&mut self.wire, to_wire);
169            match (spec, resolved.as_deref()) {
170                // Ease only toward a live resolved chain. An emptied or
171                // unresolvable target has no component to write into
172                // (unset `filter` demotes the layer; an all-invalid chain
173                // attaches none), so it snaps below.
174                (Some(spec), Some(chain)) if !self.wire.0.is_empty() => {
175                    self.channel.interp.plan = Some(crate::filters::plan_filter_ease(
176                        &from_wire,
177                        &self.wire,
178                        self.channel.current.clone(),
179                        chain.passes.clone(),
180                        registry,
181                        assets,
182                        chain.scale,
183                    ));
184                    self.channel.arm(chain.passes.clone(), spec);
185                }
186                _ => {
187                    // Snap: adopt whatever the resolver produced.
188                    self.channel.interp.plan = None;
189                    self.channel.init(
190                        resolved
191                            .as_deref()
192                            .map(|c| c.passes.clone())
193                            .unwrap_or_default(),
194                    );
195                }
196            }
197        }
198        let mut wrote = false;
199        if self.channel.runner.is_some() {
200            match resolved.as_mut() {
201                Some(resolved) => {
202                    // Advance the ease: `tick` samples the plan, and on
203                    // completion writes the plan's settle list (bit-exact —
204                    // see `FilterEaseInterp::settle`) and drops the runner.
205                    self.channel.tick(dt);
206                    // Compare via `Deref` first so a no-op frame doesn't
207                    // trip change detection.
208                    if resolved.passes != self.channel.current {
209                        let chain = &mut **resolved;
210                        chain.passes = self.channel.current.clone();
211                        chain.version = chain.version.wrapping_add(1);
212                        wrote = true;
213                    }
214                }
215                None => {
216                    // The chain vanished mid-ease (demotion tore the
217                    // layer down): drop the ease and forget the passes.
218                    self.channel.interp.plan = None;
219                    self.channel.init(Vec::new());
220                }
221            }
222        }
223        wrote
224    }
225}
226
227/// One scalar channel: its current reading, last target, and active driver.
228#[derive(Default)]
229pub(super) struct Channel {
230    pub(super) current: f32,
231    pub(super) target: f32,
232    pub(super) runner: Option<Runner>,
233}
234
235impl Channel {
236    /// Snap to `value` without animating (used to seed the resting state so an
237    /// element doesn't animate from zero when it first appears).
238    pub(super) fn init(&mut self, value: f32) {
239        self.current = value;
240        self.target = value;
241        self.runner = None;
242    }
243
244    /// Advance toward `target`. `spec` `Some` eases; `None` snaps. Returns the
245    /// current value.
246    pub(super) fn drive(&mut self, target: f32, spec: Option<&ChannelTransition>, dt: f32) -> f32 {
247        if target != self.target {
248            self.target = target;
249            match spec {
250                Some(s) => self.runner = Some(build_runner(&s.to_driver(target), self.current)),
251                None => {
252                    self.current = target;
253                    self.runner = None;
254                }
255            }
256        }
257        if let Some(r) = self.runner.as_mut() {
258            let (v, done) = r.step(dt);
259            self.current = v;
260            if done {
261                self.runner = None;
262            }
263        }
264        self.current
265    }
266}
267
268/// How an [`EasedChannel`] turns eased progress into readings of `T` — the
269/// seam between the shared ease machinery (retarget detection, runner
270/// lifecycle, snap-vs-ease, exact settle) and the value space it moves
271/// through. [`LerpInterp`] is the plain start→target lerp; the filter
272/// channel's interp samples a planned pass-list ease instead.
273pub(super) trait Interp<T> {
274    /// Called at retarget (with a spec): capture whatever sampling needs —
275    /// for a lerp, the start value. Domain wrappers that plan their ease
276    /// externally may make this a no-op and inject state directly.
277    fn arm(&mut self, from: &T, to: &T);
278    /// The reading at progress `p` (0→1).
279    fn sample(&self, p: f32, target: &T) -> T;
280    /// The exact final reading — completion writes THIS, never the last
281    /// sampled approximation (bit-exact settle).
282    fn settle(&self, target: &T) -> T;
283}
284
285/// A progress-eased channel: a single [`Runner`] eases progress 0→1 and an
286/// [`Interp`] turns it into readings. Used for quantities that can't be
287/// time-stepped directly in value space (a color's four channels move
288/// together; a `Length` carries a unit; a filter pass list moves as a whole).
289/// [`EasedChannel::drive`] returns the current reading every frame — a caller
290/// writing a relayout-triggering target (`Node`) compares before writing,
291/// like every other apply path.
292#[derive(Default)]
293pub(super) struct EasedChannel<T, I> {
294    pub(super) current: T,
295    pub(super) target: T,
296    pub(super) interp: I,
297    pub(super) runner: Option<Runner>,
298}
299
300impl<T: Clone + PartialEq, I: Interp<T>> EasedChannel<T, I> {
301    /// Snap to `value` without animating (used to seed the resting state so an
302    /// element doesn't animate from zero when it first appears).
303    pub(super) fn init(&mut self, value: T) {
304        self.current = value.clone();
305        self.target = value;
306        self.runner = None;
307    }
308
309    /// Arm an ease toward `target`: the interp captures its start state and a
310    /// fresh progress runner starts at 0. (Domain wrappers with external
311    /// retarget detection call this directly; [`Self::drive`] calls it on a
312    /// target change.)
313    pub(super) fn arm(&mut self, target: T, spec: &ChannelTransition) {
314        self.interp.arm(&self.current, &target);
315        self.target = target;
316        self.runner = Some(build_runner(&spec.to_driver(1.0), 0.0));
317    }
318
319    /// Advance an armed ease by `dt`, updating the current reading. Returns
320    /// whether this frame completed the ease; `None` when idle.
321    pub(super) fn tick(&mut self, dt: f32) -> Option<bool> {
322        let r = self.runner.as_mut()?;
323        let (p, done) = r.step(dt);
324        self.current = if done {
325            self.runner = None;
326            self.interp.settle(&self.target)
327        } else {
328            self.interp.sample(p, &self.target)
329        };
330        Some(done)
331    }
332
333    /// Advance toward `target`. `spec` `Some` eases; `None` snaps. Returns the
334    /// current reading.
335    pub(super) fn drive(&mut self, target: T, spec: Option<&ChannelTransition>, dt: f32) -> T {
336        if target != self.target {
337            match spec {
338                Some(s) => self.arm(target, s),
339                None => self.init(target),
340            }
341        }
342        self.tick(dt);
343        self.current.clone()
344    }
345}
346
347/// The plain value-lerp [`Interp`]: sample = `start.lerp(target, p)`.
348#[derive(Default)]
349pub(super) struct LerpInterp<T> {
350    start: T,
351}
352
353impl<T: Lerp> Interp<T> for LerpInterp<T> {
354    fn arm(&mut self, from: &T, _to: &T) {
355        self.start = *from;
356    }
357    fn sample(&self, p: f32, target: &T) -> T {
358        self.start.lerp(*target, p)
359    }
360    fn settle(&self, target: &T) -> T {
361        *target
362    }
363}
364
365/// A progress-lerped channel (colors, [`Length`]s) — the [`EasedChannel`]
366/// instantiated with the plain lerp.
367pub(super) type ProgressChannel<T> = EasedChannel<T, LerpInterp<T>>;
368
369/// Interpolate two lengths of the same unit; mixed units or `auto` can't be
370/// interpolated, so it snaps to the target.
371impl Lerp for Length {
372    fn lerp(self, other: Self, t: f32) -> Self {
373        use Length::*;
374        let lerp = |x: f32, y: f32| x + (y - x) * t;
375        match (self, other) {
376            (Px(x), Px(y)) => Px(lerp(x, y)),
377            (Percent(x), Percent(y)) => Percent(lerp(x, y)),
378            (Vw(x), Vw(y)) => Vw(lerp(x, y)),
379            (Vh(x), Vh(y)) => Vh(lerp(x, y)),
380            (VMin(x), VMin(y)) => VMin(lerp(x, y)),
381            (VMax(x), VMax(y)) => VMax(lerp(x, y)),
382            _ => other,
383        }
384    }
385}