Merge pull request #1334

This commit is contained in:
Brandon Thomas committed 2026-10-18 09:35:33 -04:00
commit b72425a28d
3 files changed
+370 -33

No files matched your search

+291 -2
View File
@@ -180,7 +180,8 @@ fn the_ease_presets_panel_eases_every_selected_pair_in_one_undo_step() {
assert!(list.as_array().unwrap().iter().any(|e| e["name"] == PANEL && e["panel"] == true && e["source"] == "extension"));
let steps = s.history.undo.len();
set(&mut s, win, "presets", json!("Ease In-Out"));
assert_eq!(status(&mut s, win), "Eased 3 keyframe pair(s) with \u{201c}Ease In-Out\u{201d}");
// In-Out (the default) on every key: each pair gets the whole curve, as before.
assert_eq!(status(&mut s, win), "Eased 5 keyframe(s) (In-Out) with \u{201c}Ease In-Out\u{201d}");
assert_eq!(s.history.undo.len(), steps + 1, "one undo step");
assert_eq!(s.history.undo.last().map(|(label, _)| label.as_str()), Some("Apply Ease Preset"));
assert_eq!(s.state.selected_keys, selected, "the keys stay selected for the next preset");
@@ -203,10 +204,298 @@ fn the_ease_presets_panel_eases_every_selected_pair_in_one_undo_step() {
s.execute("edit.deselectAll", json!({})).unwrap();
let steps = s.history.undo.len();
set(&mut s, win, "presets", json!("Linear"));
assert_eq!(status(&mut s, win), "Select two or more neighbouring keyframes of a property");
assert_eq!(status(&mut s, win), "Select keyframes with another keyframe next to it");
assert_eq!(s.history.undo.len(), steps);
}
/// Select only the keys at `keys` (property path, time in seconds).
fn select_keys(s: &mut Session, l: u64, keys: &[(&str, f64)]) {
s.execute("edit.deselectAll", json!({})).unwrap();
let keys: Vec<Value> = keys.iter().map(|(path, t)| json!({"layer": l, "path": path, "time": t})).collect();
s.execute("keys.select", json!({"keys": keys})).unwrap();
}
/// Type a curve (Out %, Out speed, In %, In speed), choose the sides to ease and click Apply.
fn apply_typed(s: &mut Session, win: u64, mode: &str, c: [f64; 4]) {
for (name, v) in ["outInfluence", "outSpeed", "inInfluence", "inSpeed"].iter().zip(c) {
set(s, win, name, json!(v.to_string()));
}
set(s, win, "mode", json!(mode));
click(s, win, "apply");
}
fn eased(e: Ease, speed: f64, influence: f64) -> bool {
close(e.speed, speed, 1e-9) && close(e.influence, influence, 1e-9)
}
/// #1329: one selected keyframe eases its incoming side (In), its outgoing side (Out) or both,
/// each scaled to the segment on that side (+100/s in, −40/s out here), without changing its
/// neighbours, key times, values or the selection; one undo step restores it.
#[test]
fn one_selected_keyframe_eases_its_in_out_or_both_sides() {
let store = Arc::new(MemoryConfig::default());
let mut s = session(&store);
let l = setup(&mut s);
animate(&mut s, l, "transform/opacity", &[(0.0, json!(0)), (1.0, json!(100)), (3.0, json!(20))]);
let win = open_panel(&mut s);
// In-Out is the default.
assert_eq!(control(&mut s, win, "mode")["selection"], json!([2]));
let before = prop(&s, l, "transform/opacity");
for mode in ["In", "Out", "In-Out"] {
select_keys(&mut s, l, &[("transform/opacity", 1.0)]);
let selected = s.state.selected_keys.clone();
let steps = s.history.undo.len();
apply_typed(&mut s, win, mode, [30.0, 0.5, 60.0, 2.0]);
assert_eq!(status(&mut s, win), format!("Eased 1 keyframe(s) ({mode}) with the curve"));
assert_eq!(s.history.undo.len(), steps + 1, "{mode}: one undo step");
assert_eq!(s.state.selected_keys, selected, "{mode}: the selection stays");
let op = prop(&s, l, "transform/opacity");
assert_eq!((&op.keys[0], &op.keys[2]), (&before.keys[0], &before.keys[2]), "{mode}: the neighbours don't change");
assert_eq!((op.keys[1].time, op.keys[1].value.as_f64()), (before.keys[1].time, 100.0), "{mode}");
let k = &op.keys[1];
let (ins, outs) = (mode != "Out", mode != "In");
assert_eq!(k.in_interp, if ins { Interp::Bezier } else { Interp::Linear }, "{mode}");
assert_eq!(k.out_interp, if outs { Interp::Bezier } else { Interp::Linear }, "{mode}");
if ins {
assert!(eased(k.in_ease[0], 2.0 * 100.0, 0.6), "{mode}: {:?}", k.in_ease);
} else {
assert!(close(op.value_at(at(0.5)).as_f64(), 50.0, 1e-6), "{mode}: the in side still plays linearly");
}
if outs {
assert!(eased(k.out_ease[0], 0.5 * -40.0, 0.3), "{mode}: {:?}", k.out_ease);
} else {
assert!(close(op.value_at(at(2.0)).as_f64(), 60.0, 1e-6), "{mode}: the out side still plays linearly");
}
s.execute("edit.undo", json!({})).unwrap();
assert_eq!(prop(&s, l, "transform/opacity").keys, before.keys, "{mode}: undone");
}
}
/// A side without a neighbouring key isn't eased; when no selected key has one, nothing changes
/// (no undo step) and the panel says why. Keys that aren't neighbours, and keys of several
/// properties, each ease on their own.
#[test]
fn first_last_and_scattered_keyframes_ease_the_sides_that_have_a_segment() {
let store = Arc::new(MemoryConfig::default());
let mut s = session(&store);
let l = setup(&mut s);
animate(&mut s, l, "transform/opacity", &[(0.0, json!(0)), (1.0, json!(100)), (3.0, json!(20))]);
animate(&mut s, l, "transform/rotation", &[(0.0, json!(0)), (2.0, json!(90))]);
let win = open_panel(&mut s);
let before = prop(&s, l, "transform/opacity");
let c = [30.0, 0.5, 60.0, 2.0];
for (mode, t, why) in [("In", 0.0, "before it"), ("Out", 3.0, "after it")] {
select_keys(&mut s, l, &[("transform/opacity", t)]);
let steps = s.history.undo.len();
apply_typed(&mut s, win, mode, c);
assert_eq!(status(&mut s, win), format!("Select keyframes with another keyframe {why}"));
assert_eq!(s.history.undo.len(), steps, "{mode}: no undo step");
assert_eq!(prop(&s, l, "transform/opacity").keys, before.keys);
}
// In-Out on the first key eases only its out side.
select_keys(&mut s, l, &[("transform/opacity", 0.0)]);
apply_typed(&mut s, win, "In-Out", c);
let op = prop(&s, l, "transform/opacity");
assert_eq!((op.keys[0].in_interp, op.keys[0].out_interp), (Interp::Linear, Interp::Bezier));
assert!(eased(op.keys[0].out_ease[0], 0.5 * 100.0, 0.3), "{:?}", op.keys[0].out_ease);
assert_eq!(op.keys[1..], before.keys[1..]);
s.execute("edit.undo", json!({})).unwrap();
// The first and last Opacity keys (not neighbours) and the last Rotation key.
select_keys(&mut s, l, &[("transform/opacity", 0.0), ("transform/opacity", 3.0), ("transform/rotation", 2.0)]);
let steps = s.history.undo.len();
apply_typed(&mut s, win, "In-Out", c);
assert_eq!(status(&mut s, win), "Eased 3 keyframe(s) (In-Out) with the curve");
assert_eq!(s.history.undo.len(), steps + 1);
let op = prop(&s, l, "transform/opacity");
assert!(eased(op.keys[0].out_ease[0], 0.5 * 100.0, 0.3), "{:?}", op.keys[0].out_ease);
assert!(eased(op.keys[2].in_ease[0], 2.0 * -40.0, 0.6), "{:?}", op.keys[2].in_ease);
assert_eq!(op.keys[1], before.keys[1], "the unselected middle key");
let rot = prop(&s, l, "transform/rotation");
assert!(eased(rot.keys[1].in_ease[0], 2.0 * 45.0, 0.6), "{:?}", rot.keys[1].in_ease);
assert_eq!(rot.keys[0].out_interp, Interp::Linear, "the unselected neighbour");
s.execute("edit.undo", json!({})).unwrap();
assert_eq!(prop(&s, l, "transform/opacity").keys, before.keys);
assert!(prop(&s, l, "transform/rotation").keys.iter().all(|k| k.in_interp == Interp::Linear && k.out_interp == Interp::Linear));
}
/// A change from the pair-only panel: In-Out on a selected pair also eases the pair's outer sides
/// that face an unselected key (the selected keys' own sides), and Out on the pair eases only
/// out sides.
#[test]
fn a_selected_pair_eases_the_sides_its_mode_names() {
let store = Arc::new(MemoryConfig::default());
let mut s = session(&store);
let l = setup(&mut s);
animate(&mut s, l, "transform/opacity", &[(0.0, json!(0)), (1.0, json!(100)), (3.0, json!(20))]);
let win = open_panel(&mut s);
let before = prop(&s, l, "transform/opacity");
let c = [30.0, 0.5, 60.0, 2.0];
select_keys(&mut s, l, &[("transform/opacity", 0.0), ("transform/opacity", 1.0)]);
apply_typed(&mut s, win, "In-Out", c);
let op = prop(&s, l, "transform/opacity");
assert!(eased(op.keys[0].out_ease[0], 50.0, 0.3) && eased(op.keys[1].in_ease[0], 200.0, 0.6));
assert!(eased(op.keys[1].out_ease[0], -20.0, 0.3), "the side facing the unselected key: {:?}", op.keys[1].out_ease);
assert_eq!(op.keys[2], before.keys[2]);
s.execute("edit.undo", json!({})).unwrap();
apply_typed(&mut s, win, "Out", c);
let op = prop(&s, l, "transform/opacity");
assert!(eased(op.keys[0].out_ease[0], 50.0, 0.3) && eased(op.keys[1].out_ease[0], -20.0, 0.3));
assert_eq!((op.keys[0].in_interp, op.keys[1].in_interp), (Interp::Linear, Interp::Linear));
}
/// Easing one side of a key leaves the other playing as it did: a Hold stays a Hold, and an
/// auto-Bezier side keeps its shape. A preset click uses the chosen sides too.
#[test]
fn the_side_not_eased_keeps_playing_hold_and_auto_bezier() {
let store = Arc::new(MemoryConfig::default());
let mut s = session(&store);
let l = setup(&mut s);
animate(&mut s, l, "transform/opacity", &[(0.0, json!(0)), (1.0, json!(100)), (3.0, json!(20))]);
animate(&mut s, l, "transform/rotation", &[(0.0, json!(0)), (1.0, json!(90)), (3.0, json!(30))]);
ok(
&mut s,
r#"var t = app.project.activeItem.layer(1).transform, K = KeyframeInterpolationType;
t.opacity.setInterpolationTypeAtKey(2, K.LINEAR, K.HOLD);
t.rotation.setInterpolationTypeAtKey(2, K.BEZIER, K.BEZIER);
t.rotation.setTemporalAutoBezierAtKey(2, true);"#,
);
let rot = prop(&s, l, "transform/rotation");
let after: Vec<f64> = [1.5, 2.0, 2.5].iter().map(|t| rot.value_at(at(*t)).as_f64()).collect();
let win = open_panel(&mut s);
set(&mut s, win, "mode", json!("In"));
select_keys(&mut s, l, &[("transform/opacity", 1.0), ("transform/rotation", 1.0)]);
set(&mut s, win, "presets", json!("Ease In-Out"));
assert_eq!(status(&mut s, win), "Eased 2 keyframe(s) (In) with \u{201c}Ease In-Out\u{201d}");
let op = prop(&s, l, "transform/opacity");
assert_eq!((op.keys[1].in_interp, op.keys[1].out_interp), (Interp::Bezier, Interp::Hold));
assert!(eased(op.keys[1].in_ease[0], 0.0, 0.5), "{:?}", op.keys[1].in_ease);
assert_eq!(op.value_at(at(2.0)).as_f64(), 100.0, "still holds");
let eased_rot = prop(&s, l, "transform/rotation");
assert!(eased(eased_rot.keys[1].in_ease[0], 0.0, 0.5), "{:?}", eased_rot.keys[1].in_ease);
for (t, want) in [1.5, 2.0, 2.5].iter().zip(after) {
assert!(close(eased_rot.value_at(at(*t)).as_f64(), want, 1e-6), "the auto-Bezier out side at {t}");
}
assert!(!close(eased_rot.value_at(at(0.25)).as_f64(), rot.value_at(at(0.25)).as_f64(), 1e-3), "the in side changed");
}
/// Roving keys pass speed straight through (the engine keeps their sides linear), so an ease set
/// on one wouldn't play: they are skipped and the panel says so, rather than reporting them eased.
#[test]
fn roving_keyframes_are_skipped_and_reported() {
let store = Arc::new(MemoryConfig::default());
let mut s = session(&store);
let l = setup(&mut s);
animate(&mut s, l, "transform/position", &[(0.0, json!([0, 0, 0])), (1.0, json!([100, 0, 0])), (2.0, json!([200, 0, 0]))]);
animate(&mut s, l, "transform/opacity", &[(0.0, json!(0)), (1.0, json!(100)), (3.0, json!(20))]);
ok(&mut s, "app.project.activeItem.layer(1).transform.position.setRovingAtKey(2, true)");
let pos = prop(&s, l, "transform/position");
assert!(pos.keys[1].roving);
let roving_at = pos.keys[1].time.seconds();
let win = open_panel(&mut s);
// Only the roving key: nothing changes, no undo step.
select_keys(&mut s, l, &[("transform/position", roving_at)]);
let steps = s.history.undo.len();
set(&mut s, win, "presets", json!("Ease In-Out"));
assert_eq!(status(&mut s, win), "Nothing eased: skipped 1 roving keyframe(s); turn off Roving to ease them");
assert_eq!(s.history.undo.len(), steps);
assert_eq!(prop(&s, l, "transform/position").keys, pos.keys);
// With an ordinary key: that one is eased, the roving one is counted as skipped.
select_keys(&mut s, l, &[("transform/position", roving_at), ("transform/opacity", 1.0)]);
set(&mut s, win, "presets", json!("Linear"));
set(&mut s, win, "presets", json!("Ease In-Out"));
assert_eq!(
status(&mut s, win),
"Eased 1 keyframe(s) (In-Out) with \u{201c}Ease In-Out\u{201d}; skipped 1 roving keyframe(s); turn off Roving to ease them"
);
let k = &prop(&s, l, "transform/position").keys[1];
assert!(k.roving && k.in_interp == Interp::Linear && k.out_interp == Interp::Linear, "{k:?}");
let op = prop(&s, l, "transform/opacity");
assert!(eased(op.keys[1].in_ease[0], 0.0, 0.5) && eased(op.keys[1].out_ease[0], 0.0, 0.5));
}
/// Out alone leaves an auto-Bezier in side playing as it did, and undo and redo restore each state.
#[test]
fn out_alone_keeps_the_in_side_playing_through_undo_and_redo() {
let store = Arc::new(MemoryConfig::default());
let mut s = session(&store);
let l = setup(&mut s);
animate(&mut s, l, "transform/rotation", &[(0.0, json!(0)), (1.0, json!(90)), (3.0, json!(30))]);
ok(
&mut s,
r#"var r = app.project.activeItem.layer(1).transform.rotation, K = KeyframeInterpolationType;
r.setInterpolationTypeAtKey(2, K.BEZIER, K.BEZIER);
r.setTemporalAutoBezierAtKey(2, true);"#,
);
let before = prop(&s, l, "transform/rotation");
let win = open_panel(&mut s);
select_keys(&mut s, l, &[("transform/rotation", 1.0)]);
apply_typed(&mut s, win, "Out", [60.0, 0.0, 33.0, 1.0]);
let after = prop(&s, l, "transform/rotation");
assert!(eased(after.keys[1].out_ease[0], 0.0, 0.6), "{:?}", after.keys[1].out_ease);
for t in [0.25, 0.5, 0.75] {
assert!(close(after.value_at(at(t)).as_f64(), before.value_at(at(t)).as_f64(), 1e-6), "the in side at {t}");
}
assert!(!close(after.value_at(at(2.0)).as_f64(), before.value_at(at(2.0)).as_f64(), 1e-3), "the out side changed");
s.execute("edit.undo", json!({})).unwrap();
assert_eq!(prop(&s, l, "transform/rotation").keys, before.keys);
s.execute("edit.redo", json!({})).unwrap();
assert_eq!(prop(&s, l, "transform/rotation").keys, after.keys);
}
/// On a curved path, a single key's in and out speeds follow the length of the path on each side
/// (unequal lengths and durations), and the side not eased plays as it did.
#[test]
fn a_single_key_on_a_curved_path_eases_along_each_side() {
let store = Arc::new(MemoryConfig::default());
let mut s = session(&store);
let l = setup(&mut s);
animate(&mut s, l, "transform/position", &[(0.0, json!([0, 0, 0])), (1.0, json!([100, 0, 0])), (3.0, json!([100, 300, 0]))]);
let before = prop(&s, l, "transform/position");
let (len_in, len_out) = (effectcraft_keyframe::spatial_segment_length(&before.keys, 0), effectcraft_keyframe::spatial_segment_length(&before.keys, 1));
let win = open_panel(&mut s);
select_keys(&mut s, l, &[("transform/position", 1.0)]);
apply_typed(&mut s, win, "In", [40.0, 1.5, 50.0, 2.0]);
let pos = prop(&s, l, "transform/position");
let k = &pos.keys[1];
// In speed: twice the incoming segment's average (its path length over 1 s).
assert!(k.in_ease.iter().all(|e| close(e.speed, 2.0 * len_in, 0.05 * len_in) && close(e.influence, 0.5, 1e-9)), "{:?} vs {len_in}", k.in_ease);
// The out side (2 s along a longer path) plays as before.
for t in [1.5, 2.0, 2.5] {
let (a, b) = (pos.value_at(at(t)).as_vec2(), before.value_at(at(t)).as_vec2());
assert!(close(a[0], b[0], 1e-6) && close(a[1], b[1], 1e-6), "out side at {t}: {a:?} vs {b:?}");
}
// Playback on the in side: arriving at twice the average speed, the last tenth of a second
// covers more of the path than a constant speed would (len_in / 10).
let (p9, p10) = (pos.value_at(at(0.9)).as_vec2(), pos.value_at(at(1.0)).as_vec2());
let step = ((p10[0] - p9[0]).powi(2) + (p10[1] - p9[1]).powi(2)).sqrt();
assert!(step > len_in / 10.0 * 1.2, "{step} vs {len_in}");
assert!(len_out > len_in, "{len_out} {len_in}");
}
/// On one key, each side scales to its own segment: per dimension, and along the motion path.
#[test]
fn single_keys_scale_each_side_to_its_dimensions_and_motion_path() {
let store = Arc::new(MemoryConfig::default());
let mut s = session(&store);
let l = setup(&mut s);
// Scale: +100/−50 per second into the key, 0/+50 per second out of it.
animate(&mut s, l, "transform/scale", &[(0.0, json!([100, 100])), (1.0, json!([200, 50])), (3.0, json!([200, 150]))]);
// Position on a straight path: 100 px/s into the key, 50 px/s out of it.
animate(&mut s, l, "transform/position", &[(0.0, json!([0, 0, 0])), (1.0, json!([100, 0, 0])), (3.0, json!([200, 0, 0]))]);
let win = open_panel(&mut s);
select_keys(&mut s, l, &[("transform/scale", 1.0), ("transform/position", 1.0)]);
apply_typed(&mut s, win, "In-Out", [40.0, 1.5, 50.0, 2.0]);
assert_eq!(status(&mut s, win), "Eased 2 keyframe(s) (In-Out) with the curve");
let k = prop(&s, l, "transform/scale").keys[1].clone();
assert!(eased(k.in_ease[0], 200.0, 0.5) && eased(k.in_ease[1], -100.0, 0.5), "{:?}", k.in_ease);
assert!(eased(k.out_ease[0], 0.0, 0.4) && eased(k.out_ease[1], 75.0, 0.4), "{:?}", k.out_ease);
let k = prop(&s, l, "transform/position").keys[1].clone();
assert!(k.in_ease.iter().all(|e| close(e.speed, 200.0, 0.5) && close(e.influence, 0.5, 1e-9)), "{:?}", k.in_ease);
assert!(k.out_ease.iter().all(|e| close(e.speed, 75.0, 0.5) && close(e.influence, 0.4, 1e-9)), "{:?}", k.out_ease);
let pos = prop(&s, l, "transform/position");
assert_eq!(pos.value_at(at(1.0)).as_vec2()[0], 100.0, "the key's value stays");
}
#[test]
fn a_typed_curve_scales_to_each_dimension_and_follows_motion_paths() {
let store = Arc::new(MemoryConfig::default());
+13 -6
View File
@@ -103,17 +103,24 @@ name in your Scripts folders takes their place.
### Ease Presets (`extensions/scriptui-panels/Ease Presets.jsx`)
Window ▸ Ease Presets.jsx: easing curves kept by name and applied to pairs of neighbouring
selected keyframes (#254; it was a core panel in v0.6.0). A curve is the out side of a segment's
Window ▸ Ease Presets.jsx: easing curves kept by name and applied to the sides of selected
keyframes (#254; it was a core panel in v0.6.0). A curve is the out side of a segment's
first key and the in side of its second, in the Keyframe Velocity dialog's terms: influence in
percent, and speed relative to the segment's average speed (1 = as fast as a straight line, 0 = at
rest), so one curve fits any duration and change of value.
* Click a preset to apply it to every pair of neighbouring selected keyframes of each selected
property, in one undo step (per dimension, and along the motion path for spatial properties;
the keys' other sides keep playing as they did). The keys stay selected, so you can try another.
* Click a preset to apply it to the selected keyframes of each selected property, in one undo
step. **Ease** chooses the sides (#1329): **In** eases each key's incoming side with the curve's
in side, **Out** its outgoing side with the curve's out side, **In-Out** (the default) both. A
side is eased when it has a neighbouring keyframe, selected or not (the neighbour doesn't
change), with its speeds scaled to the segment on that side: per dimension, and along the
motion path for spatial properties. So a single keyframe works, a selected pair gets the whole
curve, and the first and last keys ease the side that has a segment. The side not eased keeps
playing as it did (a Hold stays a Hold). Roving keyframes are skipped, and the status says how
many: they pass speed straight through, so an ease on one wouldn't play (turn off Roving to
ease it). The keys stay selected, so you can try another.
* The drawing shows the working curve; type its numbers (Out % and Speed, In % and Speed) and
click Apply, or click From Keys to read the curve between the first selected pair.
click Apply, or click From Keys to read the curve between the first selected pair of neighbouring keys.
* Save keeps the working curve under the typed name (replacing a preset of that name); Rename
and Delete act on the selected preset of your own. The twelve built-in presets can't change.
* User presets are kept with `app.settings` (section `Ease Presets`, key `userPresets`, in
+66 -25
View File
@@ -1,4 +1,4 @@
// Ease Presets: easing curves kept by name and applied to pairs of neighbouring selected keyframes.
// Ease Presets: easing curves kept by name and applied to the sides of selected keyframes.
//
// A ScriptUI panel that ships with EffectCraft as an optional extension (Window ▸ Ease
// Presets.jsx). It uses only After Effects' documented scripting API: comp.selectedProperties,
@@ -12,6 +12,13 @@
// speeds are scaled to it: per dimension, along the motion path for spatial properties, or as
// overall progress for shapes.
//
// Applied to selected keyframes, In eases each key's incoming side with the curve's in side, Out
// its outgoing side with the curve's out side, In-Out both: a side is eased when it has a
// neighbouring key (selected or not; the neighbour isn't changed), its speeds scaled to the
// segment on that side. So a selected pair gets the whole curve, and a single key works too.
// Roving keys are skipped: they pass speed straight through (their sides stay linear), so an ease
// set on them wouldn't play.
//
// User presets are kept with app.settings (section "Ease Presets", key "userPresets") as JSON:
// {"version": 1, "presets": [{"name": …, "curve": {"outInfluence", "outSpeed", "inInfluence",
// "inSpeed"}}]}.
@@ -98,9 +105,8 @@
// ------------------------------------------------------------------ keyframes
// Pairs of neighbouring selected keyframes of the active comp: {prop, key} (the earlier key's
// index), by property.
function selectedPairs() {
// Selected properties of the active comp whose keys can ease: {prop, keys} (their selected keys).
function selectedKeyed() {
var comp = app.project.activeItem;
var out = [];
if (!(comp instanceof CompItem)) return out;
@@ -109,7 +115,17 @@
var p = props[i];
if (p.propertyType !== PropertyType.PROPERTY || p.numKeys < 2 || !p.isInterpolationTypeValid(BEZIER)) continue;
var keys = p.selectedKeys;
for (var k = 0; k + 1 < keys.length; k++) if (keys[k + 1] === keys[k] + 1) out.push({ prop: p, key: keys[k] });
if (keys.length) out.push({ prop: p, keys: keys });
}
return out;
}
// Pairs of neighbouring selected keyframes: {prop, key} (the earlier key's index), by property.
function selectedPairs() {
var keyed = selectedKeyed(), out = [];
for (var i = 0; i < keyed.length; i++) {
var keys = keyed[i].keys;
for (var k = 0; k + 1 < keys.length; k++) if (keys[k + 1] === keys[k] + 1) out.push({ prop: keyed[i].prop, key: keys[k] });
}
return out;
}
@@ -175,29 +191,43 @@
}
}
// Ease every pair of neighbouring selected keyframes with `c` (one undo step). Returns the
// number of pairs eased.
function applyCurve(c) {
var pairs = selectedPairs(), n = 0;
if (!pairs.length) return 0;
// The eases of one side with `speed` (relative to the segment's average) and `influence`.
function eases(s, speed, influence) {
var out = [];
for (var d = 0; d < s.length; d++) out.push(new KeyframeEase(speed * s[d], influence));
return out;
}
// Ease the sides `mode` names ("In", "Out" or "In-Out") of every selected keyframe with `c`, in
// one undo step: a side with a neighbouring key, scaled to the segment on that side. Returns
// {eased, roving}: the number of keyframes eased, and of selected roving keyframes skipped.
function applyCurve(c, mode) {
var keyed = selectedKeyed(), sides = [], keys = 0, roving = 0;
var doIn = mode !== "Out", doOut = mode !== "In";
// Every side and its eases first: the segments are measured before any key changes.
for (var i = 0; i < keyed.length; i++) {
var p = keyed[i].prop;
for (var j = 0; j < keyed[i].keys.length; j++) {
var k = keyed[i].keys[j], before = sides.length;
if (p.keyRoving(k)) {
roving++;
continue;
}
var s = doIn && k > 1 ? slopes(p, k - 1) : null;
if (s) sides.push({ prop: p, key: k, out: false, eases: eases(s, c.inSpeed, c.inInfluence) });
s = doOut && k < p.numKeys ? slopes(p, k) : null;
if (s) sides.push({ prop: p, key: k, out: true, eases: eases(s, c.outSpeed, c.outInfluence) });
if (sides.length > before) keys++;
}
}
if (!sides.length) return { eased: 0, roving: roving };
app.beginUndoGroup("Apply Ease Preset");
try {
for (var i = 0; i < pairs.length; i++) {
var p = pairs[i].prop, a = pairs[i].key, s = slopes(p, a);
if (!s) continue;
var outE = [], inE = [];
for (var d = 0; d < s.length; d++) {
outE.push(new KeyframeEase(c.outSpeed * s[d], c.outInfluence));
inE.push(new KeyframeEase(c.inSpeed * s[d], c.inInfluence));
}
easeSide(p, a, true, outE);
easeSide(p, a + 1, false, inE);
n++;
}
for (var e = 0; e < sides.length; e++) easeSide(sides[e].prop, sides[e].key, sides[e].out, sides[e].eases);
} finally {
app.endUndoGroup();
}
return n;
return { eased: keys, roving: roving };
}
// The curve between the first selected pair of keyframes (measured on the dimension that
@@ -288,6 +318,13 @@
var inInfluence = field(inRow, "In %", "inInfluence");
var inSpeed = field(inRow, "Speed", "inSpeed");
var modeRow = ui.add("group");
modeRow.add("statictext", undefined, "Ease");
var MODES = ["In", "Out", "In-Out"];
var mode = modeRow.add("dropdownlist", undefined, MODES, { name: "mode" });
mode.selection = 2;
mode.helpTip = "The sides of each selected keyframe to ease: incoming, outgoing or both";
var actions = ui.add("group");
var applyButton = actions.add("button", undefined, "Apply", { name: "apply" });
var fromKeys = actions.add("button", undefined, "From Keys", { name: "fromKeys" });
@@ -346,8 +383,12 @@
}
function apply(c, what) {
var n = applyCurve(c);
status.text = n > 0 ? "Eased " + n + " keyframe pair(s) with " + what : "Select two or more neighbouring keyframes of a property";
var m = mode.selection ? mode.selection.text : "In-Out";
var r = applyCurve(c, m), n = r.eased;
var side = m === "In" ? "before it" : m === "Out" ? "after it" : "next to it";
var skipped = r.roving > 0 ? "skipped " + r.roving + " roving keyframe(s); turn off Roving to ease them" : "";
if (n > 0) status.text = "Eased " + n + " keyframe(s) (" + m + ") with " + what + (skipped ? "; " + skipped : "");
else status.text = skipped ? "Nothing eased: " + skipped : "Select keyframes with another keyframe " + side;
return n;
}