diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index 1068acab..c2e335d2 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -59,6 +59,8 @@ pub enum Icon { Keyframe, PickWhip, Flowchart, + /// The Timeline's composition marker bin: a marker tab resting in an open tray. + MarkerBin, // project items Folder, Comp, @@ -412,6 +414,10 @@ pub fn paint(painter: &Painter, rect: Rect, icon: Icon, color: Color32) { pen.rect(10.0, 9.5, 14.5, 13.5); pen.line(&[(6.0, 4.5), (12.25, 4.5), (12.25, 9.5)]); } + MarkerBin => { + pen.fill(&[(5.0, 2.0), (11.0, 2.0), (11.0, 7.5), (8.0, 10.5), (5.0, 7.5)]); + pen.line(&[(2.0, 9.0), (2.0, 14.0), (14.0, 14.0), (14.0, 9.0)]); + } Folder => pen.closed(&[(1.5, 4.0), (6.0, 4.0), (7.5, 5.5), (14.5, 5.5), (14.5, 13.0), (1.5, 13.0)]), Comp => { pen.rect(2.0, 3.0, 14.0, 13.0); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index d1bd1880..3b7110e2 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1512,6 +1512,16 @@ pub fn handle_shortcuts(app: &mut EffectcraftApp, ctx: &egui::Context) { }) .collect() }); + // Numpad * (or `*` typed with Shift+8): Add Marker (#275). egui has no key for it, so it + // comes as text. + if clipboard && app.dialog.is_none() && ctx.input(|i| i.events.iter().any(|e| matches!(e, egui::Event::Text(t) if t == "*"))) { + let bound = app.session.shortcuts().bindings().into_iter().find(|(k, _)| *k == "Num*").map(|(_, b)| (b.command.clone(), b.params.clone())); + if let Some((id, params)) = bound + && let Err(e) = invoke(app, ctx, &id, if params.is_null() { json!({}) } else { params }) + { + app.ui.status = e; + } + } if events.is_empty() { return; } diff --git a/crates/ui-egui/src/panels/markers_ui.rs b/crates/ui-egui/src/panels/markers_ui.rs index 16fff58e..e66d9c3c 100644 --- a/crates/ui-egui/src/panels/markers_ui.rs +++ b/crates/ui-egui/src/panels/markers_ui.rs @@ -4,15 +4,17 @@ //! Gestures, as in After Effects: drag moves a marker (snapping to the current time, the work //! area and other markers), Alt-drag sets its duration, Cmd-click (Ctrl-click elsewhere) //! deletes it, double-click opens the Marker dialog, right-click offers Settings / Convert / -//! Delete. Protected regions (Responsive Design) shade the timeline. Every change is a -//! `markers.*` engine command, so it is undoable and agent-drivable. +//! Delete. Protected regions (Responsive Design) shade the timeline. A marker dragged out of the +//! composition marker bin, at the right end of the time ruler, adds a composition marker where it +//! is dropped. Every change is an engine command, so it is undoable and agent-drivable. use effectcraft_engine::project::{Comp, Layer, Marker}; use effectcraft_engine::time::Tick; -use egui::{Align2, Color32, Rect, Sense, Stroke, pos2, vec2}; +use egui::{Align2, Color32, Pos2, Rect, Sense, Stroke, pos2, vec2}; use serde_json::{Value, json}; use super::timeline::TMap; +use crate::icons::{self, Icon}; use crate::theme::Tokens; use crate::{Dialog, EffectcraftApp}; @@ -253,6 +255,17 @@ fn snap(app: &EffectcraftApp, comp: &Comp, tm: TMap, t: f64, skip: MarkerRef) -> t } +/// A marker's colour: its label's, pale yellow without one. +fn marker_color(t: &Tokens, label: effectcraft_engine::color::Label) -> Color32 { + if label == effectcraft_engine::color::Label::None { Color32::from_rgb(0xd8, 0xd8, 0x60) } else { t.label(label) } +} + +/// The marker glyph at `x` from `y0`, `h` points tall: a tab pointing down at the time. +fn marker_shape(x: f32, y0: f32, h: f32) -> Vec { + let ym = y0 + h * 0.6; + vec![pos2(x - 4.0, y0), pos2(x + 4.0, y0), pos2(x + 4.0, ym), pos2(x, y0 + h), pos2(x - 4.0, ym)] +} + /// Draw one marker (triangle + duration bar + comment) at `x` in a strip `[y0, y1]` and handle /// its gestures. `comp_t` is the marker's comp time. #[allow(clippy::too_many_arguments)] @@ -260,9 +273,8 @@ fn marker(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, comp: let t = app.tokens; let ctx = ui.ctx().clone(); let x = tm.x(comp_t); - let col = if m.label == effectcraft_engine::color::Label::None { Color32::from_rgb(0xd8, 0xd8, 0x60) } else { t.label(m.label) }; + let col = marker_color(&t, m.label); let h = (y1 - y0).min(10.0); - let ym = y0 + h * 0.6; if m.duration > Tick(0) { let xe = tm.x(comp_t + m.duration.seconds()); let bar = Rect::from_min_max(pos2(x, y0 + 1.0), pos2(xe, y0 + h - 1.0)); @@ -276,7 +288,7 @@ fn marker(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter, comp: } p.line_segment([pos2(xe, y0), pos2(xe, y0 + h)], Stroke::new(1.0, col)); } - p.add(egui::Shape::convex_polygon(vec![pos2(x - 4.0, y0), pos2(x + 4.0, y0), pos2(x + 4.0, ym), pos2(x, y0 + h), pos2(x - 4.0, ym)], col, Stroke::NONE)); + p.add(egui::Shape::convex_polygon(marker_shape(x, y0, h), col, Stroke::NONE)); if !m.comment.is_empty() { p.text(pos2(x + 7.0, y0 + h / 2.0), Align2::LEFT_CENTER, m.comment.lines().next().unwrap_or_default(), Tokens::ui(10.0), t.text); } @@ -365,6 +377,37 @@ pub(crate) fn comp_markers(app: &mut EffectcraftApp, ui: &mut egui::Ui, comp: &C } } +/// The composition marker bin at the right end of the time ruler (`bin`), as in After Effects +/// (#275): drag a marker out of it and it follows the pointer along the marker strip `strip` +/// (snapping like a dragged marker); dropping it there adds a composition marker at that time, +/// one undo step. Dropped back on the bin or outside the time graph's width, nothing is added. +pub(crate) fn marker_bin(app: &mut EffectcraftApp, ui: &mut egui::Ui, comp: &Comp, tm: TMap, bin: Rect, strip: Rect) { + let t = app.tokens; + let resp = ui.interact(bin, egui::Id::new("marker-bin"), Sense::drag()).on_hover_text("Composition marker bin: drag a marker to the time ruler"); + app.auto.add("timeline.markerBin", bin, "Composition marker bin"); + let hot = resp.hovered() || resp.dragged(); + if hot { + ui.ctx().set_cursor_icon(if resp.dragged() { egui::CursorIcon::Grabbing } else { egui::CursorIcon::Grab }); + } + icons::paint(ui.painter(), bin, Icon::MarkerBin, if hot { t.icon_active } else { t.icon }); + let at = resp.interact_pointer_pos().filter(|p| p.x >= strip.min.x && p.x < strip.max.x).map(|p| { + let snapped = snap(app, comp, tm, tm.t(p.x), MarkerRef { layer: None, index: usize::MAX }); + snapped.min(comp.duration.seconds()) + }); + let Some(at) = at else { return }; + if resp.dragged() { + let h = strip.height().min(10.0); + let col = marker_color(&t, effectcraft_engine::color::Label::None); + let p = ui.painter().with_clip_rect(strip); + p.add(egui::Shape::convex_polygon(marker_shape(tm.x(at), strip.min.y, h), col, Stroke::NONE)); + } + if resp.drag_stopped() + && let Err(e) = app.session.execute("comp.addMarker", json!({"time": at})) + { + app.ui.status = e.to_string(); + } +} + /// Layer markers on a layer's row `r`; on a precomp layer, its comp's markers too (read-only, /// outlined: hover names them, a double-click opens the nested comp at the marker). pub(crate) fn layer_markers(app: &mut EffectcraftApp, ui: &mut egui::Ui, clip: Rect, comp: &Comp, layer: &Layer, tm: TMap, r: Rect) { @@ -399,10 +442,8 @@ fn nested_markers(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: &egui::Painter let (y0, h) = (r.min.y + 3.0, (r.height() - 6.0).min(10.0)); for (ct, i, m) in list { let x = tm.x(ct.seconds()); - let col = if m.label == effectcraft_engine::color::Label::None { Color32::from_rgb(0xd8, 0xd8, 0x60) } else { t.label(m.label) }.gamma_multiply(0.75); - let ym = y0 + h * 0.6; - let pts = vec![pos2(x - 4.0, y0), pos2(x + 4.0, y0), pos2(x + 4.0, ym), pos2(x, y0 + h), pos2(x - 4.0, ym)]; - p.add(egui::Shape::closed_line(pts, Stroke::new(1.2, col))); + let col = marker_color(&t, m.label).gamma_multiply(0.75); + p.add(egui::Shape::closed_line(marker_shape(x, y0, h), Stroke::new(1.2, col))); if !m.comment.is_empty() { p.text(pos2(x + 7.0, y0 + h / 2.0), Align2::LEFT_CENTER, m.comment.lines().next().unwrap_or_default(), Tokens::ui(10.0), t.text_dim); } diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index fe19e376..e1059c56 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -394,6 +394,9 @@ fn tl_map_id() -> egui::Id { egui::Id::new("timeline-map") } +/// The column right of the time graph: the composition marker bin over the layers' scroll bar. +const GUTTER_W: f32 = 18.0; + /// The most a time ruler zooms in (pixels per second). const MAX_PPS: f64 = 4000.0; @@ -1128,7 +1131,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let oscroll = app.ui.timeline.outline_scroll; let cw = cols(rect.min.x - oscroll, left_w + overflow, vis); let graph_x0 = rect.min.x + left_w + 1.0; - let graph_x1 = rect.max.x - 10.0; + let graph_x1 = rect.max.x - GUTTER_W; ctx.data_mut(|d| d.insert_temp(egui::Id::new("timeline-graph-area"), (graph_x0, graph_x1 - graph_x0))); let fit_pps = fit_pps(graph_x1 - graph_x0, &comp); let pps = app.ui.timeline.pps.unwrap_or(fit_pps); @@ -1140,7 +1143,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let top = rect.min.y; let rows_top = top + header_h + colhdr_h; let rows_rect = Rect::from_min_max(pos2(rect.min.x, rows_top), pos2(rect.max.x, rect.max.y - footer_h)); - let graph_rect = Rect::from_min_max(pos2(graph_x0, top + header_h - ruler_h + colhdr_h), pos2(graph_x1 + 10.0, rect.max.y - footer_h)); + let graph_rect = Rect::from_min_max(pos2(graph_x0, top + header_h - ruler_h + colhdr_h), pos2(rect.max.x, rect.max.y - footer_h)); p.rect_filled(Rect::from_min_max(pos2(graph_x0, top), rect.max), 0.0, t.tl_bg); p.line_segment([pos2(graph_x0 - 1.0, top), pos2(graph_x0 - 1.0, rect.max.y)], Stroke::new(1.0, t.app_bg)); @@ -1208,6 +1211,8 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { // ---- ruler (right). let ruler = Rect::from_min_max(pos2(graph_x0, top + header_h - ruler_h), pos2(rect.max.x, top + header_h)); + // The composition marker bin, at the right end of the ruler above the layers' scroll bar. + let bin = Rect::from_min_max(pos2(graph_x1 + 2.0, ruler.max.y - 16.0), pos2(rect.max.x - 2.0, ruler.max.y - 2.0)); let nav = Rect::from_min_max(pos2(graph_x0 + 6.0, top + 6.0), pos2(graph_x1, top + 14.0)); // Time navigator: the visible span as a grey bar with blue end handles (as in After // Effects), over a dark track. @@ -1366,7 +1371,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let secs_per_label = label_frames as f64 * fd; let first = (tm.start / secs_per_label).floor() * secs_per_label; let mut s = first; - let pr = p.with_clip_rect(Rect::from_min_max(pos2(graph_x0, ruler.min.y), ruler.max)); + let pr = p.with_clip_rect(Rect::from_min_max(pos2(graph_x0, ruler.min.y), pos2(bin.min.x, ruler.max.y))); while s <= tm.t(graph_x1) + secs_per_label { let x = tm.x(s); pr.line_segment([pos2(x, ruler.max.y - 10.0), pos2(x, ruler.max.y)], Stroke::new(1.0, t.tl_ruler_tick)); @@ -1385,7 +1390,8 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { } // Scrub in the ruler: the current time jumps to the pointer as soon as the button goes down // (After Effects), then follows it. - let rresp = ui.interact(Rect::from_min_max(pos2(graph_x0, ruler.min.y + 12.0), ruler.max), egui::Id::new("tl-ruler"), Sense::click_and_drag()); + let rresp = + ui.interact(Rect::from_min_max(pos2(graph_x0, ruler.min.y + 12.0), pos2(bin.min.x, ruler.max.y)), egui::Id::new("tl-ruler"), Sense::click_and_drag()); if ((rresp.is_pointer_button_down_on() && ui.input(|i| i.pointer.primary_down())) || rresp.clicked()) && let Some(pt) = rresp.interact_pointer_pos() { @@ -2721,9 +2727,10 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { } // Composition markers in the ruler; protected regions shade the layer rows. - let mstrip = Rect::from_min_max(pos2(graph_x0, ruler.max.y - 12.0), pos2(graph_x1 + 10.0, ruler.max.y)); + let mstrip = Rect::from_min_max(pos2(graph_x0, ruler.max.y - 12.0), pos2(bin.min.x, ruler.max.y)); let mrows = rows_rect.intersect(Rect::from_min_max(pos2(graph_x0, rows_rect.min.y), rows_rect.max)); super::markers_ui::comp_markers(app, ui, &comp, tm, mstrip, mrows); + super::markers_ui::marker_bin(app, ui, &comp, tm, bin, mstrip); // Spacebar held: the Hand tool. Dragging the ruler or the time graph scrolls it in time when // zoomed in (registered last, so it takes the drag from the bars, keys and markers there). diff --git a/crates/ui-egui/tests/ui_timeline_zoom.rs b/crates/ui-egui/tests/ui_timeline_zoom.rs index 6a238a93..48b19cb8 100644 --- a/crates/ui-egui/tests/ui_timeline_zoom.rs +++ b/crates/ui-egui/tests/ui_timeline_zoom.rs @@ -1,6 +1,6 @@ -//! Zooming the Timeline's time ruler with real input: Alt+wheel zooms out until the whole comp -//! shows (#158); the Time Navigator's ends drag to zoom and `;` toggles frame level / the whole -//! comp (#159). +//! The Timeline's time ruler with real input: Alt+wheel zooms out until the whole comp shows +//! (#158); the Time Navigator's ends drag to zoom and `;` toggles frame level / the whole comp +//! (#159); markers dragged out of the composition marker bin (#275). use effectcraft_engine::Session; use effectcraft_ui_egui::EffectcraftApp; @@ -89,7 +89,8 @@ fn drag_with(h: &mut Harness<'_, EffectcraftApp>, from: Pos2, to: Pos2, modifier fn visible(h: &Harness<'_, EffectcraftApp>) -> (f64, f64) { let e = h.state().auto.find("timeline.ruler").unwrap().clone(); let (start, pps) = e.label.split_once(',').map(|(a, b)| (a.parse::().unwrap(), b.parse::().unwrap())).unwrap(); - (start, start + (e.rect[2] - 16.0) as f64 / pps) + // (The time graph ends 18 px before the ruler, at the marker bin; it maps time from 6 px in.) + (start, start + (e.rect[2] - 24.0) as f64 / pps) } /// #159: dragging the navigator's end handles changes that side of the visible span; pulling @@ -271,3 +272,42 @@ fn a_press_in_the_ruler_moves_the_current_time() { h.event(Event::PointerButton { pos: at + vec2(100.0, 0.0), button: egui::PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }); h.step(); } + +/// #275: a marker dragged out of the composition marker bin, at the right end of the time ruler, +/// adds a composition marker where it is dropped, as one undo step; dragged back onto the bin, +/// none. Numpad * (typed `*`) adds one at the current time. +#[test] +fn dragging_out_of_the_marker_bin_adds_a_comp_marker() { + let (mut h, _) = harness(); + h.state_mut().ui.timeline.pps = Some(200.0); + h.state_mut().ui.timeline.start = 10.0; + h.run_steps(2); + let markers = |h: &Harness<'_, EffectcraftApp>| -> Vec { h.state().session.active_comp().unwrap().markers.iter().map(|m| m.time.seconds()).collect() }; + let bin = rect(&h, "timeline.markerBin"); + let ruler = rect(&h, "timeline.ruler"); + assert!(ruler.contains_rect(bin) && bin.max.x > ruler.max.x - 20.0, "at the ruler's right end: {bin:?} in {ruler:?}"); + // Out to the ruler and back onto the bin: nothing. + let at = pos2(ruler.min.x + 6.0 + 600.0, ruler.center().y); + h.event(Event::PointerMoved(bin.center())); + h.step(); + h.event(Event::PointerButton { pos: bin.center(), button: egui::PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE }); + h.step(); + for p in [at, bin.center()] { + h.event(Event::PointerMoved(p)); + h.step(); + } + h.event(Event::PointerButton { pos: bin.center(), button: egui::PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE }); + h.run_steps(2); + assert!(markers(&h).is_empty(), "dropped on the bin: {:?}", markers(&h)); + // Dropped 3 s into the visible span (from 10 s at 200 px/s): a marker at 13 s. + let undo = h.state().session.history.undo.len(); + drag(&mut h, bin.center(), at); + assert_eq!(markers(&h), vec![13.0]); + let history = &h.state().session.history.undo; + assert_eq!((history.len(), history.last().map(|u| u.0.as_str())), (undo + 1, Some("Add Marker")), "one undo step"); + // Numpad * (it arrives as text): Add Marker at the current time. + h.state_mut().session.execute("time.set", json!({"time": 20.0})).unwrap(); + h.event(Event::Text("*".into())); + h.run_steps(2); + assert_eq!(markers(&h), vec![13.0, 20.0]); +} diff --git a/docs/parity.md b/docs/parity.md index 10cbf174..11c97bf8 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -171,7 +171,7 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.) | Web | 98% | 0.1 | No shared-memory threads inside one engine instance (the decided design is one engine instance per worker; a threaded build needs nightly `build-std`) (M13.30: Render Queue, analyses and Content-Aware Fill render on each job worker's own WebGPU device in passes, particles and Advanced 3D read back under keys, layer buffers in the browser's disk cache; browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2; viewer frames in frame workers fed by project diffs, Roto Brush propagation in a worker, non-blocking `wait: true` jobs and a browser Media Browser (File System Access folders, browser storage) in M13.10; WEB-1 in M13.24: GPU effects and the GPU compositor in the frame workers on their own WebGPU devices with deferred readbacks (frames render in passes; Backend Auto per comp), the disk cache in the Origin Private File System (written by the workers with sync access handles, LRU under the settings' limit, served after a reload), the storage manager (Settings ▸ Disk ▸ Browser Storage, `storage.info` / `storage.persist` / `storage.clear`), and a WGSL constant Chrome rejected (it disabled every GPU kernel in Chrome) fixed) | | 3D | 88% | 5.5 | reflections of other layers (Advanced 3D surfaces reflect only the Environment light; Material Options ▸ Appears in Reflections is stored but has no visible effect yet, #261), multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6; Advanced 3D end to end on the GPU (motion blur, iris depth of field, compositing) in M13.11 | | Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (280 run on the GPU since M13.29, EFF-5: M13.28 added the remaining pixel effects and the geometry and audio generators (Lightning, Advanced Lightning, Beam, Lens Flare, Radio Waves, Vegas, Stroke, Scribble, Write-on, Paint Bucket, Eyedropper Fill, CC Glue Gun, CC Threads, Audio Spectrum / Waveform, Basic / Path Text); M13.29 added the particle effects (CC Particle World / Systems II, Particle Playground, CC Ball Action, CC Pixel Polly, CC Scatterize, Curl Noise) and removed the fallbacks of Shatter's wireframe views, Foam's extras, Timewarp's Matte Layer, strong Warp Fisheye / Twist bends and custom `.ocio` configs; M13.23 added the 3D Channel and Immersive Video families, Apply Color LUT, the OCIO effects, Color Profile Converter and the simulations' render passes (Card Wipe included); M13.22 added the CC light family, the CC transitions, Block Dissolve, Radial Shadow, CC Bender / Blobbylize / Cylinder / Sphere / Spotlight / Environment, 3D Glasses, Numbers, Timecode and the time effects joined the 166 of M13.13; Fractal and the analysis / tool effects still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) | -| Interface | 75% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (G2 on 9 October: the in-window menu bar works from the keyboard (Alt, arrows, Enter, Escape) and the app's shortcuts wait while a menu is open, #279; G2 on 7 October: Project items, files and effects dropped on the Composition viewer, Project items dropped in the Timeline between layers and at a time, #85, #88, #89; popup menus moved to fit the window take clicks, so Render Queue templates apply, #117; View ▸ New Viewer landed in M13.32; an automation id on every control of Composition and Solid Settings, Group / Ungroup Shapes in the Layer menu, Window ▸ Learn, the flowchart shortcuts and Reveal in Explorer landed in M3.10 and M3.13; the Home ▸ Templates gallery (eight original built-in templates, user templates from File ▸ Save as Template…) and View ▸ Simulate Output ▸ My Custom RGB… landed in M13.25; Timeline layer reordering by drag, a non-snapping viewer pan, a working rename field and twirl arrows, and the full set of property reveal shortcuts — double presses, Alt+Shift keyframes, Ctrl+` — landed in M13.17–M13.20; the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; visual editors for Lumetri RGB / hue-saturation curves, Colorama's output cycle wheel, Glow's colour map and Reshape's correspondence points (viewer handles), a scrolling Preview panel and AE-style Composition / Timeline tabs (close, label swatch, viewer lock) landed in M13.10; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) | +| Interface | 75% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (G2 on 9 October: the in-window menu bar works from the keyboard (Alt, arrows, Enter, Escape) and the app's shortcuts wait while a menu is open, #279; the Timeline's composition marker bin and numpad * add comp markers, #275; G2 on 7 October: Project items, files and effects dropped on the Composition viewer, Project items dropped in the Timeline between layers and at a time, #85, #88, #89; popup menus moved to fit the window take clicks, so Render Queue templates apply, #117; View ▸ New Viewer landed in M13.32; an automation id on every control of Composition and Solid Settings, Group / Ungroup Shapes in the Layer menu, Window ▸ Learn, the flowchart shortcuts and Reveal in Explorer landed in M3.10 and M3.13; the Home ▸ Templates gallery (eight original built-in templates, user templates from File ▸ Save as Template…) and View ▸ Simulate Output ▸ My Custom RGB… landed in M13.25; Timeline layer reordering by drag, a non-snapping viewer pan, a working rename field and twirl arrows, and the full set of property reveal shortcuts — double presses, Alt+Shift keyframes, Ctrl+` — landed in M13.17–M13.20; the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; visual editors for Lumetri RGB / hue-saturation curves, Colorama's output cycle wheel, Glow's colour map and Reshape's correspondence points (viewer handles), a scrolling Preview panel and AE-style Composition / Timeline tabs (close, label swatch, viewer lock) landed in M13.10; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) | | Project | ≈ 68% | 5.5 | OCIO displays beyond the built-in tone map; relative footage paths and relinking when a project moves (unsaved-changes prompts, a modified mark that follows undo and project files that name their version and always reopen landed in M3.9 and M3.14; auto-save and crash recovery in M3.7; Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3) | | Masks & roto | 74% | 5.0 | (audit estimates; Roto Brush 2.0 / 3.0's trained model, MobileSAM in the swappable `segment` module, has since landed in M13.35; variable-width mask feather points with the Mask Feather tool landed in M13.5; mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) | | Preview | 77% | 2.8 | GPU effects for the audio-driven effects and the stepped particle systems; content-keyed RAM preview frames (an edit drops every comp's), audio scrubbing, more snapshot slots (G2 on 7 October: Audio, Lock and Shy switches keep the cached frames and playback with audio waits for frames instead of skipping them, #103; running out of video memory renders the frame on the CPU and keeps fewer GPU frames instead of crashing, #106; RAM preview keys with the render options, least-recently-shown eviction, Auto resolution while playing, Cache Before Playback that fits, an achieved-fps readout, Cache Frames When Idle and purges that clear what they name landed in M4.5–M4.8; 3D channel, VR, OCIO, Card Wipe and the simulations' render passes run on the GPU since M13.23; Advanced 3D layers with blend modes / track mattes / Preserve Transparency and environment backgrounds composite on the GPU since M13.22; pooled GPU textures and fused quantisation since M13.13: Lower Third GPU warm 21 → 4 ms/frame, and Auto now picks the CPU or the GPU per comp from measured frame times; Advanced 3D runs — raster, motion blur, iris depth of field, compositing — and wireframes run on the GPU since M13.11: CPU warm 3394 → GPU warm 290 ms/frame at 1920×1080 (11.7×; Half 968 → 85 ms), GPU ≠ CPU on 0.005 % of pixels, on an M4 Pro under load; Classic 3D bokeh depth of field — iris shapes, highlights, fringe, progressive blur on tilted planes — runs in WGSL since M13.6: 3D Showcase GPU warm 205 → 54 ms/frame at full size on an M4 Pro; the Preview panel's five shortcuts with their own Include / Loop / Cache Before Playback / Range / Play From / Frame Rate / Skip / Resolution / Full Screen / stop options and `playback.settings.get/set` landed in M13.8; Classic 3D runs and adjustment layers composite on the GPU since M12.7; persistent disk cache with the blue cache bar landed; region of interest, snapshots, exposure and Fast Previews landed in M0.13) |