Files
effectcraft/crates/engine/src/commands/puppet.rs
T

738 lines
33 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Puppet tools: pins on the layer's Puppet effect (Position, Advanced, Bend, Starch and
//! Overlap pins), mesh options, and a query of the current mesh for overlays and agents.
//!
//! Pins are selected like properties (`puppet.selectPins`; Edit ▸ Clear / Delete removes the
//! selected pins, not their layer). Follow-Through (`puppet.follow`) makes pins trail a leader pin
//! with a delay, for hair, cloth and overlapping action.
//!
//! Pin positions are layer space. A new pin is attached to the mesh under it: the click is
//! mapped back through the current deformation to the rest mesh (its hidden rest position), and
//! Position / Advanced pins get a Position keyframe at the current time, as in After Effects.
use effectcraft_effects::puppet::{self, MeshOpts, PinKind};
use effectcraft_effects::{Buf, Params};
use effectcraft_keyframe::{Keyframe, Value as KV};
use effectcraft_project::build::Ids;
use effectcraft_project::{ItemId, Layer, LayerId, Node, PropGroup, Uid};
use effectcraft_render::{EvalCtx, RenderOpts, Renderer};
use effectcraft_time::Tick;
use serde_json::{Value, json};
use super::{CommandSpec, bad, f_p, has_comp, layer_mut, layer_p, merge_p, str_p};
use crate::{EngineError, Result, Session, cmd};
fn pt(v: Option<&Value>) -> Option<[f64; 2]> {
let a = v?.as_array()?;
Some([a.first()?.as_f64()?, a.get(1)?.as_f64()?])
}
/// The Puppet effect's input pixels and evaluated parameters at comp time `t`.
pub fn puppet_eval(s: &Session, cid: ItemId, lid: LayerId, fx_uid: Uid, t: Tick) -> Option<(Buf, Params)> {
let comp = s.project.comp(cid)?;
let layer = comp.layer(lid)?;
let fx = layer.effects()?;
let index = fx.groups().position(|g| g.uid == fx_uid)?;
let g = fx.groups().nth(index)?;
let ctx = EvalCtx { project: &s.project, comp_id: cid, comp, time: t, expr: s.expr.as_deref(), footage: None };
let mut r = Renderer::new(&s.project, &*s.footage, RenderOpts::default());
r.expr = s.expr.as_deref();
r.cache = Some(&s.layer_cache);
let buf = (*r.layer_input(&ctx, layer, index)?).clone();
let params = effectcraft_effects::flatten_params(g, &mut |p| ctx.value(layer, p));
Some((buf, params))
}
fn find_puppet(s: &Session, cid: ItemId, lid: LayerId) -> Option<Uid> {
s.project.comp(cid)?.layer(lid)?.effects()?.groups().find(|g| puppet::is_puppet(g)).map(|g| g.uid)
}
fn snapped(s: &Session, cid: ItemId, p: &Value) -> Result<Tick> {
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
Ok(comp.frame_rate.snap_nearest(super::time_p(s, p, Some(comp))))
}
fn layer_time(s: &Session, cid: ItemId, lid: LayerId, t: Tick) -> Result<Tick> {
let l = s.project.comp(cid).and_then(|c| c.layer(lid)).ok_or(EngineError::NoComp)?;
Ok(l.layer_time(t))
}
fn add_pin(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "puppet.addPin";
let (cid, lid) = layer_p(s, p, cmd)?;
let kind = match str_p(p, "kind") {
Some(k) => PinKind::from_name(k).ok_or_else(|| bad(cmd, format!("unknown kind `{k}` (position | advanced | bend | starch | overlap)")))?,
None => PinKind::Position,
};
let at = pt(p.get("position")).ok_or_else(|| bad(cmd, "missing `position` [x, y] (layer space)"))?;
let t = snapped(s, cid, p)?;
let lt = layer_time(s, cid, lid, t)?;
{
let l = s.project.comp(cid).and_then(|c| c.layer(lid)).ok_or(EngineError::NoComp)?;
if l.effects().is_none() || !l.source.is_av() {
return Err(bad(cmd, "the Puppet tools work on footage, solid, text, shape and precomp layers"));
}
}
// Which mesh: the one under the click (through the current deformation), else a new one.
let fx_uid = find_puppet(s, cid, lid);
let want_mesh = p.get("mesh").and_then(Value::as_u64);
let new_mesh = p.get("newMesh").and_then(Value::as_bool).unwrap_or(false);
let mut target: Option<(Uid, [f64; 2])> = None;
if let Some(fu) = fx_uid
&& !new_mesh
&& let Some((buf, params)) = puppet_eval(s, cid, lid, fu, t)
{
let meshes = puppet::overlay(&buf, &params);
for (uid, mesh, def, _) in &meshes {
if want_mesh.is_some_and(|w| w != *uid) || mesh.tris.is_empty() {
continue;
}
let inside = mesh.tris.iter().any(|tr| {
let (a, b, c) = (def[tr[0]], def[tr[1]], def[tr[2]]);
let cr = |o: [f64; 2], u: [f64; 2], v: [f64; 2]| (u[0] - o[0]) * (v[1] - o[1]) - (u[1] - o[1]) * (v[0] - o[0]);
cr(a, b, at) >= 0.0 && cr(b, c, at) >= 0.0 && cr(c, a, at) >= 0.0
});
let near = def.iter().map(|v| (v[0] - at[0]).hypot(v[1] - at[1])).fold(f64::MAX, f64::min) < 12.0;
if inside || near || want_mesh.is_some() {
target = Some((*uid, puppet::unmap(mesh, def, at)));
break;
}
}
if target.is_none()
&& let Some(w) = want_mesh
{
return Err(bad(cmd, format!("no mesh {w}")));
}
}
let opts = MeshOpts {
density: f_p(p, "density").unwrap_or(s.state.puppet.density),
expansion: f_p(p, "expansion").unwrap_or(s.state.puppet.expansion),
triangles: f_p(p, "triangles").unwrap_or(s.state.puppet.triangles),
};
let size = {
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
let l = comp.layer(lid).ok_or(EngineError::NoComp)?;
let (w, h) = effectcraft_render::source_size(&s.project, l);
if w == 0 { [comp.width as f64, comp.height as f64] } else { [w as f64, h as f64] }
};
let label = match kind {
PinKind::Starch => "Add Starch Pin",
PinKind::Overlap => "Add Overlap Pin",
_ => "Add Puppet Pin",
};
let (fx, mesh, pin) = s.edit(label, None, |proj, st| {
let mut next = proj.next_id;
let l = layer_mut(proj, cid, lid)?;
let fxg = l.props.sub_mut("effects").ok_or_else(|| bad(cmd, "no effects"))?;
let mut ids = Ids(&mut next);
let fx_uid = match fx_uid {
Some(u) => u,
None => {
let spec = effectcraft_effects::find(puppet::ID).ok_or_else(|| EngineError::Other("Puppet effect missing".into()))?;
let g = effectcraft_effects::instantiate(spec, &mut ids, "Puppet", size);
let u = g.uid;
fxg.children.push(g.into());
u
}
};
let pg = fxg.find_group_mut(fx_uid).ok_or_else(|| bad(cmd, "Puppet effect gone"))?;
let (mesh_uid, rest) = match target {
Some((m, r)) => (m, r),
None => {
let n = puppet::meshes(pg).count();
let g = puppet::mesh_group(&mut ids, &format!("Mesh {}", n + 1), at, &opts);
let u = g.uid;
pg.children.push(g.into());
(u, at)
}
};
let name = puppet::next_pin_name(pg, kind);
let mg = pg.find_group_mut(mesh_uid).ok_or_else(|| bad(cmd, "mesh gone"))?;
let mut pin = puppet::pin_group(&mut ids, &name, kind, rest);
if let Some(pos) = pin.get_mut("position") {
if kind.moves() {
pos.value = KV::Vec2(at);
pos.set_animated(true, lt);
} else {
pos.value = KV::Vec2(rest);
}
}
let pin_uid = pin.uid;
let deform = mg.sub_mut("deform").ok_or_else(|| bad(cmd, "mesh has no Deform group"))?;
deform.children.push(pin.into());
proj.next_id = next;
st.selected_props = vec![(lid, pin_uid)];
Ok((fx_uid, mesh_uid, pin_uid))
})?;
Ok(json!({"effect": fx, "mesh": mesh, "pin": pin, "rest": [target.map(|x| x.1).unwrap_or(at)[0], target.map(|x| x.1).unwrap_or(at)[1]]}))
}
/// The pin groups of a layer's Puppet effects, in stack order.
pub(crate) fn layer_pins(l: &Layer) -> impl Iterator<Item = &PropGroup> {
l.effects().into_iter().flat_map(|fx| fx.groups().filter(|g| puppet::is_puppet(g))).flat_map(puppet::meshes).flat_map(puppet::pins)
}
/// Selected puppet pins of the active comp: (layer, pin uid).
pub(crate) fn selected_pins(s: &Session) -> Vec<(LayerId, Uid)> {
let Some(c) = s.active_comp() else { return vec![] };
s.state.selected_props.iter().filter(|(l, u)| c.layer(*l).is_some_and(|l| layer_pins(l).any(|g| g.uid == *u))).copied().collect()
}
/// Edit ▸ Select All with puppet pins selected: every pin of the selected pins' kinds on their
/// layers. `None` when no pin is selected (Select All then selects layers).
pub(crate) fn select_all_of_kind(s: &mut Session) -> Option<Value> {
let sel = selected_pins(s);
let comp = s.active_comp()?;
let kind = |g: &PropGroup| PinKind::from_index(g.get("kind").map(|k| k.value.as_enum()).unwrap_or(0));
let mut kinds = vec![];
for (l, u) in &sel {
if let Some(g) = comp.layer(*l).and_then(|l| l.props.find_group(*u)) {
kinds.push((*l, kind(g)));
}
}
if kinds.is_empty() {
return None;
}
let all: Vec<(LayerId, Uid)> =
comp.layers.iter().flat_map(|l| layer_pins(l).filter(|g| kinds.contains(&(l.id, kind(g)))).map(|g| (l.id, g.uid)).collect::<Vec<_>>()).collect();
s.state.selected_props = all.clone();
Some(json!({ "pins": all.iter().map(|(_, u)| *u).collect::<Vec<_>>() }))
}
/// Find a pin group (uid or name) in the layer's Puppet effects.
fn pin_uid(s: &Session, cid: ItemId, lid: LayerId, key: &Value, cmd: &str) -> Result<Uid> {
let l = s.project.comp(cid).and_then(|c| c.layer(lid)).ok_or(EngineError::NoComp)?;
let hit = |pin: &&PropGroup| match key {
Value::Number(n) => n.as_u64() == Some(pin.uid),
Value::String(name) => &pin.name == name,
_ => false,
};
layer_pins(l).find(hit).map(|g| g.uid).ok_or_else(|| bad(cmd, format!("no puppet pin {key}")))
}
/// Pins named by `pin` or `pins` (uids or names) on `layer`, else the selected pins (of `layer`
/// when given): (comp, layer, pin uid).
pub(crate) fn pins_p(s: &Session, p: &Value, cmd: &str) -> Result<Vec<(ItemId, LayerId, Uid)>> {
let keys: Vec<Value> = match (p.get("pin"), p.get("pins")) {
(Some(k), _) => vec![k.clone()],
(None, Some(Value::Array(a))) => a.clone(),
(None, Some(_)) => return Err(bad(cmd, "`pins` must be an array of pin uids or names")),
(None, None) => vec![],
};
if !keys.is_empty() {
let (cid, lid) = layer_p(s, p, cmd)?;
return keys.iter().map(|k| pin_uid(s, cid, lid, k, cmd).map(|u| (cid, lid, u))).collect();
}
let cid = s.active_comp_id().ok_or(EngineError::NoComp)?;
let only = match p.get("layer") {
Some(_) => Some(layer_p(s, p, cmd)?.1),
None => None,
};
let sel: Vec<_> = selected_pins(s).into_iter().filter(|(l, _)| only.is_none_or(|o| o == *l)).map(|(l, u)| (cid, l, u)).collect();
if sel.is_empty() {
return Err(bad(cmd, "missing `pin` / `pins` (uids or names), and no puppet pins are selected"));
}
Ok(sel)
}
/// Select pins (the viewer's click / Shift-click on a pin). Plain: only these pins; `add`: add
/// them; `toggle`: flip each. An empty `pins` deselects every pin.
fn select_pins(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "puppet.selectPins";
let want = match p.get("pins") {
Some(Value::Array(a)) if a.is_empty() => vec![],
_ => pins_p(s, p, cmd)?,
};
let (add, toggle) = (super::b_p(p, "add").unwrap_or(false), super::b_p(p, "toggle").unwrap_or(false));
let st = &mut s.state;
if !add && !toggle {
st.selected_props.clear();
st.selected_keys.clear();
}
for (_, l, u) in want {
let at = st.selected_props.iter().position(|x| *x == (l, u));
match at {
Some(i) if toggle => {
st.selected_props.remove(i);
}
Some(_) => {}
None => st.selected_props.push((l, u)),
}
if !st.selected_layers.contains(&l) {
st.selected_layers.push(l);
}
}
Ok(json!({ "pins": selected_pins(s).iter().map(|(_, u)| *u).collect::<Vec<_>>() }))
}
fn with_pin<T>(s: &mut Session, p: &Value, cmd: &str, label: &str, f: impl FnOnce(&mut PropGroup, Tick) -> Result<T>) -> Result<T> {
let (cid, lid) = layer_p(s, p, cmd)?;
let key = p.get("pin").cloned().ok_or_else(|| bad(cmd, "missing `pin` (uid or name)"))?;
let uid = pin_uid(s, cid, lid, &key, cmd)?;
let t = snapped(s, cid, p)?;
let lt = layer_time(s, cid, lid, t)?;
s.edit(label, merge_p(p), |proj, st| {
let l = layer_mut(proj, cid, lid)?;
let g = l.props.find_group_mut(uid).ok_or_else(|| bad(cmd, "pin gone"))?;
let r = f(g, lt)?;
st.selected_props = vec![(lid, uid)];
Ok(r)
})
}
fn move_pin(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "puppet.movePin";
let to = pt(p.get("position")).ok_or_else(|| bad(cmd, "missing `position` [x, y] (layer space)"))?;
with_pin(s, p, cmd, "Move Puppet Pin", |g, lt| {
let kind = PinKind::from_index(g.get("kind").map(|k| k.value.as_enum()).unwrap_or(0));
let pos = g.get_mut("position").ok_or_else(|| bad(cmd, "Bend pins have no position (use puppet.setPin rotation/scale)"))?;
pos.set_value_at(lt, KV::Vec2(to));
// Starch and Overlap pins sit on the rest mesh: moving them moves their region.
if !kind.moves()
&& let Some(r) = g.get_mut("rest")
{
r.value = KV::Vec2(to);
}
Ok(())
})?;
Ok(Value::Null)
}
fn set_pin(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "puppet.setPin";
let fields: Vec<(&str, KV)> = [("scale", "scale"), ("rotation", "rotation"), ("amount", "amount"), ("extent", "extent"), ("inFront", "in_front")]
.iter()
.filter_map(|(k, m)| f_p(p, k).map(|v| (*m, KV::Scalar(v))))
.chain(pt(p.get("position")).map(|v| ("position", KV::Vec2(v))))
.collect();
if fields.is_empty() {
return Err(bad(cmd, "nothing to set (position, scale, rotation, amount, extent, inFront)"));
}
with_pin(s, p, cmd, "Edit Puppet Pin", |g, lt| {
for (m, v) in fields {
let pr = g.get_mut(m).ok_or_else(|| bad(cmd, format!("this pin has no `{m}`")))?;
pr.set_value_at(lt, v);
}
Ok(())
})?;
Ok(Value::Null)
}
fn remove_pin(s: &mut Session, p: &Value) -> Result<Value> {
let pins = pins_p(s, p, "puppet.removePin")?;
let label = if pins.len() == 1 { "Delete Puppet Pin" } else { "Delete Puppet Pins" };
s.edit(label, None, |proj, st| {
for (cid, lid, uid) in &pins {
let l = layer_mut(proj, *cid, *lid)?;
if let Some(parent) = l.props.parent_of_mut(*uid) {
parent.children.retain(|c| c.uid() != *uid);
}
st.selected_props.retain(|(_, u)| u != uid);
}
Ok(())
})?;
Ok(json!({ "removed": pins.len() }))
}
fn mesh_opts(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "puppet.mesh";
let fields: Vec<(&str, f64)> = ["density", "expansion", "triangles"].iter().filter_map(|k| f_p(p, k).map(|v| (*k, v))).collect();
if let Some(b) = p.get("showMesh").and_then(Value::as_bool) {
s.state.puppet.show_mesh = b;
}
if fields.is_empty() {
return Ok(serde_json::to_value(&s.state.puppet).unwrap_or(Value::Null));
}
// Without a layer: set the tool defaults for new meshes.
let has_layer = p.get("layer").is_some() || !s.state.selected_layers.is_empty();
let (cid, lid) = match layer_p(s, p, cmd) {
Ok(x) => x,
Err(_) if !has_layer => {
for (k, v) in &fields {
match *k {
"density" => s.state.puppet.density = v.clamp(0.0, 100.0),
"expansion" => s.state.puppet.expansion = *v,
_ => s.state.puppet.triangles = v.max(10.0),
}
}
return Ok(serde_json::to_value(&s.state.puppet).unwrap_or(Value::Null));
}
Err(e) => return Err(e),
};
let fx = find_puppet(s, cid, lid).ok_or_else(|| bad(cmd, "the layer has no Puppet effect (add a pin first)"))?;
let want = p.get("mesh").and_then(Value::as_u64);
let t = snapped(s, cid, p)?;
let lt = layer_time(s, cid, lid, t)?;
for (k, v) in &fields {
match *k {
"density" => s.state.puppet.density = v.clamp(0.0, 100.0),
"expansion" => s.state.puppet.expansion = *v,
_ => s.state.puppet.triangles = v.max(10.0),
}
}
s.edit("Puppet Mesh", merge_p(p), |proj, _| {
let l = layer_mut(proj, cid, lid)?;
let g = l.props.find_group_mut(fx).ok_or_else(|| bad(cmd, "Puppet effect gone"))?;
let mut done = 0;
for c in g.children.iter_mut() {
let Node::Group(m) = c else { continue };
if m.match_id != "mesh" || want.is_some_and(|w| w != m.uid) {
continue;
}
for (k, v) in &fields {
if let Some(pr) = m.get_mut(k) {
pr.set_value_at(lt, KV::Scalar(*v));
}
}
done += 1;
}
if done == 0 { Err(bad(cmd, "no such mesh")) } else { Ok(()) }
})?;
Ok(Value::Null)
}
/// Parse recorded samples `[[t, x, y]…]` (t = seconds since the drag began, layer space).
fn samples(p: &Value, cmd: &str) -> Result<Vec<(f64, [f64; 2])>> {
let arr = p
.get("samples")
.and_then(Value::as_array)
.ok_or_else(|| bad(cmd, "missing `samples`: [[t, x, y]…] (t = seconds since the drag began, layer space)"))?;
let mut rec = Vec::with_capacity(arr.len());
for (i, v) in arr.iter().enumerate() {
let a: Vec<f64> = v.as_array().map(|a| a.iter().filter_map(Value::as_f64).collect()).unwrap_or_default();
if a.len() < 3 || a.iter().any(|x| !x.is_finite()) {
return Err(bad(cmd, format!("sample {i}: expected [t, x, y]")));
}
rec.push((a[0], [a[1], a[2]]));
}
if rec.is_empty() {
return Err(bad(cmd, "need at least one sample"));
}
rec.sort_by(|a, b| a.0.total_cmp(&b.0));
Ok(rec)
}
/// Puppet tool Record mode (⌘/Ctrl-drag a pin): the pin's drag, sampled in real time, becomes
/// Position keyframes at the comp frame rate from the current time on, replacing the keys in
/// the recorded span. Record Options: `speed` (%: 100 plays back at the speed of the drag,
/// 200 twice as slow) and `smoothing` (removes extraneous keys like the Smoother; 0 = a key
/// on every frame). The current time stays where recording began.
fn record_pin(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "puppet.recordPin";
let (cid, lid) = layer_p(s, p, cmd)?;
let key = p.get("pin").cloned().ok_or_else(|| bad(cmd, "missing `pin` (uid or name)"))?;
let uid = pin_uid(s, cid, lid, &key, cmd)?;
let rec = samples(p, cmd)?;
// Several selected pins dragged together (`pins`): each follows the drag's displacement from
// where it is when recording begins; one undo step.
if let Some(Value::Array(others)) = p.get("pins")
&& !others.is_empty()
{
let start = match p.get("start").and_then(Value::as_f64) {
Some(t) => Tick::from_seconds_f64(t),
None => s.time_of(cid),
};
let lt = layer_time(s, cid, lid, start)?;
let mut pins = vec![(uid, rec[0].1)];
for k in others {
let u = pin_uid(s, cid, lid, k, cmd)?;
if pins.iter().any(|(x, _)| *x == u) {
continue;
}
let l = s.project.comp(cid).and_then(|c| c.layer(lid)).ok_or(EngineError::NoComp)?;
let at = l.props.find_group(u).and_then(|g| g.get("position")).map(|pr| pr.value_at(lt).as_vec2());
pins.push((u, at.ok_or_else(|| bad(cmd, "only Position and Advanced pins record motion"))?));
}
let o = rec[0].1;
return super::app_more::grouped(s, "Record Puppet Pins", |s| {
let mut last = Value::Null;
for (u, base) in &pins {
let moved: Vec<Value> = rec.iter().map(|(t, q)| json!([t, base[0] + q[0] - o[0], base[1] + q[1] - o[1]])).collect();
let mut one = p.clone();
if let Some(m) = one.as_object_mut() {
m.remove("pins");
m.insert("pin".into(), json!(u));
m.insert("samples".into(), Value::Array(moved));
}
last = s.execute(cmd, one)?;
}
last["pins"] = json!(pins.iter().map(|(u, _)| *u).collect::<Vec<_>>());
Ok(last)
});
}
let opts = &s.state.puppet;
let speed = f_p(p, "speed").unwrap_or(opts.record_speed).clamp(1.0, 10_000.0) / 100.0;
// Smoothing 10 (the default) keeps the path within 1 layer pixel of the drag.
let smoothing = f_p(p, "smoothing").unwrap_or(opts.record_smoothing).max(0.0) / 10.0;
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
let fr = comp.frame_rate;
let fd = fr.frame_duration().seconds();
let start = match p.get("start").and_then(Value::as_f64) {
Some(t) => fr.snap_nearest(Tick::from_seconds_f64(t)),
None => s.time_of(cid),
};
let f0 = fr.frame_at(start);
let end_frame = fr.frame_at(comp.duration - comp.frame_duration());
let t_rec0 = rec[0].0;
let span = (rec[rec.len() - 1].0 - t_rec0) * speed;
let nframes = (span / fd).round().max(0.0) as i64;
let at = |secs: f64| {
let i = rec.partition_point(|r| r.0 <= secs).clamp(1, rec.len().max(2) - 1);
if rec.len() == 1 {
return rec[0].1;
}
let (a, b) = (rec[i - 1], rec[i]);
let f = if b.0 > a.0 { ((secs - a.0) / (b.0 - a.0)).clamp(0.0, 1.0) } else { 1.0 };
[a.1[0] + (b.1[0] - a.1[0]) * f, a.1[1] + (b.1[1] - a.1[1]) * f]
};
let n = s.edit("Record Puppet Pin", None, |proj, st| {
let l = layer_mut(proj, cid, lid)?;
let lc = l.clone();
let g = l.props.find_group_mut(uid).ok_or_else(|| bad(cmd, "pin gone"))?;
let kind = PinKind::from_index(g.get("kind").map(|k| k.value.as_enum()).unwrap_or(0));
if !kind.moves() {
return Err(bad(cmd, "only Position and Advanced pins record motion"));
}
let pos = g.get_mut("position").ok_or_else(|| bad(cmd, "the pin has no Position"))?;
let mut new: Vec<Keyframe> = vec![];
for k in 0..=nframes {
let f = f0 + k;
if f > end_frame {
break;
}
let q = at(t_rec0 + k as f64 * fd / speed);
new.push(Keyframe::new(lc.layer_time(fr.tick_of(f)), KV::Vec2(q)));
}
let (Some(t0), Some(t1)) = (new.first().map(|k| k.time), new.last().map(|k| k.time)) else { return Ok(0) };
let mut keys: Vec<Keyframe> = pos.keys.iter().filter(|k| k.time < t0 || k.time > t1).cloned().collect();
keys.extend(new);
keys.sort_by_key(|k| k.time);
let (a, b) = (keys.iter().position(|k| k.time == t0).unwrap_or(0), keys.iter().position(|k| k.time == t1).unwrap_or(0));
if smoothing > 0.0 && b > a + 1 {
let samples: Vec<(Tick, Vec<f64>)> = keys[a..=b].iter().map(|k| (k.time, k.value.components())).collect();
keys = super::anim_tools::smooth_keys(&keys, a, b, &samples, smoothing, true);
}
pos.keys = keys;
pos.value = KV::Vec2(rec[0].1);
let puid = pos.uid;
st.selected_props = vec![(lid, uid)];
st.selected_keys = pos.keys.iter().filter(|k| k.time >= t0 && k.time <= t1).map(|k| crate::KeyRef { layer: lid, prop: puid, time: k.time }).collect();
Ok(st.selected_keys.len())
})?;
// The current time returns to where recording began.
s.state.times.insert(cid, start);
let end = fr.tick_of((f0 + nframes).min(end_frame));
Ok(json!({"keys": n, "start": start.seconds(), "end": end.seconds(), "frames": nframes + 1}))
}
/// Follow-Through (hair, cloth, tails, overlapping action): each follower pin trails the leader
/// pin's motion by a delay, through an expression on its Position. The followers nearest the
/// leader trail least; with `cascade` the k-th nearest trails k × `delay`, otherwise all trail by
/// `delay`. `amount` (%) scales the motion they inherit; their own keyframes still add on top.
/// The leader is `leader`, else the first selected pin; the followers are `pins`, else the other
/// selected pins of the leader's layer. One undo step.
fn follow(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "puppet.follow";
let (cid, lid, leader, mut followers) = match p.get("leader") {
Some(k) => {
let (cid, lid) = layer_p(s, p, cmd)?;
let leader = pin_uid(s, cid, lid, k, cmd)?;
let rest: Vec<Uid> = pins_p(s, p, cmd)?.into_iter().filter(|(_, l, u)| *l == lid && *u != leader).map(|x| x.2).collect();
(cid, lid, leader, rest)
}
None => {
let sel = pins_p(s, p, cmd)?;
let (cid, lid, leader) = sel[0];
(cid, lid, leader, sel.iter().skip(1).filter(|(_, l, _)| *l == lid).map(|x| x.2).collect())
}
};
followers.dedup();
if followers.is_empty() {
return Err(bad(cmd, "select the leader pin first, then the pins that follow it (or pass `leader` and `pins`)"));
}
let delay = f_p(p, "delay").unwrap_or(0.1).clamp(0.0, 10.0);
let amount = f_p(p, "amount").unwrap_or(100.0) / 100.0;
let cascade = super::b_p(p, "cascade").unwrap_or(true);
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
let layer = comp.layer(lid).ok_or(EngineError::NoComp)?;
// (Position uid, rest) of a pin that moves.
let moving = |uid: Uid| -> Result<(Uid, [f64; 2], String)> {
let g = layer.props.find_group(uid).ok_or_else(|| bad(cmd, "pin gone"))?;
if !PinKind::from_index(g.get("kind").map(|k| k.value.as_enum()).unwrap_or(0)).moves() {
return Err(bad(cmd, format!("`{}` has no Position (only Position and Advanced pins follow or lead)", g.name)));
}
let pos = g.get("position").ok_or_else(|| bad(cmd, "the pin has no Position"))?;
Ok((pos.uid, g.get("rest").map(|r| r.value.as_vec2()).unwrap_or([0.0; 2]), g.name.clone()))
};
let (lead_pos, lead_rest, lead_name) = moving(leader)?;
let reference = super::link::reference(comp, layer, layer, lead_pos, true).ok_or_else(|| bad(cmd, "can't reference the leader pin"))?;
let mut rigs = followers.iter().map(|u| moving(*u).map(|(pos, rest, _)| (*u, pos, rest))).collect::<Result<Vec<_>>>()?;
let dist = |r: [f64; 2]| (r[0] - lead_rest[0]).hypot(r[1] - lead_rest[1]);
rigs.sort_by(|a, b| dist(a.2).total_cmp(&dist(b.2)));
let out = super::app_more::grouped(s, "Follow-Through", |s| {
let mut out = vec![];
for (k, (pin, pos, _)) in rigs.iter().enumerate() {
let d = if cascade { delay * (k + 1) as f64 } else { delay };
let e = format!(
"// Follow-Through: trails {} by {d} s\nvar lead = {reference};\nvalue + (lead.valueAtTime(time - {d}) - [{}, {}]) * {amount}",
serde_json::to_string(&lead_name).unwrap_or_default(),
lead_rest[0],
lead_rest[1]
);
s.execute("prop.setExpression", json!({"comp": cid.0, "layer": lid.0, "prop": pos, "expression": e}))?;
out.push(json!({"pin": pin, "delay": d}));
}
Ok(out)
})?;
Ok(json!({"leader": leader, "pins": out}))
}
/// Record Options (the Puppet tool's Record Options… dialog): `speed`, `smoothing`,
/// `useDraftDeformation`, `showMesh`. Returns the options.
fn record_options(s: &mut Session, p: &Value) -> Result<Value> {
let o = &mut s.state.puppet;
if let Some(v) = f_p(p, "speed") {
o.record_speed = v.clamp(1.0, 10_000.0);
}
if let Some(v) = f_p(p, "smoothing") {
o.record_smoothing = v.clamp(0.0, 100.0);
}
if let Some(v) = p.get("useDraftDeformation").and_then(Value::as_bool) {
o.record_draft = v;
}
if let Some(v) = p.get("showMesh").and_then(Value::as_bool) {
o.record_show_mesh = v;
}
Ok(json!({"speed": o.record_speed, "smoothing": o.record_smoothing, "useDraftDeformation": o.record_draft, "showMesh": o.record_show_mesh}))
}
/// Meshes, pins and the deformed mesh at the current time (layer space).
fn info(s: &mut Session, p: &Value) -> Result<Value> {
let (cid, lid) = layer_p(s, p, "puppet.info")?;
let Some(fx) = find_puppet(s, cid, lid) else { return Ok(json!({"meshes": []})) };
let t = snapped(s, cid, p)?;
let (buf, params) = puppet_eval(s, cid, lid, fx, t).ok_or_else(|| bad("puppet.info", "the layer has no pixels"))?;
let meshes: Vec<Value> = puppet::overlay(&buf, &params)
.into_iter()
.map(|(uid, mesh, def, pins)| {
json!({
"mesh": uid,
"triangles": mesh.tris.len(),
"vertices": mesh.verts.len(),
"outline": mesh.outline,
"deformed": def,
"tris": mesh.tris,
"pins": pins.iter().map(|pn| json!({"pin": pn.uid, "kind": pn.kind.name(), "rest": pn.rest, "position": pn.position})).collect::<Vec<_>>(),
})
})
.collect();
Ok(json!({"effect": fx, "meshes": meshes}))
}
pub fn specs() -> Vec<CommandSpec> {
vec![
cmd!(
"puppet.addPin",
"Add Puppet Pin",
[],
None,
"{layer?, kind?: position|advanced|bend|starch|overlap, position: [x,y] (layer space), time? (s) | frame?, mesh?: uid, newMesh?, density?, expansion?, triangles?}",
has_comp,
add_pin
),
cmd!("puppet.movePin", "Move Puppet Pin", [], None, "{layer?, pin: uid | name, position: [x,y], time? | frame?, merge?}", has_comp, move_pin),
cmd!(
"puppet.setPin",
"Edit Puppet Pin",
[],
None,
"{layer?, pin: uid | name, position?, scale? (%), rotation? (°), amount? (%), extent? (px), inFront?, time? | frame?, merge?}",
has_comp,
set_pin
),
cmd!(
"puppet.removePin",
"Delete Puppet Pin",
[],
None,
"{layer?, pin?: uid | name, pins?: [uid | name…]} (default: the selected pins)",
has_comp,
remove_pin
),
cmd!("puppet.selectPins", "Select Puppet Pins", [], None, "{layer?, pins: [uid | name…] ([] deselects), add?, toggle?}", has_comp, select_pins),
cmd!(
"puppet.mesh",
"Puppet Mesh Options",
[],
None,
"{layer?, mesh?: uid, density?, expansion?, triangles?, showMesh?, time?, merge?}",
super::always,
mesh_opts
),
crate::query!("puppet.info", "Puppet Mesh Info", "{layer?, time? | frame?}", info),
cmd!(
"puppet.recordPin",
"Record Puppet Pin",
[],
None,
"{layer?, pin: uid | name, samples: [[t, x, y]…] (t = seconds since the drag began, layer space), pins?: [uid | name…] (more pins that move by the same displacement), start? (s, default current time), speed? (%, Record Options), smoothing? (Record Options)}",
has_comp,
record_pin
),
cmd!(
"puppet.follow",
"Follow-Through...",
[],
None,
"{layer?, leader?: uid | name (default: the first selected pin), pins?: [uid | name…] (default: the other selected pins), delay? (s, 0.1), amount? (%, 100), cascade? (true: the k-th nearest pin trails k × delay)}",
has_comp,
follow
),
cmd!(
"puppet.recordOptions",
"Record Options...",
[],
None,
"{speed? (%, 100), smoothing? (0-100), useDraftDeformation?, showMesh?}",
super::always,
record_options
),
]
}
/// Puppet tool options (the Tools bar's Mesh: Show / Expansion / Density for new meshes).
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct PuppetOptions {
pub density: f64,
pub expansion: f64,
pub triangles: f64,
/// Show the mesh in the viewer.
pub show_mesh: bool,
/// Record Options ▸ Speed (%): 100 plays a recording back at the speed of the drag.
pub record_speed: f64,
/// Record Options ▸ Smoothing: removes extraneous keys from recorded motion (0 = a key per
/// frame).
pub record_smoothing: f64,
/// Record Options ▸ Use Draft Deformation: the viewer shows the draft (unexpanded) mesh
/// while recording.
pub record_draft: bool,
/// Record Options ▸ Show Mesh while recording.
pub record_show_mesh: bool,
}
impl Default for PuppetOptions {
fn default() -> Self {
let d = MeshOpts::default();
PuppetOptions {
density: d.density,
expansion: d.expansion,
triangles: d.triangles,
show_mesh: false,
record_speed: 100.0,
record_smoothing: 10.0,
record_draft: false,
record_show_mesh: false,
}
}
}