Skip to main content

oxframe_core/
transition.rs

1//! What a scene records when it starts a transition, and how two layers
2//! combine. The builder users call lives in `oxframe`.
3
4use crate::CubicBezier;
5use crate::TIME_EPSILON;
6use crate::ease::Ease;
7
8/// Edge a slide or a wipe moves toward.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Dir {
11    /// Toward the left.
12    Left,
13    /// Toward the right.
14    Right,
15    /// Toward the top.
16    Up,
17    /// Toward the bottom.
18    Down,
19}
20
21impl Dir {
22    pub(crate) fn name(self) -> &'static str {
23        match self {
24            Self::Left => "left",
25            Self::Right => "right",
26            Self::Up => "up",
27            Self::Down => "down",
28        }
29    }
30}
31
32/// How one of the two layers is drawn. Offsets are fractions of the frame
33/// (positive x moves the layer right, positive y moves it down). Scale is
34/// around the frame center. A wipe reveals that fraction of the layer from
35/// `dir` and hides the rest.
36#[derive(Debug, Clone, Copy, PartialEq)]
37pub struct LayerPose {
38    /// 0 is invisible, 1 is solid.
39    pub opacity: f64,
40    /// Horizontal shift, as a fraction of the frame width.
41    pub dx: f64,
42    /// Vertical shift, as a fraction of the frame height.
43    pub dy: f64,
44    /// Scale around the center. 1 leaves the layer as it is.
45    pub scale: f64,
46    /// Reveal this much of the layer (0..1) from this edge. `None` shows it all.
47    pub wipe: Option<(Dir, f64)>,
48}
49
50impl Default for LayerPose {
51    fn default() -> Self {
52        Self {
53            opacity: 1.0,
54            dx: 0.0,
55            dy: 0.0,
56            scale: 1.0,
57            wipe: None,
58        }
59    }
60}
61
62impl LayerPose {
63    /// Fully visible, unmoved, unscaled.
64    pub fn new() -> Self {
65        Self::default()
66    }
67
68    /// Multiplies the layer's alpha.
69    pub fn opacity(mut self, opacity: f64) -> Self {
70        self.opacity = opacity;
71        self
72    }
73
74    /// Shifts the layer by fractions of the frame.
75    pub fn offset(mut self, dx: f64, dy: f64) -> Self {
76        self.dx = dx;
77        self.dy = dy;
78        self
79    }
80
81    /// Scales the layer around the center.
82    pub fn scale(mut self, scale: f64) -> Self {
83        self.scale = scale;
84        self
85    }
86
87    /// Shows `amount` of the layer (0..1) starting from `dir`.
88    pub fn wipe(mut self, dir: Dir, amount: f64) -> Self {
89        self.wipe = Some((dir, amount));
90        self
91    }
92}
93
94/// The two layers of one transition frame.
95///
96/// `mix` crossfades them. Otherwise the back layer is painted, then the
97/// front one, using each pose's opacity, offset, scale and wipe.
98#[derive(Debug, Clone, Copy, PartialEq)]
99pub struct Blend {
100    /// The outgoing scene.
101    pub from: LayerPose,
102    /// The incoming scene.
103    pub to: LayerPose,
104    /// The incoming layer is painted after the outgoing one.
105    pub incoming_on_top: bool,
106    /// Crossfade instead of painting one layer over the other.
107    pub mix: bool,
108}
109
110impl Default for Blend {
111    fn default() -> Self {
112        Self {
113            from: LayerPose::new(),
114            to: LayerPose::new(),
115            incoming_on_top: true,
116            mix: false,
117        }
118    }
119}
120
121/// Which built-in, or a function of progress. Not part of the prelude;
122/// scenes build one through [`crate`] re-exports in `oxframe::Transition`.
123#[derive(Clone, Copy)]
124pub enum Kind {
125    /// Crossfade.
126    Fade,
127    /// Both layers travel toward `dir`.
128    Slide(Dir),
129    /// The incoming layer is revealed from `dir`.
130    Wipe(Dir),
131    /// The incoming layer grows onto the outgoing one.
132    ZoomIn,
133    /// The outgoing layer grows and fades off the incoming one.
134    ZoomOut,
135    /// `progress` in 0..1 (already eased) decides the poses.
136    Custom(fn(f64) -> Blend),
137}
138
139impl Kind {
140    pub(crate) fn name(self) -> &'static str {
141        match self {
142            Self::Fade => "fade",
143            Self::Slide(_) => "slide",
144            Self::Wipe(_) => "wipe",
145            Self::ZoomIn => "zoom_in",
146            Self::ZoomOut => "zoom_out",
147            Self::Custom(_) => "custom",
148        }
149    }
150
151    pub(crate) fn dir(self) -> Option<Dir> {
152        match self {
153            Self::Slide(d) | Self::Wipe(d) => Some(d),
154            _ => None,
155        }
156    }
157}
158
159/// A transition a scene has started. Copied out of the executor after the
160/// scene has been measured.
161#[derive(Clone, Copy)]
162pub struct Recorded {
163    pub(crate) kind: Kind,
164    pub(crate) seconds: f64,
165    pub(crate) ease: CubicBezier,
166    pub(crate) overlap: bool,
167    /// Scene time at which the blend starts.
168    pub(crate) start: f64,
169}
170
171impl Recorded {
172    /// A transition that starts at `start` seconds into its scene.
173    pub fn new(kind: Kind, seconds: f64, ease: CubicBezier, overlap: bool, start: f64) -> Self {
174        Self {
175            kind,
176            seconds,
177            ease,
178            overlap,
179            start,
180        }
181    }
182
183    /// `"fade"`, `"slide"`, `"wipe"`, `"zoom_in"`, `"zoom_out"` or `"custom"`.
184    pub fn kind_name(self) -> &'static str {
185        self.kind.name()
186    }
187
188    /// `"left"`, `"right"`, `"up"` or `"down"` for a slide or a wipe.
189    pub fn dir_name(self) -> Option<&'static str> {
190        self.kind.dir().map(Dir::name)
191    }
192
193    /// The previous scene keeps playing instead of holding its last frame.
194    pub fn overlap(self) -> bool {
195        self.overlap
196    }
197
198    /// Blend length in seconds.
199    pub fn seconds(self) -> f64 {
200        self.seconds
201    }
202
203    /// Scene time at which the blend starts.
204    pub fn start(self) -> f64 {
205        self.start
206    }
207
208    /// This scene time is still inside the blend.
209    pub fn active(self, now: f64) -> bool {
210        self.seconds > 0.0
211            && now + TIME_EPSILON >= self.start
212            && now + TIME_EPSILON < self.start + self.seconds
213    }
214
215    /// Eased progress at `now`, clamped to 0..=1.
216    pub fn progress(self, now: f64) -> f64 {
217        if self.seconds <= 0.0 {
218            return 1.0;
219        }
220        let t = ((now - self.start) / self.seconds).clamp(0.0, 1.0);
221        self.ease.ease(t).clamp(0.0, 1.0)
222    }
223
224    /// Layer poses at `now`.
225    pub fn blend_at(self, now: f64) -> Blend {
226        blend(self.kind, self.progress(now))
227    }
228}
229
230/// Poses for a built-in at eased progress `t` (0 = outgoing, 1 = incoming).
231pub(crate) fn blend(kind: Kind, t: f64) -> Blend {
232    let t = t.clamp(0.0, 1.0);
233    match kind {
234        Kind::Fade => Blend {
235            mix: true,
236            ..Blend::default()
237        },
238        Kind::Slide(dir) => {
239            let (fx, fy, tx, ty) = shift(dir, t);
240            Blend {
241                from: LayerPose::new().offset(fx, fy),
242                to: LayerPose::new().offset(tx, ty),
243                ..Blend::default()
244            }
245        }
246        Kind::Wipe(dir) => Blend {
247            to: LayerPose::new().wipe(dir, t),
248            ..Blend::default()
249        },
250        Kind::ZoomIn => Blend {
251            to: LayerPose::new().opacity(t).scale(0.6 + 0.4 * t),
252            ..Blend::default()
253        },
254        Kind::ZoomOut => Blend {
255            from: LayerPose::new().opacity(1.0 - t).scale(1.0 + 0.4 * t),
256            incoming_on_top: false,
257            ..Blend::default()
258        },
259        Kind::Custom(f) => f(t),
260    }
261}
262
263fn shift(dir: Dir, t: f64) -> (f64, f64, f64, f64) {
264    match dir {
265        Dir::Left => (-t, 0.0, 1.0 - t, 0.0),
266        Dir::Right => (t, 0.0, t - 1.0, 0.0),
267        Dir::Up => (0.0, -t, 0.0, 1.0 - t),
268        Dir::Down => (0.0, t, 0.0, t - 1.0),
269    }
270}