Skip to content

Commit 0c50c00

Browse files
committed
fix(transition): dissolve travelling backgrounds instead of butting them together
`background: travel` slid each background locked to its foreground, at the same distance. The two are opaque full-frame images that tile exactly, so they met on a line — and a line between two different ambiences is a scene boundary, which is the one thing this transition exists to avoid. It was clearly visible whenever consecutive beats had different palettes. Three changes, each needed for the others to work: Backgrounds now drift at a fraction of the foreground's distance. That is how parallax behaves — what is far away moves less — and it makes them overlap across nearly the whole frame rather than sharing an edge. They are drawn overscaled by the largest drift either can take. Translating an opaque image uncovers a strip on the opposite side, and that strip reads as a hard edge exactly as a join would; the overscale means neither ever exposes one. The outgoing background stays opaque and the incoming one dissolves over it. Fading both leaves the frame under-covered wherever only one of them reaches, which trades the seam for a dark band — measurably worse than the seam it replaced. Largest adjacent-column luminance step across the background of examples/dark-premium.json: 7.74 before, 5.48 after, and what remains sits mid -frame on a gradient rather than on a layer edge.
1 parent 295bf7a commit 0c50c00

1 file changed

Lines changed: 34 additions & 4 deletions

File tree

crates/rustmotion-core/src/engine/transition.rs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -427,11 +427,41 @@ pub fn camera_pan_transition(
427427
let (in_x, in_y) = (dx * (1.0 - t), dy * (1.0 - t));
428428

429429
match bg_b.and_then(|b| frame_to_image(b, width, height)) {
430-
// Travelling: each background is locked to its own foreground, so a
431-
// beat moves as one plane.
430+
// Travelling: each background moves with its own scene, but at a
431+
// fraction of the foreground's distance and fading across the pan.
432+
//
433+
// Two reasons for the fraction. It is how parallax actually works —
434+
// what is far away moves less — and it makes the two backgrounds
435+
// overlap across most of the frame instead of meeting edge to edge.
436+
// Opaque images laid side by side join on a hard line no crossfade can
437+
// hide; overlapping ones dissolve into each other.
432438
Some(img_bg_b) => {
433-
canvas.draw_image(&img_bg_a, (out_x, out_y), None);
434-
canvas.draw_image(&img_bg_b, (in_x, in_y), None);
439+
// Backgrounds drift at a fraction of the foreground's distance —
440+
// that is how parallax works, and it keeps them overlapping
441+
// instead of meeting edge to edge, where two opaque images join on
442+
// a line no fade can hide.
443+
const BG_PARALLAX: f32 = 0.12;
444+
let (bax, bay) = (out_x * BG_PARALLAX, out_y * BG_PARALLAX);
445+
let (bbx, bby) = (in_x * BG_PARALLAX, in_y * BG_PARALLAX);
446+
447+
// Translating an opaque image uncovers a strip on the opposite
448+
// side, and that strip reads as a hard edge just as much as a
449+
// join would. Overscaling by the largest drift either background
450+
// can take means neither ever exposes one.
451+
let mx = (dx * BG_PARALLAX).abs();
452+
let my = (dy * BG_PARALLAX).abs();
453+
let w = width as f32;
454+
let h = height as f32;
455+
let spread =
456+
|ox: f32, oy: f32| Rect::from_ltrb(-mx + ox, -my + oy, w + mx + ox, h + my + oy);
457+
458+
// The outgoing background stays opaque so the frame is always
459+
// covered; the incoming one dissolves over it. Fading both would
460+
// darken wherever only one of them reaches.
461+
canvas.draw_image_rect(&img_bg_a, None, spread(bax, bay), &Paint::default());
462+
let mut incoming = Paint::default();
463+
incoming.set_alpha_f(t);
464+
canvas.draw_image_rect(&img_bg_b, None, spread(bbx, bby), &incoming);
435465
}
436466
// Fixed backdrop: drawn once at rest, so a shared ambience stays
437467
// continuous and the join is invisible.

0 commit comments

Comments
 (0)