mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Viewer: a straight motion path has Bezier handles to curve it (#290)
The Position motion path drew a key's spatial handles only where its tangent wasn't zero, and auto-Bezier keys have none at the path's ends, so a two-key path (the usual start) had no handles to pull it into a curve. Auto-Bezier keys now show a handle on such a side too, pointing where the path leaves the key, a third of the way to the next key; dragging it makes the key Bezier with that tangent. Linear keys still show none. Keyframe Interpolation > Spatial Bezier copied those zero tangents, so the keys stayed straight with no handles (and read back as Linear); it now gives them the same handles. The path keeps its shape and timing. The handles come from the new keyframe::spatial_handles. Tests: spatial_bezier_gives_a_straight_path_handles (engine), a_straight_motion_path_has_handles_to_curve_it (ui_viewer). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
5b05a5ae80
commit
ed5ad56eb5
5 files changed
+84
-2
No files matched your search
@@ -589,7 +589,8 @@ fn interpolation(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let roving = b_p(p, "roving");
|
||||
edit_keys(s, "Keyframe Interpolation", None, move |keys, i, _| {
|
||||
if let Some(sp) = spatial.as_deref() {
|
||||
let (tin, tout) = effectcraft_keyframe::spatial_tangents(keys, i);
|
||||
// (Bezier keeps the current handles, with handles where the path left straight.)
|
||||
let (tin, tout) = effectcraft_keyframe::spatial_handles(keys, i);
|
||||
let k = &mut keys[i];
|
||||
match sp {
|
||||
"linear" => {
|
||||
|
||||
@@ -163,6 +163,22 @@ fn velocity_dialog_values_per_dimension_and_continuous() {
|
||||
undo_redo_roundtrip(&mut s, after, |s| assert_eq!(prop(s, l, "transform/scale").keys[1].in_interp, Interp::Linear));
|
||||
}
|
||||
|
||||
/// Spatial Bezier on a straight two-key path gives both keys handles a third of the way along it
|
||||
/// (they had none), so the path can be pulled into a curve; the motion stays the same (#290).
|
||||
#[test]
|
||||
fn spatial_bezier_gives_a_straight_path_handles() {
|
||||
let (mut s, l) = setup();
|
||||
animate(&mut s, l, "transform/position", &[(0.0, json!([0, 0])), (2.0, json!([300, 0]))]);
|
||||
let mid = prop(&s, l, "transform/position").value_at(effectcraft_time::Tick::from_seconds_f64(0.7)).as_vec3();
|
||||
select_prop_keys(&mut s, l, "transform/position");
|
||||
s.execute("keys.interpolation", json!({"spatial": "bezier"})).unwrap();
|
||||
let p = prop(&s, l, "transform/position");
|
||||
assert!(p.keys.iter().all(|k| !k.spatial_auto));
|
||||
assert_eq!((p.keys[0].spatial_out, p.keys[1].spatial_in), ([100.0, 0.0, 0.0], [-100.0, 0.0, 0.0]));
|
||||
let now = p.value_at(effectcraft_time::Tick::from_seconds_f64(0.7)).as_vec3();
|
||||
assert!((now[0] - mid[0]).abs() < 0.01 && now[1].abs() < 1e-9, "{mid:?} → {now:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interpolation_dialog_temporal_spatial_and_roving() {
|
||||
let (mut s, l) = setup();
|
||||
|
||||
@@ -299,6 +299,37 @@ pub fn spatial_tangents(keys: &[Keyframe], i: usize) -> ([f64; 3], [f64; 3]) {
|
||||
if key.spatial_auto { auto_spatial(keys, i) } else { (key.spatial_in, key.spatial_out) }
|
||||
}
|
||||
|
||||
/// Spatial tangents of key `i` as motion-path handles: [`spatial_tangents`], with a side that
|
||||
/// has none but a neighbour (the ends of an auto-Bezier path, a straight two-key path) pointing
|
||||
/// where the path leaves the key, toward the segment's next control point, a third of the way to
|
||||
/// the neighbouring key. After Effects shows Bezier handles there to drag the path into a curve.
|
||||
pub fn spatial_handles(keys: &[Keyframe], i: usize) -> ([f64; 3], [f64; 3]) {
|
||||
let (mut tin, mut tout) = spatial_tangents(keys, i);
|
||||
let Some(p) = keys.get(i).map(|k| v3(&k.value)) else { return (tin, tout) };
|
||||
// Toward key `j`'s control point on this segment (its in side when `j` follows `i`).
|
||||
let toward = |j: usize, after: bool| -> Option<[f64; 3]> {
|
||||
let q = v3(&keys.get(j)?.value);
|
||||
let (jin, jout) = spatial_tangents(keys, j);
|
||||
let t = if after { jin } else { jout };
|
||||
let d = [q[0] + t[0] - p[0], q[1] + t[1] - p[1], q[2] + t[2] - p[2]];
|
||||
let dl = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
|
||||
let len = ((q[0] - p[0]).powi(2) + (q[1] - p[1]).powi(2) + (q[2] - p[2]).powi(2)).sqrt() / 3.0;
|
||||
(dl > 1e-9).then(|| d.map(|v| v * len / dl))
|
||||
};
|
||||
let none = |t: [f64; 3]| t == [0.0; 3];
|
||||
if none(tout)
|
||||
&& let Some(v) = toward(i + 1, true)
|
||||
{
|
||||
tout = v;
|
||||
}
|
||||
if none(tin)
|
||||
&& let Some(v) = i.checked_sub(1).and_then(|j| toward(j, false))
|
||||
{
|
||||
tin = v;
|
||||
}
|
||||
(tin, tout)
|
||||
}
|
||||
|
||||
/// Effective temporal ease of key `i` for dimension `d` (auto-Bezier keys get the neighbour slope).
|
||||
fn effective_ease(keys: &[Keyframe], i: usize, d: usize, spatial: bool, out: bool) -> Ease {
|
||||
let k = &keys[i];
|
||||
|
||||
@@ -292,7 +292,13 @@ pub(crate) fn motion_path(app: &mut EffectcraftApp, painter: &egui::Painter, map
|
||||
}
|
||||
let v = k.value.as_vec3();
|
||||
let s = to_scr(v);
|
||||
let (tin, tout) = effectcraft_engine::keyframe::spatial_tangents(&pos.keys, i);
|
||||
// Auto-Bezier keys show handles on a side the path leaves straight too (a two-key path's
|
||||
// ends), to drag it into a curve; Linear keys (no tangents) show none.
|
||||
let (tin, tout) = if k.spatial_auto {
|
||||
effectcraft_engine::keyframe::spatial_handles(&pos.keys, i)
|
||||
} else {
|
||||
effectcraft_engine::keyframe::spatial_tangents(&pos.keys, i)
|
||||
};
|
||||
for (out, tg) in [(false, tin), (true, tout)] {
|
||||
let has = (out && i + 1 < pos.keys.len()) || (!out && i > 0);
|
||||
if !has || (tg[0] == 0.0 && tg[1] == 0.0) {
|
||||
|
||||
@@ -479,6 +479,34 @@ fn motion_path_key_drag_edits_that_key() {
|
||||
assert_eq!(k[0].value.as_vec3(), [100.0, 100.0, 0.0]);
|
||||
}
|
||||
|
||||
/// A straight two-key motion path shows Bezier handles at both keys (it had none), and dragging
|
||||
/// one pulls the path into a curve (#290).
|
||||
#[test]
|
||||
fn a_straight_motion_path_has_handles_to_curve_it() {
|
||||
let mut h = harness();
|
||||
let box_id: LayerId = h.state().session.active_comp().unwrap().layers[0].id;
|
||||
let s = &mut h.state_mut().session;
|
||||
s.execute("layer.select", json!({"layers": [box_id.0]})).unwrap();
|
||||
s.execute("prop.addKey", json!({"layer": box_id.0, "path": "transform/position", "time": 0.0, "value": [100, 100, 0]})).unwrap();
|
||||
s.execute("prop.addKey", json!({"layer": box_id.0, "path": "transform/position", "time": 2.0, "value": [400, 100, 0]})).unwrap();
|
||||
s.execute("view.snapping", json!({"value": false})).unwrap();
|
||||
s.set_time(effectcraft_engine::time::Tick::from_seconds_f64(1.0));
|
||||
h.run_steps(3);
|
||||
let (out0, in1) = (format!("viewer.motionPath.{}.0.out", box_id.0), format!("viewer.motionPath.{}.1.in", box_id.0));
|
||||
assert!(h.state().auto.find(&in1).is_some(), "the second key has a handle");
|
||||
// The first key's handle lies a third of the way along the path: drag it down 150 px.
|
||||
let from = rect(&h, &out0).center();
|
||||
assert!(from.distance(screen(&h, [200.0, 100.0])) < 2.0, "{from:?}");
|
||||
let to = screen(&h, [200.0, 250.0]);
|
||||
drag(&mut h, from, to);
|
||||
let p = h.state().session.active_comp().unwrap().layer(box_id).unwrap().props.prop("transform/position").unwrap().clone();
|
||||
assert!(!p.keys[0].spatial_auto);
|
||||
let t = p.keys[0].spatial_out;
|
||||
assert!((t[0] - 100.0).abs() < 2.0 && (t[1] - 150.0).abs() < 2.0, "{t:?}");
|
||||
let mid = p.value_at(effectcraft_engine::time::Tick::from_seconds_f64(1.0)).as_vec3();
|
||||
assert!(mid[1] > 130.0, "the path curves: {mid:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn graph_editor_transform_box_scales_selected_keys_in_time() {
|
||||
let mut h = harness();
|
||||
|
||||
Reference in new issue
Block a user