From 3a953859f227be26b85d6cb9c1f9d127fd9da6fc Mon Sep 17 00:00:00 2001 From: tylerjenningsw Date: Tue, 13 Oct 2026 14:28:49 -0400 Subject: [PATCH] Ease Presets: ease the incoming, outgoing or both sides of selected keyframes (#1329) The panel eased only pairs of neighbouring selected keyframes, so a single selected key (or keys that aren't neighbours) changed nothing. An Ease selector (In, Out, In-Out; In-Out by default) now chooses the sides of each selected key to ease. In eases a key's incoming side with the curve's in side, Out its outgoing side with the curve's out side. A side is eased when it has a neighbouring key, selected or not; the neighbour is left as it is. Each side's speeds scale to the segment on that side, so a key between a +100/s and a -40/s segment gets each its own. The side not eased keeps playing as it did (easeSide, unchanged). Sides are measured before any key changes, and no undo step opens when no selected key has a segment on the chosen side; the status says which neighbour is missing. A selected pair in In-Out still gets the whole curve; its outer sides now ease too when they face an unselected key. From Keys still reads the curve of a selected pair. --- crates/script/src/tests_ease_presets.rs | 5 +- extensions/scriptui-panels/Ease Presets.jsx | 83 ++++++++++++++------- 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/crates/script/src/tests_ease_presets.rs b/crates/script/src/tests_ease_presets.rs index c803c27a5..1006d1bf1 100644 --- a/crates/script/src/tests_ease_presets.rs +++ b/crates/script/src/tests_ease_presets.rs @@ -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,7 +204,7 @@ 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); } diff --git a/extensions/scriptui-panels/Ease Presets.jsx b/extensions/scriptui-panels/Ease Presets.jsx index a08fdbcc2..0dd3f43ae 100644 --- a/extensions/scriptui-panels/Ease Presets.jsx +++ b/extensions/scriptui-panels/Ease Presets.jsx @@ -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,11 @@ // 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. +// // 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 +103,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 +113,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 +189,39 @@ } } - // 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 the + // number of keyframes eased. + function applyCurve(c, mode) { + var keyed = selectedKeyed(), sides = [], keys = 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; + 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 0; 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 keys; } // The curve between the first selected pair of keyframes (measured on the dimension that @@ -288,6 +312,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 +377,10 @@ } 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 n = applyCurve(c, m); + var side = m === "In" ? "before it" : m === "Out" ? "after it" : "next to it"; + status.text = n > 0 ? "Eased " + n + " keyframe(s) (" + m + ") with " + what : "Select keyframes with another keyframe " + side; return n; }