mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-29 20:47:18 -06:00
Merge pull request #289 from storytold/fix/227-shape-effect-points
Effects: effect points on shape layers start at the layer centre (#227) # Conflicts: # crates/ui-egui/src/panels/puppet_tool.rs # crates/ui-egui/src/panels/viewer.rs
This commit is contained in:
@@ -320,8 +320,7 @@ impl Session {
|
||||
if times.len() < 2 {
|
||||
return Err("the layer must be at least two frames long".into());
|
||||
}
|
||||
let (w, h) = effectcraft_render::source_size(&self.project, l);
|
||||
let size = if w == 0 { [c.width as f64, c.height as f64] } else { [w as f64, h as f64] };
|
||||
let size = effectcraft_render::effect_bounds(&self.project, c, l).0;
|
||||
let params = static_params(g);
|
||||
let key = tracks_key(&self.project, comp, l, g);
|
||||
let settings = ct::settings(¶ms);
|
||||
|
||||
@@ -78,13 +78,7 @@ fn apply(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let sizes: Vec<_> = {
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||||
ids.iter()
|
||||
.filter_map(|id| comp.layer(*id))
|
||||
.map(|l| {
|
||||
let (w, h) = effectcraft_render::source_size(&s.project, l);
|
||||
(l.id, if w == 0 { [comp.width as f64, comp.height as f64] } else { [w as f64, h as f64] })
|
||||
})
|
||||
.collect()
|
||||
ids.iter().filter_map(|id| comp.layer(*id)).map(|l| (l.id, effectcraft_render::effect_bounds(&s.project, comp, l).0)).collect()
|
||||
};
|
||||
let uids = s.edit(&format!("Apply {}", spec.name), None, |proj, st| {
|
||||
let mut out = vec![];
|
||||
@@ -368,8 +362,7 @@ fn reset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (cid, lid, uid) = find_fx(s, p, "effect.reset")?;
|
||||
let comp = s.project.comp(cid).ok_or(EngineError::NoComp)?;
|
||||
let layer = comp.layer(lid).ok_or(EngineError::NoComp)?;
|
||||
let (w, h) = effectcraft_render::source_size(&s.project, layer);
|
||||
let size = if w == 0 { [comp.width as f64, comp.height as f64] } else { [w as f64, h as f64] };
|
||||
let size = effectcraft_render::effect_bounds(&s.project, comp, layer).0;
|
||||
let lt = layer.layer_time(s.time());
|
||||
let g = layer.effects().and_then(|fx| fx.groups().find(|g| g.uid == uid)).ok_or_else(|| bad("effect.reset", "gone"))?;
|
||||
let spec = match &g.kind {
|
||||
@@ -450,7 +443,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Pick Effect Colour",
|
||||
[],
|
||||
None,
|
||||
"{layer?, effect?: index|uid|name, param: id (e.g. screenColour) | prop: uid, x, y (layer px), average?: bool (5×5), time?} — the colour of the effect's input there",
|
||||
"{layer?, effect?: index|uid|name, param: id (e.g. screenColour) | prop: uid, x, y (effect space), average?: bool (5×5), time?} — the colour of the effect's input there",
|
||||
has_layers,
|
||||
pick_color
|
||||
),
|
||||
|
||||
@@ -886,7 +886,8 @@ pub fn read_template(bytes: &[u8]) -> std::result::Result<(Value, Project, Vec<(
|
||||
return Err("the template was made by a newer EffectCraft".into());
|
||||
}
|
||||
let text = String::from_utf8(get("project.ecproj").ok_or("template has no project.ecproj")?).map_err(|e| e.to_string())?;
|
||||
let project = Project::from_json(&text).map_err(|e| e.to_string())?;
|
||||
let mut project = Project::from_json(&text).map_err(|e| e.to_string())?;
|
||||
crate::upgrade_effects(&mut project);
|
||||
Ok((manifest, project, entries))
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ fn stroke(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.iter()
|
||||
.position(|t| t.eq_ignore_ascii_case(tool_name))
|
||||
.ok_or_else(|| bad(cmd, format!("unknown tool `{tool_name}` ({})", LIQUIFY_TOOLS.join(" | "))))?;
|
||||
let raw = p.get("points").and_then(Value::as_array).ok_or_else(|| bad(cmd, "missing `points` [[x, y], …] (layer space)"))?;
|
||||
let raw = p.get("points").and_then(Value::as_array).ok_or_else(|| bad(cmd, "missing `points` [[x, y], …] (effect space)"))?;
|
||||
let mut points = vec![];
|
||||
for q in raw {
|
||||
let a = q.as_array().ok_or_else(|| bad(cmd, "each point is [x, y]"))?;
|
||||
@@ -66,8 +66,7 @@ fn stroke(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
return Err(bad(cmd, "Liquify works on footage, solid, text, shape and precomp layers"));
|
||||
}
|
||||
let lt = layer.layer_time(t);
|
||||
let (w, h) = effectcraft_render::source_size(&s.project, layer);
|
||||
let size = if w == 0 { [comp.width as f64, comp.height as f64] } else { [w as f64, h as f64] };
|
||||
let size = effectcraft_render::effect_bounds(&s.project, comp, layer).0;
|
||||
let want = p.get("effect").and_then(Value::as_u64);
|
||||
let (fx_uid, count) = s.edit("Liquify Stroke", None, |proj, _st| {
|
||||
let mut next = proj.next_id;
|
||||
@@ -147,7 +146,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Liquify Stroke",
|
||||
[],
|
||||
None,
|
||||
"{layer?, effect?: uid, tool?: warp|turbulence|twirlClockwise|twirlCounterclockwise|pucker|bloat|shiftPixels|reflection|clone|reconstruction|freeze|thaw, points: [[x,y],…] (layer space), size?, pressure? (1-100), jitter? (1-100), cloneOffset?: [dx,dy], time? (s) | frame?}",
|
||||
"{layer?, effect?: uid, tool?: warp|turbulence|twirlClockwise|twirlCounterclockwise|pucker|bloat|shiftPixels|reflection|clone|reconstruction|freeze|thaw, points: [[x,y],…] (effect space), size?, pressure? (1-100), jitter? (1-100), cloneOffset?: [dx,dy], time? (s) | frame?}",
|
||||
has_comp,
|
||||
stroke
|
||||
),
|
||||
|
||||
@@ -53,7 +53,7 @@ pub struct PaintOptions {
|
||||
pub clone_preset: u32,
|
||||
/// Source layer (None = the layer being painted).
|
||||
pub clone_source: Option<u64>,
|
||||
/// Source point set with Alt/Option-click (layer space of the source layer).
|
||||
/// Source point set with Alt/Option-click (effect space of the source layer).
|
||||
pub clone_point: Option<[f64; 2]>,
|
||||
/// Aligned: keep the source/destination offset between strokes.
|
||||
pub aligned: bool,
|
||||
@@ -271,7 +271,7 @@ fn preset(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn set_clone_source(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let point = pt(p.get("point")).ok_or_else(|| bad("paint.setCloneSource", "missing `point` [x, y] (layer space)"))?;
|
||||
let point = pt(p.get("point")).ok_or_else(|| bad("paint.setCloneSource", "missing `point` [x, y] (effect space)"))?;
|
||||
let (_, lid) = layer_p(s, p, "paint.setCloneSource")?;
|
||||
s.state.paint.clone_source = Some(lid.0);
|
||||
s.state.paint.clone_point = Some(point);
|
||||
@@ -300,7 +300,7 @@ fn stroke(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Some(k) => StrokeKind::from_name(k).ok_or_else(|| bad(cmd, format!("unknown kind `{k}` (brush | clone | eraser)")))?,
|
||||
None => StrokeKind::Brush,
|
||||
};
|
||||
let raw = p.get("points").and_then(Value::as_array).ok_or_else(|| bad(cmd, "missing `points` [[x, y, pressure?], …] (layer space)"))?;
|
||||
let raw = p.get("points").and_then(Value::as_array).ok_or_else(|| bad(cmd, "missing `points` [[x, y, pressure?], …] (effect space)"))?;
|
||||
let mut points = vec![];
|
||||
let mut pressure = vec![];
|
||||
for q in raw {
|
||||
@@ -325,8 +325,7 @@ fn stroke(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let t = fr.snap_nearest(super::time_p(s, p, Some(comp)));
|
||||
let layer = comp.layer(lid).ok_or_else(|| bad(cmd, "no layer"))?;
|
||||
let lt = layer.layer_time(t);
|
||||
let (w, h) = effectcraft_render::source_size(&s.project, layer);
|
||||
let size = if w == 0 { [comp.width as f64, comp.height as f64] } else { [w as f64, h as f64] };
|
||||
let size = effectcraft_render::effect_bounds(&s.project, comp, layer).0;
|
||||
if layer.effects().is_none() || !layer.source.is_av() {
|
||||
return Err(bad(cmd, "this layer can't be painted on"));
|
||||
}
|
||||
@@ -463,7 +462,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Paint Stroke",
|
||||
[],
|
||||
None,
|
||||
"{layer?, kind?: brush|clone|eraser, points: [[x,y,pressure?],…] (layer space), time? (s) | frame?, duration? (s, drawing time for Write On), color?: [r,g,b,a], diameter?, angle?, hardness?, roundness?, spacing?, opacity?, flow?, mode?, channels?: RGBA|RGB|Alpha, durationMode?: constant|writeOn|singleFrame|custom, customFrames?, eraseMode?: layerSourceAndPaint|paintOnly|lastStrokeOnly, cloneSource?: layer id, clonePosition?: [x,y], cloneTimeShift? (s), lockSourceTime?, cloneTime? (s), sizePressure?, minSize?, opacityPressure?, flowPressure?}",
|
||||
"{layer?, kind?: brush|clone|eraser, points: [[x,y,pressure?],…] (effect space), time? (s) | frame?, duration? (s, drawing time for Write On), color?: [r,g,b,a], diameter?, angle?, hardness?, roundness?, spacing?, opacity?, flow?, mode?, channels?: RGBA|RGB|Alpha, durationMode?: constant|writeOn|singleFrame|custom, customFrames?, eraseMode?: layerSourceAndPaint|paintOnly|lastStrokeOnly, cloneSource?: layer id, clonePosition?: [x,y], cloneTimeShift? (s), lockSourceTime?, cloneTime? (s), sizePressure?, minSize?, opacityPressure?, flowPressure?}",
|
||||
has_comp,
|
||||
stroke
|
||||
),
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! 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
|
||||
//! Pin positions are in effect 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.
|
||||
|
||||
@@ -64,7 +64,7 @@ fn add_pin(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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 at = pt(p.get("position")).ok_or_else(|| bad(cmd, "missing `position` [x, y] (effect space)"))?;
|
||||
let t = snapped(s, cid, p)?;
|
||||
let lt = layer_time(s, cid, lid, t)?;
|
||||
{
|
||||
@@ -108,8 +108,7 @@ fn add_pin(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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] }
|
||||
effectcraft_render::effect_bounds(&s.project, comp, l).0
|
||||
};
|
||||
let label = match kind {
|
||||
PinKind::Starch => "Add Starch Pin",
|
||||
@@ -278,7 +277,7 @@ fn with_pin<T>(s: &mut Session, p: &Value, cmd: &str, label: &str, f: impl FnOnc
|
||||
|
||||
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)"))?;
|
||||
let to = pt(p.get("position")).ok_or_else(|| bad(cmd, "missing `position` [x, y] (effect 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)"))?;
|
||||
@@ -387,12 +386,12 @@ fn mesh_opts(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
/// Parse recorded samples `[[t, x, y]…]` (t = seconds since the drag began, layer space).
|
||||
/// Parse recorded samples `[[t, x, y]…]` (t = seconds since the drag began, effect 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)"))?;
|
||||
.ok_or_else(|| bad(cmd, "missing `samples`: [[t, x, y]…] (t = seconds since the drag began, effect 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();
|
||||
@@ -602,7 +601,7 @@ fn record_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
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).
|
||||
/// Meshes, pins and the deformed mesh at the current time (effect 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": []})) };
|
||||
@@ -632,7 +631,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"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?}",
|
||||
"{layer?, kind?: position|advanced|bend|starch|overlap, position: [x,y] (effect space), time? (s) | frame?, mesh?: uid, newMesh?, density?, expansion?, triangles?}",
|
||||
has_comp,
|
||||
add_pin
|
||||
),
|
||||
@@ -671,7 +670,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"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)}",
|
||||
"{layer?, pin: uid | name, samples: [[t, x, y]…] (t = seconds since the drag began, effect 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
|
||||
),
|
||||
|
||||
@@ -949,31 +949,46 @@ impl Session {
|
||||
|
||||
/// Bring effect instances saved by earlier versions up to date with the effect registry
|
||||
/// (renamed / regrouped / added parameters, reordered popups; see
|
||||
/// [`effectcraft_effects::migrate`]).
|
||||
/// [`effectcraft_effects::migrate`]), and a project from before
|
||||
/// [`effectcraft_project::SCHEMA_EFFECT_SPACE`] into effect space
|
||||
/// ([`effectcraft_effects::migrate::to_effect_space`]). Stamps the current schema.
|
||||
pub fn upgrade_effects(p: &mut Project) {
|
||||
let mut sizes = std::collections::HashMap::new();
|
||||
let to_effect_space = p.schema < effectcraft_project::SCHEMA_EFFECT_SPACE;
|
||||
// Effect bounds per layer: (size, top-left in layer coordinates).
|
||||
let mut bounds = std::collections::HashMap::new();
|
||||
for (cid, c) in p.comps() {
|
||||
for l in &c.layers {
|
||||
let (w, h) = effectcraft_render::source_size(p, l);
|
||||
sizes.insert((*cid, l.id), if w == 0 { [c.width as f64, c.height as f64] } else { [w as f64, h as f64] });
|
||||
bounds.insert((*cid, l.id), effectcraft_render::effect_bounds(p, c, l));
|
||||
}
|
||||
}
|
||||
let mut next = p.next_id;
|
||||
for (cid, it) in p.items.iter_mut() {
|
||||
let effectcraft_project::ItemKind::Comp(c) = &mut it.kind else { continue };
|
||||
for l in &mut std::sync::Arc::make_mut(c).layers {
|
||||
let size = sizes.get(&(*cid, l.id)).copied().unwrap_or([100.0, 100.0]);
|
||||
let (size, origin) = bounds.get(&(*cid, l.id)).copied().unwrap_or(([100.0, 100.0], [0.0; 2]));
|
||||
// Effect space starts at the bounds' top-left: positions move by minus that corner.
|
||||
// Adjustment layers run their effects on the comp below as it is: nothing moves.
|
||||
let shift = |o: [f64; 2]| [-o[0], -o[1]];
|
||||
let d = if l.switches.adjustment { [0.0; 2] } else { shift(origin) };
|
||||
let clone_d = |src: Option<u64>| match src {
|
||||
Some(id) => bounds.get(&(*cid, effectcraft_project::LayerId(id))).map_or([0.0; 2], |b| shift(b.1)),
|
||||
None => d,
|
||||
};
|
||||
let Some(fx) = l.props.sub_mut("effects") else { continue };
|
||||
for n in &mut fx.children {
|
||||
let effectcraft_project::Node::Group(g) = n else { continue };
|
||||
let effectcraft_project::GroupKind::Effect { effect } = &g.kind else { continue };
|
||||
if let Some(spec) = effectcraft_effects::find(effect) {
|
||||
effectcraft_effects::migrate::upgrade_instance(spec, g, &mut effectcraft_project::build::Ids(&mut next), size);
|
||||
if to_effect_space {
|
||||
effectcraft_effects::migrate::to_effect_space(spec, g, size, d, &clone_d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
p.next_id = next;
|
||||
p.schema = effectcraft_project::SCHEMA_VERSION;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -242,8 +242,7 @@ pub(crate) fn params_at(p: &Project, cid: ItemId, layer: &Layer, g: &PropGroup,
|
||||
|
||||
/// The layer's source size (layer pixels).
|
||||
pub(crate) fn layer_size(p: &Project, comp: &effectcraft_project::Comp, layer: &Layer) -> [f64; 2] {
|
||||
let (w, h) = effectcraft_render::source_size(p, layer);
|
||||
if w == 0 { [comp.width as f64, comp.height as f64] } else { [w as f64, h as f64] }
|
||||
effectcraft_render::effect_bounds(p, comp, layer).0
|
||||
}
|
||||
|
||||
/// First and last layer frame of the layer inside the comp (inclusive).
|
||||
|
||||
@@ -257,3 +257,63 @@ fn opening_an_old_project_upgrades_effect_instances() {
|
||||
// The upgraded project renders.
|
||||
let _ = s2.render(cid, s2.time(), Default::default());
|
||||
}
|
||||
|
||||
/// A project saved before schema 2 (#227): effect points on a shape layer were measured from
|
||||
/// its origin. Opening converts them to effect space so points that were set keep their place
|
||||
/// in the comp; points at their default become the layer centre; layers with a source
|
||||
/// rectangle, adjustment layers (their effects see the comp below as it is) and expression
|
||||
/// controls keep their values. Saving writes schema 2, and opening that again changes nothing.
|
||||
#[test]
|
||||
fn opening_a_schema_1_project_moves_shape_layer_effect_points_into_effect_space() {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "Main", "width": 200, "height": 100, "frameRate": 30, "duration": 2})).unwrap();
|
||||
let shape = s.execute("layer.newShape", json!({"kind": "rect", "size": [180, 80]})).unwrap()["layer"].as_u64().unwrap();
|
||||
let solid = s.execute("layer.newSolid", json!({"color": "#0000ff", "width": 200, "height": 100})).unwrap()["layer"].as_u64().unwrap();
|
||||
let point = |s: &Session, lid: u64, fx: u64, id: &str| -> KV {
|
||||
let l = layer(s, lid);
|
||||
let g = l.effects().unwrap().groups().find(|g| g.uid == fx).unwrap();
|
||||
g.props().find(|p| p.match_id == id || (id.is_empty() && p.ui == effectcraft_project::ParamUi::Point)).unwrap().value.clone()
|
||||
};
|
||||
let set = |s: &mut Session, lid: u64, fx: u64, id: &str, v: [f64; 2]| {
|
||||
let l = layer(s, lid);
|
||||
let g = l.effects().unwrap().groups().find(|g| g.uid == fx).unwrap();
|
||||
let uid = g.props().find(|p| p.match_id == id || (id.is_empty() && p.ui == effectcraft_project::ParamUi::Point)).unwrap().uid;
|
||||
s.execute("prop.set", json!({"layer": lid, "prop": uid, "value": v})).unwrap();
|
||||
};
|
||||
let twirl = apply(&mut s, shape, "Twirl");
|
||||
set(&mut s, shape, twirl, "center", [-30.0, -10.0]);
|
||||
let bulge = apply(&mut s, shape, "Bulge");
|
||||
let control = apply(&mut s, shape, "Point Control");
|
||||
set(&mut s, shape, control, "", [5.0, 5.0]);
|
||||
let on_solid = apply(&mut s, solid, "Twirl");
|
||||
set(&mut s, solid, on_solid, "center", [30.0, 20.0]);
|
||||
let adjust = s.execute("layer.newShape", json!({"kind": "rect", "size": [50, 50]})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.setSwitch", json!({"layers": [adjust], "switch": "adjustment", "value": true})).unwrap();
|
||||
let on_adjust = apply(&mut s, adjust, "Twirl");
|
||||
set(&mut s, adjust, on_adjust, "center", [40.0, 30.0]);
|
||||
assert_eq!(point(&s, shape, bulge, "center"), KV::Vec2([100.0, 50.0]), "Bulge's default centre");
|
||||
|
||||
let old = s.project.to_file_json().unwrap().replacen("\"schema\": 2", "\"schema\": 1", 1);
|
||||
assert!(old.contains("\"schema\": 1"));
|
||||
let dir = std::env::temp_dir().join(format!("ec-effect-space-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("old.ecproj").to_string_lossy().to_string();
|
||||
std::fs::write(&path, old).unwrap();
|
||||
let mut s2 = Session::default();
|
||||
s2.execute("file.open", json!({"path": path})).unwrap();
|
||||
assert_eq!(s2.project.schema, effectcraft_project::SCHEMA_VERSION);
|
||||
let check = |s2: &Session| {
|
||||
assert_eq!(point(s2, shape, twirl, "center"), KV::Vec2([70.0, 40.0]), "a set point keeps its place in the comp");
|
||||
assert_eq!(point(s2, shape, bulge, "center"), KV::Vec2([100.0, 50.0]), "a default point becomes the layer centre");
|
||||
assert_eq!(point(s2, shape, control, ""), KV::Vec2([5.0, 5.0]), "expression controls keep their values");
|
||||
assert_eq!(point(s2, solid, on_solid, "center"), KV::Vec2([30.0, 20.0]), "a solid's effect space is its layer space");
|
||||
assert_eq!(point(s2, adjust, on_adjust, "center"), KV::Vec2([40.0, 30.0]), "an adjustment layer's effects see the comp");
|
||||
};
|
||||
check(&s2);
|
||||
// Saved as schema 2 and opened again: nothing moves twice.
|
||||
s2.execute("file.save", json!({"path": path})).unwrap();
|
||||
let mut s3 = Session::default();
|
||||
s3.execute("file.open", json!({"path": path})).unwrap();
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
check(&s3);
|
||||
}
|
||||
|
||||
@@ -548,8 +548,8 @@ pub fn apply(p: &mut Project, cid: ItemId, src: LayerId, tracker: Uid, dims: Dim
|
||||
}
|
||||
TrackKind::Affine | TrackKind::Perspective => {
|
||||
let (ms, _) = ctx.layer_to_comp(&source);
|
||||
let (mt, _) = ctx.layer_to_comp(&target);
|
||||
let inv = mt.inverse().ok_or("the target layer's transform is not invertible")?;
|
||||
// Corner Pin's corners are effect points: in the target's effect space.
|
||||
let inv = ctx.effect_to_comp(&target).inverse().ok_or("the target layer's transform is not invertible")?;
|
||||
let n = pts.len();
|
||||
if n < 3 {
|
||||
return Err("corner pin tracks need 3 or 4 track points".into());
|
||||
@@ -564,13 +564,11 @@ pub fn apply(p: &mut Project, cid: ItemId, src: LayerId, tracker: Uid, dims: Dim
|
||||
}
|
||||
}
|
||||
let n = track.len();
|
||||
let tsize = effectcraft_render::source_size(p, &target);
|
||||
let size = effectcraft_render::effect_bounds(p, &comp, &target).0;
|
||||
let mut ids_next = p.next_id;
|
||||
let layer = p.comp_mut(cid).and_then(|c| c.layer_mut(target_id)).ok_or("no target layer")?;
|
||||
if !corner_keys.is_empty() {
|
||||
let spec = effectcraft_effects::find("ec.distort.cornerpin").ok_or("Corner Pin is not available")?;
|
||||
let size = tsize;
|
||||
let size = if size.0 == 0 { [comp.width as f64, comp.height as f64] } else { [size.0 as f64, size.1 as f64] };
|
||||
let fx = layer.props.sub_mut("effects").ok_or("the target layer can't have effects")?;
|
||||
let same = fx.groups().filter(|g| g.match_id == spec.id).count();
|
||||
let name = if same == 0 { spec.name.to_string() } else { format!("{} {}", spec.name, same + 1) };
|
||||
|
||||
@@ -324,8 +324,7 @@ impl Session {
|
||||
if times.len() < 2 {
|
||||
return Err("the layer must be at least two frames long".into());
|
||||
}
|
||||
let (w, h) = effectcraft_render::source_size(&self.project, l);
|
||||
let size = if w == 0 { [c.width as f64, c.height as f64] } else { [w as f64, h as f64] };
|
||||
let size = effectcraft_render::effect_bounds(&self.project, c, l).0;
|
||||
let detailed = g.prop("advanced/detailedAnalysis").is_some_and(|p| p.value.as_bool());
|
||||
let n = times.len();
|
||||
let work = WarpWork {
|
||||
|
||||
Reference in New Issue
Block a user