mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 15:48:33 +00:00
Scale Composition: keep the ease of depth moves under 2D parents, and scale depth once through a 3D ancestor
Position speeds stretch with the path's direction at each key and Scale's Z speed with the factor, so the ease keeps its shape. A 3D layer under a 2D parent scales its own Z only when no 3D ancestor's Scale already carries it.
This commit is contained in:
1 parent
76369bc10a
commit
76e02f88b7
2 files changed
+94
-5
No files matched your search
@@ -91,19 +91,48 @@ fn scale_prop(pr: &mut effectcraft_project::Property, f: f64) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Multiply only the Z of a 3D value (and of its keys and spatial tangents) by `f`.
|
||||
/// Multiply only the Z of a 3D value (and of its keys and spatial tangents) by `f`. Speeds follow:
|
||||
/// a spatial side's speed by how much the path's direction there stretches, so the ease keeps
|
||||
/// its shape; a per-dimension one (Scale) by `f` for Z only.
|
||||
fn scale_prop_z(pr: &mut effectcraft_project::Property, f: f64) {
|
||||
use effectcraft_keyframe::Value as KV;
|
||||
let stretch = |v: [f64; 3]| {
|
||||
let n = v[0].hypot(v[1]).hypot(v[2]);
|
||||
if n > 1e-12 && n.is_finite() { v[0].hypot(v[1]).hypot(v[2] * f) / n } else { 1.0 }
|
||||
};
|
||||
let pos = |k: &effectcraft_project::Keyframe| k.value.as_vec3();
|
||||
let sub = |a: [f64; 3], b: [f64; 3]| [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
||||
let is_zero = |v: [f64; 3]| v.iter().all(|x| x.abs() <= 1e-12);
|
||||
// Read every side's direction before anything changes (auto tangents come from the neighbours).
|
||||
let sides: Vec<(f64, f64)> = (0..pr.keys.len())
|
||||
.map(|i| {
|
||||
let (tin, tout) = effectcraft_keyframe::spatial_tangents(&pr.keys, i);
|
||||
let here = pr.keys.get(i).map(pos).unwrap_or_default();
|
||||
let chord = |j: Option<usize>| j.and_then(|j| pr.keys.get(j)).map(|k| sub(pos(k), here)).unwrap_or_default();
|
||||
let tin = if is_zero(tin) { chord(i.checked_sub(1)) } else { tin };
|
||||
let tout = if is_zero(tout) { chord(Some(i + 1)) } else { tout };
|
||||
(stretch(tin), stretch(tout))
|
||||
})
|
||||
.collect();
|
||||
let spatial = pr.spatial;
|
||||
let mul = |v: &mut KV| {
|
||||
if let KV::Vec3(a) = v {
|
||||
a[2] *= f;
|
||||
}
|
||||
};
|
||||
mul(&mut pr.value);
|
||||
for k in &mut pr.keys {
|
||||
for (k, (s_in, s_out)) in pr.keys.iter_mut().zip(sides) {
|
||||
mul(&mut k.value);
|
||||
k.spatial_in[2] *= f;
|
||||
k.spatial_out[2] *= f;
|
||||
if spatial {
|
||||
k.in_ease.iter_mut().for_each(|e| e.speed *= s_in);
|
||||
k.out_ease.iter_mut().for_each(|e| e.speed *= s_out);
|
||||
} else {
|
||||
for e in [k.in_ease.get_mut(2), k.out_ease.get_mut(2)].into_iter().flatten() {
|
||||
e.speed *= f;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,10 +165,22 @@ fn scale_comp(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// A parent's Scale carries its children (X and Y only when the parent is 2D); a camera or
|
||||
// light parent has none.
|
||||
let scaling_parents: std::collections::HashMap<_, _> =
|
||||
c.layers.iter().filter(|l| l.transform().is_some_and(|tr| tr.get("scale").is_some())).map(|l| (l.id, l.is_3d())).collect();
|
||||
c.layers.iter().filter(|l| l.transform().is_some_and(|tr| tr.get("scale").is_some())).map(|l| (l.id, (l.is_3d(), l.parent))).collect();
|
||||
// A 3D ancestor's Scale carries depth through any 2D layers between (bounded: parent
|
||||
// chains in a file can loop).
|
||||
let depth_carried = |mut cur: Option<effectcraft_project::LayerId>| {
|
||||
for _ in 0..64 {
|
||||
let Some((three_d, up)) = cur.and_then(|id| scaling_parents.get(&id)) else { return false };
|
||||
if *three_d {
|
||||
return true;
|
||||
}
|
||||
cur = *up;
|
||||
}
|
||||
false
|
||||
};
|
||||
for l in &mut c.layers {
|
||||
let parent_3d = l.parent.and_then(|id| scaling_parents.get(&id).copied());
|
||||
let flat_parent = parent_3d == Some(false) && l.is_3d();
|
||||
let parent_3d = l.parent.and_then(|id| scaling_parents.get(&id).map(|p| p.0));
|
||||
let flat_parent = parent_3d == Some(false) && l.is_3d() && !depth_carried(l.parent);
|
||||
let Some(tr) = l.transform_mut() else { continue };
|
||||
match parent_3d {
|
||||
None => {
|
||||
|
||||
@@ -147,6 +147,54 @@ fn scale_comp_scales_depth_under_a_2d_parent_and_children_of_a_camera() {
|
||||
assert_eq!(v3(&s, held, "transform/scale", 0.0), held_scale0.map(|v| v * 2.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scale_comp_keeps_the_ease_of_depth_moves_under_a_2d_parent() {
|
||||
let mut s = comp(200, 100);
|
||||
let card = solid(&mut s, "#ff0000", 20, 10);
|
||||
let rig = s.execute("layer.newNull", json!({})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.setSwitch", json!({"layers": [card], "switch": "threeD", "value": true})).unwrap();
|
||||
s.execute("layer.setParent", json!({"layers": [card], "parent": rig})).unwrap();
|
||||
s.execute("prop.addKey", json!({"layer": card, "path": "transform/position", "time": 0, "value": [0, 0, 0]})).unwrap();
|
||||
s.execute("prop.addKey", json!({"layer": card, "path": "transform/position", "time": 1, "value": [0, 0, 100]})).unwrap();
|
||||
s.execute("keys.setEase", json!({"layer": card, "path": "transform/position", "time": 0, "side": "out", "speed": 100, "influence": 50})).unwrap();
|
||||
s.execute("prop.addKey", json!({"layer": card, "path": "transform/scale", "time": 0, "value": [100, 100, 100]})).unwrap();
|
||||
s.execute("prop.addKey", json!({"layer": card, "path": "transform/scale", "time": 1, "value": [100, 100, 300]})).unwrap();
|
||||
s.execute("keys.setEase", json!({"layer": card, "path": "transform/scale", "time": 0, "side": "out", "speed": 300, "influence": 50})).unwrap();
|
||||
let (pos0, scale0) = (v3(&s, card, "transform/position", 0.25), v3(&s, card, "transform/scale", 0.25));
|
||||
|
||||
s.execute("comp.scale", json!({"factor": 2})).unwrap();
|
||||
// Twice the distance in the same time: the same ease, every frame twice as deep.
|
||||
let (pos, scale) = (v3(&s, card, "transform/position", 0.25), v3(&s, card, "transform/scale", 0.25));
|
||||
assert!((pos[2] - pos0[2] * 2.0).abs() < 1e-6, "{pos0:?} → {pos:?}");
|
||||
assert!((scale[2] - scale0[2] * 2.0).abs() < 1e-6, "{scale0:?} → {scale:?}");
|
||||
assert_eq!(scale[..2], scale0[..2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scale_comp_scales_depth_once_through_a_3d_grandparent() {
|
||||
let mut s = comp(200, 100);
|
||||
s.execute("layer.newCamera", json!({})).unwrap();
|
||||
// card (3D) → A (2D null) → B (3D null): B's Scale already carries the depth.
|
||||
let card = solid(&mut s, "#ff0000", 20, 10);
|
||||
let a = s.execute("layer.newNull", json!({"name": "A"})).unwrap()["layer"].as_u64().unwrap();
|
||||
let b = s.execute("layer.newNull", json!({"name": "B"})).unwrap()["layer"].as_u64().unwrap();
|
||||
s.execute("layer.setSwitch", json!({"layers": [card, b], "switch": "threeD", "value": true})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": b, "path": "transform/position", "value": [100, 50, 0]})).unwrap();
|
||||
s.execute("layer.setParent", json!({"layers": [a], "parent": b})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": a, "path": "transform/position", "value": [0, 0]})).unwrap();
|
||||
s.execute("layer.setParent", json!({"layers": [card], "parent": a})).unwrap();
|
||||
s.execute("prop.set", json!({"layer": card, "path": "transform/position", "value": [-40, 0, 500]})).unwrap();
|
||||
let card0 = bounds(&s, 0.0, 0);
|
||||
assert!(card0.4 > 0, "the card is in view");
|
||||
|
||||
s.execute("comp.scale", json!({"factor": 2})).unwrap();
|
||||
assert_eq!(v3(&s, card, "transform/position", 0.0), [-40.0, 0.0, 500.0]);
|
||||
let card1 = bounds(&s, 0.0, 0);
|
||||
let near =
|
||||
[(card1.0, card0.0 * 2), (card1.1, card0.1 * 2), (card1.2, card0.2 * 2 + 1), (card1.3, card0.3 * 2 + 1)].iter().all(|&(x, y)| (x - y).abs() <= 2);
|
||||
assert!(near, "{card0:?} → {card1:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scale_comp_refuses_a_bad_factor_and_changes_nothing() {
|
||||
let mut s = comp(640, 360);
|
||||
|
||||
Reference in new issue
Block a user