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 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-01 21:57:08 -04:00
co-authored by Claude Opus 5.5
parent 7f43d537ed
commit 9d70623624
5 changed files with 445 additions and 102 deletions
+40 -2
View File
@@ -13,12 +13,50 @@ pub(crate) fn rate_p(p: &Value) -> Option<FrameRate> {
}
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<CommandSpec> {
"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<CommandSpec> {
"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
),
+24
View File
@@ -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);
}
+357
View File
@@ -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<String>,
pub dur_tc: Option<String>,
}
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<String>, 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<serde_json::Value> {
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));
}
}
+23 -100
View File
@@ -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 => {}
}
}
+1
View File
@@ -1,5 +1,6 @@
//! Panel bodies.
pub mod comp_settings;
pub mod dialogs;
pub mod effect_controls;
pub mod effects_presets;