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/// A transition drawn by a shader preset from both scenes' pixels.
122///
123/// Built with `oxframe::shaders::water_wipe()` and passed to
124/// `Transition::shader`.
125#[derive(Debug, Clone, Copy, PartialEq)]
126#[must_use = "a shader transition does nothing until it is passed to `Transition::shader`"]
127pub struct ShaderTransition {
128    preset: ShaderPreset,
129    wave: f64,
130}
131
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133enum ShaderPreset {
134    WaterWipe,
135}
136
137impl ShaderTransition {
138    /// A wavy waterline rises over the frame. Below it is the incoming
139    /// scene, rippled near the line as if seen through water.
140    pub fn water_wipe() -> Self {
141        Self {
142            preset: ShaderPreset::WaterWipe,
143            wave: 0.04,
144        }
145    }
146
147    /// Height of the waves on the line, as a fraction of the frame
148    /// height. The default is 0.04; 0 is a straight line.
149    pub fn wave(mut self, height: f64) -> Self {
150        self.wave = if height.is_finite() {
151            height.max(0.0)
152        } else {
153            0.0
154        };
155        self
156    }
157
158    /// The wave height set with [`wave`](Self::wave).
159    pub fn wave_height(self) -> f64 {
160        self.wave
161    }
162
163    /// `"water_wipe"`.
164    pub fn name(self) -> &'static str {
165        match self.preset {
166            ShaderPreset::WaterWipe => "water_wipe",
167        }
168    }
169}
170
171/// Which built-in, or a function of progress. Not part of the prelude;
172/// scenes build one through [`crate`] re-exports in `oxframe::Transition`.
173#[derive(Clone, Copy)]
174pub enum Kind {
175    /// Crossfade.
176    Fade,
177    /// Both layers travel toward `dir`.
178    Slide(Dir),
179    /// The incoming layer is revealed from `dir`.
180    Wipe(Dir),
181    /// The incoming layer grows onto the outgoing one.
182    ZoomIn,
183    /// The outgoing layer grows and fades off the incoming one.
184    ZoomOut,
185    /// `progress` in 0..1 (already eased) decides the poses.
186    Custom(fn(f64) -> Blend),
187    /// A shader preset paints the frame from both layers.
188    Shader(ShaderTransition),
189}
190
191impl Kind {
192    pub(crate) fn name(self) -> &'static str {
193        match self {
194            Self::Fade => "fade",
195            Self::Slide(_) => "slide",
196            Self::Wipe(_) => "wipe",
197            Self::ZoomIn => "zoom_in",
198            Self::ZoomOut => "zoom_out",
199            Self::Custom(_) => "custom",
200            Self::Shader(s) => s.name(),
201        }
202    }
203
204    pub(crate) fn dir(self) -> Option<Dir> {
205        match self {
206            Self::Slide(d) | Self::Wipe(d) => Some(d),
207            _ => None,
208        }
209    }
210}
211
212/// A transition a scene has started. Copied out of the executor after the
213/// scene has been measured.
214#[derive(Clone, Copy)]
215pub struct Recorded {
216    pub(crate) kind: Kind,
217    pub(crate) seconds: f64,
218    pub(crate) ease: CubicBezier,
219    pub(crate) overlap: bool,
220    /// Scene time at which the blend starts.
221    pub(crate) start: f64,
222}
223
224impl Recorded {
225    /// A transition that starts at `start` seconds into its scene.
226    pub fn new(kind: Kind, seconds: f64, ease: CubicBezier, overlap: bool, start: f64) -> Self {
227        Self {
228            kind,
229            seconds,
230            ease,
231            overlap,
232            start,
233        }
234    }
235
236    /// `"fade"`, `"slide"`, `"wipe"`, `"zoom_in"`, `"zoom_out"`, `"custom"`,
237    /// or a shader preset such as `"water_wipe"`.
238    pub fn kind_name(self) -> &'static str {
239        self.kind.name()
240    }
241
242    /// The shader preset that paints this transition, if it is one.
243    pub fn shader(self) -> Option<ShaderTransition> {
244        match self.kind {
245            Kind::Shader(s) => Some(s),
246            _ => None,
247        }
248    }
249
250    /// `"left"`, `"right"`, `"up"` or `"down"` for a slide or a wipe.
251    pub fn dir_name(self) -> Option<&'static str> {
252        self.kind.dir().map(Dir::name)
253    }
254
255    /// The previous scene keeps playing instead of holding its last frame.
256    pub fn overlap(self) -> bool {
257        self.overlap
258    }
259
260    /// Blend length in seconds.
261    pub fn seconds(self) -> f64 {
262        self.seconds
263    }
264
265    /// Scene time at which the blend starts.
266    pub fn start(self) -> f64 {
267        self.start
268    }
269
270    /// This scene time is still inside the blend.
271    pub fn active(self, now: f64) -> bool {
272        self.seconds > 0.0
273            && now + TIME_EPSILON >= self.start
274            && now + TIME_EPSILON < self.start + self.seconds
275    }
276
277    /// Eased progress at `now`, clamped to 0..=1.
278    pub fn progress(self, now: f64) -> f64 {
279        if self.seconds <= 0.0 {
280            return 1.0;
281        }
282        let t = ((now - self.start) / self.seconds).clamp(0.0, 1.0);
283        self.ease.ease(t).clamp(0.0, 1.0)
284    }
285
286    /// Layer poses at `now`.
287    pub fn blend_at(self, now: f64) -> Blend {
288        blend(self.kind, self.progress(now))
289    }
290}
291
292/// Poses for a built-in at eased progress `t` (0 = outgoing, 1 = incoming).
293pub(crate) fn blend(kind: Kind, t: f64) -> Blend {
294    let t = t.clamp(0.0, 1.0);
295    match kind {
296        Kind::Fade => Blend {
297            mix: true,
298            ..Blend::default()
299        },
300        Kind::Slide(dir) => {
301            let (fx, fy, tx, ty) = shift(dir, t);
302            Blend {
303                from: LayerPose::new().offset(fx, fy),
304                to: LayerPose::new().offset(tx, ty),
305                ..Blend::default()
306            }
307        }
308        Kind::Wipe(dir) => Blend {
309            to: LayerPose::new().wipe(dir, t),
310            ..Blend::default()
311        },
312        Kind::ZoomIn => Blend {
313            to: LayerPose::new().opacity(t).scale(0.6 + 0.4 * t),
314            ..Blend::default()
315        },
316        Kind::ZoomOut => Blend {
317            from: LayerPose::new().opacity(1.0 - t).scale(1.0 + 0.4 * t),
318            incoming_on_top: false,
319            ..Blend::default()
320        },
321        Kind::Custom(f) => f(t),
322        // Painted from pixels by the engine; the poses are not used.
323        Kind::Shader(_) => Blend::default(),
324    }
325}
326
327fn shift(dir: Dir, t: f64) -> (f64, f64, f64, f64) {
328    match dir {
329        Dir::Left => (-t, 0.0, 1.0 - t, 0.0),
330        Dir::Right => (t, 0.0, t - 1.0, 0.0),
331        Dir::Up => (0.0, -t, 0.0, 1.0 - t),
332        Dir::Down => (0.0, t, 0.0, t - 1.0),
333    }
334}