diff --git a/README.md b/README.md index d190e9eb..b73a317b 100644 --- a/README.md +++ b/README.md @@ -154,7 +154,7 @@ deckcraft-cli mcp --connect 7990 # drive the running app (deckcraft --con ## Roadmap -DeckCraft is in **alpha**. It covers **79% of PowerPoint's features by breadth** (92% of the core +DeckCraft is in **alpha**. It covers **80% of PowerPoint's features by breadth** (92% of the core ones, measured) and is about **55% of the way to replacing PowerPoint for real work** (estimated). See [ROADMAP.md](ROADMAP.md) for the stage, numbers by dimension and what's next, [docs/target-app-parity.md](docs/target-app-parity.md) for the full assessment, diff --git a/ROADMAP.md b/ROADMAP.md index ce2d2b7f..bcc3106a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -34,6 +34,7 @@ > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: Trim Media dialog follows the selection; breadth stays 79%; previous: minor, hours to ~95%) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: chart trendlines are drawn; previous: trivial, chart trendlines round-trip; hours to ~95% per audience added; ready ~55%; stage stays alpha; full re-measure against PowerPoint for Mac 16.113.4) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: copy and paste rich text and pictures with other apps; previous: hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** trivial (animation preview on the canvas; breadth 79% → 80%, Animations 79% → 85%; stage stays alpha) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) DeckCraft aims at full PowerPoint parity, and to be better: faster, open (a documented zip+JSON format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and available everywhere @@ -46,6 +47,7 @@ format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and av | | Value | Kind | |---|---|---| | **Feature breadth** | **79%** weighted over 191 PowerPoint features (P0 92%, P1 75%, P2 39%) | measured: `cargo xtask parity` over [docs/parity-checklist.md](docs/parity-checklist.md) | +| **Feature breadth** | **80%** weighted over 191 PowerPoint features (P0 92%, P1 75%, P2 36%) | measured: `cargo xtask parity` over [docs/parity-checklist.md](docs/parity-checklist.md) | | Menu-bar coverage | 59% of 217 app-specific PowerPoint menu items matched by name (lower bound) | measured: script over the PowerPoint menu dump and the 226 commands | | **Ready for real work** | **~55%** | estimated: weighted dimensions below | | Mainstream practitioner | ~53% | estimated: weekly-use areas × discounts for interaction (×0.92), stability (×0.92) and `.pptx` exchange (×0.88), [method](docs/target-app-parity.md#mainstream-practitioner-and-essentials-user) | @@ -76,7 +78,7 @@ EMF/SVG pictures, the data-loss and startup-crash bugs (#60, #40, #59) and the b | Dimension | Ready | Hours to full | Doc | |---|---:|---|---| -| Features (breadth 79% measured, with depth) | ~62% | 140–240 | [docs/parity-checklist.md](docs/parity-checklist.md) | +| Features (breadth 80% measured, with depth) | ~62% | 140–240 | [docs/parity-checklist.md](docs/parity-checklist.md) | | UI/UX fidelity | ~60% | 30–50 | [docs/ui-parity.md](docs/ui-parity.md) | | File formats (PPTX, PDF, legacy, export) | ~58% | 60–110 | [docs/file-format-parity.md](docs/file-format-parity.md) | | Hardware (GPU raster, video decode, displays, pens) | ~40% | 25–45 | [docs/hardware-parity.md](docs/hardware-parity.md) | @@ -108,6 +110,8 @@ EMF/SVG pictures, the data-loss and startup-crash bugs (#60, #40, #59) and the b | Transitions | 92% | ~80% | 4–8 | [animation-parity](docs/animation-parity.md) | | Animations | 79% | ~65% | 10–16 | [animation-parity](docs/animation-parity.md) | | Slide show and presenter view | 68% | ~60% | 10–16 | [slideshow-parity](docs/slideshow-parity.md) | +| Animations | 85% | ~65% | 10–16 | [animation-parity](docs/animation-parity.md) | +| Slide show and presenter view | 66% | ~60% | 10–16 | [slideshow-parity](docs/slideshow-parity.md) | | Review | 60% | ~45% | 8–14 | [gaps](docs/gaps.md) | | Draw (ink) | 33% | ~30% | 5–9 | [hardware-parity](docs/hardware-parity.md) | | Undo and clipboard | 88% | ~75% | 3–5 | [ui-parity](docs/ui-parity.md) | @@ -211,6 +215,7 @@ app's JSON control channel (`deckcraft --control PORT`). | 2026-10-14 | Slide Master: Preserve on the open master or layout, and Title and Footers placeholder checkboxes. Two commands (`master.preserve`, `master.placeholders`); dialogs stay 27; breadth stays 79% | | 2026-10-14 | Trim Media: start, end and fades open from the selected clip, so OK no longer resets them to zero | | 2026-10-14 | Copy and paste with other apps: bold, italic and underline travel as HTML (Pages keeps them); a shape with no text still copies as DeckCraft objects so Ctrl+V pastes it (#182) | +| 2026-10-13 | Animations ▸ Preview plays the current slide's animations on the editor canvas (Play All does the same); Escape or a click stops it. Breadth 79% → 80%, Animations 79% → 85% | | 2026-10-13 | Slide transitions like PowerPoint: every transition and option measured frame by frame against PowerPoint's video export and retuned (curves, directions, soft edges, 3-D perspective, contents moving over backgrounds); PowerPoint's own XML elements and defaults read and written (diagonal Wipe, Zoom, Shape In/Out, Glitter Hexagons, Shred In/Out, Page Curl Single); `deckcraft-cli render-transition` renders frames headless | | 2026-10-13 | Wild text cases measured against PowerPoint (102 synthetic slides): bidi by run language, East Asian vertical text upright, mongolianVert/wordArtVert/wordArtVertRtl, underline styles and strikethrough at the font's metrics, centre/right/decimal tabs, East Asian, Thai, Hindi, Arabic and Hebrew numbering, small caps, all-caps "ß", transparent text, soft hyphens, kinsoku, rtlCol and just/dist anchoring, emoji presentation, fallback faces that no longer move the baseline | | 2026-10-13 | Render matches PowerPoint more closely on wild shape, picture and table cases: even-odd custom paths, dashes and caps, arrowheads, radial and path gradients, tile rects, rotWithShape, gradient lines, shadows cast by what is drawn (group and perspective too), group fill, picture borders, tiles, reflection, recolour (biLevel, alphaInv, brightness/contrast), table borders and grid default, 20+ preset shapes by their ECMA formulas, default miter joins, and approximate 3-D bevels, cameras and face lighting | @@ -278,6 +283,7 @@ app's JSON control channel (`deckcraft --control PORT`). | 2026-10-14 | trivial | Progress log: Slide Master Preserve and Title/Footers placeholders. Percentages unchanged | | 2026-10-14 | trivial | Progress log: Trim Media dialog follows the selection | | 2026-10-14 | trivial | Progress log: copy and paste rich text and pictures with other apps | +| 2026-10-13 | trivial | Progress log: animation preview on the editor canvas; breadth 80%, Animations 85% | | 2026-10-13 | trivial | Progress log: wild text cases | | 2026-10-13 | trivial | Progress log: slide show for interactive decks | | 2026-10-12 | trivial | Progress log: File tab (Backstage, #76) | diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index dcb54e73..3e471fe4 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -96,7 +96,7 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) { if resp.hovered() && !zooming { if overflow != egui::Vec2::ZERO { offset -= wheel; - } else if wheel.y.abs() > 18.0 && app.session.tool.marquee.is_none() && !app.session.tool.dragging() { + } else if wheel.y.abs() > 18.0 && !app.anim_preview_active() && app.session.tool.marquee.is_none() && !app.session.tool.dragging() { // Wheel over a fitted slide pages through slides (like PowerPoint). let id = if wheel.y < 0.0 { "slide.next" } else { "slide.previous" }; let gate = ui.id().with("wheel_gate"); @@ -130,6 +130,7 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) { }; crate::media::gif_repaint(ui.ctx(), &gifs); let times: Vec<_> = gifs.iter().map(|g| (g.id, g.t)).collect(); + let anim = app.anim_preview_draw(); let st = app.session.active(); // One texture pixel in screen points, and the canvas in pixels from the slide's corner. let pixel = vec2(slide_rect.width() / px.0 as f32, slide_rect.height() / px.1 as f32); @@ -138,7 +139,8 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) { Some((Target::Slides, d)) => { let idx = d.selection.slide; let doc = d.doc.clone(); - app.textures.slide(ui.ctx(), &doc, idx, px, visible, true, crate::media::gif_key(&gifs), app.ui.grayscale, ×) + let frame = anim.as_ref().map(|(tl, t)| (tl, *t)); + app.textures.slide(ui.ctx(), &doc, idx, px, visible, true, crate::media::gif_key(&gifs), app.ui.grayscale, ×, frame) } Some((Target::Master { master }, d)) => master_tex(ui.ctx(), &d.doc, master, None, px, visible), Some((Target::Layout { master, layout }, d)) => master_tex(ui.ctx(), &d.doc, master, Some(layout), px, visible), @@ -224,11 +226,13 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) { ruler(&painter, avail, slide_rect, scale, &t); } pointer(app, ui, &resp, xf); - overlays(app, ui, &painter, xf, &t); - ime_output(app, ui, xf); - media_bar(app, ui, xf); - gif_buttons(app, ui, xf, &gifs); - context_menu(app, &resp); + if !app.anim_preview_active() { + overlays(app, ui, &painter, xf, &t); + ime_output(app, ui, xf); + media_bar(app, ui, xf); + gif_buttons(app, ui, xf, &gifs); + context_menu(app, &resp); + } } /// The canvas `avail` in texture pixels (`pixel`: one pixel's size in points) from the slide's @@ -436,6 +440,14 @@ fn pointer(app: &mut SlideApp, ui: &Ui, resp: &egui::Response, xf: Xf) { if crate::eyedropper::canvas(app, ui, resp, xf) { return; } + if app.anim_preview_active() { + // A click on the slide ends the preview, then selects as usual. + if resp.hovered() && ui.input(|i| i.pointer.primary_pressed()) { + app.stop_anim_preview(); + } else { + return; + } + } let tol = (4.0 / xf.scale) as f64; let m = mods(ui); let Some(pos) = resp.interact_pointer_pos().or(resp.hover_pos()) else { return }; diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index c25b1a35..263f9d37 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -83,6 +83,7 @@ pub fn inspect(app: &SlideApp, ctx: &egui::Context) -> Value { let now = ctx.input(|i| i.time); s.showing_value(n, aspect, ctx.content_rect(), now) }), + "animPreview": app.anim_preview_report().map(|(slide, time, duration)| json!({"slide": slide, "time": time, "duration": duration})), "documents": app.session.documents().iter().map(|d| json!({"title": d.title(), "dirty": d.is_dirty(), "path": d.path})).collect::>(), "status": app.status.as_ref().map(|s| s.0.clone()), "perf": {"frameMs": app.perf.frame_ms, "renderMs": app.perf.render_ms, "fps": app.perf.fps}, @@ -141,6 +142,10 @@ pub fn handle(app: &mut SlideApp, ctx: &egui::Context, req: &ControlRequest) -> } "ui.key" => { let Some(k) = s("key") else { return err("missing `key`") }; + if app.anim_preview_active() && app.show.is_none() && matches!(k.to_ascii_lowercase().as_str(), "escape" | "esc") { + app.stop_anim_preview(); + return ok(json!({"preview": false})); + } if let Some(show) = app.show.as_ref() && !show.ended && let Some(key) = key_from(k) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index d5f3e36f..590cbbfb 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -20,6 +20,7 @@ pub mod media; pub mod menus; pub mod panes; pub mod qat; +mod preview; pub mod ribbon; pub mod selpane; pub mod settings; @@ -231,6 +232,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ ("show.start", "Slide Show", Some("F5"), "{from?: index, custom?: name}"), ("show.presenter", "Presenter View", None, "{on?: bool}"), ("show.end", "End Show", None, "{}"), + ("animation.preview", "Preview Animations", None, "{on?: bool}"), ("app.home", "Home", None, "{on?}"), ("app.backstage", "File", Some("Alt+F"), "{page?: home|new|open|info|saveAs|print|export|account, on?: bool}"), ("app.closePresentation", "Close", Some("Cmd+W"), "{}"), @@ -263,6 +265,18 @@ pub struct Perf { pub fps: f64, } +/// Playhead for the in-canvas animation preview. +struct AnimPreview { + uid: u64, + slide: usize, + /// `Arc` address of the slide when preview started. An edit replaces the slide and stops playback. + slide_ptr: usize, + /// egui time when the first frame latched the clock (`None` until then). + started: Option, + elapsed: f64, + timeline: deckcraft_anim::Timeline, +} + pub struct SlideApp { /// Table Design's border pen (`color`, `width`, `dash` for `table.borders`). pub table_pen: serde_json::Value, @@ -273,6 +287,8 @@ pub struct SlideApp { pub control_rx: Option>, pub dialog: Option, pub show: Option, + /// Current slide's animations playing on the editor canvas (not the full-screen show). + anim_preview: Option, /// Audio/video playback (editor control bar, slide show, `media.*` commands). pub media: media::MediaHost, pub status: Option<(String, f64)>, @@ -339,6 +355,7 @@ impl SlideApp { control_rx: None, dialog: None, show: None, + anim_preview: None, media: media::MediaHost::new(audio_out), status: None, toast: None, @@ -630,6 +647,13 @@ impl SlideApp { self.show = None; self.media.stop_owned(media::Owner::Show); } + "animation.preview" => { + if p.get("on").and_then(Value::as_bool) == Some(false) { + self.anim_preview = None; + return Ok(json!({"playing": false})); + } + return self.start_anim_preview(); + } "app.home" => { let on = self.flag(p, self.backstage_open()); if on { @@ -713,6 +737,7 @@ impl SlideApp { } pub fn start_show(&mut self, from: usize, reading: bool) { + self.anim_preview = None; self.media.stop_owned(media::Owner::Editor); if let Some(d) = self.session.active() { self.show = Some(show::Show::new(&d.doc, from, reading, self.ui.presenter_view && !reading)); @@ -730,6 +755,84 @@ impl SlideApp { } } + /// The current slide's animations are playing on the editor canvas. + pub fn anim_preview_active(&self) -> bool { + self.anim_preview.is_some() + } + + /// `(slide, seconds played, sequence length)` while a preview is running. + pub fn anim_preview_report(&self) -> Option<(usize, f64, f64)> { + self.anim_preview.as_ref().map(|p| (p.slide, p.elapsed, p.timeline.total_duration())) + } + + pub fn stop_anim_preview(&mut self) { + self.anim_preview = None; + } + + /// Timeline and playhead for this frame's canvas raster. `None` when the editor should draw the slide at rest. + pub fn anim_preview_draw(&self) -> Option<(deckcraft_anim::Timeline, f64)> { + self.anim_preview.as_ref().map(|p| (p.timeline.clone(), p.elapsed)) + } + + /// Advance the preview clock. Stops when the slide changes, the slide is edited, or the sequence + /// (plus a short hold) has finished. + pub fn tick_anim_preview(&mut self, now: f64) -> bool { + let Some(p) = self.anim_preview.as_ref() else { return false }; + let (uid, slide, ptr) = (p.uid, p.slide, p.slide_ptr); + let alive = self.show.is_none() + && self.session.active().is_some_and(|d| { + d.uid == uid + && d.selection.target == deckcraft_engine::Target::Slides + && d.selection.slide == slide + && d.doc.slides.get(slide).is_some_and(|s| std::sync::Arc::as_ptr(s) as usize == ptr) + }); + if !alive { + self.anim_preview = None; + return false; + } + let Some(p) = self.anim_preview.as_mut() else { return false }; + let now = if now.is_finite() { now } else { p.started.unwrap_or(0.0) }; + let start = *p.started.get_or_insert(now); + let elapsed = (now - start).max(0.0); + if !elapsed.is_finite() || elapsed > p.timeline.total_duration() + preview::HOLD_SECS { + self.anim_preview = None; + return false; + } + p.elapsed = elapsed; + true + } + + fn start_anim_preview(&mut self) -> Result { + let (uid, slide, ptr, timeline) = { + let Some(d) = self.session.active() else { + return Err("no presentation is open".into()); + }; + if d.selection.target != deckcraft_engine::Target::Slides { + return Err("animation preview plays the current slide".into()); + } + let slide = d.selection.slide; + let Some(slide_arc) = d.doc.slides.get(slide) else { + return Err("no slide to preview".into()); + }; + let ptr = std::sync::Arc::as_ptr(slide_arc) as usize; + let timeline = deckcraft_anim::ShowState::timeline(&d.doc, slide); + (d.uid, slide, ptr, timeline) + }; + if timeline.is_empty() { + self.anim_preview = None; + self.set_status("This slide has no animations"); + return Ok(json!({"playing": false})); + } + if matches!(self.ui.view, ViewMode::Sorter | ViewMode::NotesPage) { + self.ui.view = ViewMode::Normal; + } + self.close_backstage(); + let duration = timeline.total_duration(); + self.anim_preview = Some(AnimPreview { uid, slide, slide_ptr: ptr, started: None, elapsed: 0.0, timeline }); + self.set_status("Previewing animations"); + Ok(json!({"playing": true, "slide": slide, "duration": duration})) + } + fn pick_and_open(&mut self) { if let Some(pick) = self.services.pick_open.as_mut() { if let Some(path) = pick("open") @@ -1102,6 +1205,10 @@ impl SlideApp { } } egui::Event::Key { key, pressed: true, modifiers, .. } => { + if key == egui::Key::Escape && self.anim_preview.is_some() && !modifiers.command && !modifiers.ctrl && !modifiers.alt { + self.anim_preview = None; + continue; + } let mods = Mods { shift: modifiers.shift, alt: modifiers.alt, cmd: modifiers.command || modifiers.ctrl }; if !self.session.tool.drawing_freeform() && menus::ui_shortcut(self, key, mods) { continue; @@ -1150,6 +1257,7 @@ impl SlideApp { self.tick_media(&ctx); eyedropper::tick(self, &ctx); if self.show.is_some() { + self.anim_preview = None; show::ui(self, ui); doc_window::show(self, &ctx); self.perf.frame_ms = now_ms() - t0; @@ -1163,6 +1271,10 @@ impl SlideApp { ribbon::show(self, ui); status::status_bar(self, ui); } + let now = ui.input(|i| i.time); + if self.tick_anim_preview(now) { + ctx.request_repaint(); + } egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.chrome)).show(ui, |ui| { if self.backstage.is_some() { // File tab: the Backstage view replaces everything below the title bar. @@ -1919,4 +2031,114 @@ mod tests { let cmds = &h.output().platform_output.commands; assert!(cmds.iter().any(|c| matches!(c, egui::output::OutputCommand::CopyText(t) if t == OBJECTS_MARKER)), "{cmds:?}"); } + + fn redder(px: [u8; 4]) -> i32 { + px[0] as i32 - px[1].max(px[2]) as i32 + } + + /// A fade entrance is hidden at the start of the preview clock and opaque once it has played. + #[test] + fn preview_hides_an_entrance_until_it_plays() { + let mut session = Session::with_new(); + let text = session.execute("shape.insert", &json!({"preset": "rect", "rect": [40.0, 40.0, 220.0, 80.0]})).unwrap()["id"].as_u64().unwrap(); + session.execute("text.set", &json!({"id": text, "text": "Alpha\nBeta"})).unwrap(); + session.execute("animation.set", &json!({"effect": "fade", "duration": 1000})).unwrap(); + session.execute("animation.options", &json!({"textBuild": "byParagraph"})).unwrap(); + let doc = session.active().unwrap().doc.clone(); + let tl = deckcraft_anim::ShowState::timeline(&doc, 0); + let shape = deckcraft_model::ShapeId(text as u32); + let para = |time: f64, i: usize| preview::shape_state_at(&tl, shape, time).paras.into_iter().find(|(p, _)| *p == i).map(|(_, s)| s).unwrap(); + assert!(!para(0.0, 0).visible && !para(0.0, 1).visible, "entrances have not started"); + let mid = para(0.25, 0); + assert!(mid.visible && mid.opacity > 0.05 && mid.opacity < 0.95, "{mid:?}"); + assert!(!para(0.25, 1).visible, "the next paragraph waits for its step"); + + session.execute("animation.clear", &json!({})).unwrap(); + let id = session.execute("shape.insert", &json!({"preset": "rect", "rect": [400.0, 300.0, 120.0, 80.0]})).unwrap()["id"].as_u64().unwrap(); + session.execute("shape.fill", &json!({"id": id, "color": "#CC2200"})).unwrap(); + session.execute("animation.set", &json!({"effect": "fade", "duration": 1000})).unwrap(); + let doc = session.active().unwrap().doc.clone(); + let tl = deckcraft_anim::ShowState::timeline(&doc, 0); + let (w, h) = (doc.slide_size.width.round().max(1.0) as u32, doc.slide_size.height.round().max(1.0) as u32); + let paint = |time: Option| { + let f = |sid: deckcraft_model::ShapeId| preview::shape_state_at(&tl, sid, time.unwrap_or(0.0)); + let state: Option<&dyn Fn(deckcraft_model::ShapeId) -> deckcraft_render::ShapeState> = if time.is_some() { Some(&f) } else { None }; + let opts = deckcraft_render::RenderOpts { scale: 1.0, size: Some((w, h)), state, ..Default::default() }; + deckcraft_render::render_slide(&doc, 0, &opts).pixel(460, 340) + }; + let rest = paint(None); + let start = paint(Some(0.0)); + let end = paint(Some(tl.total_duration() + 0.05)); + assert!(redder(rest) > 40, "the shape is filled at rest {rest:?}"); + assert!(redder(start) < redder(rest) / 2, "hidden at the first preview frame {start:?} vs {rest:?}"); + assert!(redder(end) > 40, "visible again once the sequence has played {end:?}"); + } + + #[test] + fn animation_preview_plays_on_the_canvas_then_stops() { + let mut harness = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).with_step_dt(0.05).build_ui_state( + |ui, app: &mut SlideApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + SlideApp::new(Session::with_new(), Services::default()), + ); + harness.run_steps(2); + let none = harness.state_mut().run("animation.preview", json!({})).unwrap(); + assert_eq!(none["playing"], false); + assert!(harness.state().status.as_ref().is_some_and(|s| s.0.contains("no animations"))); + assert!(harness.state().show.is_none()); + + let id = harness.state_mut().session.execute("shape.insert", &json!({"preset": "rect", "rect": [200.0, 280.0, 180.0, 80.0]})).unwrap()["id"] + .as_u64() + .unwrap(); + harness.state_mut().session.execute("animation.set", &json!({"effect": "fade", "duration": 400})).unwrap(); + let started = harness.state_mut().run("animation.preview", json!({})).unwrap(); + assert_eq!(started["playing"], true); + assert!(harness.state().show.is_none(), "preview stays on the editor canvas"); + harness.run_steps(1); + let elapsed = harness.state().anim_preview.as_ref().map(|p| p.elapsed).unwrap(); + assert!(elapsed < 0.02, "the clock latches on the first frame, got {elapsed}"); + let renders = harness.state().textures.canvas_renders; + harness.run_steps(6); + let elapsed = harness.state().anim_preview.as_ref().map(|p| p.elapsed).unwrap(); + assert!((0.2..0.5).contains(&elapsed), "{elapsed}"); + assert!(harness.state().textures.canvas_renders > renders, "each preview frame redraws the slide"); + assert!(harness.state().show.is_none()); + + harness.key_press(egui::Key::Escape); + harness.run_steps(1); + assert!(harness.state().anim_preview.is_none(), "Escape stops the preview"); + + harness.state_mut().run("animation.preview", json!({})).unwrap(); + harness.run_steps(1); + assert!(harness.state().anim_preview.is_some()); + harness.state_mut().run("animation.preview", json!({"on": false})).unwrap(); + assert!(harness.state().anim_preview.is_none()); + + harness.state_mut().run("animation.preview", json!({})).unwrap(); + harness.run_steps(1); + let centre = harness.state().slide_rect.unwrap().center(); + harness.hover_at(centre); + harness.run_steps(1); + harness.drag_at(centre); + harness.run_steps(2); + assert!(harness.state().anim_preview.is_none(), "a click on the slide stops the preview"); + + harness.state_mut().run("animation.preview", json!({})).unwrap(); + harness.run_steps(1); + harness.state_mut().session.execute("slide.new", &json!({})).unwrap(); + harness.run_steps(1); + assert!(harness.state().anim_preview.is_none(), "leaving the slide stops the preview"); + + harness.state_mut().session.execute("slide.previous", &json!({})).unwrap(); + harness.state_mut().session.execute("edit.select", &json!({"ids": [id]})).unwrap(); + harness.state_mut().run("animation.preview", json!({})).unwrap(); + harness.run_steps(1); + assert!(harness.state().anim_preview.is_some()); + // 400 ms effect plus the hold. + harness.run_steps(30); + assert!(harness.state().anim_preview.is_none(), "the preview ends after the sequence"); + assert!(harness.state().show.is_none()); + } } diff --git a/crates/ui-egui/src/panes.rs b/crates/ui-egui/src/panes.rs index e9fd5889..5f70c8c3 100644 --- a/crates/ui-egui/src/panes.rs +++ b/crates/ui-egui/src/panes.rs @@ -722,13 +722,9 @@ fn animation(app: &mut SlideApp, ui: &mut Ui) { let names: Vec<(u32, String)> = st.shapes().iter().map(|s| (s.id.0, s.name.clone())).collect(); let selected: Vec = st.selection.shapes.iter().map(|i| i.0).collect(); ui.horizontal(|ui| { - if ui.button("▶ Play All").clicked() { - let from = app.session.active().map(|d| d.selection.slide).unwrap_or(0); - app.start_show(from, true); - if let Some(s) = app.show.as_mut() { - s.preview_only = true; - s.autoplay = true; - } + let playing = app.anim_preview_active(); + if ui.button(if playing { "■ Stop" } else { "▶ Play All" }).clicked() { + run(app, "animation.preview", json!({"on": !playing})); } }); ui.separator(); diff --git a/crates/ui-egui/src/preview.rs b/crates/ui-egui/src/preview.rs new file mode 100644 index 00000000..432a004b --- /dev/null +++ b/crates/ui-egui/src/preview.rs @@ -0,0 +1,38 @@ +//! In-canvas animation preview: the current slide's effects played on the editor canvas +//! (Animations ▸ Preview, Animation Pane ▸ Play All). The slide show uses the same mapping. + +use deckcraft_anim::Timeline; +use deckcraft_geom::Vec2; +use deckcraft_model::ShapeId; +use deckcraft_render::{ParaState, ShapeState}; + +/// How long the finished sequence stays on the canvas before the editor draws the slide at rest. +pub const HOLD_SECS: f64 = 0.45; + +/// Shape and paragraph state when `step` main steps are done and the current step has played `t` seconds. +pub fn shape_state(tl: &Timeline, id: ShapeId, step: usize, t: f64) -> ShapeState { + let a = tl.state(id, None, step, t); + ShapeState { + visible: a.visible, + opacity: a.opacity, + offset: Vec2::new(a.offset_x, a.offset_y), + scale: (a.scale_x, a.scale_y), + rotate: a.rotate, + clip: a.clip, + tint: a.tint, + paras: tl + .paragraph_targets(id) + .into_iter() + .map(|p| { + let a = tl.state(id, Some(p), step, t); + (p, ParaState { visible: a.visible, opacity: a.opacity, offset: Vec2::new(a.offset_x, a.offset_y) }) + }) + .collect(), + } +} + +/// State on the play-everything clock (preview and Play All). +pub fn shape_state_at(tl: &Timeline, id: ShapeId, time: f64) -> ShapeState { + let (step, t) = tl.locate(time); + shape_state(tl, id, step, t) +} diff --git a/crates/ui-egui/src/ribbon.rs b/crates/ui-egui/src/ribbon.rs index f4837381..5989daac 100644 --- a/crates/ui-egui/src/ribbon.rs +++ b/crates/ui-egui/src/ribbon.rs @@ -1804,13 +1804,10 @@ fn animations(app: &mut SlideApp, ui: &mut Ui) { s.animations.iter().find(|a| a.shape == *id).cloned() }); group(ui, |ui| { - if big_button(ui, Icon::Preview, "Preview", true).clicked() { - let from = app.session.active().map(|d| d.selection.slide).unwrap_or(0); - app.start_show(from, true); - if let Some(s) = app.show.as_mut() { - s.preview_only = true; - s.autoplay = true; - } + let playing = app.anim_preview_active(); + let (resp, _) = big_button_ex(ui, Icon::Preview, "Preview", true, false, playing); + if resp.on_hover_text("Play this slide's animations on the canvas").clicked() { + run(app, "animation.preview", json!({})); } }); group(ui, |ui| { diff --git a/crates/ui-egui/src/textures.rs b/crates/ui-egui/src/textures.rs index 9f52918a..e1f8b1f5 100644 --- a/crates/ui-egui/src/textures.rs +++ b/crates/ui-egui/src/textures.rs @@ -187,6 +187,14 @@ fn same(a: &Option>, b: &Option>) -> bool { } } +/// Mix the preview playhead into the canvas cache key. Zero means "no preview", so a playhead of +/// 0s still misses the at-rest texture (entrances start hidden). +fn anim_key(anim: Option<(&deckcraft_anim::Timeline, f64)>) -> u64 { + let Some((_, t)) = anim else { return 0 }; + let ms = if t.is_finite() { (t * 1000.0).clamp(0.0, 1.0e12) as u64 } else { 0 }; + ms.wrapping_add(1).wrapping_mul(0x9E37_79B9_7F4A_7C15) +} + fn threads() -> u16 { if cfg!(target_arch = "wasm32") { 0 } else { std::thread::available_parallelism().map(|n| n.get().min(8) as u16).unwrap_or(0) } } @@ -208,6 +216,7 @@ impl Textures { extra: u64, grayscale: bool, gif_times: &[(ShapeId, f64)], + anim: Option<(&deckcraft_anim::Timeline, f64)>, ) -> Option<(TextureHandle, Region)> { let slide = p.slides.get(index)?.clone(); let master = master_of(p, &slide); @@ -232,7 +241,7 @@ impl Textures { master: master.as_ref().map(|m| Arc::as_ptr(m) as usize).unwrap_or(0), size, edit, - extra: extra ^ grayscale as u64, + extra: extra ^ grayscale as u64 ^ anim_key(anim), want, }; if let Some(e) = &self.canvas @@ -242,8 +251,18 @@ impl Textures { } let t0 = crate::now_ms(); let render = |region: Region| { - let opts = - RenderOpts { scale, edit, gif_times, threads: threads(), offset: region.offset(), size: Some(region.size()), ..Default::default() }; + let state_of = |id: ShapeId| anim.map(|(tl, time)| crate::preview::shape_state_at(tl, id, time)).unwrap_or_default(); + let state: Option<&dyn Fn(ShapeId) -> deckcraft_render::ShapeState> = if anim.is_some() { Some(&state_of) } else { None }; + let opts = RenderOpts { + scale, + edit, + gif_times, + threads: threads(), + offset: region.offset(), + size: Some(region.size()), + state, + ..Default::default() + }; deckcraft_render::render_slide_drawn(p, index, &opts) }; let (mut img, drawn) = render(want); diff --git a/docs/animation-parity.md b/docs/animation-parity.md index 00d86deb..e27daccc 100644 --- a/docs/animation-parity.md +++ b/docs/animation-parity.md @@ -2,6 +2,7 @@ > **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (interactive-deck clicks in the slide show; previous: major (transitions measured frame by frame against PowerPoint for Windows' video export and retuned; previous: created)) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (animation bounce end and repeat round-trip; rewind still #77) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (Animations ▸ Preview plays the current slide on the editor canvas; previous: major, transitions measured frame by frame) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) Transitions and animations are what make a deck a presentation, and the part of PowerPoint people notice first when it's missing. Scores roll up into [target-app-parity.md](target-app-parity.md) @@ -74,6 +75,9 @@ In, Wipe, Zoom, Float In, Grow/Shrink, Pulse, Spin, motion lines): weighted, ~85 | Animation Pane: list, reorder, remove | yes | yes | | Animation Pane: advanced timeline (bars you drag) | yes | yes: bars from the show schedule, drag the body for delay, the right edge for duration, seconds ruler (`animpane_tests`) | | Preview in the editor (current effect, from selected) | yes | partly | +| Animation Pane: advanced timeline (bars you drag) | yes | **no** | +| Preview on the editor canvas (the current slide) | yes | yes | +| Preview from the selected effect | yes | no | | Motion path editing (edit points, reverse, lock) | yes | partly (no edit points) | | Multiple animations on one object | yes | yes | | Round trip of the timing tree | yes | flattened to the pane list on read, rebuilt on write (`every_animation_effect_survives`) | @@ -83,6 +87,7 @@ In, Wipe, Zoom, Float In, Grow/Shrink, Pulse, Spin, motion lines): weighted, ~85 | Work | Hours | |---|---| | Preview from selected, effect dialog depth | 4–6 | +| Animation Pane timeline, preview from the selected effect, effect dialog depth | 4–7 | | The remaining ~126 effects (most are parameter variations of existing ones) | 3–6 | | Rewind and other effect options on round trip (#77) | 1–2 | | Transition sounds: the rest of PowerPoint's built-in list (Applause, Arrow, Bomb…) | 1 | @@ -97,6 +102,7 @@ In, Wipe, Zoom, Float In, Grow/Shrink, Pulse, Spin, motion lines): weighted, ~85 | Date | Change | Summary | |---|---|---| | 2026-10-14 | trivial | Repeat and bounce end round-trip through PPTX; rewind is still lost (#77) | +| 2026-10-13 | minor | Animations ▸ Preview and Animation Pane ▸ Play All play the current slide's animations on the editor canvas; preview from the selected effect is still open | | 2026-10-13 | major | Transitions measured frame by frame against PowerPoint's video export and retuned (mean diff 35.7 → 15.0); PowerPoint's XML defaults and option meanings; new options | | 2026-10-13 | minor | Triggers now play in the slide show (they were read and written but never started), effects of different triggers apply in the order they were clicked | | 2026-10-10 | major | Created: 48/48 transitions, 64 of ~190 animation effects, pane and option depth, estimates | diff --git a/docs/architecture.md b/docs/architecture.md index bbe1e629..acd3521d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -73,7 +73,8 @@ effects project cards through a perspective camera and `anim::layer_mesh` subdiv textures stay straight; soft edges are per-corner opacities. The show renders the slides once per transition and only composites cached textures each frame; `render::composite_transition` is the CPU twin used by `deckcraft-cli render-transition` and the tests. The show window and the presenter view are egui viewports; animation state per shape -comes from `anim::timeline` each frame. PDF export renders each slide at print resolution and +comes from `anim::timeline` each frame. Animations ▸ Preview plays that same state on the editor +canvas for the current slide, then draws the slide at rest. PDF export renders each slide at print resolution and overlays an invisible real-text layer with embedded font subsets. ## File I/O @@ -105,5 +106,6 @@ macOS, Windows, Linux, FreeBSD and web artifacts. | 2026-10-14 | minor | Insert › Date and Time dialog (`insert.dateTime` with no keys). 28 dialogs; command count stays 226 | | 2026-10-14 | minor | Command registry is 227 (`design.effects`). Dialogs stay 27. The test-count line stays 504 | | 2026-10-14 | minor | Command registry counted at 236, including `master.preserve` and `master.placeholders`. Dialogs stay 27. The test-count line stays 504. The menu measurement and the d556ff9 snapshot still count 226 | +| 2026-10-13 | trivial | Animation preview draws the current slide's timeline on the editor canvas | | 2026-10-13 | minor | Transitions: perspective meshes, background/content parts, CPU compositor | | 2026-10-10 | major | Created from the code on main (d556ff9) and AGENTS.md §4 | diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 0b7909de..0fcd7411 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -16,9 +16,12 @@ Coordinates are slide points (1/72 inch; a 16:9 slide is 960 × 540) unless note | `engine.commands` | — | Every command: id, label, menu path, shortcut, params doc, enabled / disabled reason | | `document.inspect` | — | Slides, sections, layouts, shapes, selection | | `ui.inspect` | — | UI state, active tool, window/canvas/slide rects, dialog, `home` (the File tab open over a presentation), `backstage` (its page, also the start screen), slide show state (`grid`, `zoomArm`, `zoom`, `blank`, `presenterPaused`, `presenterSecs`, `notesPx`, `custom`, `goto`; `cells` while See All Slides is open), documents, perf (fps, render ms) | +| `ui.inspect` | — | UI state, active tool, window/canvas/slide rects, dialog, `home` (the File tab open over a presentation), `backstage` (its page, also the start screen), slide show state, `animPreview` (canvas playback: slide, time, duration), documents, perf (fps, render ms) | | `ui.tool.select` | `{tool, preset?}` | `select`, `text`, `shape` + preset, any preset name, `curve`, `freeform`, `scribble`, `pen`, `highlighter`, `eraser` | | `ui.pointer` | `{events: [{kind: down\|drag\|up\|move\|doubleclick\|tripleclick\|contextclick, x, y, mods?}], mods?}` | Drive the active tool in slide points through the same code path as the mouse (`contextclick`: the right-button press that selects before the context menu) | | `ui.key` / `ui.text` | `{key, shift?, alt?, cmd?}` / `{text}` | Keyboard input (also drives a running slide show: digits then Enter go to that slide, G is See All Slides, Z arms zoom, T pauses the presenter timer, R resets it, `[` and `]` change notes size, Esc clears a slide number, the grid and zoom before it leaves the show) | +| `ui.pointer` | `{events: [{kind: down\|drag\|up\|move\|doubleclick\|tripleclick, x, y, mods?}], mods?}` | Drive the active tool in slide points through the same code path as the mouse | +| `ui.key` / `ui.text` | `{key, shift?, alt?, cmd?}` / `{text}` | Keyboard input (also drives a running slide show; Escape stops the canvas animation preview) | | `ui.ime` | `{preedit, active?: [start, end]}` / `{commit}` | An input method's composition through egui, as a real one arrives: shown in place on the slide or in a focused field; `{"preedit": ""}` cancels | | `ui.move` / `ui.click` / `ui.drag` | screen points | Real egui pointer input — reaches every widget, menu and pane | | `ui.set` | `{tab?, view?, pane?, notes?, zoom?, brightness?, ruler?, gridlines?, guides?, …}` | UI state (returns the full state) | diff --git a/docs/gaps.md b/docs/gaps.md index 01d35484..b0db5e93 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -25,6 +25,7 @@ > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Design ▸ Effects; checklist row stays P so breadth stays 79%; previous: 3-D bevel, camera and face lighting rendered) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Slide Master Preserve and Title/Footers placeholders; checklist letters unchanged, breadth stays 79%, dialogs stay 27; previous: 3-D bevel, camera and face lighting rendered) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (chart trendlines are drawn; previous: minor, chart trendlines round-trip; 3-D bevel, camera and face lighting rendered; IME composition and CJK interface text landed, #101; core-workflow alpha gate added; stage stays alpha; full re-measure against PowerPoint for Mac 16.113.4 and the open GitHub issues) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (animation preview plays on the editor canvas; previous: minor, 3-D bevel, camera and face lighting rendered) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) Every known shortfall, one entry each, ranked by how much it stops someone who presents with PowerPoint from switching. This is the work list: pick from the top. Numbers are summarized in @@ -53,6 +54,7 @@ revision history. | 8 | Image paste fails on Windows and Linux (#53) | Feature | B | 2–4 | [ui-parity.md](ui-parity.md) | | 9 | Presenter view isn't placed on the second display | Hardware | B | 3–5 | [slideshow-parity.md](slideshow-parity.md) | | 10 | Animation Pane: thin preview (timeline done); 64 of ~190 effects | Feature | B (pane) | 8–14 | [animation-parity.md](animation-parity.md) | +| 10 | Animation Pane: no timeline; preview is the whole slide, not from the selected effect; 64 of ~190 effects | Feature | B (pane) | 10–16 | [animation-parity.md](animation-parity.md) | | 11 | Charts: data grid editing, elements, `chartex` types | Feature | B (data, elements) | 12–20 | [parity-checklist.md](parity-checklist.md) | | 12 | Format Shape pane thinner than PowerPoint's | UI/UX | B | 6–10 | [ui-parity.md](ui-parity.md) | | 13 | No native macOS menu bar | UI/UX | B | 3–5 | [ui-parity.md](ui-parity.md) | @@ -252,7 +254,10 @@ Detail in [file-format-parity.md](file-format-parity.md). - **Estimate:** 6–10 h. ### Animation Pane, effect count and preview -Detail in [animation-parity.md](animation-parity.md). 10–16 h; the pane is a **beta blocker**. +Animations ▸ Preview and the pane's Play All play the current slide's animations on the editor +canvas (Escape or a click stops them). Still open: the advanced timeline, starting from the +selected effect, and most of the effect gallery. Detail in +[animation-parity.md](animation-parity.md). 10–16 h; the pane's timeline is a **beta blocker**. ### Interactive decks: media-action links, audio-triggered effects and click boxes - **Evidence:** the slide show plays click-triggered animations, keeps each slide's state across @@ -447,6 +452,7 @@ Detail in [localization-parity.md](localization-parity.md). | 2026-10-13 | trivial | Command palette skips commands that need a value, so Export opens the dialog (#79) | | 2026-10-14 | minor | Design ▸ Effects applies a named effect scheme. The checklist row stays P, so breadth stays 79%. Dialogs stay 27 | | 2026-10-14 | minor | Slide Master Preserve and Title/Footers placeholders. Checklist letters unchanged (breadth stays 79%); dialogs stay 27 | +| 2026-10-13 | minor | Animation preview plays the current slide on the editor canvas; the pane gap keeps the timeline, preview from the selected effect, and the remaining effects | | 2026-10-13 | minor | Shape, picture and table rendering checked against PowerPoint on ~150 synthetic edge-case slides; 3-D bevels, cameras and face lighting now render approximately (gap narrowed to extrusion, rigs and the 3-D pane) | | 2026-10-13 | minor | Interactive decks: triggers, return-to-slide state and click rules landed in the slide show; the remaining pieces are one new entry under Features | | 2026-10-12 | minor | IME composition in slide text and notes, and CJK interface text with real glyphs (#101): the IME gap is now Korean on the web and table-cell underlines | diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index 198f209b..6e3ed9b4 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -1,6 +1,6 @@ # PowerPoint feature checklist -> **Generated:** 2026-10-10 (summary by `cargo xtask parity`; rows re-scored 2026-10-10 against PowerPoint for Mac 16.113.4) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Generated:** 2026-10-13 (summary by `cargo xtask parity`; Preview in editor re-scored; earlier rows re-scored 2026-10-10 against PowerPoint for Mac 16.113.4) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) The summary between ` **Weighted breadth parity: 80%** over 191 features. @@ -41,6 +41,9 @@ The summary between ` | Transitions | 100% | | Animations | 79% | | Slide show | 68% | +| Transitions | 92% | +| Animations | 85% | +| Slide show | 66% | | Review | 60% | | Draw | 33% | | Undo & clipboard | 100% | @@ -221,7 +224,7 @@ Open P0 items: Contextual tabs (Shape Format, Picture Format, Table Design/Layou | Animations | By-paragraph text builds | P1 | D | | Animations | Triggers | P2 | D | | Animations | Animation Painter | P2 | D | -| Animations | Preview in editor | P1 | P | +| Animations | Preview in editor | P1 | D | | Slide show | Full-screen show from start / current | P0 | D | | Slide show | Navigation (keys, click, go to slide, blank screen) | P0 | D | | Slide show | Presenter view (notes, next slide, timer) | P0 | P | diff --git a/docs/roadmap.md b/docs/roadmap.md index b13af4b5..ce8429ff 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -16,6 +16,7 @@ > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (custom show playback; previous: trivial, presenter timer) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (go to slide shows the typed number; previous: trivial, custom show playback) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (chart trendlines are drawn; the charts gap stays open for the data grid, chartex and secondary-axis export; previous: trivial, chart trendlines round-trip; the real-deck corpus stays open; core-workflow alpha gate added; stage stays alpha) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** trivial (canvas animation preview; beta item 9 keeps the timeline; previous: minor, core-workflow alpha gate) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) Forward-looking: where DeckCraft is going and in what order. The one-page summary is [ROADMAP.md](../ROADMAP.md); every shortfall is in [gaps.md](gaps.md); numbers are in @@ -82,6 +83,8 @@ Beta means ~75% ready for real work and opening and saving PowerPoint's `.pptx` | 8 | Charts: data grid, elements | 6–10 | Features | | 9 | Animation Pane timeline, preview, effect options | 5–8 | Features | | 10 | Presenter view on the right display, swap displays | 2–4 | Hardware, Slide show | +| 9 | Animation Pane timeline, preview from the selected effect, effect options | 4–7 | Features | +| 10 | Presenter view on the right display, timer controls | 3–5 | Hardware, Slide show | | 11 | Native macOS menu bar, Format Shape pane depth, start screen and window chrome fixes | 13–22 | UI/UX 60 → 75% | | 12 | Performance baseline and the obvious fixes | 4–8 | Performance | | **Total** | | **~120–210** | Ready ~55% → ~75% | @@ -164,5 +167,6 @@ plan above. | 2026-10-14 | trivial | Present workflow notes custom show playback | | 2026-10-14 | trivial | Present workflow notes presenter timer pause/reset, clock and notes size. Display placement stays open | | 2026-10-14 | trivial | Present workflow notes See All Slides and zoom | +| 2026-10-13 | trivial | Canvas animation preview landed; beta item 9 keeps the timeline and preview from the selected effect | | 2026-10-10 | minor | Added the alpha gate (six core workflows): all pass, two with partial depth; stage stays alpha | | 2026-10-10 | major | Created: current focus, ranked beta plan with hours, milestones M0–M14 with status, the old alpha checklist as history | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 721444e4..7c676999 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -17,6 +17,7 @@ > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (essentials discount no longer cites the palette Export failure, #79; percentages unchanged; previous: minor, hours to ~95% per audience) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Design ▸ Effects; percentages unchanged; previous: hours to ~95% per audience, ready ~55%) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Slide Master Preserve and Title/Footers; automation counted at 236 commands; the menu measurement and the d556ff9 snapshot still count 226; percentages unchanged; previous: hours to ~95% per audience, ready 56% → 55%) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** trivial (Preview in editor re-scored done; breadth 79% → 80%, Animations 79% → 85%; stage stays alpha) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) The authoritative answer to "how close is DeckCraft to PowerPoint, and how much work is left?". [ROADMAP.md](../ROADMAP.md) summarizes it; [gaps.md](gaps.md) itemizes every shortfall; @@ -28,6 +29,7 @@ number. | Number | Value | Kind | |---|---|---| | **Feature breadth** (weighted checklist) | **79%** over 191 features (P0 92%, P1 75%, P2 39%) | **measured**: `cargo xtask parity` formula over [parity-checklist.md](parity-checklist.md) | +| **Feature breadth** (weighted checklist) | **80%** over 191 features (P0 92%, P1 75%, P2 36%) | **measured**: `cargo xtask parity` formula over [parity-checklist.md](parity-checklist.md); Preview in editor re-scored 2026-10-13 | | Menu-bar coverage | **59%** (128 of 217 app-specific PowerPoint menu items have a DeckCraft command or UI label of the same name) | **measured** (heuristic lower bound): script matching the 2026-10-07 PowerPoint menu dump against the 226 engine command labels and the UI's strings; excludes the app, Window and Help menus and the subtitle language lists | | **Ready for real work** | **~55%** | **estimated**: weighted dimension table below (was ~56% until the features row was recomputed from written area weights) | | **Mainstream practitioner** | **~53%** | **estimated**: [method below](#mainstream-practitioner-and-essentials-user) | @@ -80,7 +82,7 @@ The weights are what those users actually depend on, written down so the total c | Dimension | Weight | Ready | Hours to full | Doc | Evidence | |---|---:|---:|---|---|---| -| Features (breadth × depth) | 30% | ~62% | 140–240 | [parity-checklist.md](parity-checklist.md), [animation-parity.md](animation-parity.md), [slideshow-parity.md](slideshow-parity.md) | Breadth 79% measured. Depth lags: 37 of 191 rows are partial, and users found depth bugs in rows scored done within two days (#53, #60, #69, #70, #74, #77, #78) | +| Features (breadth × depth) | 30% | ~62% | 140–240 | [parity-checklist.md](parity-checklist.md), [animation-parity.md](animation-parity.md), [slideshow-parity.md](slideshow-parity.md) | Breadth 80% measured. Depth lags: partial rows remain, and users found depth bugs in rows scored done within two days (#53, #60, #69, #70, #74, #77, #78) | | File formats | 20% | ~58% | 60–110 | [file-format-parity.md](file-format-parity.md) | PPTX reads and writes every main part and keeps unknown objects; files open in PowerPoint without repair. But it has never met a corpus of real decks, EMF/WMF/SVG pictures don't decode, comments/media/rewind settings are lost on round trip, and there is no `.ppt`, `.pptm`, `.odp`, video or GIF export | | UI/UX fidelity | 15% | ~60% | 30–50 | [ui-parity.md](ui-parity.md) | All 10 ribbon tabs and the contextual tabs exist; Format Shape pane, Animation Pane, dialogs and menus are thinner than PowerPoint's; no native macOS menu bar; user reports #17, #71, #73, #76 | | Stability | 10% | ~60% | 10–20 | [gaps.md](gaps.md) | Never-crash rules, a panic guard round every command, PPTX fuzz/malformed tests. But startup crashes on some Windows GPUs (#40, #59) and Close discards edits without asking (#60) | @@ -215,12 +217,14 @@ it is the Features row of the dimension table. Automation and Platforms are scor | Transitions | 4 | 92% | ~80% | 4–8 | Transition sounds, 3-D transitions approximated in 2-D | [animation-parity.md](animation-parity.md) | | Animations | 6 | 79% | ~65% | 10–16 | 64 of PowerPoint's ~190 effects, Animation Pane timeline, preview, rewind lost on save (#77) | [animation-parity.md](animation-parity.md) | | Slide show and presenter view | 8 | 68% | ~60% | 10–16 | Second-display placement, laser from presenter view, recording, subtitles, ink depth | [slideshow-parity.md](slideshow-parity.md) | +| Animations | 6 | 85% | ~65% | 10–16 | 64 of PowerPoint's ~190 effects, Animation Pane timeline, preview from the selected effect, rewind lost on save (#77) | [animation-parity.md](animation-parity.md) | +| Slide show and presenter view | 8 | 66% | ~60% | 10–16 | Second-display placement, laser pointer, recording, subtitles, ink depth | [slideshow-parity.md](slideshow-parity.md) | | Review | 3 | 60% | ~45% | 8–14 | Modern comment threads on round trip (#70), compare, version history, real spelling dictionaries | [file-format-parity.md](file-format-parity.md) | | Draw (ink) | 2 | 33% | ~30% | 5–9 | Pen pressure, ink to shape/text/math, lasso | [hardware-parity.md](hardware-parity.md) | | Undo and clipboard | 3 | 88% | ~75% | 3–5 | Paste Special, image paste (#53) | [ui-parity.md](ui-parity.md) | | Automation ✱ | — | 100% | ahead | 0 | | [mcp.md](mcp.md) | | Platforms ✱ | — | 67% | ~80% | see platforms | | [ROADMAP.md](../ROADMAP.md) | -| **Total (feature areas)** | 110 | **79%** | **~62%** | **~140–240** | | | +| **Total (feature areas)** | 110 | **80%** | **~62%** | **~140–240** | | | ## What works today (evidence) @@ -340,6 +344,7 @@ scored done (16 open issues). | 2026-10-13 | trivial | Essentials discoverability note: the command palette's Export failure (#79) is fixed; the ×0.95 discount stays for the menu bar and start screen | | 2026-10-14 | minor | Design ▸ Effects noted under what works today. Percentages stay 79% / ~60% / ~55%. The d556ff9 snapshot and the menu measurement stay at 226 commands; the live registry is 227 | | 2026-10-14 | minor | Slide Master Preserve and Title/Footers placeholders. Automation in "what works today" is 236 commands; the menu-bar measurement and the d556ff9 snapshot still count 226. Percentages unchanged | +| 2026-10-13 | trivial | Preview in editor scored done: breadth 79% → 80%, Animations area 79% → 85% | | 2026-10-10 | minor | Added the readiness-by-audience table: each number gets its own hours to ~95% (full 340–600, mainstream 150–250, essentials 50–90). The full number stays an additive weighted sum; no change to the percentages | | 2026-10-10 | minor | Added the mainstream practitioner (~53%) and essentials user (~62%) numbers with written weights, discounts and user evidence. Features dimension recomputed from written area weights (66% judgement → 62.4% weighted mean), which moves ready for real work from ~56% to ~55%; stage unchanged | | 2026-10-10 | minor | Stage checked against the core-workflow gate (all six pass); stays alpha | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 768512f2..c3b2e4aa 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -21,6 +21,7 @@ > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Slide Master Preserve and Title/Footers placeholders; previous: File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (Trim Media dialog follows the selection; previous: File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-14 · **Change:** minor (copy and paste rich text and pictures with other apps; previous: text editing keys, #268, and File tab, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (animation preview on the editor canvas; previous: File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-14 · **Change:** minor (File › New starter decks; previous: minor, text editing keys observed in PowerPoint for Windows, #268) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Slide Sorter sections: add, rename, collapse, drag a whole section; previous: text editing keys, #268) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) @@ -95,6 +96,8 @@ menu colours taken from the theme tokens (menu `chrome`, page `card`). | Selection Pane | front-most first, groups fold, drag to restack, Bring Forward / Send Backward arrows, Show All / Hide All, eye, padlock (Lock), rename in place | same: drag restacks among siblings (a child stays in its group), Ctrl+click toggles, Shift+click selects a run, arrows / Home / End / F2 / Esc in the list, right-click menu | | Animation Pane | list + advanced timeline | list, reorder, timing; no timeline | | Comments pane (threads) | yes | yes: every slide, with Previous and Next on the Review tab | +| Animation Pane | list + advanced timeline | list, reorder, timing, play the current slide on the canvas; no timeline | +| Comments pane (threads) | yes | yes | | Design Ideas (Designer) | yes (cloud) | no | | Dialogs | ~60 (Font, Paragraph, Bullets and Numbering, Find, Replace, Replace Fonts, Hyperlink, Action Settings, Header and Footer, Slide Size, Set Up Show, Custom Shows, Print, Page Setup, Insert Object, Symbol, Equation, Chart data sheet, SmartArt chooser, Preferences…) | 28 (`crates/ui-egui/src/dialogs.rs`): Table, Slide Size, Size and Position, Header and Footer, Hyperlink, Set Up Show, Zoom, Comment, Alt Text, Outline, Rename (section, shape, layout), Paragraph, About, Preferences, Export, Symbol, Equation (placeholder), Spelling, Accessibility, Language, Custom Shows, Chart Data, Trim Media, SmartArt chooser, Quit, Notes Pages | | Dialogs | ~60 (Font, Paragraph, Bullets and Numbering, Find, Replace, Replace Fonts, Hyperlink, Action Settings, Header and Footer, Slide Size, Set Up Show, Custom Shows, Print, Page Setup, Insert Object, Symbol, Photo Album, Equation, Chart data sheet, SmartArt chooser, Preferences…) | 28 (`crates/ui-egui/src/dialogs.rs`): Table, Slide Size, Header and Footer, Hyperlink, Set Up Show, Zoom, Comment, Alt Text, Outline, Rename (section, shape, layout), Paragraph, About, Preferences, Export, Symbol, Photo Album, Equation (placeholder), Spelling, Accessibility, Language, Custom Shows, Chart Data, Trim Media, SmartArt chooser, Quit, Notes Pages | @@ -224,6 +227,7 @@ on the slide as shapes. No pixel comparison against PowerPoint screenshots has b | 2026-10-14 | minor | Slide Master: Preserve, and Title and Footers placeholders, on the open master or layout | | 2026-10-14 | trivial | Trim Media dialog: start, end and fades follow the selected clip | | 2026-10-14 | minor | Copy and paste rich text (HTML: bold, italic, underline) with other apps | +| 2026-10-13 | minor | Animations ▸ Preview and Animation Pane ▸ Play All play the current slide on the editor canvas | | 2026-10-12 | minor | File tab (Backstage) observed in PowerPoint for Windows and matched (#76); the start screen is File › Home | | 2026-10-12 | minor | Off-slide objects drawn and editable around the slide in Normal view (#270) | | 2026-10-12 | minor | Selection Pane matched to PowerPoint (#133): drag and drop, arrows, groups, eye, Lock |