1use crate::engine::animator::ease;
2use crate::schema::{
3 EasingType, PanBackground, PixelDissolveOrder, Transition, TransitionCorner,
4 TransitionDirection, TransitionType,
5};
6use skia_safe::{surfaces, Color4f, ColorType, ImageInfo, Paint, PathBuilder, Rect};
7
8#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct TransitionOptions {
16 pub corner: TransitionCorner,
18 pub cell: f32,
20 pub seed: u32,
22 pub order: PixelDissolveOrder,
24 pub direction: TransitionDirection,
26 pub aberration: f32,
28}
29
30impl Default for TransitionOptions {
31 fn default() -> Self {
32 Self {
33 corner: TransitionCorner::default(),
34 cell: 48.0,
35 seed: 11,
36 order: PixelDissolveOrder::default(),
37 direction: TransitionDirection::default(),
38 aberration: 1.0,
39 }
40 }
41}
42
43impl From<&Transition> for TransitionOptions {
44 fn from(t: &Transition) -> Self {
45 Self {
46 corner: t.corner,
47 cell: t.cell,
48 seed: t.seed,
49 order: t.order,
50 direction: t.direction,
51 aberration: t.aberration,
52 }
53 }
54}
55
56pub fn apply_transition(
59 frame_a: &[u8],
60 frame_b: &[u8],
61 width: u32,
62 height: u32,
63 progress: f64,
64 transition_type: &TransitionType,
65 opts: &TransitionOptions,
66) -> Vec<u8> {
67 let progress = progress.clamp(0.0, 1.0) as f32;
68 let TransitionOptions {
69 corner,
70 cell,
71 seed,
72 order,
73 direction,
74 aberration,
75 } = *opts;
76
77 match transition_type {
78 TransitionType::Fade => blend_fade(frame_a, frame_b, progress),
79 TransitionType::WipeLeft => {
80 wipe(frame_a, frame_b, width, height, progress, Direction::Left)
81 }
82 TransitionType::WipeRight => {
83 wipe(frame_a, frame_b, width, height, progress, Direction::Right)
84 }
85 TransitionType::WipeUp => wipe(frame_a, frame_b, width, height, progress, Direction::Up),
86 TransitionType::WipeDown => {
87 wipe(frame_a, frame_b, width, height, progress, Direction::Down)
88 }
89 TransitionType::ZoomIn => zoom_transition(frame_a, frame_b, width, height, progress, true),
90 TransitionType::ZoomOut => {
91 zoom_transition(frame_a, frame_b, width, height, progress, false)
92 }
93 TransitionType::Flip => flip_transition(frame_a, frame_b, width, height, progress),
94 TransitionType::ClockWipe => clock_wipe(frame_a, frame_b, width, height, progress),
95 TransitionType::Iris => iris_transition(frame_a, frame_b, width, height, progress),
96 TransitionType::Slide => slide_transition(frame_a, frame_b, width, height, progress),
97 TransitionType::Dissolve => dissolve_transition(frame_a, frame_b, width, height, progress),
98 TransitionType::CornerReveal => {
99 corner_reveal(frame_a, frame_b, width, height, progress, corner)
100 }
101 TransitionType::PixelDissolve => {
102 pixel_dissolve(frame_a, frame_b, width, height, progress, cell, seed, order)
103 }
104 TransitionType::CameraPan => blend_fade(frame_a, frame_b, progress),
105 TransitionType::ChromaticWipe => chromatic_wipe(
106 frame_a, frame_b, width, height, progress, direction, aberration,
107 ),
108 TransitionType::None => {
109 if progress < 0.5 {
110 frame_a.to_vec()
111 } else {
112 frame_b.to_vec()
113 }
114 }
115 }
116}
117
118fn corner_reveal(
127 frame_a: &[u8],
128 frame_b: &[u8],
129 width: u32,
130 height: u32,
131 progress: f32,
132 corner: TransitionCorner,
133) -> Vec<u8> {
134 let mut surface = match create_skia_surface(width, height) {
135 Some(s) => s,
136 None => return blend_fade(frame_a, frame_b, progress),
137 };
138 let img_a = match frame_to_image(frame_a, width, height) {
139 Some(i) => i,
140 None => return blend_fade(frame_a, frame_b, progress),
141 };
142 let img_b = match frame_to_image(frame_b, width, height) {
143 Some(i) => i,
144 None => return blend_fade(frame_a, frame_b, progress),
145 };
146
147 let (w, h) = (width as f32, height as f32);
148 let rect = corner_rect(corner, w, h, progress);
149
150 let canvas = surface.canvas();
151 canvas.draw_image(&img_a, (0.0, 0.0), None);
152 canvas.save();
153 canvas.clip_rect(rect, skia_safe::ClipOp::Intersect, false);
154 canvas.draw_image(&img_b, (0.0, 0.0), None);
155 canvas.restore();
156
157 surface_to_pixels(surface, width, height)
158}
159
160fn corner_rect(corner: TransitionCorner, w: f32, h: f32, progress: f32) -> skia_safe::Rect {
163 let p = progress.clamp(0.0, 1.0);
164 let (rw, rh) = (w * p, h * p);
165 match corner {
166 TransitionCorner::TopRight => skia_safe::Rect::from_xywh(w - rw, 0.0, rw, rh),
167 TransitionCorner::TopLeft => skia_safe::Rect::from_xywh(0.0, 0.0, rw, rh),
168 TransitionCorner::BottomRight => skia_safe::Rect::from_xywh(w - rw, h - rh, rw, rh),
169 TransitionCorner::BottomLeft => skia_safe::Rect::from_xywh(0.0, h - rh, rw, rh),
170 }
171}
172
173const SPATIAL_WEIGHT: f32 = 0.72;
177
178fn cell_hash01(col: i32, row: i32, seed: u32) -> f32 {
184 let mut h = seed
185 .wrapping_mul(0x9E37_79B9)
186 .wrapping_add((col as u32).wrapping_mul(0x85EB_CA6B))
187 .wrapping_add((row as u32).wrapping_mul(0xC2B2_AE35));
188 h ^= h >> 16;
189 h = h.wrapping_mul(0x7FEB_352D);
190 h ^= h >> 15;
191 (h & 0x00FF_FFFF) as f32 / 0x0100_0000 as f32
192}
193
194fn cell_threshold(
201 col: i32,
202 row: i32,
203 cols: i32,
204 rows: i32,
205 seed: u32,
206 order: PixelDissolveOrder,
207) -> f32 {
208 let noise = cell_hash01(col, row, seed);
209 if order == PixelDissolveOrder::Random {
210 return noise;
211 }
212 let (cx, cy) = ((cols - 1) as f32 / 2.0, (rows - 1) as f32 / 2.0);
216 let dx = if cx > 0.0 {
217 (col as f32 - cx).abs() / cx
218 } else {
219 0.0
220 };
221 let dy = if cy > 0.0 {
222 (row as f32 - cy).abs() / cy
223 } else {
224 0.0
225 };
226 let edge = dx.max(dy).clamp(0.0, 1.0);
227 let spatial = match order {
228 PixelDissolveOrder::EdgesIn => 1.0 - edge,
229 PixelDissolveOrder::CenterOut => edge,
230 PixelDissolveOrder::Random => unreachable!("handled above"),
231 };
232 (spatial * SPATIAL_WEIGHT + noise * (1.0 - SPATIAL_WEIGHT)).clamp(0.0, 1.0)
233}
234
235fn pixel_dissolve(
243 frame_a: &[u8],
244 frame_b: &[u8],
245 width: u32,
246 height: u32,
247 progress: f32,
248 cell: f32,
249 seed: u32,
250 order: PixelDissolveOrder,
251) -> Vec<u8> {
252 let mut surface = match create_skia_surface(width, height) {
253 Some(s) => s,
254 None => return blend_fade(frame_a, frame_b, progress),
255 };
256 let img_a = match frame_to_image(frame_a, width, height) {
257 Some(i) => i,
258 None => return blend_fade(frame_a, frame_b, progress),
259 };
260 let img_b = match frame_to_image(frame_b, width, height) {
261 Some(i) => i,
262 None => return blend_fade(frame_a, frame_b, progress),
263 };
264
265 let cell = cell.max(1.0);
266 let cols = (width as f32 / cell).ceil() as i32;
267 let rows = (height as f32 / cell).ceil() as i32;
268
269 let canvas = surface.canvas();
270 canvas.draw_image(&img_a, (0.0, 0.0), None);
271
272 const FEATHER: f32 = 0.35;
275 let p = progress.clamp(0.0, 1.0) * (1.0 + FEATHER);
276
277 for row in 0..rows {
278 for col in 0..cols {
279 let t = cell_threshold(col, row, cols, rows, seed, order);
280 let alpha = ((p - t) / FEATHER).clamp(0.0, 1.0);
281 if alpha <= 0.001 {
282 continue;
283 }
284 let rect = Rect::from_xywh(col as f32 * cell, row as f32 * cell, cell, cell);
285 canvas.save();
286 canvas.clip_rect(rect, skia_safe::ClipOp::Intersect, false);
287 let mut paint = Paint::default();
288 paint.set_alpha_f(alpha);
289 canvas.draw_image(&img_b, (0.0, 0.0), Some(&paint));
290 canvas.restore();
291 }
292 }
293
294 surface_to_pixels(surface, width, height)
295}
296
297fn blend_fade(frame_a: &[u8], frame_b: &[u8], progress: f32) -> Vec<u8> {
298 let inv = 1.0 - progress;
299 frame_a
300 .iter()
301 .zip(frame_b.iter())
302 .map(|(&a, &b)| {
303 let va = a as f32 * inv;
304 let vb = b as f32 * progress;
305 (va + vb + 0.5) as u8
306 })
307 .collect()
308}
309
310enum Direction {
311 Left,
312 Right,
313 Up,
314 Down,
315}
316
317fn wipe(
318 frame_a: &[u8],
319 frame_b: &[u8],
320 width: u32,
321 height: u32,
322 progress: f32,
323 direction: Direction,
324) -> Vec<u8> {
325 let mut surface = match create_skia_surface(width, height) {
326 Some(s) => s,
327 None => return blend_fade(frame_a, frame_b, progress),
328 };
329 let img_a = match frame_to_image(frame_a, width, height) {
330 Some(i) => i,
331 None => return blend_fade(frame_a, frame_b, progress),
332 };
333 let img_b = match frame_to_image(frame_b, width, height) {
334 Some(i) => i,
335 None => return blend_fade(frame_a, frame_b, progress),
336 };
337
338 let canvas = surface.canvas();
339 let w = width as f32;
340 let h = height as f32;
341
342 canvas.draw_image(&img_a, (0.0, 0.0), None);
344
345 let clip_rect = match direction {
347 Direction::Left => Rect::from_xywh(0.0, 0.0, w * progress, h),
348 Direction::Right => Rect::from_xywh(w * (1.0 - progress), 0.0, w * progress, h),
349 Direction::Up => Rect::from_xywh(0.0, 0.0, w, h * progress),
350 Direction::Down => Rect::from_xywh(0.0, h * (1.0 - progress), w, h * progress),
351 };
352
353 canvas.save();
354 canvas.clip_rect(clip_rect, skia_safe::ClipOp::Intersect, true);
355 canvas.draw_image(&img_b, (0.0, 0.0), None);
356 canvas.restore();
357
358 surface_to_pixels(surface, width, height)
359}
360
361fn create_skia_surface(width: u32, height: u32) -> Option<skia_safe::Surface> {
362 let info = ImageInfo::new(
363 (width as i32, height as i32),
364 ColorType::RGBA8888,
365 skia_safe::AlphaType::Premul,
366 None,
367 );
368 surfaces::raster(&info, None, None)
369}
370
371fn frame_to_image(frame: &[u8], width: u32, height: u32) -> Option<skia_safe::Image> {
372 let info = ImageInfo::new(
373 (width as i32, height as i32),
374 ColorType::RGBA8888,
375 skia_safe::AlphaType::Premul,
376 None,
377 );
378 let data = skia_safe::Data::new_copy(frame);
379 skia_safe::images::raster_from_data(&info, data, width as usize * 4)
380}
381
382fn surface_to_pixels(mut surface: skia_safe::Surface, width: u32, height: u32) -> Vec<u8> {
383 let row_bytes = width as usize * 4;
384 let mut pixels = vec![0u8; row_bytes * height as usize];
385 let info = ImageInfo::new(
386 (width as i32, height as i32),
387 ColorType::RGBA8888,
388 skia_safe::AlphaType::Premul,
389 None,
390 );
391 surface.read_pixels(&info, &mut pixels, row_bytes, (0, 0));
392 pixels
393}
394
395fn zoom_transition(
396 frame_a: &[u8],
397 frame_b: &[u8],
398 width: u32,
399 height: u32,
400 progress: f32,
401 zoom_in: bool,
402) -> Vec<u8> {
403 let mut surface = match create_skia_surface(width, height) {
404 Some(s) => s,
405 None => return blend_fade(frame_a, frame_b, progress),
406 };
407 let img_a = match frame_to_image(frame_a, width, height) {
408 Some(i) => i,
409 None => return blend_fade(frame_a, frame_b, progress),
410 };
411 let img_b = match frame_to_image(frame_b, width, height) {
412 Some(i) => i,
413 None => return blend_fade(frame_a, frame_b, progress),
414 };
415
416 let canvas = surface.canvas();
417 let w = width as f32;
418 let h = height as f32;
419
420 if zoom_in {
421 let scale = 1.0 + progress * 0.3;
423 canvas.draw_image(&img_b, (0.0, 0.0), None);
424 canvas.save();
425 canvas.translate((w / 2.0, h / 2.0));
426 canvas.scale((scale, scale));
427 canvas.translate((-w / 2.0, -h / 2.0));
428 let mut paint = Paint::default();
429 paint.set_alpha_f(1.0 - progress);
430 canvas.draw_image(&img_a, (0.0, 0.0), Some(&paint));
431 canvas.restore();
432 } else {
433 canvas.draw_image(&img_a, (0.0, 0.0), None);
435 let scale = 1.3 - progress * 0.3;
436 canvas.save();
437 canvas.translate((w / 2.0, h / 2.0));
438 canvas.scale((scale, scale));
439 canvas.translate((-w / 2.0, -h / 2.0));
440 let mut paint = Paint::default();
441 paint.set_alpha_f(progress);
442 canvas.draw_image(&img_b, (0.0, 0.0), Some(&paint));
443 canvas.restore();
444 }
445
446 surface_to_pixels(surface, width, height)
447}
448
449fn flip_transition(
450 frame_a: &[u8],
451 frame_b: &[u8],
452 width: u32,
453 height: u32,
454 progress: f32,
455) -> Vec<u8> {
456 let mut surface = match create_skia_surface(width, height) {
457 Some(s) => s,
458 None => return blend_fade(frame_a, frame_b, progress),
459 };
460 let img_a = match frame_to_image(frame_a, width, height) {
461 Some(i) => i,
462 None => return blend_fade(frame_a, frame_b, progress),
463 };
464 let img_b = match frame_to_image(frame_b, width, height) {
465 Some(i) => i,
466 None => return blend_fade(frame_a, frame_b, progress),
467 };
468
469 let canvas = surface.canvas();
470 let w = width as f32;
471
472 if progress < 0.5 {
475 let scale_x = 1.0 - progress * 2.0; canvas.clear(Color4f::new(0.0, 0.0, 0.0, 1.0));
477 canvas.save();
478 canvas.translate((w / 2.0, 0.0));
479 canvas.scale((scale_x.max(0.01), 1.0));
480 canvas.translate((-w / 2.0, 0.0));
481 canvas.draw_image(&img_a, (0.0, 0.0), None);
482 canvas.restore();
483 } else {
484 let scale_x = (progress - 0.5) * 2.0; canvas.clear(Color4f::new(0.0, 0.0, 0.0, 1.0));
486 canvas.save();
487 canvas.translate((w / 2.0, 0.0));
488 canvas.scale((scale_x.max(0.01), 1.0));
489 canvas.translate((-w / 2.0, 0.0));
490 canvas.draw_image(&img_b, (0.0, 0.0), None);
491 canvas.restore();
492 }
493
494 surface_to_pixels(surface, width, height)
495}
496
497fn clock_wipe(frame_a: &[u8], frame_b: &[u8], width: u32, height: u32, progress: f32) -> Vec<u8> {
498 let mut surface = match create_skia_surface(width, height) {
499 Some(s) => s,
500 None => return blend_fade(frame_a, frame_b, progress),
501 };
502 let img_a = match frame_to_image(frame_a, width, height) {
503 Some(i) => i,
504 None => return blend_fade(frame_a, frame_b, progress),
505 };
506 let img_b = match frame_to_image(frame_b, width, height) {
507 Some(i) => i,
508 None => return blend_fade(frame_a, frame_b, progress),
509 };
510
511 let canvas = surface.canvas();
512 let w = width as f32;
513 let h = height as f32;
514 let cx = w / 2.0;
515 let cy = h / 2.0;
516 let radius = (w * w + h * h).sqrt();
517
518 canvas.draw_image(&img_a, (0.0, 0.0), None);
520
521 let sweep_angle = progress * 360.0;
523 let start_angle = -90.0; let mut path = PathBuilder::new();
526 path.move_to((cx, cy));
527 path.arc_to(
528 Rect::from_xywh(cx - radius, cy - radius, radius * 2.0, radius * 2.0),
529 start_angle,
530 sweep_angle,
531 false,
532 );
533 path.close();
534
535 canvas.save();
536 canvas.clip_path(&path.detach(), skia_safe::ClipOp::Intersect, true);
537 canvas.draw_image(&img_b, (0.0, 0.0), None);
538 canvas.restore();
539
540 surface_to_pixels(surface, width, height)
541}
542
543fn iris_transition(
544 frame_a: &[u8],
545 frame_b: &[u8],
546 width: u32,
547 height: u32,
548 progress: f32,
549) -> Vec<u8> {
550 let mut surface = match create_skia_surface(width, height) {
551 Some(s) => s,
552 None => return blend_fade(frame_a, frame_b, progress),
553 };
554 let img_a = match frame_to_image(frame_a, width, height) {
555 Some(i) => i,
556 None => return blend_fade(frame_a, frame_b, progress),
557 };
558 let img_b = match frame_to_image(frame_b, width, height) {
559 Some(i) => i,
560 None => return blend_fade(frame_a, frame_b, progress),
561 };
562
563 let canvas = surface.canvas();
564 let w = width as f32;
565 let h = height as f32;
566 let cx = w / 2.0;
567 let cy = h / 2.0;
568 let max_radius = (w * w + h * h).sqrt() / 2.0;
569 let radius = max_radius * progress;
570
571 canvas.draw_image(&img_a, (0.0, 0.0), None);
573
574 let mut path = PathBuilder::new();
576 path.add_circle((cx, cy), radius, None);
577
578 canvas.save();
579 canvas.clip_path(&path.detach(), skia_safe::ClipOp::Intersect, true);
580 canvas.draw_image(&img_b, (0.0, 0.0), None);
581 canvas.restore();
582
583 surface_to_pixels(surface, width, height)
584}
585
586fn slide_transition(
587 frame_a: &[u8],
588 frame_b: &[u8],
589 width: u32,
590 height: u32,
591 progress: f32,
592) -> Vec<u8> {
593 let mut surface = match create_skia_surface(width, height) {
594 Some(s) => s,
595 None => return blend_fade(frame_a, frame_b, progress),
596 };
597 let img_a = match frame_to_image(frame_a, width, height) {
598 Some(i) => i,
599 None => return blend_fade(frame_a, frame_b, progress),
600 };
601 let img_b = match frame_to_image(frame_b, width, height) {
602 Some(i) => i,
603 None => return blend_fade(frame_a, frame_b, progress),
604 };
605
606 let canvas = surface.canvas();
607 let w = width as f32;
608
609 let offset = -progress * w;
611 canvas.draw_image(&img_a, (offset, 0.0), None);
612 canvas.draw_image(&img_b, (offset + w, 0.0), None);
613
614 surface_to_pixels(surface, width, height)
615}
616
617fn chromatic_wipe(
625 frame_a: &[u8],
626 frame_b: &[u8],
627 width: u32,
628 height: u32,
629 progress: f32,
630 direction: TransitionDirection,
631 aberration: f32,
632) -> Vec<u8> {
633 let (w, h) = (width as f32, height as f32);
634 let (ux, uy) = match direction {
637 TransitionDirection::Left => (-1.0, 0.0),
638 TransitionDirection::Right => (1.0, 0.0),
639 TransitionDirection::Up => (0.0, -1.0),
640 TransitionDirection::Down => (0.0, 1.0),
641 };
642
643 let slid = {
644 let mut surface = match create_skia_surface(width, height) {
645 Some(s) => s,
646 None => return blend_fade(frame_a, frame_b, progress),
647 };
648 let (Some(img_a), Some(img_b)) = (
649 frame_to_image(frame_a, width, height),
650 frame_to_image(frame_b, width, height),
651 ) else {
652 return blend_fade(frame_a, frame_b, progress);
653 };
654 let canvas = surface.canvas();
655 let (dx, dy) = (ux * progress * w, uy * progress * h);
656 canvas.draw_image(&img_a, (dx, dy), None);
657 canvas.draw_image(&img_b, (dx - ux * w, dy - uy * h), None);
658 surface_to_pixels(surface, width, height)
659 };
660
661 let peak = 1.0 - (progress * 2.0 - 1.0).abs();
664 let shift = (aberration.max(0.0) * peak * w * 0.012).round() as i32;
665 if shift == 0 {
666 return slid;
667 }
668
669 let mut out = slid.clone();
672 let (sx, sy) = (
673 (ux * shift as f32).round() as i32,
674 (uy * shift as f32).round() as i32,
675 );
676 let sample = |buf: &[u8], x: i32, y: i32, channel: usize| -> u8 {
677 let cx = x.clamp(0, width as i32 - 1);
678 let cy = y.clamp(0, height as i32 - 1);
679 buf[((cy as u32 * width + cx as u32) * 4) as usize + channel]
680 };
681 for y in 0..height as i32 {
682 for x in 0..width as i32 {
683 let base = ((y as u32 * width + x as u32) * 4) as usize;
684 out[base] = sample(&slid, x - sx, y - sy, 0);
685 out[base + 2] = sample(&slid, x + sx, y + sy, 2);
686 }
687 }
688 out
689}
690
691fn dissolve_transition(
692 frame_a: &[u8],
693 frame_b: &[u8],
694 _width: u32,
695 _height: u32,
696 progress: f32,
697) -> Vec<u8> {
698 blend_fade(frame_a, frame_b, progress)
700}
701
702#[allow(clippy::too_many_arguments)]
725pub fn camera_pan_transition(
726 bg_a: &[u8],
727 bg_b: &[u8],
728 fg_a: &[u8],
729 fg_b: &[u8],
730 width: u32,
731 height: u32,
732 progress: f64,
733 dx: f32,
734 dy: f32,
735 easing: &EasingType,
736 pan_background: PanBackground,
737) -> Vec<u8> {
738 let t = ease(progress, easing) as f32;
739
740 let mut surface = match create_skia_surface(width, height) {
741 Some(s) => s,
742 None => return bg_a.to_vec(),
743 };
744 let img_fg_a = match frame_to_image(fg_a, width, height) {
745 Some(i) => i,
746 None => return bg_a.to_vec(),
747 };
748 let img_fg_b = match frame_to_image(fg_b, width, height) {
749 Some(i) => i,
750 None => return bg_a.to_vec(),
751 };
752
753 let (out_x, out_y) = (-dx * t, -dy * t);
756 let (in_x, in_y) = (dx * (1.0 - t), dy * (1.0 - t));
757
758 let blended_bg = match pan_background {
759 PanBackground::Travel => {
768 let img_bg_a = match frame_to_image(bg_a, width, height) {
769 Some(i) => i,
770 None => return bg_a.to_vec(),
771 };
772 let img_bg_b = match frame_to_image(bg_b, width, height) {
773 Some(i) => i,
774 None => return bg_a.to_vec(),
775 };
776
777 const BG_PARALLAX: f32 = 0.12;
782 let (bax, bay) = (out_x * BG_PARALLAX, out_y * BG_PARALLAX);
783 let (bbx, bby) = (in_x * BG_PARALLAX, in_y * BG_PARALLAX);
784
785 let w = width as f32;
799 let h = height as f32;
800 let spread = |ox: f32, oy: f32| {
801 let (mx, my) = (ox.abs(), oy.abs());
802 Rect::from_ltrb(-mx + ox, -my + oy, w + mx + ox, h + my + oy)
803 };
804
805 let layer_a = match render_layer(&img_bg_a, spread(bax, bay), width, height) {
806 Some(p) => p,
807 None => return bg_a.to_vec(),
808 };
809 let layer_b = match render_layer(&img_bg_b, spread(bbx, bby), width, height) {
810 Some(p) => p,
811 None => return bg_a.to_vec(),
812 };
813 blend_fade(&layer_a, &layer_b, t)
814 }
815 PanBackground::Static => blend_fade(bg_a, bg_b, t),
824 };
825 let img_bg = match frame_to_image(&blended_bg, width, height) {
826 Some(i) => i,
827 None => return bg_a.to_vec(),
828 };
829
830 let canvas = surface.canvas();
831 canvas.draw_image(&img_bg, (0.0, 0.0), None);
832
833 const FG_DISSOLVE: f32 = 1.6;
845 let mut fg_paint = Paint::default();
846
847 fg_paint.set_alpha_f(1.0 - t.powf(FG_DISSOLVE));
848 canvas.draw_image(&img_fg_a, (out_x, out_y), Some(&fg_paint));
849
850 fg_paint.set_alpha_f(1.0 - (1.0 - t).powf(FG_DISSOLVE));
851 canvas.draw_image(&img_fg_b, (in_x, in_y), Some(&fg_paint));
852
853 surface_to_pixels(surface, width, height)
854}
855
856fn render_layer(img: &skia_safe::Image, dest: Rect, width: u32, height: u32) -> Option<Vec<u8>> {
860 let mut surface = create_skia_surface(width, height)?;
861 surface
862 .canvas()
863 .draw_image_rect(img, None, dest, &Paint::default());
864 Some(surface_to_pixels(surface, width, height))
865}
866
867#[cfg(test)]
868mod camera_pan_tests {
869 use super::*;
870
871 #[test]
877 fn the_foreground_dissolve_is_a_noop_at_both_junctions() {
878 let (w, h) = (8u32, 4u32);
879 let bg = solid(w, h, 0, 0, 0, 255);
880 let fg_a = solid(w, h, 255, 0, 0, 255);
881 let fg_b = solid(w, h, 0, 0, 255, 255);
882
883 for (progress, expected) in [(0.0, [255u8, 0, 0]), (1.0, [0, 0, 255])] {
884 let out = camera_pan_transition(
885 &bg,
886 &bg,
887 &fg_a,
888 &fg_b,
889 w,
890 h,
891 progress,
892 8.0,
893 0.0,
894 &EasingType::Linear,
895 PanBackground::Static,
896 );
897 assert_eq!(
898 &out[0..3],
899 &expected,
900 "at progress {progress} the adjacent scene must render untouched",
901 );
902 }
903 }
904
905 #[test]
908 fn mid_pan_both_planes_stay_substantially_visible() {
909 let (w, h) = (8u32, 4u32);
910 let bg = solid(w, h, 0, 0, 0, 255);
911 let fg_a = solid(w, h, 255, 0, 0, 255);
912 let fg_b = solid(w, h, 0, 0, 255, 255);
913
914 let out = camera_pan_transition(
915 &bg,
916 &bg,
917 &fg_a,
918 &fg_b,
919 w,
920 h,
921 0.5,
922 8.0,
923 0.0,
924 &EasingType::Linear,
925 PanBackground::Static,
926 );
927 let left_red = out[0];
929 let right_blue = out[((w - 1) * 4 + 2) as usize];
930 assert!(left_red > 128, "outgoing plane faded too far: {left_red}");
931 assert!(
932 right_blue > 128,
933 "incoming plane still too faint: {right_blue}"
934 );
935 }
936
937 fn solid(width: u32, height: u32, r: u8, g: u8, b: u8, a: u8) -> Vec<u8> {
938 (0..width * height).flat_map(|_| [r, g, b, a]).collect()
939 }
940
941 fn transparent(width: u32, height: u32) -> Vec<u8> {
944 solid(width, height, 0, 0, 0, 0)
945 }
946
947 #[test]
952 fn static_background_crossfades_instead_of_freezing() {
953 let (w, h) = (4, 4);
954 let bg_a = solid(w, h, 10, 10, 10, 255);
955 let bg_b = solid(w, h, 200, 200, 200, 255);
956 let fg = transparent(w, h);
957
958 let out = camera_pan_transition(
959 &bg_a,
960 &bg_b,
961 &fg,
962 &fg,
963 w,
964 h,
965 0.5,
966 0.0,
967 0.0,
968 &EasingType::Linear,
969 PanBackground::Static,
970 );
971
972 for px in out.chunks_exact(4) {
974 assert_eq!(
975 px,
976 [105, 105, 105, 255],
977 "mid-pan Static frame must be a blend of bg_a and bg_b, not a copy of either"
978 );
979 }
980 assert_ne!(out, bg_a, "must have moved away from bg_a by the midpoint");
981 assert_ne!(
982 out, bg_b,
983 "must not have already reached bg_b at the midpoint"
984 );
985 }
986
987 #[test]
991 fn static_background_reaches_bg_b_exactly_at_full_progress() {
992 let (w, h) = (4, 4);
993 let bg_a = solid(w, h, 10, 10, 10, 255);
994 let bg_b = solid(w, h, 200, 200, 200, 255);
995 let fg = transparent(w, h);
996
997 let out = camera_pan_transition(
998 &bg_a,
999 &bg_b,
1000 &fg,
1001 &fg,
1002 w,
1003 h,
1004 1.0,
1005 0.0,
1006 0.0,
1007 &EasingType::Linear,
1008 PanBackground::Static,
1009 );
1010 assert_eq!(out, bg_b, "progress=1.0 must land exactly on bg_b");
1011 }
1012
1013 #[test]
1017 fn travel_background_reaches_bg_b_exactly_at_full_progress() {
1018 let (w, h) = (4, 4);
1019 let bg_a = solid(w, h, 10, 10, 10, 255);
1020 let bg_b = solid(w, h, 200, 200, 200, 255);
1021 let fg = transparent(w, h);
1022
1023 let out = camera_pan_transition(
1024 &bg_a,
1025 &bg_b,
1026 &fg,
1027 &fg,
1028 w,
1029 h,
1030 1.0,
1031 100.0,
1032 0.0,
1033 &EasingType::Linear,
1034 PanBackground::Travel,
1035 );
1036 assert_eq!(
1037 out, bg_b,
1038 "progress=1.0 must land exactly on bg_b under Travel too"
1039 );
1040 }
1041}
1042
1043#[cfg(test)]
1044mod corner_reveal_tests {
1045 use super::*;
1046
1047 #[test]
1051 fn the_anchored_edges_never_move() {
1052 for p in [0.05, 0.3, 0.5, 0.8, 1.0] {
1053 let r = corner_rect(TransitionCorner::TopRight, 1920.0, 1080.0, p);
1054 assert!((r.right - 1920.0).abs() < 1e-3, "right edge moved at {p}");
1055 assert!(r.top.abs() < 1e-3, "top edge moved at {p}");
1056 }
1057 }
1058
1059 #[test]
1062 fn the_travelling_edges_open_from_the_corner() {
1063 let at = |p| corner_rect(TransitionCorner::TopRight, 1920.0, 1080.0, p);
1064 let (a, b, c) = (at(0.2), at(0.5), at(0.9));
1065 assert!(
1066 a.left > b.left && b.left > c.left,
1067 "left edge must travel left"
1068 );
1069 assert!(
1070 a.bottom < b.bottom && b.bottom < c.bottom,
1071 "bottom must travel down"
1072 );
1073 }
1074
1075 #[test]
1077 fn it_starts_empty_and_ends_full() {
1078 let empty = corner_rect(TransitionCorner::TopRight, 1920.0, 1080.0, 0.0);
1079 assert_eq!((empty.width(), empty.height()), (0.0, 0.0));
1080 let full = corner_rect(TransitionCorner::TopRight, 1920.0, 1080.0, 1.0);
1081 assert_eq!(
1082 (full.left, full.top, full.right, full.bottom),
1083 (0.0, 0.0, 1920.0, 1080.0)
1084 );
1085 }
1086
1087 #[test]
1089 fn every_corner_anchors_its_own_edges() {
1090 let (w, h, p) = (1920.0f32, 1080.0f32, 0.4);
1091 let tl = corner_rect(TransitionCorner::TopLeft, w, h, p);
1092 assert!(tl.left.abs() < 1e-3 && tl.top.abs() < 1e-3);
1093 let br = corner_rect(TransitionCorner::BottomRight, w, h, p);
1094 assert!((br.right - w).abs() < 1e-3 && (br.bottom - h).abs() < 1e-3);
1095 let bl = corner_rect(TransitionCorner::BottomLeft, w, h, p);
1096 assert!(bl.left.abs() < 1e-3 && (bl.bottom - h).abs() < 1e-3);
1097 }
1098
1099 #[test]
1102 fn out_of_range_progress_clamps() {
1103 for p in [-0.5, 1.5] {
1104 let r = corner_rect(TransitionCorner::TopRight, 1920.0, 1080.0, p);
1105 assert!(
1106 r.width() >= 0.0 && r.height() >= 0.0,
1107 "inverted rect at {p}"
1108 );
1109 }
1110 }
1111}
1112
1113#[cfg(test)]
1114mod pixel_dissolve_tests {
1115 use super::*;
1116
1117 #[test]
1120 fn edges_in_turns_the_border_first() {
1121 let (cols, rows) = (40, 24);
1122 let border: Vec<f32> = (0..cols)
1123 .map(|c| cell_threshold(c, 0, cols, rows, 11, PixelDissolveOrder::EdgesIn))
1124 .collect();
1125 let middle: Vec<f32> = (0..cols)
1126 .map(|c| cell_threshold(c, rows / 2, cols, rows, 11, PixelDissolveOrder::EdgesIn))
1127 .collect();
1128 let avg = |v: &Vec<f32>| v.iter().sum::<f32>() / v.len() as f32;
1129 assert!(
1130 avg(&border) < avg(&middle) - 0.15,
1131 "border {:.2} must clearly precede the middle {:.2}",
1132 avg(&border),
1133 avg(&middle)
1134 );
1135 let centre = cell_threshold(
1137 cols / 2,
1138 rows / 2,
1139 cols,
1140 rows,
1141 11,
1142 PixelDissolveOrder::EdgesIn,
1143 );
1144 assert!(centre > 0.6, "the centre cell must be late, got {centre}");
1145 }
1146
1147 #[test]
1149 fn center_out_is_the_mirror_of_edges_in() {
1150 let (cols, rows) = (40, 24);
1151 for (c, r) in [(0, 0), (20, 12), (39, 5)] {
1152 let a = cell_threshold(c, r, cols, rows, 11, PixelDissolveOrder::EdgesIn);
1153 let b = cell_threshold(c, r, cols, rows, 11, PixelDissolveOrder::CenterOut);
1154 assert!(
1156 (a + b - (SPATIAL_WEIGHT + 2.0 * (1.0 - SPATIAL_WEIGHT) * cell_hash01(c, r, 11)))
1157 .abs()
1158 < 1e-5
1159 );
1160 }
1161 }
1162
1163 #[test]
1167 fn the_front_is_ragged_not_a_closing_rectangle() {
1168 let (cols, rows) = (40, 24);
1169 let top: Vec<f32> = (0..cols)
1170 .map(|c| cell_threshold(c, 0, cols, rows, 11, PixelDissolveOrder::EdgesIn))
1171 .collect();
1172 let spread = top.iter().cloned().fold(f32::MIN, f32::max)
1173 - top.iter().cloned().fold(f32::MAX, f32::min);
1174 assert!(
1175 spread > 0.15,
1176 "the border turns as one block: spread {spread}"
1177 );
1178 }
1179
1180 #[test]
1182 fn random_ignores_position() {
1183 let t = cell_threshold(7, 3, 40, 24, 11, PixelDissolveOrder::Random);
1184 assert_eq!(t, cell_hash01(7, 3, 11));
1185 }
1186
1187 #[test]
1190 fn a_cell_keeps_its_threshold() {
1191 assert_eq!(cell_hash01(4, 9, 11), cell_hash01(4, 9, 11));
1192 let t = cell_hash01(4, 9, 11);
1193 assert!(
1194 (0.0..1.0).contains(&t),
1195 "threshold must be a fraction, got {t}"
1196 );
1197 }
1198
1199 #[test]
1201 fn the_seed_changes_the_order() {
1202 let a: Vec<f32> = (0..40).map(|i| cell_hash01(i, 0, 11)).collect();
1203 let b: Vec<f32> = (0..40).map(|i| cell_hash01(i, 0, 12)).collect();
1204 assert_ne!(a, b);
1205 }
1206
1207 #[test]
1210 fn neighbouring_cells_turn_at_unrelated_times() {
1211 let close = (0..30)
1212 .flat_map(|c| (0..30).map(move |r| (c, r)))
1213 .filter(|&(c, r)| (cell_hash01(c, r, 11) - cell_hash01(c + 1, r, 11)).abs() < 0.05)
1214 .count();
1215 assert!(
1217 close < 200,
1218 "{close}/900 neighbours turn together — that is a wipe"
1219 );
1220 }
1221
1222 #[test]
1225 fn midway_the_frame_holds_both_scenes_and_a_fading_band() {
1226 const FEATHER: f32 = 0.35;
1227 let p = 0.5 * (1.0 + FEATHER);
1228 let alphas: Vec<f32> = (0..40)
1229 .flat_map(|c| (0..40).map(move |r| cell_hash01(c, r, 11)))
1230 .map(|t| ((p - t) / FEATHER).clamp(0.0, 1.0))
1231 .collect();
1232 let done = alphas.iter().filter(|&&a| a >= 0.999).count();
1233 let waiting = alphas.iter().filter(|&&a| a <= 0.001).count();
1234 let fading = alphas.iter().filter(|&&a| a > 0.001 && a < 0.999).count();
1235 assert!(done > 100 && waiting > 100, "both states must be present");
1236 assert!(
1237 fading > 50,
1238 "cells must fade, not flip: only {fading} mid-transition"
1239 );
1240 }
1241
1242 #[test]
1245 fn every_cell_completes_by_the_end() {
1246 const FEATHER: f32 = 0.35;
1247 let p = 1.0 * (1.0 + FEATHER);
1248 for c in 0..60 {
1249 for r in 0..60 {
1250 let a = ((p - cell_hash01(c, r, 11)) / FEATHER).clamp(0.0, 1.0);
1251 assert!(
1252 a >= 0.999,
1253 "cell ({c},{r}) still at {a} when the transition ends"
1254 );
1255 }
1256 }
1257 }
1258}