From 9d7062362492a4534c29266e52aaa7143a59f894 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 1 Oct 2026 21:57:08 -0400 Subject: [PATCH] M3.1: AE Composition Settings dialog (Basic/Advanced/3D Renderer, AE presets, pixel aspect, start timecode, resize anchor) engine comp.settings/comp.new: anchor (shifts unparented layers and their position keys on resize), startTime/startTimecode, renderer. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/commands/comp.rs | 42 ++- crates/engine/src/tests.rs | 24 ++ crates/ui-egui/src/panels/comp_settings.rs | 357 +++++++++++++++++++++ crates/ui-egui/src/panels/dialogs.rs | 123 ++----- crates/ui-egui/src/panels/mod.rs | 1 + 5 files changed, 445 insertions(+), 102 deletions(-) create mode 100644 crates/ui-egui/src/panels/comp_settings.rs diff --git a/crates/engine/src/commands/comp.rs b/crates/engine/src/commands/comp.rs index bbccd5d64..8b30025b3 100644 --- a/crates/engine/src/commands/comp.rs +++ b/crates/engine/src/commands/comp.rs @@ -13,12 +13,50 @@ pub(crate) fn rate_p(p: &Value) -> Option { } fn apply_settings(c: &mut Comp, p: &Value) { + let (ow, oh) = (c.width, c.height); if let Some(w) = p.get("width").and_then(Value::as_u64) { c.width = (w as u32).clamp(4, 30000); } if let Some(h) = p.get("height").and_then(Value::as_u64) { c.height = (h as u32).clamp(4, 30000); } + // Advanced › Anchor: where the old frame sits in the resized one (0 = top left … 4 = center … + // 8 = bottom right). Unparented layers (and all their position keys) move by that offset. + if (c.width, c.height) != (ow, oh) { + let a = p.get("anchor").and_then(Value::as_u64).unwrap_or(4).min(8); + let dx = (c.width as f64 - ow as f64) * (a % 3) as f64 / 2.0; + let dy = (c.height as f64 - oh as f64) * (a / 3) as f64 / 2.0; + if dx != 0.0 || dy != 0.0 { + for l in c.layers.iter_mut().filter(|l| l.parent.is_none()) { + let Some(pr) = l.props.prop_mut("transform/position") else { continue }; + let shift = |v: &mut effectcraft_keyframe::Value| { + if let effectcraft_keyframe::Value::Vec2(x) = v { + x[0] += dx; + x[1] += dy; + } else if let effectcraft_keyframe::Value::Vec3(x) = v { + x[0] += dx; + x[1] += dy; + } + }; + shift(&mut pr.value); + for k in &mut pr.keys { + shift(&mut k.value); + } + } + } + } + if let Some(t) = f_p(p, "startTime") { + c.display_start = Tick::from_seconds_f64(t); + } else if let Some(tc) = str_p(p, "startTimecode") + && let Ok(f) = effectcraft_time::parse_timecode(tc, c.frame_rate, c.frame_rate.supports_drop_frame(), 0) + { + c.display_start = c.frame_rate.tick_of(f); + } + match str_p(p, "renderer").map(str::to_ascii_lowercase).as_deref() { + Some("classic3d" | "classic") => c.renderer = effectcraft_project::Renderer::Classic3D, + Some("advanced3d" | "advanced") => c.renderer = effectcraft_project::Renderer::Advanced3D, + _ => {} + } if let Some(r) = rate_p(p) { c.frame_rate = r; } @@ -205,7 +243,7 @@ pub fn specs() -> Vec { "New Composition…", ["Composition"], Some("Cmd+N"), - "{name?, width?, height?, frameRate?, duration? (s), background? [r,g,b]|#hex, pixelAspect?, open?}", + "{name?, width?, height?, frameRate?, duration? (s), startTime? (s) | startTimecode?, background? [r,g,b]|#hex, pixelAspect?, shutterAngle?, shutterPhase?, motionBlurSamples?, renderer? classic3D|advanced3D, anchor?, open?}", always, new_comp ), @@ -214,7 +252,7 @@ pub fn specs() -> Vec { "Composition Settings…", ["Composition"], Some("Cmd+K"), - "{comp?, name?, width?, height?, frameRate?, duration?, background?, shutterAngle?, shutterPhase?, motionBlurSamples?, pixelAspect?}", + "{comp?, name?, width?, height?, anchor? 0-8 (resize anchor, 4 = center), frameRate?, duration?, startTime? (s) | startTimecode?, background?, shutterAngle?, shutterPhase?, motionBlurSamples?, pixelAspect?, renderer? classic3D|advanced3D}", has_comp, settings ), diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 2fdff890a..f4d9adc21 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -242,3 +242,27 @@ fn layer_tree_paths_resolve() { } assert!(checked > 20, "{checked}"); } + +#[test] +fn comp_settings_anchor_start_timecode_renderer() { + let mut s = Session::default(); + s.execute("comp.new", json!({"name": "A", "width": 100, "height": 100, "duration": 2.0})).unwrap(); + let l = s.execute("layer.newSolid", json!({"color": "#ff0000"})).unwrap()["layer"].as_u64().unwrap(); + s.execute("prop.addKey", json!({"layer": l, "path": "transform/position", "time": 0.0, "value": [10, 20]})).unwrap(); + let pos = |s: &Session| { + let pr = s.active_comp().unwrap().layer(crate::project::LayerId(l)).unwrap().props.prop("transform/position").unwrap().clone(); + pr.keys[0].value.components() + }; + // Center anchor: grow by 100x50 → shift by half. + s.execute("comp.settings", json!({"width": 200, "height": 150})).unwrap(); + assert_eq!(pos(&s)[..2], [60.0, 45.0]); + // Top-left anchor: no shift. Bottom-right: full shift. + s.execute("comp.settings", json!({"width": 300, "anchor": 0})).unwrap(); + assert_eq!(pos(&s)[..2], [60.0, 45.0]); + s.execute("comp.settings", json!({"width": 200, "height": 100, "anchor": 8})).unwrap(); + assert_eq!(pos(&s)[..2], [-40.0, -5.0]); + s.execute("comp.settings", json!({"startTimecode": "0:00:01:00", "renderer": "advanced3D"})).unwrap(); + let c = s.active_comp().unwrap(); + assert_eq!(c.display_start, c.frame_rate.tick_of(30)); + assert_eq!(c.renderer, crate::project::Renderer::Advanced3D); +} diff --git a/crates/ui-egui/src/panels/comp_settings.rs b/crates/ui-egui/src/panels/comp_settings.rs new file mode 100644 index 000000000..d46b36124 --- /dev/null +++ b/crates/ui-egui/src/panels/comp_settings.rs @@ -0,0 +1,357 @@ +//! Composition Settings dialog (also New Composition), laid out like After Effects': name, then +//! Basic / Advanced / 3D Renderer tabs, Preview checkbox, Cancel / OK. + +use effectcraft_engine::time::{FrameRate, format_timecode_frames, parse_timecode}; +use egui::{Color32, vec2}; +use serde_json::json; + +use crate::theme::Tokens; + +/// AE's composition presets: (name, width, height, pixel aspect, fps). "Custom" is implicit. +pub const PRESETS: &[(&str, u32, u32, f64, f64)] = &[ + ("Social Media Portrait HD · 1080x1920 · 30 fps", 1080, 1920, 1.0, 30.0), + ("Social Media Landscape HD · 1920x1080 · 30 fps", 1920, 1080, 1.0, 30.0), + ("Social Media Square · 1080x1080 · 30 fps", 1080, 1080, 1.0, 30.0), + ("Social Media Portrait · 1080x1350 · 30 fps", 1080, 1350, 1.0, 30.0), + ("NTSC DV", 720, 480, 0.9091, 29.97), + ("NTSC DV Widescreen", 720, 480, 1.2121, 29.97), + ("NTSC D1", 720, 486, 0.9091, 29.97), + ("NTSC D1 Widescreen", 720, 486, 1.2121, 29.97), + ("NTSC D1 Square Pix", 720, 540, 1.0, 29.97), + ("NTSC D1 Widescreen Square Pix", 872, 486, 1.0, 29.97), + ("PAL D1/DV", 720, 576, 1.0940, 25.0), + ("PAL D1/DV Widescreen", 720, 576, 1.4587, 25.0), + ("PAL D1/DV Square Pix", 788, 576, 1.0, 25.0), + ("PAL D1/DV Widescreen Square Pix", 1050, 576, 1.0, 25.0), + ("HDV/HDTV 720 29.97", 1280, 720, 1.0, 29.97), + ("HDV/HDTV 720 25", 1280, 720, 1.0, 25.0), + ("DVCPRO HD 720 23.976", 960, 720, 1.3333, 23.976), + ("DVCPRO HD 720 25", 960, 720, 1.3333, 25.0), + ("DVCPRO HD 720 29.97", 960, 720, 1.3333, 29.97), + ("HDTV 1080 24", 1920, 1080, 1.0, 24.0), + ("HDTV 1080 25", 1920, 1080, 1.0, 25.0), + ("HDTV 1080 29.97", 1920, 1080, 1.0, 29.97), + ("HDV 1080 25", 1440, 1080, 1.3333, 25.0), + ("HDV 1080 29.97", 1440, 1080, 1.3333, 29.97), + ("DVCPRO HD 1080 25", 1440, 1080, 1.3333, 25.0), + ("DVCPRO HD 1080 29.97", 1280, 1080, 1.5, 29.97), + ("Cineon Half", 1828, 1332, 1.0, 24.0), + ("Cineon Full", 3656, 2664, 1.0, 24.0), + ("Film (2K)", 2048, 1556, 1.0, 24.0), + ("Film (4K)", 4096, 3112, 1.0, 24.0), + ("UHD 4K 23.976", 3840, 2160, 1.0, 23.976), + ("UHD 4K 25", 3840, 2160, 1.0, 25.0), + ("UHD 4K 29.97", 3840, 2160, 1.0, 29.97), + ("UHD 8K 23.976", 7680, 4320, 1.0, 23.976), + ("UHD 8K 25", 7680, 4320, 1.0, 25.0), + ("UHD 8K 29.97", 7680, 4320, 1.0, 29.97), +]; + +/// AE's Pixel Aspect Ratio choices. +pub const PIXEL_ASPECTS: &[(&str, f64)] = &[ + ("Square Pixels", 1.0), + ("D1/DV NTSC (0.91)", 0.9091), + ("D1/DV NTSC Widescreen (1.21)", 1.2121), + ("D1/DV PAL (1.09)", 1.0940), + ("D1/DV PAL Widescreen (1.46)", 1.4587), + ("Anamorphic 2:1 (2)", 2.0), + ("HDV 1080/DVCPRO HD 720 (1.33)", 1.3333), + ("DVCPRO HD 1080 (1.5)", 1.5), +]; + +const FRAME_RATES: &[f64] = &[8.0, 10.0, 12.0, 12.5, 15.0, 23.976, 24.0, 25.0, 29.97, 30.0, 48.0, 50.0, 59.94, 60.0, 120.0]; + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Tab { + #[default] + Basic, + Advanced, + Renderer, +} + +/// The dialog's working copy. +#[derive(Clone, Debug)] +pub struct CompDraft { + pub name: String, + pub width: u32, + pub height: u32, + pub pixel_aspect: f64, + pub fps: f64, + pub duration: f64, + pub start: f64, + pub bg: [f32; 3], + pub lock_aspect: bool, + pub anchor: u8, + pub shutter_angle: f64, + pub shutter_phase: f64, + pub samples: u32, + pub advanced_3d: bool, + pub tab: Tab, + /// Timecode text being edited (start, duration). + pub start_tc: Option, + pub dur_tc: Option, +} + +impl Default for CompDraft { + fn default() -> Self { + CompDraft { + name: "Comp 1".into(), + width: 1920, + height: 1080, + pixel_aspect: 1.0, + fps: 29.97, + duration: 10.0, + start: 0.0, + bg: [0.0, 0.0, 0.0], + lock_aspect: true, + anchor: 4, + shutter_angle: 180.0, + shutter_phase: -90.0, + samples: 16, + advanced_3d: false, + tab: Tab::Basic, + start_tc: None, + dur_tc: None, + } + } +} + +/// `16:9 (1.78)`: the nearest small whole-number ratio and its decimal. +pub fn aspect_label(w: f64, h: f64) -> String { + let r = w / h.max(1.0); + let best = (1..=32u32).map(|b| ((r * b as f64).round() as u32, b)).filter(|(a, _)| *a > 0).find(|(a, b)| (*a as f64 / *b as f64 - r).abs() < 0.006); + match best { + Some((a, b)) => format!("{a}:{b} ({r:.2})"), + None => format!("{r:.2}"), + } +} + +/// AE's name for a background color when it is one of the obvious ones. +fn color_name(c: [f32; 3]) -> &'static str { + match c.map(|v| (v * 255.0).round() as u8) { + [0, 0, 0] => "Black", + [255, 255, 255] => "White", + _ => "", + } +} + +/// AE prints the hour without a leading zero: `0;00;10;00`. +fn ae_timecode(smpte: &str) -> String { + let (neg, rest) = smpte.strip_prefix('-').map_or((false, smpte), |r| (true, r)); + let rest = rest.strip_prefix('0').filter(|r| r.as_bytes().first().is_some_and(u8::is_ascii_digit)).unwrap_or(rest); + format!("{}{rest}", if neg { "-" } else { "" }) +} + +fn timecode_field(ui: &mut egui::Ui, buf: &mut Option, secs: &mut f64, rate: FrameRate, min_frames: i64) { + let df = rate.supports_drop_frame(); + // Durations are whole frames in AE: 10 s at 29.97 fps is 0;00;10;00, not 9;29. + let shown = ae_timecode(&format_timecode_frames((*secs * rate.as_f64()).round() as i64, rate, df)); + let mut text = buf.clone().unwrap_or_else(|| shown.clone()); + let r = ui.add(egui::TextEdit::singleline(&mut text).desired_width(96.0).font(Tokens::mono(12.0))); + if r.changed() { + *buf = Some(text.clone()); + } + if r.lost_focus() { + if let Ok(f) = parse_timecode(&text, rate, df, 0) { + *secs = rate.tick_of(f.max(min_frames)).seconds(); + } + *buf = None; + } + let base = format!("Base {}{}", rate.timecode_base(), if df { "drop" } else { "" }); + ui.label(egui::RichText::new(format!("is {shown} {base}")).color(Color32::GRAY)); +} + +/// Draw the dialog. Returns `Some(params)` when OK is pressed, `Some(Null)` on Cancel. +pub fn show(ui: &mut egui::Ui, d: &mut CompDraft, t: &Tokens) -> Option { + let mut out = None; + ui.horizontal(|ui| { + ui.label("Composition Name:"); + ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(300.0)); + }); + ui.add_space(8.0); + ui.horizontal(|ui| { + for (tab, label) in [(Tab::Basic, "Basic"), (Tab::Advanced, "Advanced"), (Tab::Renderer, "3D Renderer")] { + if ui.selectable_label(d.tab == tab, label).clicked() { + d.tab = tab; + } + } + }); + ui.separator(); + let rate = FrameRate::from_f64(d.fps); + match d.tab { + Tab::Basic => { + egui::Grid::new("comp-basic").num_columns(2).spacing([12.0, 9.0]).show(ui, |ui| { + ui.label("Preset:"); + let current = PRESETS + .iter() + .find(|(_, w, h, par, f)| *w == d.width && *h == d.height && (par - d.pixel_aspect).abs() < 1e-3 && (f - d.fps).abs() < 1e-3) + .map(|p| p.0) + .unwrap_or("Custom"); + egui::ComboBox::from_id_salt("comp-preset").width(300.0).selected_text(current).show_ui(ui, |ui| { + for (name, w, h, par, f) in PRESETS { + if ui.selectable_label(*name == current, *name).clicked() { + (d.width, d.height, d.pixel_aspect, d.fps) = (*w, *h, *par, *f); + } + } + }); + ui.end_row(); + ui.label("Width:"); + ui.horizontal(|ui| { + let (ow, oh) = (d.width, d.height); + ui.add(egui::DragValue::new(&mut d.width).range(4..=30000).suffix(" px")); + let lbl = format!("Lock Aspect Ratio to {}", aspect_label(ow as f64, oh as f64)); + ui.checkbox(&mut d.lock_aspect, lbl); + if d.lock_aspect && ow != d.width && ow > 0 { + d.height = ((d.width as f64) * oh as f64 / ow as f64).round().max(4.0) as u32; + } + }); + ui.end_row(); + ui.label("Height:"); + let (ow, oh) = (d.width, d.height); + ui.add(egui::DragValue::new(&mut d.height).range(4..=30000).suffix(" px")); + if d.lock_aspect && oh != d.height && oh > 0 { + d.width = ((d.height as f64) * ow as f64 / oh as f64).round().max(4.0) as u32; + } + ui.end_row(); + ui.label("Pixel Aspect Ratio:"); + ui.horizontal(|ui| { + let cur = PIXEL_ASPECTS + .iter() + .find(|(_, v)| (v - d.pixel_aspect).abs() < 1e-3) + .map(|p| p.0.to_string()) + .unwrap_or_else(|| format!("{:.2}", d.pixel_aspect)); + egui::ComboBox::from_id_salt("comp-par").width(220.0).selected_text(cur).show_ui(ui, |ui| { + for (name, v) in PIXEL_ASPECTS { + if ui.selectable_label((v - d.pixel_aspect).abs() < 1e-3, *name).clicked() { + d.pixel_aspect = *v; + } + } + }); + ui.label( + egui::RichText::new(format!("Frame Aspect Ratio: {}", aspect_label(d.width as f64 * d.pixel_aspect, d.height as f64))) + .color(Color32::GRAY), + ); + }); + ui.end_row(); + ui.label("Frame Rate:"); + ui.horizontal(|ui| { + let txt = format!("{:.3}", d.fps).trim_end_matches('0').trim_end_matches('.').to_string(); + egui::ComboBox::from_id_salt("comp-fps").width(80.0).selected_text(txt).show_ui(ui, |ui| { + for f in FRAME_RATES { + if ui.selectable_label((d.fps - f).abs() < 1e-3, format!("{f}")).clicked() { + d.fps = *f; + } + } + }); + ui.label("frames per second"); + if rate.supports_drop_frame() { + ui.label(egui::RichText::new("Drop Frame").color(Color32::GRAY)); + } + }); + ui.end_row(); + ui.label(""); + let mb = d.width as f64 * d.height as f64 * 4.0 / (1024.0 * 1024.0); + ui.label(egui::RichText::new(format!("{} x {}, {mb:.1}MB per 8bpc frame", d.width, d.height)).color(Color32::GRAY)); + ui.end_row(); + ui.label("Start Timecode:"); + ui.horizontal(|ui| timecode_field(ui, &mut d.start_tc, &mut d.start, rate, i64::MIN / 4)); + ui.end_row(); + ui.label("Duration:"); + ui.horizontal(|ui| timecode_field(ui, &mut d.dur_tc, &mut d.duration, rate, 1)); + ui.end_row(); + ui.label("Background Color:"); + ui.horizontal(|ui| { + ui.color_edit_button_rgb(&mut d.bg); + ui.label(color_name(d.bg)); + }); + ui.end_row(); + }); + } + Tab::Advanced => { + egui::Grid::new("comp-adv").num_columns(2).spacing([12.0, 9.0]).show(ui, |ui| { + ui.label("Anchor:"); + egui::Grid::new("comp-anchor").spacing([2.0, 2.0]).show(ui, |ui| { + for i in 0..9u8 { + let on = d.anchor == i; + let b = egui::Button::new(if on { "●" } else { "" }).min_size(vec2(22.0, 22.0)).fill(if on { t.accent } else { t.field_bg }); + if ui.add(b).clicked() { + d.anchor = i; + } + if i % 3 == 2 { + ui.end_row(); + } + } + }); + ui.end_row(); + ui.label(egui::RichText::new("Motion Blur").strong()); + ui.end_row(); + ui.label("Shutter Angle:"); + ui.add(egui::DragValue::new(&mut d.shutter_angle).range(0.0..=720.0).suffix("°")); + ui.end_row(); + ui.label("Shutter Phase:"); + ui.add(egui::DragValue::new(&mut d.shutter_phase).range(-360.0..=360.0).suffix("°")); + ui.end_row(); + ui.label("Samples Per Frame:"); + ui.add(egui::DragValue::new(&mut d.samples).range(2..=64)); + ui.end_row(); + }); + } + Tab::Renderer => { + ui.horizontal(|ui| { + ui.label("Renderer:"); + egui::ComboBox::from_id_salt("comp-renderer").selected_text(if d.advanced_3d { "Advanced 3D" } else { "Classic 3D" }).show_ui(ui, |ui| { + ui.selectable_value(&mut d.advanced_3d, false, "Classic 3D"); + ui.selectable_value(&mut d.advanced_3d, true, "Advanced 3D"); + }); + }); + } + } + ui.add_space(14.0); + ui.horizontal(|ui| { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent)).clicked() { + out = Some(params(d)); + } + if ui.button("Cancel").clicked() { + out = Some(serde_json::Value::Null); + } + }); + }); + out +} + +/// `comp.new` / `comp.settings` parameters for the draft. +pub fn params(d: &CompDraft) -> serde_json::Value { + json!({ + "name": d.name, "width": d.width, "height": d.height, "pixelAspect": d.pixel_aspect, "frameRate": d.fps, + "duration": d.duration, "startTime": d.start, "background": [d.bg[0], d.bg[1], d.bg[2]], "anchor": d.anchor, + "shutterAngle": d.shutter_angle, "shutterPhase": d.shutter_phase, "motionBlurSamples": d.samples, + "renderer": if d.advanced_3d { "advanced3D" } else { "classic3D" }, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn aspect_labels_match_ae() { + assert_eq!(aspect_label(1920.0, 1080.0), "16:9 (1.78)"); + assert_eq!(aspect_label(1080.0, 1080.0), "1:1 (1.00)"); + assert_eq!(aspect_label(1080.0, 1350.0), "4:5 (0.80)"); + assert_eq!(aspect_label(720.0 * 0.9091, 480.0), "15:11 (1.36)"); + } + + #[test] + fn ae_timecode_drops_hour_padding() { + assert_eq!(ae_timecode("00;00;10;00"), "0;00;10;00"); + assert_eq!(ae_timecode("12:00:00:00"), "12:00:00:00"); + assert_eq!(ae_timecode("-00:00:01:00"), "-0:00:01:00"); + } + + #[test] + fn presets_resolve_by_value() { + let p = PRESETS.iter().find(|p| p.0 == "HDTV 1080 29.97").unwrap(); + assert_eq!((p.1, p.2), (1920, 1080)); + } +} diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index 87a2a2cfd..a78a52a6c 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -8,22 +8,7 @@ use crate::icons::{self, Icon}; use crate::theme::Tokens; use crate::{Dialog, EffectcraftApp, widgets}; -#[derive(Clone, Debug)] -pub struct CompDraft { - pub name: String, - pub width: u32, - pub height: u32, - pub fps: f64, - pub duration: f64, - pub bg: [f32; 3], - pub lock_aspect: bool, -} - -impl Default for CompDraft { - fn default() -> Self { - CompDraft { name: "Comp 1".into(), width: 1920, height: 1080, fps: 29.97, duration: 10.0, bg: [0.0, 0.0, 0.0], lock_aspect: true } - } -} +pub use super::comp_settings::CompDraft; #[derive(Clone, Debug, Default)] pub struct DialogState { @@ -50,10 +35,16 @@ pub fn open_comp_settings(app: &mut EffectcraftApp) -> Result<(), String> { name: app.session.project.item(cid).map(|i| i.name.clone()).unwrap_or_default(), width: c.width, height: c.height, + pixel_aspect: c.pixel_aspect, fps: c.frame_rate.as_f64(), duration: c.duration.seconds(), + start: c.display_start.seconds(), bg: c.background, - lock_aspect: true, + shutter_angle: c.shutter_angle, + shutter_phase: c.shutter_phase, + samples: c.motion_blur_samples, + advanced_3d: c.renderer == effectcraft_engine::project::Renderer::Advanced3D, + ..Default::default() }; app.dialog_state.editing_existing = true; app.dialog = Some(Dialog::CompSettings); @@ -172,93 +163,25 @@ fn about(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { } fn comp_settings(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens, existing: bool) { - let mut close = false; - let mut ok = false; let mut d = app.dialog_state.comp.clone(); - modal(ctx, if existing { "Composition Settings" } else { "Composition Settings — New" }, vec2(520.0, 420.0), t, |ui| { - egui::Grid::new("comp-grid").num_columns(2).spacing([14.0, 10.0]).show(ui, |ui| { - ui.label("Composition Name"); - ui.add(egui::TextEdit::singleline(&mut d.name).desired_width(260.0)); - ui.end_row(); - ui.label("Preset"); - egui::ComboBox::from_id_salt("comp-preset").selected_text(format!("{}x{} {:.2}", d.width, d.height, d.fps)).show_ui(ui, |ui| { - for (label, w, h, f) in [ - ("HD · 1920x1080 · 29.97 fps", 1920, 1080, 29.97), - ("HD · 1920x1080 · 25 fps", 1920, 1080, 25.0), - ("HD · 1920x1080 · 23.976 fps", 1920, 1080, 23.976), - ("HD · 1280x720 · 29.97 fps", 1280, 720, 29.97), - ("UHD 4K · 3840x2160 · 29.97 fps", 3840, 2160, 29.97), - ("Social 9:16 · 1080x1920 · 30 fps", 1080, 1920, 30.0), - ("Social 1:1 · 1080x1080 · 30 fps", 1080, 1080, 30.0), - ("Social 4:5 · 1080x1350 · 30 fps", 1080, 1350, 30.0), - ("Cinema 4K · 4096x2160 · 24 fps", 4096, 2160, 24.0), - ] { - if ui.selectable_label(false, label).clicked() { - d.width = w; - d.height = h; - d.fps = f; - } - } - }); - ui.end_row(); - ui.label("Width / Height"); - ui.horizontal(|ui| { - let ow = d.width; - ui.add(egui::DragValue::new(&mut d.width).range(4..=30000).suffix(" px")); - ui.label("×"); - let oh = d.height; - ui.add(egui::DragValue::new(&mut d.height).range(4..=30000).suffix(" px")); - ui.checkbox(&mut d.lock_aspect, "Lock aspect"); - if d.lock_aspect && ow != d.width && ow > 0 { - d.height = ((d.width as f64) * oh as f64 / ow as f64).round().max(4.0) as u32; - } - }); - ui.end_row(); - ui.label("Frame Rate"); - egui::ComboBox::from_id_salt("comp-fps").selected_text(format!("{:.3}", d.fps).trim_end_matches('0').trim_end_matches('.').to_string()).show_ui( - ui, - |ui| { - for f in [23.976, 24.0, 25.0, 29.97, 30.0, 50.0, 59.94, 60.0, 120.0] { - if ui.selectable_label((d.fps - f).abs() < 1e-3, format!("{f}")).clicked() { - d.fps = f; - } - } - }, - ); - ui.end_row(); - ui.label("Duration"); - ui.add(egui::DragValue::new(&mut d.duration).range(0.04..=86400.0).speed(0.1).suffix(" s")); - ui.end_row(); - ui.label("Background Color"); - ui.color_edit_button_rgb(&mut d.bg); - ui.end_row(); - }); - ui.add_space(18.0); - ui.horizontal(|ui| { - ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui.add(egui::Button::new(egui::RichText::new(" OK ").color(Color32::WHITE)).fill(t.accent)).clicked() { - ok = true; - } - if ui.button("Cancel").clicked() { - close = true; - } - }); - }); + let mut result = None; + modal(ctx, "Composition Settings", vec2(600.0, 470.0), t, |ui| { + result = super::comp_settings::show(ui, &mut d, t); }); - if ui_enter(ctx) { - ok = true; + if result.is_none() && ui_enter(ctx) { + result = Some(super::comp_settings::params(&d)); } - app.dialog_state.comp = d.clone(); - if ok { - let params = json!({"name": d.name, "width": d.width, "height": d.height, "frameRate": d.fps, "duration": d.duration, "background": [d.bg[0], d.bg[1], d.bg[2]]}); - let r = if existing { app.session.execute("comp.settings", params) } else { app.session.execute("comp.new", params) }; - if let Err(e) = r { - app.ui.status = e.to_string(); + app.dialog_state.comp = d; + match result { + Some(serde_json::Value::Null) => app.dialog = None, + Some(params) => { + let r = if existing { app.session.execute("comp.settings", params) } else { app.session.execute("comp.new", params) }; + if let Err(e) = r { + app.ui.status = e.to_string(); + } + app.dialog = None; } - close = true; - } - if close { - app.dialog = None; + None => {} } } diff --git a/crates/ui-egui/src/panels/mod.rs b/crates/ui-egui/src/panels/mod.rs index 2d5924dd0..f075f0aa7 100644 --- a/crates/ui-egui/src/panels/mod.rs +++ b/crates/ui-egui/src/panels/mod.rs @@ -1,5 +1,6 @@ //! Panel bodies. +pub mod comp_settings; pub mod dialogs; pub mod effect_controls; pub mod effects_presets;