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