mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
230 lines
8.7 KiB
Rust
230 lines
8.7 KiB
Rust
//! Composition menu.
|
|
|
|
use effectcraft_color::Label;
|
|
use effectcraft_project::{Comp, ItemId, ItemKind, Marker};
|
|
use effectcraft_time::{FrameRate, Tick};
|
|
use serde_json::{Value, json};
|
|
|
|
use super::{CommandSpec, always, b_p, bad, comp_id, f_p, has_comp, str_p, time_p};
|
|
use crate::{EngineError, Result, Session, cmd};
|
|
|
|
pub(crate) fn rate_p(p: &Value) -> Option<FrameRate> {
|
|
f_p(p, "frameRate").or(f_p(p, "fps")).map(FrameRate::from_f64)
|
|
}
|
|
|
|
fn apply_settings(c: &mut Comp, p: &Value) {
|
|
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);
|
|
}
|
|
if let Some(r) = rate_p(p) {
|
|
c.frame_rate = r;
|
|
}
|
|
if let Some(d) = f_p(p, "duration") {
|
|
let old = c.duration;
|
|
c.duration = Tick::from_seconds_f64(d.max(c.frame_rate.frame_duration().seconds()));
|
|
if c.work_area.1 == old {
|
|
c.work_area.1 = c.duration;
|
|
}
|
|
c.work_area.1 = c.work_area.1.min(c.duration);
|
|
}
|
|
if let Some(pa) = f_p(p, "pixelAspect") {
|
|
c.pixel_aspect = pa.max(0.1);
|
|
}
|
|
if let Some(Value::Array(bg)) = p.get("background") {
|
|
let g = |i: usize| bg.get(i).and_then(Value::as_f64).unwrap_or(0.0) as f32;
|
|
c.background = [g(0), g(1), g(2)];
|
|
} else if let Some(hex) = str_p(p, "background").and_then(effectcraft_color::Rgba::from_hex) {
|
|
c.background = [hex.r, hex.g, hex.b];
|
|
}
|
|
if let Some(a) = f_p(p, "shutterAngle") {
|
|
c.shutter_angle = a.clamp(0.0, 720.0);
|
|
}
|
|
if let Some(a) = f_p(p, "shutterPhase") {
|
|
c.shutter_phase = a.clamp(-360.0, 360.0);
|
|
}
|
|
if let Some(n) = p.get("motionBlurSamples").and_then(Value::as_u64) {
|
|
c.motion_blur_samples = (n as u32).clamp(2, 64);
|
|
}
|
|
}
|
|
|
|
fn new_comp(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let name = str_p(p, "name").unwrap_or("Comp 1").to_string();
|
|
let rate = rate_p(p).unwrap_or(FrameRate::FPS_29_97);
|
|
let mut c = Comp::new(1920, 1080, rate, Tick::from_seconds_f64(10.0));
|
|
apply_settings(&mut c, p);
|
|
let id = s.edit("New Composition", None, |proj, st| {
|
|
let mut name = name.clone();
|
|
let mut k = 2;
|
|
while proj.items.values().any(|i| i.name == name) {
|
|
name = format!("{} {k}", name.trim_end_matches(|ch: char| ch.is_ascii_digit()).trim_end());
|
|
k += 1;
|
|
}
|
|
let id = proj.add_item(&name, Label::Sandstone, None, ItemKind::Comp(c.into()));
|
|
st.project_selection = vec![id];
|
|
Ok(id)
|
|
})?;
|
|
if b_p(p, "open") != Some(false) {
|
|
s.open_comp(id);
|
|
}
|
|
Ok(json!({"comp": id.0}))
|
|
}
|
|
|
|
fn settings(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let cid = comp_id(s, p)?;
|
|
s.edit("Composition Settings", None, |proj, _| {
|
|
if let Some(n) = str_p(p, "name")
|
|
&& let Some(it) = proj.item_mut(cid)
|
|
{
|
|
it.name = n.to_string();
|
|
}
|
|
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
|
|
apply_settings(c, p);
|
|
Ok(())
|
|
})?;
|
|
let c = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
|
Ok(json!({"width": c.width, "height": c.height, "frameRate": c.frame_rate.as_f64(), "duration": c.duration.seconds()}))
|
|
}
|
|
|
|
fn open(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let id = match p.get("comp").or(p.get("item")) {
|
|
Some(Value::Number(n)) => ItemId(n.as_u64().unwrap_or(0)),
|
|
Some(Value::String(name)) => s.project.items.values().find(|i| &i.name == name && i.as_comp().is_some()).map(|i| i.id).ok_or(EngineError::NoComp)?,
|
|
_ => *s.state.project_selection.iter().find(|i| s.project.comp(**i).is_some()).ok_or_else(|| bad("comp.open", "missing `comp`"))?,
|
|
};
|
|
if s.project.comp(id).is_none() {
|
|
return Err(EngineError::NoComp);
|
|
}
|
|
s.open_comp(id);
|
|
Ok(json!({"comp": id.0}))
|
|
}
|
|
|
|
fn close(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let id = comp_id(s, p)?;
|
|
s.state.open_comps.retain(|c| *c != id);
|
|
if s.state.active_comp == Some(id) {
|
|
s.state.active_comp = s.state.open_comps.last().copied();
|
|
s.state.selected_layers.clear();
|
|
s.state.selected_keys.clear();
|
|
s.state.selected_props.clear();
|
|
}
|
|
Ok(Value::Null)
|
|
}
|
|
|
|
fn work_area(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let cid = comp_id(s, p)?;
|
|
let t = s.time();
|
|
let begin = f_p(p, "start").map(Tick::from_seconds_f64);
|
|
let end = f_p(p, "end").map(Tick::from_seconds_f64);
|
|
let which = str_p(p, "set");
|
|
s.edit("Work Area", super::merge_p(p), |proj, _| {
|
|
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
|
|
let fd = c.frame_duration();
|
|
match which {
|
|
Some("begin") => c.work_area.0 = t.min(c.work_area.1 - fd),
|
|
Some("end") => c.work_area.1 = (t + fd).max(c.work_area.0 + fd).min(c.duration),
|
|
_ => {}
|
|
}
|
|
if let Some(b) = begin {
|
|
c.work_area.0 = b.clamp(Tick::ZERO, c.duration - fd);
|
|
}
|
|
if let Some(e) = end {
|
|
c.work_area.1 = e.clamp(c.work_area.0 + fd, c.duration);
|
|
}
|
|
Ok(())
|
|
})?;
|
|
let c = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
|
Ok(json!({"start": c.work_area.0.seconds(), "end": c.work_area.1.seconds()}))
|
|
}
|
|
|
|
fn trim_to_wa(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let cid = comp_id(s, p)?;
|
|
s.edit("Trim Comp to Work Area", None, |proj, _| {
|
|
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
|
|
let (a, b) = c.work_area;
|
|
for l in &mut c.layers {
|
|
l.start_time -= a;
|
|
l.in_point = (l.in_point - a).max(Tick::ZERO);
|
|
l.out_point = (l.out_point - a).min(b - a);
|
|
}
|
|
c.duration = b - a;
|
|
c.work_area = (Tick::ZERO, b - a);
|
|
Ok(())
|
|
})?;
|
|
Ok(Value::Null)
|
|
}
|
|
|
|
fn comp_switch(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let cid = comp_id(s, p)?;
|
|
let name = str_p(p, "switch").ok_or_else(|| bad("comp.setSwitch", "missing `switch`"))?.to_string();
|
|
let v = b_p(p, "value");
|
|
let r = s.edit("Composition Switch", None, |proj, _| {
|
|
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
|
|
let slot = match name.as_str() {
|
|
"hideShy" | "shy" => &mut c.hide_shy,
|
|
"motionBlur" => &mut c.enable_motion_blur,
|
|
"frameBlending" => &mut c.enable_frame_blending,
|
|
"draft3d" | "draft3D" => &mut c.draft_3d,
|
|
_ => return Err(bad("comp.setSwitch", "switch: hideShy|motionBlur|frameBlending|draft3d")),
|
|
};
|
|
*slot = v.unwrap_or(!*slot);
|
|
Ok(*slot)
|
|
})?;
|
|
Ok(json!(r))
|
|
}
|
|
|
|
fn add_marker(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let cid = comp_id(s, p)?;
|
|
let t = time_p(s, p, s.project.comp(cid));
|
|
let comment = str_p(p, "comment").unwrap_or("").to_string();
|
|
s.edit("Add Marker", None, |proj, _| {
|
|
let c = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
|
|
c.markers.push(Marker { time: t, comment, ..Default::default() });
|
|
c.markers.sort_by_key(|m| m.time);
|
|
Ok(())
|
|
})?;
|
|
Ok(json!({"time": t.seconds()}))
|
|
}
|
|
|
|
fn poster(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let cid = comp_id(s, p)?;
|
|
let t = s.time();
|
|
s.edit("Set Poster Time", None, |proj, _| {
|
|
proj.comp_mut(cid).ok_or(EngineError::NoComp)?.poster_time = t;
|
|
Ok(())
|
|
})?;
|
|
Ok(Value::Null)
|
|
}
|
|
|
|
pub fn specs() -> Vec<CommandSpec> {
|
|
vec![
|
|
cmd!(
|
|
"comp.new",
|
|
"New Composition…",
|
|
["Composition"],
|
|
Some("Cmd+N"),
|
|
"{name?, width?, height?, frameRate?, duration? (s), background? [r,g,b]|#hex, open?}",
|
|
always,
|
|
new_comp
|
|
),
|
|
cmd!(
|
|
"comp.settings",
|
|
"Composition Settings…",
|
|
["Composition"],
|
|
Some("Cmd+K"),
|
|
"{comp?, name?, width?, height?, frameRate?, duration?, background?, shutterAngle?, shutterPhase?, motionBlurSamples?}",
|
|
has_comp,
|
|
settings
|
|
),
|
|
cmd!("comp.setPosterTime", "Set Poster Time", ["Composition"], None, "{}", has_comp, poster),
|
|
cmd!("comp.trimToWorkArea", "Trim Comp to Work Area", ["Composition"], Some("Cmd+Shift+X"), "{comp?}", has_comp, trim_to_wa),
|
|
cmd!("comp.open", "Open Composition", [], None, "{comp: id|name}", always, open),
|
|
cmd!("comp.close", "Close Composition", [], Some("Cmd+W"), "{comp?}", has_comp, close),
|
|
cmd!("comp.workArea", "Set Work Area", [], None, "{start?, end?, set?: begin|end (at CTI)}", has_comp, work_area),
|
|
cmd!("comp.setSwitch", "Composition Switch", [], None, "{switch: hideShy|motionBlur|frameBlending|draft3d, value?}", has_comp, comp_switch),
|
|
cmd!("comp.addMarker", "Add Marker", [], Some("Num*"), "{time?, comment?}", has_comp, add_marker),
|
|
]
|
|
}
|