mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-29 20:47:18 -06:00
Plug-ins: ScriptUI Panels folder scripts dock as panels and reopen, as in After Effects
EffectCraft's extension points compared with After Effects' (public user guide and Scripting Guide) are now in docs/plugins.md, which becomes "Plug-ins and extensions": scripts, ScriptUI panels and effect plug-ins, with a short guide and an example panel for writing your own. ScriptUI panels already came from `Scripts/ScriptUI Panels/` in the settings folder, were listed at the bottom of the Window menu and docked as tabs. What was missing: After Effects reopens the panels that were open when it quit. Now the open panels are remembered in `scriptui_panels.json` in the settings folder (a panel is added when it opens and dropped when its tab or its script closes it; quitting leaves the list), and `window.restoreScriptPanels` opens them again; the desktop and web apps run it at start-up. A panel whose script is gone or fails is reported and forgotten, and the others still open. Where a panel was docked isn't kept, as workspace layouts aren't saved between launches. Gaps recorded in plugins.md and gaps.md G8: the Startup and Shutdown script folders, keyboard shortcuts for scripts, ExtendScript's `#include` / `#target` directives and `.jsxbin`. Test: open_panels_open_again_at_the_next_launch (fails before: no restore command), a_panel_dropped_in_the_folder_is_listed_and_opens_as_a_panel (script), panels_open_at_quit_dock_again_at_launch (UI: the restored panel docks with its controls; closing the tab forgets it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6ce58040fd
commit
9da33c157e
@@ -241,6 +241,8 @@ pub async fn start(canvas_id: String) -> Result<(), JsValue> {
|
||||
let _ = session.execute("file.openDemoProject", json!({}));
|
||||
}
|
||||
api::set_info("restored", json!(restored));
|
||||
// The ScriptUI panels open on the last visit open again.
|
||||
let _ = session.execute("window.restoreScriptPanels", json!({}));
|
||||
let mut app = EffectcraftApp::new(session);
|
||||
app.set_gpu_failure_bridge(gpu_failures);
|
||||
app.ui.start_screen = home;
|
||||
|
||||
@@ -158,6 +158,9 @@ fn main() -> eframe::Result {
|
||||
eprintln!("effectcraft: {e}");
|
||||
}
|
||||
let show_home = home.unwrap_or(session.prefs.startup.show_home_on_launch && files.is_empty() && control_port.is_none());
|
||||
// The ScriptUI panels open when the app last quit open again (a failing one is
|
||||
// reported in a toast and forgotten).
|
||||
let _ = session.execute("window.restoreScriptPanels", json!({}));
|
||||
let mut app = EffectcraftApp::new(session);
|
||||
app.set_gpu_failure_bridge(gpu_failures);
|
||||
app.ui.start_screen = show_home;
|
||||
|
||||
@@ -18,6 +18,8 @@ use crate::{EngineError, Event, Result, Session, cmd, scriptui};
|
||||
/// The settings-store folders of installed scripts.
|
||||
pub const SCRIPTS_DIR: &str = "Scripts";
|
||||
pub const PANELS_DIR: &str = "Scripts/ScriptUI Panels";
|
||||
/// The ScriptUI panels open when EffectCraft last quit (settings store), reopened at launch.
|
||||
pub const OPEN_PANELS_FILE: &str = "scriptui_panels.json";
|
||||
|
||||
/// Sample scripts that ship with EffectCraft (original work): (file name, ScriptUI panel, code).
|
||||
pub const SAMPLES: &[(&str, bool, &str)] = &[
|
||||
@@ -134,6 +136,64 @@ pub fn panel_wrapper(title: &str, code: &str) -> String {
|
||||
format!("(function () {{ {code}\n}}).call(__uiDockPanel({}));", Value::String(title.to_string()))
|
||||
}
|
||||
|
||||
/// The ScriptUI panels open now (script names).
|
||||
pub(crate) fn open_panel_names(s: &Session) -> Vec<String> {
|
||||
s.script_ui.windows.iter().filter(|w| w.kind == scriptui::WindowKind::Panel).map(|w| w.script.clone()).collect()
|
||||
}
|
||||
|
||||
/// The panels open when EffectCraft last quit (`{"open": [names]}`); a missing or unreadable
|
||||
/// file is none.
|
||||
fn remembered_panels(s: &Session) -> Vec<String> {
|
||||
let text = s.config.as_ref().and_then(|c| c.read(OPEN_PANELS_FILE)).unwrap_or_default();
|
||||
let doc: Value = serde_json::from_str(&text).unwrap_or(Value::Null);
|
||||
doc.get("open").and_then(Value::as_array).into_iter().flatten().filter_map(Value::as_str).filter(|n| is_script(n)).map(str::to_string).collect()
|
||||
}
|
||||
|
||||
/// Change the remembered panels with `f` (written only when it changed). The list is kept so
|
||||
/// the next launch reopens them, as After Effects reopens the panels of its workspace; quitting
|
||||
/// leaves it as it is.
|
||||
fn edit_remembered_panels(s: &Session, f: impl FnOnce(&mut Vec<String>)) {
|
||||
let Some(cfg) = &s.config else { return };
|
||||
let before = remembered_panels(s);
|
||||
let mut list = before.clone();
|
||||
f(&mut list);
|
||||
if list != before
|
||||
&& let Err(e) = cfg.write(OPEN_PANELS_FILE, &json!({"open": list}).to_string())
|
||||
{
|
||||
log::warn!("cannot remember the open ScriptUI panels: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Forget the panels of `before` (open before a script window event) that are closed now.
|
||||
pub(crate) fn forget_closed_panels(s: &Session, before: &[String]) {
|
||||
let now = open_panel_names(s);
|
||||
let closed: Vec<&String> = before.iter().filter(|n| !now.contains(n)).collect();
|
||||
if !closed.is_empty() {
|
||||
edit_remembered_panels(s, |l| l.retain(|n| !closed.contains(&n)));
|
||||
}
|
||||
}
|
||||
|
||||
/// `window.restoreScriptPanels`: open the ScriptUI panels that were open when EffectCraft last
|
||||
/// quit (frontends call it once at launch). Panels whose script is gone or fails are reported
|
||||
/// and forgotten; the others still open.
|
||||
fn restore_panels(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let (mut opened, mut failed) = (vec![], vec![]);
|
||||
for name in remembered_panels(s) {
|
||||
let r = match script_code(s, &name, Some(true)) {
|
||||
Some(_) => open_panel(s, &json!({"name": name})).map_err(|e| e.to_string()),
|
||||
None => Err("no such ScriptUI panel".to_string()),
|
||||
};
|
||||
match r {
|
||||
Ok(_) => opened.push(name),
|
||||
Err(error) => {
|
||||
edit_remembered_panels(s, |l| l.retain(|n| *n != name));
|
||||
failed.push(json!({"name": name, "error": error}));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(json!({"opened": opened, "failed": failed}))
|
||||
}
|
||||
|
||||
/// Window ▸ <ScriptUI panel>: run the panel script (or bring its panel forward).
|
||||
fn open_panel(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let c = "window.scriptPanel";
|
||||
@@ -150,6 +210,13 @@ fn open_panel(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if let Some(id) = id {
|
||||
s.events.push(Event::Frontend { command: c.into(), params: json!({"window": id}) });
|
||||
}
|
||||
if id.is_some() {
|
||||
edit_remembered_panels(s, |l| {
|
||||
if !l.contains(&name) {
|
||||
l.push(name.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(json!({"window": id, "result": out}))
|
||||
}
|
||||
|
||||
@@ -184,6 +251,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
always,
|
||||
open_panel
|
||||
),
|
||||
cmd!(
|
||||
"window.restoreScriptPanels",
|
||||
"Restore ScriptUI Panels",
|
||||
[],
|
||||
None,
|
||||
"{} → reopens the ScriptUI panels open when EffectCraft last quit (frontends call it at launch) → {opened: [name], failed: [{name, error}]}",
|
||||
always,
|
||||
restore_panels
|
||||
),
|
||||
crate::query!("scriptui.list", "List Script Windows", "{} → [{window, title, kind: dialog|palette|window|panel, script, modal, size}]", scriptui::list),
|
||||
crate::query!(
|
||||
"scriptui.get",
|
||||
|
||||
@@ -629,7 +629,11 @@ fn widget_p(s: &Session, win: u32, p: &Value, cmd: &str) -> Result<u32> {
|
||||
|
||||
fn dispatch(s: &mut Session, ev: ScriptUiEvent) -> Result<Value> {
|
||||
let f = s.script_ui.dispatch.ok_or_else(|| EngineError::Other("scripting is not available in this build".into()))?;
|
||||
f(s, &ev).map_err(EngineError::Other)
|
||||
// The event may close a ScriptUI panel (its tab, or the script itself): forget it then.
|
||||
let open = crate::commands::scripts::open_panel_names(s);
|
||||
let r = f(s, &ev).map_err(EngineError::Other);
|
||||
crate::commands::scripts::forget_closed_panels(s, &open);
|
||||
r
|
||||
}
|
||||
|
||||
/// `scriptui.list`: the open script windows.
|
||||
|
||||
@@ -82,6 +82,8 @@ pub fn run_code(s: &mut Session, code: &str, name: &str) -> Outcome {
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
#[cfg(test)]
|
||||
mod tests_panels;
|
||||
#[cfg(test)]
|
||||
mod tests_ui;
|
||||
#[cfg(test)]
|
||||
mod tests_ui_more;
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
//! ScriptUI Panels, as in After Effects: scripts in the user's `Scripts/ScriptUI Panels` folder
|
||||
//! (in the settings folder) are listed at the bottom of the Window menu, open as dockable panels
|
||||
//! with `this` = the panel, and the panels open when the app quits open again at the next launch.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::commands::scripts::{OPEN_PANELS_FILE, PANELS_DIR};
|
||||
use effectcraft_engine::config::{ConfigStore, MemoryConfig};
|
||||
use effectcraft_engine::scriptui::WindowKind;
|
||||
use serde_json::json;
|
||||
|
||||
const PANEL: &str = "var p = this;\np.add('statictext', undefined, 'Hello from a panel');\np.layout.layout(true);\n";
|
||||
|
||||
fn session(store: &Arc<MemoryConfig>) -> Session {
|
||||
let mut s = Session::default();
|
||||
crate::install(&mut s);
|
||||
s.config = Some(store.clone());
|
||||
s
|
||||
}
|
||||
|
||||
fn panel_window(s: &Session, name: &str) -> Option<u32> {
|
||||
s.script_ui.windows.iter().find(|w| w.kind == WindowKind::Panel && w.script == name).map(|w| w.id)
|
||||
}
|
||||
|
||||
fn remembered(store: &MemoryConfig) -> serde_json::Value {
|
||||
serde_json::from_str(&store.read(OPEN_PANELS_FILE).unwrap_or_default()).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_panel_dropped_in_the_folder_is_listed_and_opens_as_a_panel() {
|
||||
let store = Arc::new(MemoryConfig::default());
|
||||
store.write(&format!("{PANELS_DIR}/Hello Panel.jsx"), PANEL).unwrap();
|
||||
// Not a script: not listed.
|
||||
store.write(&format!("{PANELS_DIR}/notes.txt"), "x").unwrap();
|
||||
let mut s = session(&store);
|
||||
let cx = effectcraft_engine::menus::DynCtx::default();
|
||||
let (entries, _) = effectcraft_engine::menus::dynamic(&s, "scriptPanels", &cx);
|
||||
let names: Vec<&str> = entries.iter().map(|e| e.label.as_str()).collect();
|
||||
assert!(names.contains(&"Hello Panel.jsx") && !names.contains(&"notes.txt"), "{names:?}");
|
||||
let e = entries.iter().find(|e| e.label == "Hello Panel.jsx").unwrap();
|
||||
assert_eq!((e.command.as_str(), &e.params), ("window.scriptPanel", &json!({"name": "Hello Panel.jsx"})));
|
||||
let r = s.execute("window.scriptPanel", json!({"name": "Hello Panel.jsx"})).unwrap();
|
||||
let id = r["window"].as_u64().unwrap() as u32;
|
||||
let w = s.script_ui.window(id).unwrap();
|
||||
assert_eq!((w.kind, w.title.as_str()), (WindowKind::Panel, "Hello Panel"));
|
||||
assert_eq!(w.root.children[0].text, "Hello from a panel");
|
||||
// Frontends dock it (`window.scriptPanel {window}` for the UI).
|
||||
let events = s.drain_events();
|
||||
assert!(
|
||||
events
|
||||
.iter()
|
||||
.any(|e| matches!(e, effectcraft_engine::Event::Frontend { command, params } if command == "window.scriptPanel" && params["window"] == id))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_panels_open_again_at_the_next_launch() {
|
||||
let store = Arc::new(MemoryConfig::default());
|
||||
store.write(&format!("{PANELS_DIR}/Hello Panel.jsx"), PANEL).unwrap();
|
||||
let mut s = session(&store);
|
||||
s.execute("window.scriptPanel", json!({"name": "Hello Panel.jsx"})).unwrap();
|
||||
s.execute("window.scriptPanel", json!({"name": "Layer Tools.jsx"})).unwrap();
|
||||
assert_eq!(remembered(&store), json!({"open": ["Hello Panel.jsx", "Layer Tools.jsx"]}));
|
||||
// Quit with both open; the next launch opens them again.
|
||||
drop(s);
|
||||
let mut s = session(&store);
|
||||
let r = s.execute("window.restoreScriptPanels", json!({})).unwrap();
|
||||
assert_eq!(r, json!({"opened": ["Hello Panel.jsx", "Layer Tools.jsx"], "failed": []}));
|
||||
assert!(panel_window(&s, "Hello Panel.jsx").is_some() && panel_window(&s, "Layer Tools.jsx").is_some());
|
||||
// Closing a panel (its tab) forgets it.
|
||||
let id = panel_window(&s, "Layer Tools.jsx").unwrap();
|
||||
s.execute("scriptui.close", json!({"window": id})).unwrap();
|
||||
assert_eq!(remembered(&store), json!({"open": ["Hello Panel.jsx"]}));
|
||||
// A panel whose script was removed, or that fails, is reported and forgotten; the others open.
|
||||
store.write(OPEN_PANELS_FILE, &json!({"open": ["Gone.jsx", "Broken.jsx", "Hello Panel.jsx"]}).to_string()).unwrap();
|
||||
store.write(&format!("{PANELS_DIR}/Broken.jsx"), "throw new Error('nope');").unwrap();
|
||||
let mut s = session(&store);
|
||||
let r = s.execute("window.restoreScriptPanels", json!({})).unwrap();
|
||||
assert_eq!(r["opened"], json!(["Hello Panel.jsx"]));
|
||||
let failed: Vec<&str> = r["failed"].as_array().unwrap().iter().map(|f| f["name"].as_str().unwrap()).collect();
|
||||
assert_eq!(failed, ["Gone.jsx", "Broken.jsx"]);
|
||||
assert_eq!(remembered(&store), json!({"open": ["Hello Panel.jsx"]}));
|
||||
// A corrupt list opens nothing (and is no error).
|
||||
store.write(OPEN_PANELS_FILE, "{not json").unwrap();
|
||||
let mut s = session(&store);
|
||||
assert_eq!(s.execute("window.restoreScriptPanels", json!({})).unwrap(), json!({"opened": [], "failed": []}));
|
||||
}
|
||||
@@ -90,6 +90,28 @@ fn script_windows_are_drawn_and_clickable() {
|
||||
h.state_mut().session.execute("scriptui.close", json!({})).unwrap();
|
||||
}
|
||||
|
||||
/// A ScriptUI panel open when the app quit opens again, docked, at the next launch (the
|
||||
/// frontends call `window.restoreScriptPanels` before building the app).
|
||||
#[test]
|
||||
fn panels_open_at_quit_dock_again_at_launch() {
|
||||
use effectcraft_engine::config::{ConfigStore, MemoryConfig};
|
||||
let store = std::sync::Arc::new(MemoryConfig::default());
|
||||
store.write("Scripts/ScriptUI Panels/Hello Panel.jsx", "this.add('button', undefined, 'Hi', { name: 'hi' });").unwrap();
|
||||
store.write(effectcraft_engine::commands::scripts::OPEN_PANELS_FILE, r#"{"open": ["Hello Panel.jsx"]}"#).unwrap();
|
||||
let mut s = effectcraft_host::session();
|
||||
s.config = Some(store.clone());
|
||||
s.execute("window.restoreScriptPanels", json!({})).unwrap();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_| EffectcraftApp::new(s));
|
||||
h.run_steps(3);
|
||||
let panel = h.state().session.script_ui.windows.iter().find(|w| w.script == "Hello Panel.jsx").expect("the panel's window").id;
|
||||
assert!(h.state().ui.dock.contains(PanelKind::ScriptPanel(panel)), "the panel docks");
|
||||
assert!(h.state().auto.find(&format!("scriptui.{panel}.hi")).is_some(), "its controls are drawn");
|
||||
// Closing its tab forgets it for the next launch.
|
||||
h.state_mut().close_panel(PanelKind::ScriptPanel(panel));
|
||||
h.run_steps(2);
|
||||
assert_eq!(store.read(effectcraft_engine::commands::scripts::OPEN_PANELS_FILE).as_deref(), Some(r#"{"open":[]}"#));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_panel_jumps_between_branches() {
|
||||
let mut h = harness();
|
||||
|
||||
@@ -194,6 +194,12 @@ The most important missing piece: it turns every other estimate here into a meas
|
||||
- After Effects SDK plug-ins cannot run in EffectCraft. Our own WebAssembly plug-in API exists
|
||||
([plugins.md](plugins.md)). Grow it: documentation, examples, and original effects that cover
|
||||
what the most common third-party plug-ins are used for (particles, glows, 3D objects, sabers).
|
||||
- Features that come from third parties in After Effects are extensions in EffectCraft too, built
|
||||
on the same extension points as After Effects: scripts, ScriptUI panels and effect plug-ins
|
||||
([plugins.md](plugins.md#extension-points-compared-with-after-effects) compares them). ScriptUI
|
||||
panels in the user's ScriptUI Panels folder are listed in the Window menu, dock like any panel
|
||||
and open again at the next launch. Missing: the Startup and Shutdown script folders, keyboard
|
||||
shortcuts for scripts, ExtendScript's `#include` / `#target` directives and `.jsxbin`.
|
||||
- Done so far: ease presets (Window ▸ Ease Presets, `keys.easePreset.*`, #254) cover what
|
||||
ease-curve plug-ins are used for: easing curves kept by name, drawn and edited in a value graph,
|
||||
and applied to pairs of keyframes ([parity.md](parity.md#update-ease-presets-254)).
|
||||
|
||||
+98
-7
@@ -1,4 +1,95 @@
|
||||
# Effect plug-ins
|
||||
# Plug-ins and extensions
|
||||
|
||||
EffectCraft's core aims at After Effects parity. What After Effects users get from third parties
|
||||
(effect plug-ins, scripts, ScriptUI panels) is an extension in EffectCraft too, and EffectCraft
|
||||
offers the same kinds of extension points as After Effects so that anyone can write their own.
|
||||
|
||||
## Extension points compared with After Effects
|
||||
|
||||
From After Effects' public documentation (the user guide's scripting pages and the Scripting
|
||||
Guide):
|
||||
|
||||
| After Effects | EffectCraft | |
|
||||
|---|---|---|
|
||||
| Effect plug-ins (C/C++ SDK, `.aex` / `.plugin`) | [Effect plug-ins](#effect-plug-ins): WebAssembly modules (plug-in API v1) or Rust; the `Plug-ins` folder next to the settings loads at start-up | Different ABI: After Effects SDK plug-ins can't run (G8) |
|
||||
| Scripts folder: its scripts are listed in File ▸ Scripts | `Scripts/` in the settings folder (File ▸ Scripts ▸ Install Script File… copies a script there), listed in File ▸ Scripts with the bundled samples | Same |
|
||||
| File ▸ Scripts ▸ Run Script File… (`.jsx`, `.jsxbin`) | `.jsx` / `.js` | Gap: `.jsxbin` (an undocumented binary encoding) |
|
||||
| ExtendScript preprocessor directives (`#include`, `#target`, `#targetengine`) | Not understood: a script that starts with one doesn't parse | Gap |
|
||||
| `Scripts/Startup` and `Scripts/Shutdown`: run in alphabetical order when the app starts and quits | Not run | Gap |
|
||||
| `Scripts/ScriptUI Panels`: listed at the bottom of the Window menu, dockable like the app's own panels, `this` is the panel | `Scripts/ScriptUI Panels/` in the settings folder (File ▸ Scripts ▸ Install ScriptUI Panel… copies one there), listed at the bottom of the Window menu, docked like any panel, `this` is the panel; the panels open when the app quits open again at the next launch | Same. Where a panel was docked isn't kept: workspace layouts aren't saved between launches |
|
||||
| Keyboard shortcuts for scripts (the Keyboard Shortcuts editor lists the scripts of the Scripts folder) | Shortcuts bind to commands and menu entries; the File ▸ Scripts and Window menu entries of scripts aren't offered | Gap |
|
||||
| Running a script from the command line (`afterfx -r script.jsx`) | `effectcraft-cli script`, the control channel's and MCP's `script.run` | Same purpose, other entry points |
|
||||
| `app.settings` (kept in the preferences between sessions) | Kept for the run only | Gap |
|
||||
| Allow Scripts to Write Files and Access Network | Settings ▸ Scripting & Expressions | Same |
|
||||
|
||||
The settings folder is `~/Library/Application Support/EffectCraft` on macOS, `%APPDATA%\EffectCraft`
|
||||
on Windows and `$XDG_CONFIG_HOME/effectcraft` (or `~/.config/effectcraft`) on Linux
|
||||
([preferences.md](preferences.md)); the web app keeps it in browser storage.
|
||||
|
||||
## Scripts
|
||||
|
||||
Scripts use After Effects' scripting object model (`app`, `app.project`, `CompItem`, layers,
|
||||
`Property` with keyframes and eases, `app.beginUndoGroup`…), so most scripts written for After
|
||||
Effects' documented API run unchanged. Every edit a script makes is an engine command: undoable,
|
||||
journaled and the same as the UI's action. The `effectcraft-script` crate's docs
|
||||
(`crates/script/src/lib.rs`) list the object model.
|
||||
|
||||
* Run one: File ▸ Scripts ▸ Run Script File…, the Script Console panel, `effectcraft-cli script
|
||||
file.jsx`, or `script.run {code, name}` over the control channel and MCP.
|
||||
* Install one: File ▸ Scripts ▸ Install Script File… (or copy it into `Scripts/` in the settings
|
||||
folder). It is listed in File ▸ Scripts at once (After Effects asks for a restart).
|
||||
* `file.scripts.list` lists installed and bundled scripts and panels; `file.runScript {name}` runs
|
||||
one; `file.uninstallScript {name}` removes an installed one.
|
||||
|
||||
## ScriptUI panels
|
||||
|
||||
A ScriptUI panel is a script that builds its user interface into the panel it is given as `this`.
|
||||
Put it in `Scripts/ScriptUI Panels/` in the settings folder (or use File ▸ Scripts ▸ Install
|
||||
ScriptUI Panel…) and it is listed at the bottom of the Window menu under its file name; choosing it
|
||||
runs the script and docks its panel as a tab, which you can move, float, maximize and close like any
|
||||
other panel. Closing the tab closes the panel (its `onClose` runs). Panels open when EffectCraft
|
||||
quits open again at the next launch (`window.restoreScriptPanels`, which the desktop and web apps
|
||||
run at start-up; the list is `scriptui_panels.json` in the settings folder).
|
||||
|
||||
Run from File ▸ Scripts instead, the same script gets no panel, so it should make a floating
|
||||
palette itself. A minimal panel:
|
||||
|
||||
```js
|
||||
// Nudge.jsx: moves the selected layers 10 px right, as a dockable panel.
|
||||
(function (thisObj) {
|
||||
var ui = thisObj instanceof Panel ? thisObj : new Window("palette", "Nudge", undefined, { resizeable: true });
|
||||
ui.orientation = "column";
|
||||
ui.alignChildren = ["fill", "top"];
|
||||
var go = ui.add("button", undefined, "Nudge Right", { name: "nudge" });
|
||||
var status = ui.add("statictext", undefined, "Select layers, then click.");
|
||||
go.onClick = function () {
|
||||
var comp = app.project.activeItem;
|
||||
if (!(comp instanceof CompItem)) return;
|
||||
var layers = comp.selectedLayers;
|
||||
app.beginUndoGroup("Nudge Right");
|
||||
for (var i = 0; i < layers.length; i++) {
|
||||
var p = layers[i].transform.position;
|
||||
var v = p.value;
|
||||
if (p.numKeys === 0) p.setValue([v[0] + 10, v[1]]);
|
||||
}
|
||||
app.endUndoGroup();
|
||||
status.text = "Moved " + layers.length + " layer(s).";
|
||||
};
|
||||
if (ui instanceof Window) ui.show(); else ui.layout.layout(true);
|
||||
})(this);
|
||||
```
|
||||
|
||||
Supported controls: `panel`, `group`, `button`, `iconbutton`, `statictext`, `edittext`,
|
||||
`checkbox`, `radiobutton`, `slider`, `scrollbar`, `progressbar`, `dropdownlist`, `listbox`,
|
||||
`tabbedpanel` / `tab` and `image`, with ScriptUI's automatic layout (`orientation`,
|
||||
`alignChildren`, `alignment`, `margins`, `spacing`, `preferredSize`), resource strings, event
|
||||
handlers (`onClick`, `onChange`, `onChanging`, `onClose`, `addEventListener`) and `onDraw` with
|
||||
ScriptUIGraphics. Agents use a panel like a user: `scriptui.list`, `scriptui.get` (its controls),
|
||||
`scriptui.click`, `scriptui.set` and `scriptui.close`; every control has the automation id
|
||||
`scriptui.<window>.<control id or name>`. The bundled sample `Layer Tools.jsx`
|
||||
(`crates/engine/scripts/ScriptUI Panels/`) is a longer example.
|
||||
|
||||
## Effect plug-ins
|
||||
|
||||
EffectCraft loads third-party effects through a small, versioned plug-in API (**API version 1**).
|
||||
A plug-in effect behaves exactly like a built-in one: it is listed in Effects & Presets (and the
|
||||
@@ -18,7 +109,7 @@ There are two ways to write one:
|
||||
`effect.plugins.list` lists what is loaded: `{api, wasm, plugins: [{id, name, category, version,
|
||||
author, source, params}]}`.
|
||||
|
||||
## The contract
|
||||
### The contract
|
||||
|
||||
* **Pixels** are premultiplied RGBA `f32` (0–1; values may exceed 1 in 32 bpc projects), row
|
||||
major, processed **in place**. The buffer is the layer's (after masks and the effects above),
|
||||
@@ -31,7 +122,7 @@ author, source, params}]}`.
|
||||
plug-in gets one instance per thread).
|
||||
* A failed render (an error code, a trap, running out of fuel) leaves the frame unchanged.
|
||||
|
||||
## The manifest
|
||||
### The manifest
|
||||
|
||||
Every plug-in describes itself with a JSON manifest:
|
||||
|
||||
@@ -64,7 +155,7 @@ Every plug-in describes itself with a JSON manifest:
|
||||
parameters; a slider needs `min ≤ default ≤ max` (and `sliderMin ≤ sliderMax`); the manifest
|
||||
is at most 1 MiB.
|
||||
|
||||
## WebAssembly ABI (v1)
|
||||
### WebAssembly ABI (v1)
|
||||
|
||||
A module with **no imports** that exports:
|
||||
|
||||
@@ -81,7 +172,7 @@ little-endian `f64`s) followed by the pixels (`width × height × 4` little-endi
|
||||
calls `ec_render`, and reads the pixels back. Each call gets a fuel budget proportional to the
|
||||
frame size; a module that loops forever stops with an error instead of hanging the app.
|
||||
|
||||
### Example
|
||||
#### Example
|
||||
|
||||
[`examples/plugins/posterize-bands`](../examples/plugins/posterize-bands) is a complete plug-in in
|
||||
plain Rust (no dependencies):
|
||||
@@ -97,7 +188,7 @@ effectcraft-cli exec effect.plugins.load '{"path": "target/wasm32-unknown-unknow
|
||||
Any language that compiles to WebAssembly works (C, Zig, AssemblyScript…) as long as the module
|
||||
has no imports. `crates/plugin/src/tests.rs` has a plug-in written directly in WebAssembly text.
|
||||
|
||||
## Rust plug-ins
|
||||
### Rust plug-ins
|
||||
|
||||
```rust
|
||||
use std::sync::Arc;
|
||||
@@ -121,7 +212,7 @@ register_plugin(Arc::new(Swap(manifest)))?; // before the UI builds its menus
|
||||
`PluginParams` reads values by parameter id (`f`, `b`, `point`, `color`). At most 64 plug-in
|
||||
effects can be registered per process.
|
||||
|
||||
## Compatibility
|
||||
### Compatibility
|
||||
|
||||
The API version is checked when a plug-in loads: a manifest or module for another version is
|
||||
refused with a message. Within a version the contract above does not change; additions (new
|
||||
|
||||
Reference in New Issue
Block a user