673 lines
28 KiB
Rust
673 lines
28 KiB
Rust
//! Masks drawn and edited in the viewer: Pen tool (new mask, add vertex, Bezier tangents, close
|
|
//! path), vertex selection, moving and deleting vertices, and Layer ▸ Mask ▸ Remove (All).
|
|
//!
|
|
//! Coordinates are in layer space. Edits of an animated Mask Path set a key at the current time.
|
|
|
|
use effectcraft_keyframe::{ShapePath, Value as KV};
|
|
use effectcraft_project::build::{self, Ids};
|
|
use effectcraft_project::{Layer, MaskMode, PropGroup, Uid};
|
|
use serde_json::{Value, json};
|
|
|
|
use super::{CommandSpec, b_p, bad, has_comp, has_layers, layer_mut, layer_p, merge_p, resolve_layer, str_p};
|
|
use crate::{EngineError, Result, Session, VertexRef, cmd};
|
|
|
|
fn pt(v: Option<&Value>) -> Option<[f64; 2]> {
|
|
let a = v?.as_array()?;
|
|
Some([a.first()?.as_f64()?, a.get(1)?.as_f64()?])
|
|
}
|
|
|
|
fn pts(v: Option<&Value>) -> Vec<[f64; 2]> {
|
|
v.and_then(Value::as_array).map(|a| a.iter().filter_map(|p| pt(Some(p))).collect()).unwrap_or_default()
|
|
}
|
|
|
|
/// Index of a mask group by uid, 1-based index or name.
|
|
fn mask_index(masks: &PropGroup, key: &Value) -> Option<usize> {
|
|
match key {
|
|
Value::Number(n) => {
|
|
let n = n.as_u64()?;
|
|
masks.children.iter().position(|c| c.uid() == n).or_else(|| (n as usize).checked_sub(1).filter(|i| *i < masks.children.len()))
|
|
}
|
|
Value::String(s) => masks.children.iter().position(|c| c.name() == s),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Find a mask group by uid, 1-based index or name.
|
|
fn find_mask<'a>(masks: &'a mut PropGroup, key: &Value) -> Option<&'a mut PropGroup> {
|
|
let i = mask_index(masks, key)?;
|
|
masks.children.get_mut(i)?.as_group_mut()
|
|
}
|
|
|
|
/// Whether a group holds an editable Bezier path (a mask, or a shape layer's Path item).
|
|
pub(crate) fn is_path_group(g: &PropGroup) -> bool {
|
|
matches!(g.get("path").map(|p| &p.value), Some(KV::Path(_))) && (matches!(g.kind, effectcraft_project::GroupKind::Mask { .. }) || g.match_id == "path")
|
|
}
|
|
|
|
/// A mask (uid, 1-based index or name) or, by uid, a shape layer's Path item.
|
|
pub(crate) fn path_group<'a>(props: &'a mut PropGroup, key: &Value) -> Option<&'a mut PropGroup> {
|
|
if let Some(i) = props.sub("masks").and_then(|m| mask_index(m, key)) {
|
|
return props.sub_mut("masks")?.children.get_mut(i)?.as_group_mut();
|
|
}
|
|
let uid = key.as_u64()?;
|
|
props.find_group_mut(uid).filter(|g| is_path_group(g))
|
|
}
|
|
|
|
/// Edit the mask path of (layer, mask) at the current time.
|
|
fn edit_path<T>(s: &mut Session, p: &Value, label: &str, f: impl FnOnce(&mut ShapePath) -> Result<T>) -> Result<(Uid, T)> {
|
|
edit_path_counted(s, p, label, |sp| f(sp).map(|r| (r, None)))
|
|
}
|
|
|
|
/// A change of a path's vertex count, repeated on the other keyframes of an animated path when
|
|
/// Settings ▸ General ▸ Preserve Constant Vertex and Feather Point Count is on.
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) enum CountOp {
|
|
/// A vertex was inserted at this index.
|
|
Insert(usize),
|
|
/// These vertices were removed.
|
|
Remove(Vec<usize>),
|
|
}
|
|
|
|
/// Apply a vertex-count change to another keyframe's path: an inserted vertex splits the
|
|
/// matching segment in half (the shape doesn't change), removed vertices go.
|
|
pub(crate) fn apply_count_op(sp: &mut ShapePath, op: &CountOp) {
|
|
match op {
|
|
CountOp::Insert(at) => {
|
|
let n = sp.vertices.len();
|
|
if n == 0 {
|
|
return;
|
|
}
|
|
if *at > 0 && split_segment(sp, at - 1, 0.5).is_some() {
|
|
return;
|
|
}
|
|
let at = (*at).min(n);
|
|
let v = sp.vertices[at.min(n - 1)];
|
|
sp.in_tangents.resize(n, [0.0; 2]);
|
|
sp.out_tangents.resize(n, [0.0; 2]);
|
|
sp.vertices.insert(at, v);
|
|
sp.in_tangents.insert(at, [0.0; 2]);
|
|
sp.out_tangents.insert(at, [0.0; 2]);
|
|
}
|
|
CountOp::Remove(idx) => {
|
|
let mut idx = idx.clone();
|
|
idx.sort_unstable();
|
|
idx.dedup();
|
|
for &i in idx.iter().rev() {
|
|
if i < sp.vertices.len() {
|
|
sp.vertices.remove(i);
|
|
if i < sp.in_tangents.len() {
|
|
sp.in_tangents.remove(i);
|
|
}
|
|
if i < sp.out_tangents.len() {
|
|
sp.out_tangents.remove(i);
|
|
}
|
|
}
|
|
}
|
|
if sp.vertices.len() < 3 {
|
|
sp.closed = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Repeat `op` on every keyframe of `pr` other than the one at `lt`.
|
|
fn sync_keys(pr: &mut effectcraft_project::Property, lt: effectcraft_time::Tick, op: &CountOp) {
|
|
for k in pr.keys.iter_mut().filter(|k| k.time != lt) {
|
|
if let KV::Path(sp) = &mut k.value {
|
|
apply_count_op(sp, op);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// [`edit_path`] whose edit may change the vertex count (`f` says how).
|
|
fn edit_path_counted<T>(s: &mut Session, p: &Value, label: &str, f: impl FnOnce(&mut ShapePath) -> Result<(T, Option<CountOp>)>) -> Result<(Uid, T)> {
|
|
let (cid, lid) = layer_p(s, p, label)?;
|
|
let key = p.get("mask").cloned().ok_or_else(|| bad(label, "missing `mask` (uid, index or name)"))?;
|
|
let t = s.time();
|
|
let preserve = s.prefs.general.preserve_constant_vertex_count;
|
|
s.edit(label, merge_p(p), |proj, _| {
|
|
let l = layer_mut(proj, cid, lid)?;
|
|
let lt = l.layer_time(t);
|
|
let g = path_group(&mut l.props, &key).ok_or_else(|| bad(label, "no such mask or shape path"))?;
|
|
let uid = g.uid;
|
|
let roto = super::paths::is_roto(g);
|
|
let pr = g.get_mut("path").ok_or_else(|| bad(label, "mask has no path"))?;
|
|
let KV::Path(mut sp) = pr.value_at(lt) else { return Err(bad(label, "not a path")) };
|
|
let (r, op) = f(&mut sp)?;
|
|
if roto {
|
|
super::paths::roto_smooth(&mut sp);
|
|
}
|
|
pr.set_value_at(lt, KV::Path(sp));
|
|
if let Some(op) = op.filter(|_| preserve) {
|
|
sync_keys(pr, lt, &op);
|
|
}
|
|
Ok((uid, r))
|
|
})
|
|
}
|
|
|
|
/// Pen tool: a new (open by default) mask from points.
|
|
fn new_mask(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let (cid, lid) = layer_p(s, p, "mask.new")?;
|
|
let v = pts(p.get("vertices"));
|
|
if v.is_empty() {
|
|
return Err(bad("mask.new", "need `vertices` [[x,y], …] in layer space"));
|
|
}
|
|
let n = v.len();
|
|
let mut ins = pts(p.get("inTangents"));
|
|
let mut outs = pts(p.get("outTangents"));
|
|
ins.resize(n, [0.0; 2]);
|
|
outs.resize(n, [0.0; 2]);
|
|
let path = ShapePath { vertices: v, in_tangents: ins, out_tangents: outs, closed: b_p(p, "closed").unwrap_or(false), feather: Vec::new() };
|
|
let mode = str_p(p, "mode").and_then(MaskMode::from_name).unwrap_or(MaskMode::Add);
|
|
let cycle = s.prefs.appearance.cycle_mask_colors;
|
|
let uid = s.edit("New Mask", None, |proj, st| {
|
|
let mut next = proj.next_id;
|
|
let l = layer_mut(proj, cid, lid)?;
|
|
let masks = l.props.sub_mut("masks").ok_or_else(|| bad("mask.new", "this layer can't have masks"))?;
|
|
let k = masks.children.len();
|
|
let g = build::mask(&mut Ids(&mut next), &format!("Mask {}", k + 1), path, mode, crate::prefs::mask_color(cycle, k));
|
|
let uid = g.uid;
|
|
masks.children.push(g.into());
|
|
proj.next_id = next;
|
|
st.selected_vertices = vec![VertexRef { layer: lid, mask: uid, index: n - 1 }];
|
|
Ok(uid)
|
|
})?;
|
|
Ok(json!({"mask": uid}))
|
|
}
|
|
|
|
fn add_vertex(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let point = pt(p.get("point")).ok_or_else(|| bad("mask.addVertex", "missing `point` [x,y]"))?;
|
|
let tin = pt(p.get("in")).unwrap_or([0.0; 2]);
|
|
let tout = pt(p.get("out")).unwrap_or([0.0; 2]);
|
|
let at = p.get("index").and_then(Value::as_u64).map(|i| i as usize);
|
|
let (_, lid) = layer_p(s, p, "mask.addVertex")?;
|
|
let (uid, i) = edit_path_counted(s, p, "mask.addVertex", |sp| {
|
|
let i = at.unwrap_or(sp.vertices.len()).min(sp.vertices.len());
|
|
sp.vertices.insert(i, point);
|
|
sp.in_tangents.insert(i, tin);
|
|
sp.out_tangents.insert(i, tout);
|
|
sp.feather_after_insert(i, None);
|
|
Ok((i, Some(CountOp::Insert(i))))
|
|
})?;
|
|
s.state.selected_vertices = vec![VertexRef { layer: lid, mask: uid, index: i }];
|
|
Ok(json!(i))
|
|
}
|
|
|
|
fn set_vertex(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let i = p.get("index").and_then(Value::as_u64).ok_or_else(|| bad("mask.setVertex", "missing `index`"))? as usize;
|
|
let (point, tin, tout) = (pt(p.get("point")), pt(p.get("in")), pt(p.get("out")));
|
|
edit_path(s, p, "mask.setVertex", |sp| {
|
|
if i >= sp.vertices.len() {
|
|
return Err(bad("mask.setVertex", "no such vertex"));
|
|
}
|
|
if let Some(v) = point {
|
|
sp.vertices[i] = v;
|
|
}
|
|
if let Some(v) = tin {
|
|
sp.in_tangents[i] = v;
|
|
}
|
|
if let Some(v) = tout {
|
|
sp.out_tangents[i] = v;
|
|
}
|
|
Ok(())
|
|
})?;
|
|
Ok(Value::Null)
|
|
}
|
|
|
|
fn set_closed(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let c = b_p(p, "closed").unwrap_or(true);
|
|
edit_path(s, p, "mask.setClosed", |sp| {
|
|
sp.closed = c;
|
|
Ok(())
|
|
})?;
|
|
Ok(json!(c))
|
|
}
|
|
|
|
/// Selected (or given) vertices grouped per (layer, mask).
|
|
fn vertex_groups(s: &Session, p: &Value) -> Result<std::collections::BTreeMap<(u64, Uid), Vec<usize>>> {
|
|
let comp = s.active_comp().ok_or(EngineError::NoComp)?;
|
|
let mut g: std::collections::BTreeMap<(u64, Uid), Vec<usize>> = Default::default();
|
|
if let Some(Value::Array(a)) = p.get("vertices") {
|
|
for v in a {
|
|
let l = v.get("layer").and_then(|x| resolve_layer(comp, x)).ok_or_else(|| bad("mask", "vertex needs `layer`"))?;
|
|
let m = v.get("mask").and_then(Value::as_u64).ok_or_else(|| bad("mask", "vertex needs `mask` uid"))?;
|
|
let i = v.get("index").and_then(Value::as_u64).ok_or_else(|| bad("mask", "vertex needs `index`"))?;
|
|
g.entry((l.0, m)).or_default().push(i as usize);
|
|
}
|
|
} else {
|
|
for v in &s.state.selected_vertices {
|
|
g.entry((v.layer.0, v.mask)).or_default().push(v.index);
|
|
}
|
|
}
|
|
Ok(g)
|
|
}
|
|
|
|
fn select_vertices(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let g = vertex_groups(s, &json!({"vertices": p.get("vertices").cloned().unwrap_or(json!([]))}))?;
|
|
let sel: Vec<VertexRef> =
|
|
g.into_iter().flat_map(|((l, m), is)| is.into_iter().map(move |i| VertexRef { layer: effectcraft_project::LayerId(l), mask: m, index: i })).collect();
|
|
if b_p(p, "add").unwrap_or(false) {
|
|
for v in sel {
|
|
if let Some(i) = s.state.selected_vertices.iter().position(|x| *x == v) {
|
|
if b_p(p, "toggle").unwrap_or(false) {
|
|
s.state.selected_vertices.remove(i);
|
|
}
|
|
} else {
|
|
s.state.selected_vertices.push(v);
|
|
}
|
|
}
|
|
} else {
|
|
s.state.selected_vertices = sel;
|
|
}
|
|
Ok(json!(s.state.selected_vertices.len()))
|
|
}
|
|
|
|
fn has_vertices(s: &Session) -> std::result::Result<(), String> {
|
|
has_comp(s)?;
|
|
if s.state.selected_vertices.is_empty() { Err("select mask vertices first".into()) } else { Ok(()) }
|
|
}
|
|
|
|
/// Apply `f` to the selected vertices' paths (each mask once).
|
|
fn edit_vertices(s: &mut Session, p: &Value, label: &str, f: impl Fn(&mut ShapePath, &[usize])) -> Result<Value> {
|
|
edit_vertices_counted(s, p, label, false, f)
|
|
}
|
|
|
|
/// [`edit_vertices`]; `removes`: `f` deletes the given vertices (repeated on the other keyframes
|
|
/// when Preserve Constant Vertex Count is on).
|
|
fn edit_vertices_counted(s: &mut Session, p: &Value, label: &str, removes: bool, f: impl Fn(&mut ShapePath, &[usize])) -> Result<Value> {
|
|
let cid = s.active_comp_id().ok_or(EngineError::NoComp)?;
|
|
let groups = vertex_groups(s, p)?;
|
|
let t = s.time();
|
|
let preserve = removes && s.prefs.general.preserve_constant_vertex_count;
|
|
s.edit(label, merge_p(p), |proj, _| {
|
|
let comp = proj.comp_mut(cid).ok_or(EngineError::NoComp)?;
|
|
for ((l, m), idx) in &groups {
|
|
let Some(layer) = comp.layer_mut(effectcraft_project::LayerId(*l)) else { continue };
|
|
let lt = layer.layer_time(t);
|
|
let Some(g) = layer.props.find_group_mut(*m).filter(|g| is_path_group(g)) else { continue };
|
|
let roto = super::paths::is_roto(g);
|
|
let Some(pr) = g.get_mut("path") else { continue };
|
|
let KV::Path(mut sp) = pr.value_at(lt) else { continue };
|
|
f(&mut sp, idx);
|
|
if roto {
|
|
super::paths::roto_smooth(&mut sp);
|
|
}
|
|
pr.set_value_at(lt, KV::Path(sp));
|
|
if preserve {
|
|
sync_keys(pr, lt, &CountOp::Remove(idx.clone()));
|
|
}
|
|
}
|
|
Ok(())
|
|
})?;
|
|
Ok(json!(groups.values().map(Vec::len).sum::<usize>()))
|
|
}
|
|
|
|
fn move_vertices(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let d = pt(p.get("delta")).ok_or_else(|| bad("mask.moveVertices", "missing `delta` [dx,dy] (layer space)"))?;
|
|
edit_vertices(s, p, "Move Mask Vertices", |sp, idx| {
|
|
for &i in idx {
|
|
if let Some(v) = sp.vertices.get_mut(i) {
|
|
v[0] += d[0];
|
|
v[1] += d[1];
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fn delete_vertices(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let n = edit_vertices_counted(s, p, "Delete Mask Vertices", true, |sp, idx| {
|
|
let mut idx = idx.to_vec();
|
|
idx.sort_unstable();
|
|
idx.dedup();
|
|
for &i in idx.iter().rev() {
|
|
if i < sp.vertices.len() {
|
|
sp.vertices.remove(i);
|
|
sp.in_tangents.remove(i);
|
|
sp.out_tangents.remove(i);
|
|
sp.feather_after_remove(i);
|
|
}
|
|
}
|
|
if sp.vertices.len() < 3 {
|
|
sp.closed = false;
|
|
}
|
|
})?;
|
|
s.state.selected_vertices.clear();
|
|
Ok(n)
|
|
}
|
|
|
|
/// Convert Vertex tool: a corner vertex becomes smooth (tangents along its neighbours, a sixth
|
|
/// of their distance each way), a smooth one becomes a corner (no tangents). `smooth` forces one.
|
|
fn convert_vertex(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let i = p.get("index").and_then(Value::as_u64).ok_or_else(|| bad("mask.convertVertex", "missing `index`"))? as usize;
|
|
let force = b_p(p, "smooth");
|
|
let (_, smooth) = edit_path(s, p, "Convert Vertex", |sp| {
|
|
let n = sp.vertices.len();
|
|
if i >= n {
|
|
return Err(bad("mask.convertVertex", "no such vertex"));
|
|
}
|
|
sp.in_tangents.resize(n, [0.0; 2]);
|
|
sp.out_tangents.resize(n, [0.0; 2]);
|
|
let is_smooth = sp.in_tangents[i] != [0.0; 2] || sp.out_tangents[i] != [0.0; 2];
|
|
let make_smooth = force.unwrap_or(!is_smooth);
|
|
if make_smooth {
|
|
let prev = if i > 0 {
|
|
Some(i - 1)
|
|
} else if sp.closed {
|
|
Some(n - 1)
|
|
} else {
|
|
None
|
|
};
|
|
let next = if i + 1 < n {
|
|
Some(i + 1)
|
|
} else if sp.closed {
|
|
Some(0)
|
|
} else {
|
|
None
|
|
};
|
|
let v = sp.vertices[i];
|
|
let a = prev.map(|j| sp.vertices[j]).unwrap_or(v);
|
|
let b = next.map(|j| sp.vertices[j]).unwrap_or(v);
|
|
let mut d = [(b[0] - a[0]) / 6.0, (b[1] - a[1]) / 6.0];
|
|
if d == [0.0, 0.0] {
|
|
d = [10.0, 0.0];
|
|
}
|
|
sp.out_tangents[i] = d;
|
|
sp.in_tangents[i] = [-d[0], -d[1]];
|
|
} else {
|
|
sp.in_tangents[i] = [0.0; 2];
|
|
sp.out_tangents[i] = [0.0; 2];
|
|
}
|
|
Ok(make_smooth)
|
|
})?;
|
|
Ok(json!({"smooth": smooth}))
|
|
}
|
|
|
|
/// Split cubic segment `i` (vertex i → i+1) at parameter `t`; the path keeps its shape.
|
|
pub(crate) fn split_segment(sp: &mut ShapePath, seg: usize, t: f64) -> Option<usize> {
|
|
let n = sp.vertices.len();
|
|
let j = if seg + 1 < n {
|
|
seg + 1
|
|
} else if sp.closed && seg + 1 == n {
|
|
0
|
|
} else {
|
|
return None;
|
|
};
|
|
sp.in_tangents.resize(n, [0.0; 2]);
|
|
sp.out_tangents.resize(n, [0.0; 2]);
|
|
let p0 = sp.vertices[seg];
|
|
let p3 = sp.vertices[j];
|
|
let p1 = [p0[0] + sp.out_tangents[seg][0], p0[1] + sp.out_tangents[seg][1]];
|
|
let p2 = [p3[0] + sp.in_tangents[j][0], p3[1] + sp.in_tangents[j][1]];
|
|
let lerp = |a: [f64; 2], b: [f64; 2]| [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
|
|
let (a, b, c) = (lerp(p0, p1), lerp(p1, p2), lerp(p2, p3));
|
|
let (d, e) = (lerp(a, b), lerp(b, c));
|
|
let m = lerp(d, e);
|
|
sp.out_tangents[seg] = [a[0] - p0[0], a[1] - p0[1]];
|
|
sp.in_tangents[j] = [c[0] - p3[0], c[1] - p3[1]];
|
|
let at = seg + 1;
|
|
sp.vertices.insert(at, m);
|
|
sp.in_tangents.insert(at, [d[0] - m[0], d[1] - m[1]]);
|
|
sp.out_tangents.insert(at, [e[0] - m[0], e[1] - m[1]]);
|
|
sp.feather_after_insert(at, Some(t));
|
|
Some(at)
|
|
}
|
|
|
|
/// Nearest (segment, t, distance) of a path to a point (layer space), sampled per segment.
|
|
pub(crate) fn nearest_on_path(sp: &ShapePath, q: [f64; 2]) -> Option<(usize, f64, f64)> {
|
|
let n = sp.vertices.len();
|
|
let mut best: Option<(usize, f64, f64)> = None;
|
|
for seg in 0..sp.segment_count() {
|
|
let j = (seg + 1) % n;
|
|
let p0 = sp.vertices[seg];
|
|
let p3 = sp.vertices[j];
|
|
let o = sp.out_tangents.get(seg).copied().unwrap_or([0.0; 2]);
|
|
let i = sp.in_tangents.get(j).copied().unwrap_or([0.0; 2]);
|
|
let (p1, p2) = ([p0[0] + o[0], p0[1] + o[1]], [p3[0] + i[0], p3[1] + i[1]]);
|
|
let at = |t: f64| {
|
|
let u = 1.0 - t;
|
|
let (a, b, c, d) = (u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t);
|
|
[a * p0[0] + b * p1[0] + c * p2[0] + d * p3[0], a * p0[1] + b * p1[1] + c * p2[1] + d * p3[1]]
|
|
};
|
|
let dist = |t: f64| {
|
|
let p = at(t);
|
|
(p[0] - q[0]).hypot(p[1] - q[1])
|
|
};
|
|
let (mut bt, mut bd) = (0.0, f64::INFINITY);
|
|
for k in 0..=64 {
|
|
let t = k as f64 / 64.0;
|
|
let d = dist(t);
|
|
if d < bd {
|
|
(bt, bd) = (t, d);
|
|
}
|
|
}
|
|
// Refine by bisection-style narrowing.
|
|
let mut step = 1.0 / 64.0;
|
|
for _ in 0..20 {
|
|
step *= 0.5;
|
|
for t in [bt - step, bt + step] {
|
|
let t = t.clamp(0.0, 1.0);
|
|
let d = dist(t);
|
|
if d < bd {
|
|
(bt, bd) = (t, d);
|
|
}
|
|
}
|
|
}
|
|
if best.is_none_or(|b| bd < b.2) {
|
|
best = Some((seg, bt, bd));
|
|
}
|
|
}
|
|
best
|
|
}
|
|
|
|
/// Whether a layer-space point is inside a closed path.
|
|
fn inside_path(sp: &ShapePath, q: [f64; 2]) -> bool {
|
|
sp.closed && {
|
|
let cov = effectcraft_path::fill_coverage(
|
|
&[effectcraft_path::to_kurbo(sp)],
|
|
&effectcraft_geom::Mat3::translate(effectcraft_geom::vec2(-q[0] + 0.5, -q[1] + 0.5)),
|
|
1,
|
|
1,
|
|
effectcraft_path::FillRule::NonZero,
|
|
);
|
|
cov.data[0] >= 0.5
|
|
}
|
|
}
|
|
|
|
/// Point of segment `seg` at parameter `t`.
|
|
fn point_on(sp: &ShapePath, seg: usize, t: f64) -> Option<[f64; 2]> {
|
|
let n = sp.vertices.len();
|
|
if seg >= sp.segment_count() {
|
|
return None;
|
|
}
|
|
let j = (seg + 1) % n;
|
|
let (p0, p3) = (sp.vertices[seg], sp.vertices[j]);
|
|
let o = sp.out_tangents.get(seg).copied().unwrap_or([0.0; 2]);
|
|
let i = sp.in_tangents.get(j).copied().unwrap_or([0.0; 2]);
|
|
let (p1, p2) = ([p0[0] + o[0], p0[1] + o[1]], [p3[0] + i[0], p3[1] + i[1]]);
|
|
let u = 1.0 - t;
|
|
let (a, b, c, d) = (u * u * u, 3.0 * u * u * t, 3.0 * u * t * t, t * t * t);
|
|
Some([a * p0[0] + b * p1[0] + c * p2[0] + d * p3[0], a * p0[1] + b * p1[1] + c * p2[1] + d * p3[1]])
|
|
}
|
|
|
|
fn feather_index(p: &Value, sp: &ShapePath, c: &str) -> Result<usize> {
|
|
let i = p.get("index").and_then(Value::as_u64).ok_or_else(|| bad(c, "missing `index` (feather point, 0-based)"))? as usize;
|
|
if i >= sp.feather.len() {
|
|
return Err(bad(c, format!("no feather point {i} (the mask has {})", sp.feather.len())));
|
|
}
|
|
Ok(i)
|
|
}
|
|
|
|
/// Mask Feather tool: add a feather point on the path, by `segment`/`t` or at the path position
|
|
/// nearest `point` (layer space). Without `radius`, a `point` off the path sets it from its
|
|
/// distance (outside the mask = outer feather, inside = inner).
|
|
fn feather_add(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let c = "mask.featherPoint.add";
|
|
let seg = p.get("segment").and_then(Value::as_u64).map(|v| v as usize);
|
|
let t = super::f_p(p, "t").map(|v| v.clamp(0.0, 1.0));
|
|
let q = pt(p.get("point"));
|
|
let radius = super::f_p(p, "radius");
|
|
let tension = super::f_p(p, "tension").unwrap_or(0.0).clamp(0.0, 100.0) / 100.0;
|
|
let (uid, i) = edit_path(s, p, c, |sp| {
|
|
let (seg, t, r) = match (seg, q) {
|
|
(Some(seg), _) => {
|
|
if seg >= sp.segment_count() {
|
|
return Err(bad(c, format!("no segment {seg}")));
|
|
}
|
|
(seg, t.unwrap_or(0.5), radius.unwrap_or(0.0))
|
|
}
|
|
(None, Some(q)) => {
|
|
let (seg, t, d) = nearest_on_path(sp, q).ok_or_else(|| bad(c, "the path has no segments"))?;
|
|
(seg, t, radius.unwrap_or(if inside_path(sp, q) { -d } else { d }))
|
|
}
|
|
(None, None) => return Err(bad(c, "give `segment` (and `t`) or `point`")),
|
|
};
|
|
sp.feather.push(effectcraft_keyframe::FeatherPoint { segment: seg, t, radius: r, tension });
|
|
Ok(sp.feather.len() - 1)
|
|
})?;
|
|
Ok(json!({"mask": uid, "index": i}))
|
|
}
|
|
|
|
/// Drag a feather point: move it along the path (`segment`/`t`, or nearest `point`) and/or set
|
|
/// its `radius` (signed: negative = inner) and `tension` (%).
|
|
fn feather_set(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let c = "mask.featherPoint.set";
|
|
let seg = p.get("segment").and_then(Value::as_u64).map(|v| v as usize);
|
|
let t = super::f_p(p, "t").map(|v| v.clamp(0.0, 1.0));
|
|
let q = pt(p.get("point"));
|
|
let radius = super::f_p(p, "radius");
|
|
let tension = super::f_p(p, "tension").map(|v| v.clamp(0.0, 100.0) / 100.0);
|
|
let toward = pt(p.get("toward"));
|
|
let (_, out) = edit_path(s, p, c, |sp| {
|
|
let i = feather_index(p, sp, c)?;
|
|
if let Some(q) = q {
|
|
let (sg, tt, _) = nearest_on_path(sp, q).ok_or_else(|| bad(c, "the path has no segments"))?;
|
|
sp.feather[i].segment = sg;
|
|
sp.feather[i].t = tt;
|
|
}
|
|
if let Some(sg) = seg {
|
|
if sg >= sp.segment_count() {
|
|
return Err(bad(c, format!("no segment {sg}")));
|
|
}
|
|
sp.feather[i].segment = sg;
|
|
}
|
|
if let Some(t) = t {
|
|
sp.feather[i].t = t;
|
|
}
|
|
if let Some(r) = radius {
|
|
sp.feather[i].radius = r;
|
|
}
|
|
// Dragging the radius handle: the distance from the point's path position to `toward`,
|
|
// outer outside the mask, inner inside.
|
|
if let Some(w) = toward {
|
|
let f = sp.feather[i];
|
|
let at = point_on(sp, f.segment, f.t).ok_or_else(|| bad(c, "the feather point is off the path"))?;
|
|
let d = (w[0] - at[0]).hypot(w[1] - at[1]);
|
|
sp.feather[i].radius = if inside_path(sp, w) { -d } else { d };
|
|
}
|
|
if let Some(k) = tension {
|
|
sp.feather[i].tension = k;
|
|
}
|
|
Ok(serde_json::to_value(sp.feather[i]).unwrap_or(Value::Null))
|
|
})?;
|
|
Ok(out)
|
|
}
|
|
|
|
fn feather_remove(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let c = "mask.featherPoint.remove";
|
|
let all = super::b_p(p, "all").unwrap_or(false);
|
|
let (_, n) = edit_path(s, p, c, |sp| {
|
|
if all {
|
|
sp.feather.clear();
|
|
} else {
|
|
let i = feather_index(p, sp, c)?;
|
|
sp.feather.remove(i);
|
|
}
|
|
Ok(sp.feather.len())
|
|
})?;
|
|
Ok(json!({"remaining": n}))
|
|
}
|
|
|
|
fn feather_list(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let c = "mask.featherPoint.list";
|
|
let (cid, lid) = layer_p(s, p, c)?;
|
|
let key = p.get("mask").cloned().ok_or_else(|| bad(c, "missing `mask`"))?;
|
|
let t = s.time();
|
|
let mut props = s.project.comp(cid).and_then(|cc| cc.layer(lid)).ok_or(EngineError::NoComp)?.props.clone();
|
|
let lt = s.project.comp(cid).and_then(|cc| cc.layer(lid)).map(|l| l.layer_time(t)).unwrap_or(t);
|
|
let g = path_group(&mut props, &key).ok_or_else(|| bad(c, "no such mask"))?;
|
|
let Some(KV::Path(sp)) = g.get("path").map(|pr| pr.value_at(lt)) else { return Err(bad(c, "mask has no path")) };
|
|
Ok(
|
|
json!({"points": sp.feather.iter().map(|f| json!({"segment": f.segment, "t": f.t, "radius": f.radius, "tension": f.tension * 100.0})).collect::<Vec<_>>()}),
|
|
)
|
|
}
|
|
|
|
/// Add Vertex tool: insert a vertex on segment `segment` at parameter `t` (default 0.5).
|
|
fn insert_vertex(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let seg =
|
|
p.get("segment").and_then(Value::as_u64).ok_or_else(|| bad("mask.insertVertex", "missing `segment` (index of the segment's first vertex)"))? as usize;
|
|
let t = super::f_p(p, "t").unwrap_or(0.5).clamp(0.0, 1.0);
|
|
let (_, lid) = layer_p(s, p, "mask.insertVertex")?;
|
|
let (uid, i) = edit_path_counted(s, p, "Add Vertex", |sp| {
|
|
let i = split_segment(sp, seg, t).ok_or_else(|| bad("mask.insertVertex", "no such segment"))?;
|
|
Ok((i, Some(CountOp::Insert(i))))
|
|
})?;
|
|
s.state.selected_vertices = vec![VertexRef { layer: lid, mask: uid, index: i }];
|
|
Ok(json!(i))
|
|
}
|
|
|
|
fn remove_masks(s: &mut Session, p: &Value, all: bool) -> Result<Value> {
|
|
let (cid, lid) = layer_p(s, p, "mask.remove")?;
|
|
let key = p.get("mask").cloned();
|
|
if !all && key.is_none() {
|
|
return Err(bad("mask.remove", "missing `mask`"));
|
|
}
|
|
s.edit(if all { "Remove All Masks" } else { "Remove Mask" }, None, |proj, st| {
|
|
let l: &mut Layer = layer_mut(proj, cid, lid)?;
|
|
let masks = l.props.sub_mut("masks").ok_or_else(|| bad("mask.remove", "this layer has no masks"))?;
|
|
if all {
|
|
masks.children.clear();
|
|
} else if let Some(k) = &key {
|
|
let uid = find_mask(masks, k).map(|g| g.uid).ok_or_else(|| bad("mask.remove", "no such mask"))?;
|
|
masks.children.retain(|c| c.uid() != uid);
|
|
}
|
|
st.selected_vertices.retain(|v| v.layer != lid);
|
|
Ok(())
|
|
})?;
|
|
Ok(Value::Null)
|
|
}
|
|
|
|
pub fn specs() -> Vec<CommandSpec> {
|
|
vec![
|
|
cmd!("mask.new", "New Mask from Points", [], None, "{layer?, vertices: [[x,y]…], inTangents?, outTangents?, closed?, mode?}", has_layers, new_mask),
|
|
cmd!("mask.addVertex", "Add Mask Vertex", [], None, "{layer?, mask, point: [x,y], in?, out?, index?}", has_layers, add_vertex),
|
|
cmd!("mask.setVertex", "Set Mask Vertex", [], None, "{layer?, mask, index, point?, in?, out?, merge?}", has_layers, set_vertex),
|
|
cmd!("mask.setClosed", "Closed", ["Layer", "Mask and Shape Path"], None, "{layer?, mask, closed?}", has_layers, set_closed),
|
|
cmd!("mask.selectVertices", "Select Mask Vertices", [], None, "{vertices: [{layer, mask, index}], add?, toggle?}", has_comp, select_vertices),
|
|
cmd!("mask.moveVertices", "Move Mask Vertices", [], None, "{vertices?: [{layer, mask, index}], delta: [dx,dy], merge?}", has_vertices, move_vertices),
|
|
cmd!("mask.deleteVertices", "Delete Mask Vertices", [], None, "{vertices?}", has_vertices, delete_vertices),
|
|
cmd!("mask.convertVertex", "Convert Vertex", [], None, "{layer?, mask: uid (mask or shape Path item), index, smooth?}", has_comp, convert_vertex),
|
|
cmd!("mask.insertVertex", "Add Vertex", [], None, "{layer?, mask: uid (mask or shape Path item), segment, t?: 0..1}", has_comp, insert_vertex),
|
|
cmd!(
|
|
"mask.featherPoint.add",
|
|
"Add Mask Feather Point",
|
|
[],
|
|
None,
|
|
"{layer?, mask, segment?, t?: 0..1, point?: [x,y] (layer space), radius? (px, negative = inner), tension? (%)} → {index}",
|
|
has_comp,
|
|
feather_add
|
|
),
|
|
cmd!(
|
|
"mask.featherPoint.set",
|
|
"Move Mask Feather Point",
|
|
[],
|
|
None,
|
|
"{layer?, mask, index, segment?, t?, point? (slide along the path), radius?, toward?: [x,y] (radius from the drag point, layer space), tension? (%), merge?}",
|
|
has_comp,
|
|
feather_set
|
|
),
|
|
cmd!("mask.featherPoint.remove", "Delete Mask Feather Point", [], None, "{layer?, mask, index? | all?}", has_comp, feather_remove),
|
|
crate::query!("mask.featherPoint.list", "Mask Feather Points", "{layer?, mask}", feather_list),
|
|
cmd!("mask.remove", "Remove Mask", [], None, "{layer?, mask}", has_layers, |s, p| remove_masks(s, p, false)),
|
|
cmd!("mask.removeAll", "Remove All Masks", [], None, "{layer?}", has_layers, |s, p| remove_masks(s, p, true)),
|
|
]
|
|
}
|