Browse / github.com/storytold/deckcraft / patch
fix(show): start a slide transition's clock after its first frame is rendered (#1385)
fix(show): start a slide transition's clock after its first frame is rendered (#1385) Part of #1385 (transitions not animated during slideshow playback). ## Cause `Show::apply` stamped the transition with the time of the key press. The new slide is then rendered on the UI thread in the same frame, and for Pan, Reveal, Ferris Wheel and the other transitions that move content over backgrounds, `part_textures` renders four more pictures. On a large or busy screen that takes longer than the transition's duration, so the first frame that drew the transition was already at progress 1 and the next slide appeared at once. The delay and the missing animation are the same render. ## Fi
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- SofiaAFigueredo (imported from pull request #1390, open)
- Base
51eef9927fa949e66b5966e0a06efe02ac8a0769- Size
- +54 −4 in 2 files
- Review
- pending
- Shared
- 2026-10-28
- Id
c40aee44ba03ddafcb1a8b8f
How it went
No other operator has reported using it yet.
Diff
raw · contributors/people.toml crates/ui-egui/src/show.rs
@@ -65,3 +65,6 @@ sync_github_name = true [people.Katacc] sync_github_name = true++[people.SofiaAFigueredo]+real_name = "Sofia Alves Figueredo"@@ -271,12 +271,35 @@ struct Transition { /// moves contents over backgrounds (Pan, Ferris Wheel, Reveal…): old back, old content, new /// back, new content. parts: Option<[Option<TextureHandle>; 4]>,+ /// The time of the key press. A hidden show, which draws nothing, expires the transition+ /// from it. start: f64,+ /// Where the drawn transition's clock starts: the frame after the first one (see `progress`),+ /// because rendering the new slide and the transition's pictures can take longer than the+ /// whole transition (#1385). `None` until then.+ clock: Option<f64>,+ /// The first frame of the transition has been drawn: the new slide and the pictures the+ /// transition needs exist, so the time they took is not part of the transition.+ primed: bool, kind: String, option: String, dur: f64, } +impl Transition {+ /// How far the transition has come at frame time `now`, from 0 to 1. The first frame is the+ /// one that renders the new slide and the transition's pictures and the second one starts the+ /// clock, so a slow render never eats the transition's duration (#1385).+ fn progress(&mut self, now: f64) -> f64 {+ if !self.primed {+ self.primed = true;+ return 0.0;+ }+ let clock = *self.clock.get_or_insert(now);+ ((now - clock) / self.dur.max(0.01)).clamp(0.0, 1.0)+ }+}+ /// A copy of the show's clock origins for regressions that must not depend on wall time. #[cfg(test)] #[derive(Clone, Copy, Debug, PartialEq)]@@ -583,7 +606,7 @@ impl Show { self.last_viewed = from; } self.trans = if kind != "none" && dur > 0.0 && !doc.show.without_animation {- Some(Transition { old, from, morph: None, parts: None, start: now, kind, option, dur })+ Some(Transition { old, from, morph: None, parts: None, start: now, clock: None, primed: false, kind, option, dur }) } else { None };@@ -1091,11 +1114,12 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) { } } let mut drew = false;- let morphing = show.trans.as_ref().is_some_and(|tr| tr.kind == "morph" && tr.from.is_some() && now - tr.start < tr.dur);+ let progress = show.trans.as_mut().map(|tr| tr.progress(now));+ let morphing = show.trans.as_ref().is_some_and(|tr| tr.kind == "morph" && tr.from.is_some()) && progress.is_some_and(|p| p < 1.0); let tl = if morphing { Some(show.timeline(&doc).clone()) } else { None }; let morph = match (&show.trans, tl) { (Some(tr), Some(tl)) => {- let p = ((now - tr.start) / tr.dur.max(0.01)).clamp(0.0, 1.0);+ let p = progress.unwrap_or(1.0); let hidden_b = |id: ShapeId| { let st = state_for(&tl, id, 0, 0.0, &[], &[]); !st.visible || st.opacity <= 0.0 || hidden.contains(&id)@@ -1137,7 +1161,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) { drew = true; ctx.request_repaint(); } else if let (Some(tr), Some(new)) = (show.trans.as_mut(), &tex) {- let p = ((now - tr.start) / tr.dur.max(0.01)).clamp(0.0, 1.0);+ let p = progress.unwrap_or(1.0); if p < 1.0 { // Slides split into background and content are rendered once per transition; // every frame only composites cached textures.@@ -2795,6 +2819,29 @@ mod tests { assert!(show.tex.is_none(), "the new slide gets a texture of its own instead of overwriting the old one"); } + #[test]+ fn a_slow_first_frame_does_not_use_up_the_transition() {+ // #1385: the clock started at the key press, so rendering the new slide for longer than the+ // transition's duration showed the next slide with no transition at all.+ let mut s = deckcraft_engine::Session::with_new();+ s.execute("slide.new", &json!({})).unwrap();+ s.execute("transition.set", &json!({"kind": "fade", "index": 1, "duration": 500})).unwrap();+ let doc = s.active().unwrap().doc.clone();+ let mut show = Show::new(&doc, 0, false, false);+ assert!(show.apply(ShowAction::Slide(1), &doc, 10.0));+ let tr = show.trans.as_mut().expect("the fade plays");+ assert!((tr.dur - 0.5).abs() < 1e-9);+ // The first frame renders for two seconds; it still shows the start of the transition.+ assert_eq!(tr.progress(10.0), 0.0);+ // The frame after it starts the clock.+ assert_eq!(tr.progress(12.0), 0.0);+ assert!((tr.progress(12.25) - 0.5).abs() < 1e-9);+ assert_eq!(tr.progress(12.5), 1.0);+ assert_eq!(tr.progress(20.0), 1.0);+ // A hidden show still expires it from the key press.+ assert_eq!(tr.start, 10.0);+ }+ #[test] fn media_in_the_click_sequence_plays_with_its_step() { // #116: media set to play in the click sequence never started; only a click on it did.