effectcraft/crates/engine/src/commands/layer_time.rs

189 lines
8.4 KiB
Rust

//! Layer ▸ Time: Time Stretch, Time-Reverse Layer, Enable Time Remapping, Freeze Frame and
//! Freeze on Last Frame.
//!
//! Time Remap is a `timeRemap` property at the top of the layer's tree whose value is the
//! source time (seconds) to show; its keys live in layer time like every other property.
use effectcraft_keyframe::{Keyframe, Value as KV};
use effectcraft_project::build::Ids;
use effectcraft_project::{Layer, LayerSource, Node, ParamUi, Property};
use effectcraft_time::Tick;
use serde_json::{Value, json};
use super::{CommandSpec, b_p, bad, f_p, has_layers, layers_p, str_p};
use crate::{EngineError, Result, Session, cmd};
/// Layers whose source has its own time (precomps and footage) can be remapped.
fn remappable(l: &Layer) -> bool {
matches!(l.source, LayerSource::Comp { .. } | LayerSource::Footage { .. })
}
fn time_stretch(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, ids) = layers_p(s, p)?;
let reverse = str_p(p, "op") == Some("reverse");
let pct = f_p(p, "percent");
let dur = f_p(p, "duration");
let hold = str_p(p, "hold").unwrap_or("in").to_string();
let cti = s.time();
let comp_rate = s.project.comp(cid).ok_or(EngineError::NoComp)?.frame_rate;
let fd = comp_rate.frame_duration();
s.edit(if reverse { "Time-Reverse Layer" } else { "Time Stretch" }, None, |proj, _| {
let comp = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
for l in comp.layers.iter_mut().filter(|l| ids.contains(&l.id)) {
let old = l.stretch;
if reverse {
// Keep the in/out points; play the visible span backwards.
let last = l.layer_time(l.out_point - fd);
l.stretch = -old;
l.start_time = l.in_point - Tick((last.0 as f64 * l.stretch / 100.0).round() as i64);
continue;
}
let span = (l.out_point - l.in_point).seconds();
let new = match (pct, dur) {
(Some(v), _) => v,
(None, Some(d)) if span > 0.0 => old * d / span,
_ => old,
};
if new.abs() < 1.0 {
return Err(bad("layer.timeStretch", "the stretch factor must be at least 1 %"));
}
// The hold point keeps showing the same layer time.
let h = match hold.as_str() {
"current" => cti,
"out" => l.out_point,
_ => l.in_point,
};
let lt_h = l.layer_time(h);
let (lin, lout) = (l.layer_time(l.in_point), l.layer_time(l.out_point));
l.stretch = new;
l.start_time = h - Tick((lt_h.0 as f64 * new / 100.0).round() as i64);
let (a, b) = (l.comp_time(lin), l.comp_time(lout));
let fr = comp_rate;
l.in_point = fr.snap_nearest(a.min(b));
l.out_point = fr.snap_nearest(a.max(b)).max(l.in_point + fr.frame_duration());
}
Ok(())
})?;
Ok(Value::Null)
}
fn remap_prop(ids: &mut Ids) -> Property {
let mut p = ids.prop("timeRemap", "Time Remap", KV::Scalar(0.0)).with_ui(ParamUi::Number);
p.ui = ParamUi::Number;
p
}
/// Source time (seconds) a layer shows at comp time `t` without remapping.
fn source_secs(l: &Layer, t: Tick) -> f64 {
l.layer_time(t).seconds()
}
fn enable_time_remap(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, ids) = layers_p(s, p)?;
let want = b_p(p, "value");
let fd = s.project.comp(cid).ok_or(EngineError::NoComp)?.frame_duration();
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
if !comp.layers.iter().filter(|l| ids.contains(&l.id)).any(remappable) {
return Err(bad("layer.enableTimeRemap", "time remapping needs a footage or composition layer"));
}
let on = s.edit("Enable Time Remapping", None, |proj, _| {
let mut next = proj.next_id;
let comp = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
let mut on = false;
for l in comp.layers.iter_mut().filter(|l| ids.contains(&l.id) && remappable(l)) {
let has = l.props.get("timeRemap").is_some();
let target = want.unwrap_or(!has);
on |= target;
if target && !has {
let mut pr = remap_prop(&mut Ids(&mut next));
let (lin, lout) = (l.layer_time(l.in_point), l.layer_time(l.out_point));
let (a, b) = (lin.min(lout), lin.max(lout));
pr.value = KV::Scalar(source_secs(l, l.in_point));
pr.keys = vec![Keyframe::new(a, KV::Scalar(a.seconds())), Keyframe::new(b, KV::Scalar(b.seconds()))];
if lin > lout {
// Reversed layers keep showing what they showed.
pr.keys[0].value = KV::Scalar(source_secs(l, l.out_point - fd));
pr.keys[1].value = KV::Scalar(source_secs(l, l.in_point));
}
l.props.children.insert(0, Node::Prop(pr));
} else if !target && has {
l.props.children.retain(|c| c.match_id() != "timeRemap");
}
}
proj.next_id = next;
Ok(on)
})?;
Ok(json!(on))
}
/// Freeze Frame: time remapping holding the current source frame for the whole layer.
fn freeze(s: &mut Session, p: &Value, last: bool) -> Result<Value> {
let (cid, ids) = layers_p(s, p)?;
let cti = s.time();
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
let (fd, cdur) = (comp.frame_duration(), comp.duration);
if !comp.layers.iter().filter(|l| ids.contains(&l.id)).any(remappable) {
return Err(bad("layer.freezeFrame", "freezing needs a footage or composition layer"));
}
// What each layer shows now (honouring an existing remap) — evaluated before the edit.
let ectx = effectcraft_render::EvalCtx { project: &s.project, comp_id: cid, comp, time: cti, expr: s.expr.as_deref() };
let now: Vec<(effectcraft_project::LayerId, f64)> = comp
.layers
.iter()
.filter(|l| ids.contains(&l.id) && remappable(l))
.map(|l| {
let at = if last { ectx.at(l.out_point - fd) } else { ectx };
(l.id, at.source_time(l).seconds())
})
.collect();
s.edit(if last { "Freeze On Last Frame" } else { "Freeze Frame" }, None, |proj, _| {
let mut next = proj.next_id;
let comp = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
for (lid, src) in &now {
let Some(l) = comp.layer_mut(*lid) else { continue };
if l.props.get("timeRemap").is_none() {
l.props.children.insert(0, Node::Prop(remap_prop(&mut Ids(&mut next))));
}
let lt = if last { l.layer_time(l.out_point - fd) } else { l.layer_time(cti) };
let lin = l.layer_time(l.in_point);
let pr = l.props.get_mut("timeRemap").ok_or(EngineError::NoComp)?;
pr.value = KV::Scalar(*src);
if last {
// Play normally up to the last frame, then hold it to the end of the comp.
let mut end = Keyframe::new(lt.max(lin), KV::Scalar(*src));
end.out_interp = effectcraft_keyframe::Interp::Hold;
pr.keys = vec![Keyframe::new(lin.min(lt), KV::Scalar(lin.min(lt).seconds())), end];
pr.keys.dedup_by_key(|k| k.time);
l.out_point = cdur;
} else {
pr.keys = vec![Keyframe::new(lt, KV::Scalar(*src)).hold()];
}
}
proj.next_id = next;
Ok(())
})?;
Ok(Value::Null)
}
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(
"layer.timeStretch",
"Time Stretch...",
["Layer", "Time"],
None,
"{layers?, percent?|duration? (s), hold?: in|current|out, op?: reverse}",
has_layers,
time_stretch
),
cmd!("layer.timeReverse", "Time-Reverse Layer", ["Layer", "Time"], Some("Cmd+Alt+R"), "{layers?}", has_layers, |s, p| {
let mut p = p.clone();
p["op"] = json!("reverse");
time_stretch(s, &p)
}),
cmd!("layer.enableTimeRemap", "Enable Time Remapping", ["Layer", "Time"], Some("Cmd+Alt+T"), "{layers?, value?}", has_layers, enable_time_remap),
cmd!("layer.freezeFrame", "Freeze Frame", ["Layer", "Time"], None, "{layers?}", has_layers, |s, p| freeze(s, p, false)),
cmd!("layer.freezeOnLastFrame", "Freeze On Last Frame", ["Layer", "Time"], None, "{layers?}", has_layers, |s, p| freeze(s, p, true)),
]
}