mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
Plug-ins: Ease Presets moves out of core into a bundled ScriptUI panel
The Ease Presets panel (#254) is modelled on a third-party panel, so, as with third-party tools in After Effects, it isn't core any more: it is `extensions/scriptui-panels/Ease Presets.jsx`, a ScriptUI panel bundled as an optional extension (Window ▸ Ease Presets.jsx), written only against the public scripting API. Clicking a preset eases every pair of neighbouring selected keyframes in one undo step (per dimension, along the motion path for spatial properties, the keys' other sides as they play); the fields edit the working curve, which the panel draws; From Keys, Save, Rename and Delete as before. The twelve built-in curves are unchanged. Scripting API fixes it needed (After Effects parity): - `app.settings` lasts between runs (`script_settings.json` in the settings folder; `script.settings.get` / `script.settings.save`). - Per-key methods (`setTemporalEaseAtKey`, `setInterpolationTypeAtKey`, `removeKey`, the continuous / auto-Bezier / roving setters) no longer change the user's key selection (they selected the key they edited). - `keyInTemporalEase` / `keyOutTemporalEase` report linear and auto-Bezier sides as they play, `keyIn/OutSpatialTangent` the tangents the motion path plays. - `selectedProperties` includes the properties of selected keyframes. - `isInterpolationTypeValid` is false for Linear / Bezier on values that only hold. Removed from core: Window ▸ Ease Presets, the `keys.easePreset.*` commands, `engine::ease_presets` and the `ease_presets.json` store, `keyframe::EaseCurve` (nothing else used it) and the egui panel. User presets saved by the v0.6.0 panel move into the panel's app.settings section the first time settings load (the old file is left as is). Docs: plugins.md (bundled extensions), parity.md, gaps.md G8, agents.md. Test: app_settings_last_between_runs, per_key_methods_keep_the_key_selection_and_report_eases_as_they_play, spatial_tangents_are_reported_as_the_path_plays (each fails before), the_ease_presets_panel_eases_every_selected_pair_in_one_undo_step, a_typed_curve_scales_to_each_dimension_and_follows_motion_paths, user_presets_save_rename_delete_and_persist, presets_saved_by_the_old_core_panel_carry_over (script, through the panel's controls), ease_presets_panel_eases_the_selected_keys_from_its_list (UI: the panel docks and a click on a preset eases the keys). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9da33c157e
commit
da165724e9
@@ -121,11 +121,17 @@ pub(crate) fn js_str(s: &str) -> JsValue {
|
||||
/// target comp made active for its duration.
|
||||
fn native_exec(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let id = arg_string(args, 0, ctx)?;
|
||||
let params = arg_json(args, 1, ctx)?;
|
||||
let mut params = arg_json(args, 1, ctx)?;
|
||||
if id == "script.run" || id == "file.runScript" {
|
||||
return Err(throw("scripts can't run scripts"));
|
||||
}
|
||||
let r = with_active(|a| Ok(exec(&mut a.session, &id, params)));
|
||||
let keys = params.as_object_mut().and_then(|o| o.remove("__keys"));
|
||||
let r = with_active(|a| {
|
||||
Ok(match &keys {
|
||||
Some(k) => exec_on_keys(&mut a.session, &id, params, k),
|
||||
None => exec(&mut a.session, &id, params),
|
||||
})
|
||||
});
|
||||
match r {
|
||||
Ok(Ok(v)) => Ok(js_str(&v.to_string())),
|
||||
Ok(Err(e)) | Err(e) => Err(throw(e)),
|
||||
@@ -164,6 +170,35 @@ pub(crate) fn exec(s: &mut Session, id: &str, params: J) -> Result<J, String> {
|
||||
r.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// Run a keyframe command that acts on the selected keys (`keys.velocity`, `keys.interpolation`,
|
||||
/// `keys.delete`) on the keys `targets` names instead (`[{layer, prop, index}]`, 0-based, in the
|
||||
/// command's comp): After Effects' per-key methods (`setTemporalEaseAtKey`, `removeKey`…) leave
|
||||
/// the user's key selection as it was. Selected keys that no longer exist drop out of it.
|
||||
fn exec_on_keys(s: &mut Session, id: &str, params: J, targets: &J) -> Result<J, String> {
|
||||
use effectcraft_engine::KeyRef;
|
||||
use effectcraft_engine::project::{ItemId, LayerId};
|
||||
let cid = params.get("comp").and_then(J::as_u64).map(ItemId).or(s.state.active_comp).ok_or("no composition is open")?;
|
||||
let comp = s.project.comp(cid).ok_or("the composition no longer exists")?;
|
||||
let mut keys = vec![];
|
||||
for t in targets.as_array().into_iter().flatten() {
|
||||
let (Some(layer), Some(prop), Some(index)) = (t["layer"].as_u64(), t["prop"].as_u64(), t["index"].as_u64()) else {
|
||||
return Err(format!("bad keyframe reference {t}"));
|
||||
};
|
||||
let l = comp.layer(LayerId(layer)).ok_or("the layer no longer exists")?;
|
||||
if l.switches.locked {
|
||||
return Err(format!("Can not change keyframes of \"{}\": the layer is locked", l.name));
|
||||
}
|
||||
let key = l.props.find(prop).and_then(|pr| pr.keys.get(usize::try_from(index).ok()?)).ok_or("the keyframe no longer exists")?;
|
||||
keys.push(KeyRef { layer: LayerId(layer), prop, time: key.time });
|
||||
}
|
||||
let saved = std::mem::replace(&mut s.state.selected_keys, keys);
|
||||
let r = exec(s, id, params);
|
||||
let comp = s.active_comp();
|
||||
let exists = |k: &KeyRef| comp.and_then(|c| c.layer(k.layer)).and_then(|l| l.props.find(k.prop)).is_some_and(|pr| pr.keys.iter().any(|x| x.time == k.time));
|
||||
s.state.selected_keys = saved.into_iter().filter(exists).collect();
|
||||
r
|
||||
}
|
||||
|
||||
/// `__query(kind, json)` → JSON.
|
||||
fn native_query(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult<JsValue> {
|
||||
let kind = arg_string(args, 0, ctx)?;
|
||||
|
||||
Reference in New Issue
Block a user