1use crate::{Color, Node, NodeModifiers, Point, Rect, SceneContext, Shape};
33
34pub trait Transition: Send + Sync {
36 fn compose(
38 &self,
39 progress: f64,
40 outgoing: Node,
41 incoming: Node,
42 context: &SceneContext<'_>,
43 ) -> Node;
44
45 fn easing(&self) -> crate::Easing {
47 crate::Easing::Linear
48 }
49}
50
51impl Transition for std::sync::Arc<dyn Transition> {
52 fn compose(
53 &self,
54 progress: f64,
55 outgoing: Node,
56 incoming: Node,
57 context: &SceneContext<'_>,
58 ) -> Node {
59 self.as_ref().compose(progress, outgoing, incoming, context)
60 }
61
62 fn easing(&self) -> crate::Easing {
63 self.as_ref().easing()
64 }
65}
66
67struct Failure(crate::ValidationError);
68
69impl crate::Drawable for Failure {
70 fn bounds(&self, _: &crate::DrawContext) -> Rect {
71 Rect::default()
72 }
73
74 fn try_bounds(&self, _: &crate::DrawContext) -> Result<Rect, crate::ValidationError> {
75 Err(self.0.clone())
76 }
77
78 fn draw(
79 &self,
80 _: &crate::DrawContext,
81 _: &mut crate::Canvas<'_>,
82 ) -> Result<(), crate::ValidationError> {
83 Err(self.0.clone())
84 }
85}
86
87pub fn diagnostic(error: crate::ValidationError) -> Node {
89 Node::from(Failure(error))
90}
91
92pub(crate) fn failure(field: &str, message: &str) -> Node {
93 diagnostic(crate::ValidationError::invalid(
94 "transition",
95 field,
96 message,
97 ))
98}
99
100fn sum(samples: impl IntoIterator<Item = (Node, f64)>) -> Node {
101 Node::weighted_sum(samples).expect("bounded built-in transition weights")
102}
103
104fn endpoint(
105 progress: f64,
106 outgoing: &Node,
107 incoming: &Node,
108 context: &SceneContext<'_>,
109) -> Option<Node> {
110 if !progress.is_finite() || !(0.0..=1.0).contains(&progress) {
111 return Some(failure(
112 "progress",
113 "Transition progress must be finite in 0..=1",
114 ));
115 }
116 if progress == 0.0 {
117 return Some(
118 Node::group([
119 outgoing.clone(),
120 Node::group([incoming.clone()]).opacity(0.0),
121 ])
122 .clip_with(context.bounds(), crate::ClipMode::Geometric),
123 );
124 }
125 if progress == 1.0 {
126 return Some(
127 Node::group([
128 Node::group([outgoing.clone()]).opacity(0.0),
129 incoming.clone(),
130 ])
131 .clip_with(context.bounds(), crate::ClipMode::Geometric),
132 );
133 }
134 None
135}
136
137fn select(mask: Node, outgoing: Node, incoming: Node, context: &SceneContext<'_>) -> Node {
138 sum([
139 (
140 outgoing.mask(mask.clone(), crate::MaskMode::InvertedAlpha),
141 1.0,
142 ),
143 (incoming.mask(mask, crate::MaskMode::Alpha), 1.0),
144 ])
145 .clip_with(context.bounds(), crate::ClipMode::Geometric)
146}
147
148#[derive(Clone, Copy, Debug, Default)]
150pub struct CrossFade;
151
152impl Transition for CrossFade {
153 fn compose(
154 &self,
155 progress: f64,
156 outgoing: Node,
157 incoming: Node,
158 context: &SceneContext<'_>,
159 ) -> Node {
160 if let Some(node) = endpoint(progress, &outgoing, &incoming, context) {
161 return node;
162 }
163 sum([(outgoing, 1.0 - progress), (incoming, progress)])
164 .clip_with(context.bounds(), crate::ClipMode::Geometric)
165 }
166}
167
168#[derive(Clone, Copy, Debug)]
170pub struct Slide {
171 pub direction: Direction,
173}
174
175#[derive(Clone, Copy, Debug)]
177pub struct Push {
178 pub direction: Direction,
180}
181
182fn displacement(direction: Direction, context: &SceneContext<'_>) -> (f64, f64) {
183 match direction {
184 Direction::Left => (-context.size().width, 0.0),
185 Direction::Right => (context.size().width, 0.0),
186 Direction::Up => (0.0, -context.size().height),
187 Direction::Down => (0.0, context.size().height),
188 }
189}
190
191impl Transition for Slide {
192 fn compose(
193 &self,
194 progress: f64,
195 outgoing: Node,
196 incoming: Node,
197 context: &SceneContext<'_>,
198 ) -> Node {
199 if let Some(node) = endpoint(progress, &outgoing, &incoming, context) {
200 return node;
201 }
202 let (horizontal, vertical) = displacement(self.direction, context);
203 let offset = (-horizontal * (1.0 - progress), -vertical * (1.0 - progress));
204 let mask = Node::from(context.bounds().fill(Color::WHITE)).translate(offset.0, offset.1);
205 let incoming = incoming
206 .clip_with(context.bounds(), crate::ClipMode::Geometric)
207 .translate(offset.0, offset.1);
208 sum([
209 (outgoing.mask(mask, crate::MaskMode::InvertedAlpha), 1.0),
210 (incoming, 1.0),
211 ])
212 .clip_with(context.bounds(), crate::ClipMode::Geometric)
213 }
214}
215
216impl Transition for Push {
217 fn compose(
218 &self,
219 progress: f64,
220 outgoing: Node,
221 incoming: Node,
222 context: &SceneContext<'_>,
223 ) -> Node {
224 if let Some(node) = endpoint(progress, &outgoing, &incoming, context) {
225 return node;
226 }
227 let (horizontal, vertical) = displacement(self.direction, context);
228 sum([
229 (
230 outgoing
231 .clip_with(context.bounds(), crate::ClipMode::Geometric)
232 .translate(horizontal * progress, vertical * progress),
233 1.0,
234 ),
235 (
236 incoming
237 .clip_with(context.bounds(), crate::ClipMode::Geometric)
238 .translate(-horizontal * (1.0 - progress), -vertical * (1.0 - progress)),
239 1.0,
240 ),
241 ])
242 .clip_with(context.bounds(), crate::ClipMode::Geometric)
243 }
244}
245
246#[derive(Clone, Copy, Debug, Default)]
248pub struct Wipe {
249 pub angle: f64,
251 pub feather: f64,
253}
254
255impl Transition for Wipe {
256 fn compose(
257 &self,
258 progress: f64,
259 outgoing: Node,
260 incoming: Node,
261 context: &SceneContext<'_>,
262 ) -> Node {
263 if !self.angle.is_finite() || !self.feather.is_finite() || self.feather < 0.0 {
264 return failure("wipe", "Wipe requires finite angle and nonnegative feather");
265 }
266 if let Some(node) = endpoint(progress, &outgoing, &incoming, context) {
267 return node;
268 }
269 let normal = Point::new(libm::cos(self.angle), libm::sin(self.angle));
270 let projections = [
271 0.0,
272 context.size().width * normal.x,
273 context.size().height * normal.y,
274 context.size().width * normal.x + context.size().height * normal.y,
275 ];
276 let minimum = projections.iter().copied().fold(f64::INFINITY, f64::min);
277 let maximum = projections
278 .iter()
279 .copied()
280 .fold(f64::NEG_INFINITY, f64::max);
281 let position = minimum - self.feather * 0.5 + (maximum - minimum + self.feather) * progress;
282 let mask = if self.feather == 0.0 {
283 let radius = libm::hypot(context.size().width, context.size().height) * 2.0;
284 Node::from(Rect::new(-radius, -radius, position, radius).fill(Color::WHITE))
285 .pivot(crate::Pivot::Point(Point::ORIGIN))
286 .rotate(self.angle)
287 } else {
288 let first = Point::new(
289 normal.x * (position - self.feather * 0.5),
290 normal.y * (position - self.feather * 0.5),
291 );
292 let last = Point::new(
293 normal.x * (position + self.feather * 0.5),
294 normal.y * (position + self.feather * 0.5),
295 );
296 Node::from(
297 context.bounds().fill(
298 crate::LinearGradient::new(first, last)
299 .stop(0.0, Color::WHITE)
300 .stop(1.0, Color::WHITE.with_alpha(0.0))
301 .units(crate::GradientUnits::UserSpace)
302 .space(crate::GradientSpace::LinearRgb),
303 ),
304 )
305 };
306 select(mask, outgoing, incoming, context)
307 }
308}
309
310#[derive(Clone, Copy, Debug, Default)]
312pub enum IrisShape {
313 #[default]
315 Circle,
316 Rectangle,
318}
319
320#[derive(Clone, Copy, Debug, Default)]
322pub struct Iris {
323 pub shape: IrisShape,
325}
326
327impl Transition for Iris {
328 fn compose(
329 &self,
330 progress: f64,
331 outgoing: Node,
332 incoming: Node,
333 context: &SceneContext<'_>,
334 ) -> Node {
335 if let Some(node) = endpoint(progress, &outgoing, &incoming, context) {
336 return node;
337 }
338 let center = context.center();
339 let mask = match self.shape {
340 IrisShape::Circle => Node::from(
341 crate::Circle::new(center, libm::hypot(center.x, center.y) * progress)
342 .fill(Color::WHITE),
343 ),
344 IrisShape::Rectangle => Node::from(
345 Rect::new(
346 center.x * (1.0 - progress),
347 center.y * (1.0 - progress),
348 center.x * (1.0 + progress),
349 center.y * (1.0 + progress),
350 )
351 .fill(Color::WHITE),
352 ),
353 };
354 select(mask, outgoing, incoming, context)
355 }
356}
357
358#[derive(Clone, Copy, Debug, Default)]
360pub struct ZoomBlur;
361
362impl Transition for ZoomBlur {
363 fn compose(
364 &self,
365 progress: f64,
366 outgoing: Node,
367 incoming: Node,
368 context: &SceneContext<'_>,
369 ) -> Node {
370 if let Some(node) = endpoint(progress, &outgoing, &incoming, context) {
371 return node;
372 }
373 let blur = |node: Node, base: f64| {
374 let samples = (0..12).map(|index| {
375 node.clone()
376 .clip_with(context.bounds(), crate::ClipMode::Geometric)
377 .pivot(crate::Pivot::Point(context.center()))
378 .scale(
379 base + libm::sin(std::f64::consts::PI * progress)
380 * 0.2
381 * (f64::from(index) + 0.5)
382 / 12.0,
383 )
384 });
385 Node::accumulate(samples).expect("fixed zoom samples")
386 };
387 CrossFade.compose(
388 progress,
389 blur(outgoing, 1.0 + progress * 0.25),
390 blur(incoming, 0.75 + progress * 0.25),
391 context,
392 )
393 }
394}
395
396#[derive(Clone, Copy, Debug, PartialEq, Eq)]
398pub enum Direction {
399 Left,
401 Right,
403 Up,
405 Down,
407}