Driving Anima from agents
Everything in Anima is an engine command with a stable id and JSON params. Menus, shortcuts, panel gestures, the CLI, the control channel and MCP all go through the same commands, so anything a user can do, an agent can do too. There are three ways in:
| Interface | Best for | Needs a window |
|---|---|---|
MCP (anima-cli mcp) |
Claude Code and other MCP clients | no (headless), or yes with --bridge |
CLI (anima-cli exec/get/set/...) |
one-shot scripting and CI; JSON output with --json |
no |
Control channel
(anima --control 9877) |
driving and seeing the live UI; see control-protocol.md | yes |
MCP setup
Build once with cargo build --release -p anima-cli, then
register the server. For Claude Code, add a .mcp.json at
the project root:
{
"mcpServers": {
"anima": {
"command": "/path/to/anima/target/release/anima-cli",
"args": ["mcp"]
}
}
}
This repository ships a ready-made .mcp.json: anima
(headless, demo project loaded) and anima-app (bridged to a
desktop app started with --control 9877). Both run through
cargo run --release, so the first start compiles; run
cargo build --release -p anima-cli once beforehand to avoid
an MCP startup timeout.
You can also register it from the command line:
claude mcp add anima -- /path/to/target/release/anima-cli mcp.
Other clients (Claude Desktop, Cursor and the like) take the same
command and args.
- Headless (
["mcp"]): an in-process session with no window. Add"--demo"or"--project", "file.ecproj"to start with content. Startup is instant. It renders on the CPU unless you add"--gpu"(["mcp", "--gpu"]), which attaches the GPU compositor on a device of its own; if no adapter is usable the server says why and exits.get_project'srenderer(render.backend) reportsactive(gpu/cpu) and, on the CPU,why. - Bridge (
["mcp", "--bridge", "9877"]): drives a runninganima --control 9877, so you see every change live. Bridge mode addsscreenshotand theui_*tools.
After a broken connection, the bridge retries explicitly read-only control queries once. Engine commands, UI edits and unknown methods whose transmission or reply fails report an unknown outcome: the app may already have applied them. Inspect the app before explicitly retrying. A later call reconnects; timeouts and malformed replies are never automatically resent.
From an installed release
The release packages ship anima-cli alongside the
desktop app, so no build is needed:
| Install | CLI |
|---|---|
| Windows (MSI) | C:\Program Files\Anima\anima-cli.exe by default
(wherever you installed it otherwise), not on PATH |
| Linux (deb, rpm) | /usr/bin/anima-cli |
| macOS | the separate
anima-cli-<version>-macos-<arch>.zip release
asset (the .app holds only the desktop app) |
# Windows, default install folder
claude mcp add anima -- "C:\Program Files\Anima\anima-cli.exe" mcp
# Linux, or macOS with the CLI unzipped onto PATH
claude mcp add anima -- anima-cli mcp
The server speaks JSON-RPC 2.0 over stdio, one message per line, and
supports MCP protocol versions 2025-06-18, 2025-03-26 and 2024-11-05
(initialize, ping, tools/list,
tools/call).
Keeping headless work across restarts
Start anima-cli mcp --autosave (MCP args
["mcp", "--autosave"]) to preserve unsaved headless work.
Each tool call that changes a dirty project writes a checkpoint before
its reply is sent, so even killing the server after that reply leaves
the work on disk. EOF and transport errors also flush outstanding
changes. Queries do not rotate the saved versions. A failed checkpoint
adds a warning to the tool reply; the edit remains in memory and the
next call retries. Use save_project {path} to a writable
location before disconnecting if warned.
Each server has its own folder under
<config directory>/Anima Auto-Save/MCP/ (or
<custom Auto-Save folder>/MCP/). The maximum-version
setting applies within that folder. --autosave enables
these checkpoints regardless of the desktop's Auto-Save toggle and
interval; it reads settings without writing desktop preferences,
shortcuts or crash-recovery state. ANIMA_CONFIG_DIR
overrides the platform config directory. Session folders are retained
for recovery; remove old folders once their work is saved elsewhere.
initialize instructions and
_meta.animaAutoSave list the current folder and up to five
previous dirty sessions; get_project.autosave also reports
the latest checkpoint and errors. Recover explicitly with
open_project {"path":"<checkpoint .ecproj>"}, then
save_project {"path":"Recovered.ecproj"}. Prior sessions
may still be running and are never opened automatically. Replacing a
dirty project with open_project or
file.newProject discards it intentionally; save it first.
The metadata describes the current project in each session.
Without --autosave, headless work is in memory until
explicitly saved. The flag is headless only; a bridged desktop app owns
its auto-saves. An edit interrupted before its reply is received may be
absent from the last checkpoint.
See MCP conventions for core tools, resources, strict arguments, and render progress/cancellation.
Tools
| Tool | What it does |
|---|---|
list_commands {filter?, enabled_only?, schemas?} |
Discover command ids, their param docs (and, with
schemas, a JSON Schema each), and whether each can run
now. |
describe_command {command | id} |
One command in full: label, menu, shortcut, params doc, JSON
schema, enabled / why. |
execute_command {command | id, params?} |
Run any command (undoable). Unknown parameter keys are rejected with the accepted list. |
get_state |
Editor state (active comp, time, selections, tool) plus
app: version, command and effect counts, export formats,
parity summary. |
run_script {code, name?} |
Run JavaScript with the After Effects-style scripting object model
(app.project, comp.layers.addText(…),
layer.property("ADBE Transform Group").property("ADBE Position").setValueAtTime(…)…).
Returns
{ok, result, output, error: {message, line, column}}; edits
are undoable. |
get_project / get_comp {comp?} |
Project items, comp settings and layers. |
get_layer {layer, comp?, time?, depth?, flat?} |
A layer's property tree (depth limits how many group
levels expand). Every node has a path. |
get_property {layer, path, comp?, time?} |
A leaf property's value at a time, keyframes and expression. Group
paths (for example geometryOptions) contain children:
inspect them with get_layer, then query a leaf such as
geometryOptions/extrusionDepth. |
set_property {layer, path, value?, time?, timeBase?, expression?, comp?} |
Sets a static value. With time it sets a keyframe
(timeBase: "comp" for comp seconds); with
expression it sets an expression. |
add_keyframe {layer, path, time+value | keys:[...], timeBase?, interpolation?, comp?} |
Adds keys (layer seconds, or comp seconds with
timeBase: "comp"), then optionally applies
linear/bezier/hold/easyEase. |
list_effects {filter?} |
Effect ids, names, categories, GPU / 32-bpc support and parameters. |
list_fonts {query?, variable?, rescan?} |
Font families text layers can use, bundled and installed, with their
styles, origin, whether each is a variable font and own-language name;
variable lists only variable fonts; rescan
picks up fonts installed since launch. |
add_effect {layer, effect, values?, comp?} |
Apply an effect and set its parameters in one call and one undo step
(a failing value leaves nothing applied); returns the instance path
(effects/#n) and its parameter paths. |
render_frame {comp?, time?, max_side?, path?, inline?, transparent?} |
Returns a PNG image of a frame; transparent: true keeps
the alpha (as an RGB + Alpha render writes it) instead of compositing
over the comp background. |
open_project {path | demo | new} /
save_project {path?} |
Open and save files. |
undo {steps?} / redo {steps?} |
History. |
history {goto?} |
The branching undo history (History panel): every state with its
index, id, label, branch depth and whether it's current;
goto (an index or id) jumps to any state, on any branch,
without losing the others. |
script_ui {action?, window?, widget?, value?, result?} |
Drive ScriptUI windows that scripts opened: list
(default) the open windows, get a window's control tree,
click a button / checkbox / radio button, set
text, a slider or a list item, close a window
(result: what a dialog's show() returns,
default 2 = Cancel). |
batch {steps: [{command, params}], label?, atomic?} |
Several commands in one call and one undo step
(engine.batch); "$N.key" in a param is step
N's result ("$1.layer"). A failing step rolls the batch
back and names the step. |
(bridge)
screenshot {panel?, id?, max_side?, path?},
ui_inspect, ui_elements {prefix?},
ui_click, ui_drag, ui_key,
ui_type, ui_set,
control {method, params} |
Look at and operate the live window. |
command_list |
List commands (same arguments as list_commands). |
command_run {id, params?} |
Run a command. |
command_batch {steps, stop_on_error?} |
Run commands in order with per-step results; each edit has its own undo step. |
doc_inspect |
Project overview and active composition. |
render_preview |
Render a PNG without writing a file (comp,
time, max_side,
transparent). |
Layers are referenced by id (from get_comp),
"#n" (1-based index from the top) or name. Comps are
referenced by id or name, and default to the active comp. An id may also
be sent as a string ("98"): an all-digit string that
matches no name is taken as the id. Property paths come from
get_layer, for example transform/position,
transform/opacity, effects/#1/blurriness or
@57 (by uid). Times are in seconds. Keyframe times are
layer time, which equals comp time unless the layer is offset or
stretched.
Use MCP execute_command for engine commands
prop.set, prop.addKey, keys.set
and keys.select with
timeBase: "layer" | "comp"; omitted means
layer. The convenience tools set_property and
add_keyframe take the same timeBase (layer
time when omitted). The input base applies to both
keys.set.time and newTime, and to every
keys.select.keys[].time. Omitted prop.set or
prop.addKey times use the target comp's CTI. Supplied write
times snap to its nearest comp frame before conversion to stored layer
time, including stretched or reversed layers; existing
keys.set neighbor constraints and roving adjustment still
apply.
Explicit bases add timing metadata to mutation replies;
keys.select reports timing in keys[] for the
resulting selection. Returned key time and
layerTime are layer seconds; compTime is comp
seconds. Omitted-base prop.set, prop.addKey
and keys.select retain their existing JSON value/count
replies even outside the active range. Timing metadata carries
outside-range warnings; the existing objects from keys.set
and prop.get.keys[] also report both bases and warnings.
prop.get.time is comp seconds. Separated Position
prop.set retains per-axis replies; for these commands,
path or prop identifies a property, not a
group.
Set offsets with layer.timing {layer, start: 10.5};
in, out and delta also use comp
seconds; inDelta / outDelta trim each layer
from its own in/out point. For an unstretched layer starting at 10.5 s
in a 30 fps comp,
prop.addKey {layer: "A", path: "transform/opacity", time: 15.3, timeBase: "comp", value: 0}
writes a key at comp 15.3 s (layer 4.8 s), matching
prop.get {layer: "A", path: "transform/opacity", time: 15.3}.
Command params are checked against each command's params
doc. An unknown key, such as layer.select {"index": 2},
returns an error that lists the accepted keys (here
layers, add, toggle) instead of being ignored.
A typical session
execute_command {"command":"comp.new","params":{"name":"Intro","width":1920,"height":1080,"frameRate":30,"duration":4}}execute_command {"command":"layer.newText","params":{"text":"Hello","size":160,"fill":"#ffffff"}}returns{"layer": 2}.add_keyframe {"layer":2,"path":"transform/position","keys":[{"time":0,"value":[-300,540]},{"time":1.5,"value":[960,540]}],"interpolation":"easyEase"}execute_command {"command":"effect.apply","params":{"layer":2,"effect":"Gaussian Blur"}}, thenset_property {"layer":2,"path":"effects/#1/blurriness","value":8}render_frame {"time":1.0}lets you look at the result, andsave_project {"path":"intro.ecproj"}saves it.
Cookbook
Short recipes; every step is one tool call.
| Goal | Calls |
|---|---|
| Orient yourself | get_state → get_project →
get_comp {} |
| Find the right command | list_commands {"filter":"mask"} →
describe_command {"command":"mask.new"} (params + JSON
Schema) |
| Title card | execute_command comp.new {...} →
execute_command layer.newText {"text":"Hi","size":120} →
render_frame {"time":0} |
| Animate | add_keyframe {"layer":2,"path":"transform/scale","keys":[{"time":0,"value":[0,0]},{"time":0.5,"value":[100,100]}],"interpolation":"easyEase"} |
| Effect with settings | list_effects {"filter":"glow"} →
add_effect {"layer":2,"effect":"Glow","values":{"threshold":40,"radius":30}}
→
set_property {"layer":2,"path":"effects/#1/intensity","value":2} |
| Key a green screen | add_effect {"layer":2,"effect":"Keylight (1.2)"} →
execute_command effect.pickColor {"layer":2,"effect":1,"param":"screenColour","x":20,"y":20,"average":true}
(a screen pixel, layer space; samples the effect's input) →
set_property {"layer":2,"path":"effects/#1/screenMatte/clipBlack","value":10}
→ render_frame {"transparent":true} |
| Key objects by Cryptomatte | add_effect {"layer":2,"effect":"Cryptomatte"} →
execute_command effect.pickCryptomatte {"layer":2,"effect":1,"x":120,"y":80}
(the ID under that layer pixel replaces the Selection;
"mode":"add" / "remove" edit it;
effect.cryptomatteAt with the same point only reports the
name) → render_frame |
| Drive with an expression | set_property {"layer":2,"path":"transform/rotation","expression":"time*90"} |
| Inspect a layer | get_layer {"layer":2,"flat":true} (every property with
its path, value, key count) |
| Check the result | render_frame {"time":1.5,"max_side":640}; in bridge
mode screenshot {"panel":"Timeline"} |
| Undo a mistake | undo {}, or history {} then
history {"goto": n} |
| Missing media after opening | execute_command footage.check {"wait":true} →
{checked, missing, probed}; then
file.findMissing {"what":"footage"} |
| Save | save_project {"path":"out.ecproj"} |
The desktop app checks footage in the background after every open (a
"Checking footage" job in the Progress panel, jobs.list);
headless sessions run footage.check when they want it.
Pitfalls
Things that tripped up a long agent-driven session (a multi-scene 3D piece built entirely over MCP).
- Layer time vs comp time.
add_keyframeandset_propertywithtimetake layer time;get_propertyandrender_frametake comp time. Those tools and the engine keyframe commands default to layer time; passtimeBase: "comp"to supply comp seconds on offset or stretched layers.run_script'ssetValueAtTimeandkeyTimealso use comp time. Without an explicit engine time base, subtract the layer's start time before keying an unstretched layer (#257). - Moving a layer in time.
execute_command layer.timing {"layers":["Scene 2"],"start":5.1}sets the start time in comp seconds and moves the in and out points with it. - Loops in scripts.
run_scriptcan call any engine command withapp.run(id, params), for exampleapp.run("layer.new3dPrimitive", {kind: "cube", position: [x, y, z]})inside a loop. A script that throws keeps the edits it already made: wrap it inapp.beginUndoGroup()/app.endUndoGroup()so oneundo {}removes them all. (batchrolls back on its own.) - Save early in headless mode. Without
--autosavethe headless server keeps the project in memory only, so a server restart loses unsaved work: start it withmcp --autosave(see above) or callsave_project {"path": …}after each milestone. - Environment light.
layer.newLightalso accepts"kind":"Environment": an equirectangular image that metallic surfaces reflect. Set itslightOptions/sourceto a footage, comp or solid layer by layer id; with no source it uses the comp's Environment Layer (layer.environment) (#261).
Cookbook (from end-to-end QA)
The scenarios in crates/automation/tests/qa/ and
apps/anima-cli/tests/it/qa_template.rs build real projects
through these interfaces; they are worked examples of everything
below.
- Name things when you create them:
layer.newText,layer.newShape,layer.newSolid,layer.newNull,layer.newCameraandlayer.newLighttakename, so later calls can say"layer": "Title". A reference that matches no layer is an error, never a silent no-op. - Nested comps:
comp.settings {preserveFrameRate, preserveResolution}(Composition Settings ▸ Advanced);comp.info/get_compreport both. A precomp layer shows nothing outside its nested comp's span. - Viewer Resolution belongs to each comp and is saved
with the project, as in After Effects:
view.res.full|half|third|quarter|auto {comp?}orview.res.custom {factor: 1..40, comp?}(headless too; no undo step, the project becomes modified).comp.inforeportsresolutionandresolutionFactor(nullfor Auto). - Several footage items at once:
file.newCompFromSelection {single, dimensionsFrom, duration, sequence, overlap, overlapDuration, transition, addToRenderQueue}is one undo step (select the items first:project.select). - Where an item is used:
project.usedIn {item?}→{item, count, comps: [{id, name, layers}]}counts the layers, in every comp, whose source the item is (direct uses only, as the Project panel's "Name ▾, used 3 times" header; its ▾ lists the comps and opens the one picked). - Locked layers are left alone:
edit.clear,layer.arrange,layer.transformandlayer.centerAnchorskip them and fail when every target is locked ("… is locked"); Select All andlayer.selectNext/selectPreviousstep over them. Unlock withlayer.setSwitch {"layers": [...], "switch": "lock", "value": false}orlayer.unlockAll. - Solid Settings:
layer.settings {layer, name?, color?, width?, height?, pixelAspect?, affectAll?}edits a solid or adjustment layer's solid (and renames the layer and the solid). A solid shared by several layers (duplicates share theirs) changes for all of them unlessaffectAll: false, which gives this layer its own copy; the reply names it ({layer, solid}). Cameras and lights take their own settings through the same command. - Use the paths replies give you:
effect.applyreturns{effects: [uid], paths: ["effects/#2"]};layer.addShapeItem {kind, group?: uid|path}returns{uid, path}(e.g.contents/group/contents/gfill), ready forset_property.@uidworks as a whole path. - One call, one undo step:
batch {"steps":[{"command":"layer.newShape","params":{"kind":"ellipse","name":"Ring"}},{"command":"layer.addShapeItem","params":{"layer":"$1.layer","kind":"trim"}},{"command":"prop.addKey","params":{"layer":"$1.layer","path":"transform/opacity","time":0,"value":0}}]}(a string param that is exactly"$N"or"$N.key"is replaced;$2.pathis the trim's path). - Expressions:
set_property/get_propertyreplies carryevaluated(what renders) next tovalue(the keyframed value) andexpressionErrorwhen the expression fails. An expression with a syntax error is kept but disabled, so it rendersvalueand reports the syntax error. - Keyframes like a person:
keys.select {keys, toggle: true}(Shift+click),keys.selectEqual/selectPrevious/selectFollowing,keys.move {delta, merge}(steps sharing a merge key are one drag: each applies to the keys as they were before it, so a key passed over survives) andkeys.transform {…, merge, fromStart: true}(values are the whole transform since the drag started). Quick Offset (Ctrl/Cmd+Alt drag in the Timeline):layer.quickOffset {delta, layers?}andkeys.quickOffset {delta}stagger the selected layers, or each layer's selected keys together, in selection order: the first stays, the last moves bydelta(comp seconds) and the ones between by equal steps, onto subframes;merge+fromStart: truemakedeltathe whole drag. Keys of locked layers can't be selected. Copy / paste:keys.copy, thenkeys.pasteat the current time (keys copied from several layers go to as many selected layers in order). Whole effects:effect.copy, theneffect.paste {layers}places the earliest copied effect key at the destination comp's current time, preserving the offsets between all copied effects and parameters, including on offset or time-stretched layers.time.nextKey/time.previousKey/keys.selectAlltakevisible: [{layer, prop}]to act only on those properties (the Timeline passes its revealed ones, as J / K and Ctrl+Alt+A do). - Easing with Keyframe Velocity: select keys by path,
keys.select {"keys":[{"layer":"Ring","path":"contents/trim/end","time":0}]}, thenkeys.velocity {"outSpeed":0,"outInfluence":33.33}. - Gradients:
set_property {"path":"contents/gfill/colors","value":["#0080ff","#ffff00"]}or{"colors":[[0,"#0080ff"],[1,[1,1,0]]],"opacities":[[0,1],[1,0.5]]}. - Image sequences:
file.import {"paths":["/r/shot_0001.png"]}imports the file's whole numbered run as one item (several picked frames: that range; a folder: each run in it;"sequence": false: one still; dropped files,"drag": true, import as stills unless"sequence": true, as in After Effects;"alphabetical": true: Force Alphabetical Order, the files play one after another) atframeRateor 30 fps (Settings ▸ Import). Missing frames show colour bars, as in After Effects.file.interpretFootage {"items":[id], "frameRate":12, "startTimecode":1001}conforms them and sets Override Start (the item and layers that ran to its end get the new length). See footage.md. - Colour of footage:
file.interpretFootage {"items":[id], "preserveRgb":true}is Interpret Footage ▸ Preserve RGB: the file's values unconverted (float EXRs stay linear), for a per-layer OCIO conversion or data passes. - OCIO effects: with Configuration ▸ Custom,
configFileis a.ociopath or the config's text;effect.warning {layer, effect}says what Effect Controls warns about (a config it can't read, a colour space it lacks, transforms that pass colours through) ornull. - Multi-layer OpenEXR: the footage shows the beauty
pass (an unnamed RGBA layer, else Combined / Beauty / Image / RGBA…,
never Cryptomatte or depth).
layer.channels {layer}lists a footage layer's EXR layers and channels ({"layers":[{"name":"diffuse","rgba":["diffuse.R","diffuse.G","diffuse.B",""]}]}); EXtractoR shows them when itsred/green/blue/alphaare set to those names (prop.set {"path":"effects/#1/red","value":"depth.Z"}). Effect Controls offers them as popups. - Masks:
layer.addMask {layer, shape: rect|ellipse, rect}first, thenlayer.mask.set {field: feather|expansion|opacity, value}. - Renders:
renderQueue.add {comp, format: h264|prores|webm|png…, channels: rgba, proresProfile: 4444, output}thenrenderQueue.render {"wait": true}. Check a render by importing it (file.import,file.newCompFromSelection) andrender_frame {transparent: true}for its alpha (ProRes 4444 and WebM VP9 alpha both read back as straight alpha). - Essential Graphics: controls can be addressed by
name
(
essential.set {"layer":"#1", "control":"Title","value":"John Smith"}); a command that targets an explicitcompruns even when the active comp would disable it (essential.exportTemplate {"comp":"Lower Third", …}). The CLI also renders an exported template directly:anima-cli render --template lt.ectemplate --values values.json --out 'title_[#####].png'.values.jsonis an object such as{"Title":"John Smith","Bar Color":"#00c080","Bar Opacity":50}; names followessential.set, including its case-insensitive matching. A Media Replacement control takes a footage file path ({"Clip":"/in/clipB.mp4"}, or{"path": …}), which is imported and set as the control'sitem. Values override one imported instance; the template file and its source controls keep their defaults. Omit--valuesto render those defaults, or use the same options withrender-framefor a PNG. Values must fit in 1 MiB. Invalid JSON, unknown controls and invalid values fail before rendering. Template mode cannot be combined with--project,--demo,--bridge,--queueor--comp. - History: Levels of Undo defaults to 32
(
prefs.set {"key":"general.undoLevels","value":99});historylists branches andhistory {"goto": id}jumps between them. A command that changes nothing (a value set to what it already is) records no undo step.get_statereportsdirty(unsaved changes); it follows undo, so undoing back to the saved state clears it. - Unsaved changes: commands run through MCP /
engine.executenever ask to save. In the desktop app,ui.menu.invokeof a command that closes a modified project (file.open,file.newProject,file.closeProject,file.revert…) and the window's close button show the Save / Don't Save / Cancel prompt (ui_clickondialog.unsaved.save/dontSave/cancel;dialog.unsaved.revertfor Revert); the control channel'sapp.quit {"force": true}skips it.
Effect positions
Effect points, Puppet pins (puppet.addPin,
puppet.recordPin), Paint, Liquify and Roto Brush strokes,
effect.pickColor and effect.pickCryptomatte
are in effect space: layer pixels from the top-left of
the layer's effect bounds. For footage, solids and precomps that is the
layer's own pixel grid. Shape and text layers have comp-sized bounds
centred on their content's origin, so in a w ×
h comp the point [w / 2, h / 2] is that origin
(a new effect's default point). prop.get /
prop.set read and write these values as they are.
Motion tracking
Trackers live on the tracked layer
(Motion Trackers ▸ Tracker n ▸ Track Point n) and are
driven with track.* commands. track.analyze
runs in the background in the app (poll track.status); pass
"wait": true to block until it finishes, which is what the
CLI and headless MCP want.
execute_command {"command":"track.motion","params":{"layer":"clip.mov"}}creates a Transform tracker (Motion Target: the layer above). Usetrack.stabilizefor Stabilize, ortrack.newwithkindtransform|stabilize|affine|perspective|rawandrotation/scale.execute_command {"command":"track.setPoint","params":{"point":1,"center":[812,440],"featureSize":[40,40],"searchSize":[96,96]}}execute_command {"command":"track.analyze","params":{"direction":"forward","wait":true}}keys Feature Center, Confidence and Attach Point on every frame from the current time to the layer's end.execute_command {"command":"track.apply","params":{"dimensions":"xy"}}keys the target's Position (and Rotation/Scale), the layer's own Anchor Point and Position for Stabilize, or a Corner Pin effect foraffine/perspectivetracks.track.optionssets the channel, blur/enhance, adapt-feature and "If Confidence is Below" behaviour;track.statusreports the track points.
anima-cli run clip.ecproj track.motion '{"layer":"#2"}' \
track.setPoint '{"point":1,"center":[812,440]}' \
track.analyze '{"wait":true}' track.apply '{}' --save
Mask tracking and Mask Interpolation
track.mask follows the pixels inside a mask and keys its
Mask Path on every frame (vertices and tangents move with the fitted
motion). method is position,
positionScale, positionScaleRotation (the
default, also kept as the Tracker panel's Method),
positionScaleRotationSkew, perspective,
faceOutline (Face Tracking (Outline Only): the mask, drawn
around a face, becomes the face outline on every frame) or
faceDetailed (Face Tracking (Detailed Features): also keys
a Face Track Points effect with one point per landmark — eyebrows, eyes,
pupils, nose tip, nostrils, mouth corners and lips, chin, jaw);
direction is
forward|backward|frameForward|frameBackward from the
current time (or start / end in seconds). Like
track.analyze it runs in the background in the app
(progress in track.status under mask,
track.stop cancels and keeps what was tracked) and is one
undo step; "wait": true blocks.
track.extractFaceMeasurements {layer?} (Extract & Copy
Face Measurements) keys a Face Measurements effect from the Face Track
Points (head position, scale and orientation X/Y/Z, eye openness,
eyebrow distance from eye, mouth openness, width and offset), puts those
keys on the keyframe clipboard and returns them per frame plus a
tab-separated clipboard text.
Face tracking's trained model works like Roto Brush's (below):
face.models lists the built-in tracker and MediaPipe Face
Landmarker with its authors and licence;
face.model.download {id} /
face.model.install {path, id?} install it (verified);
face.model.select {id: classical|mediapipe-face} chooses
it; face.model.remove {id} deletes it. Downloads and
loading run in the background; "wait": true on download,
install or select returns once the model is in use (or with its error),
which scripts need before tracking. track.mask with a face
method returns faceModel, the engine it tries first (the
classical one when the model finds no face in the mask).
anima-cli run clip.ecproj face.model.download '{"id":"mediapipe-face","wait":true}' face.model.select '{"id":"mediapipe-face","wait":true}' \
track.mask '{"layer":"#1","mask":1,"method":"faceDetailed","direction":"forward","wait":true}' --save
mask.interpolate (Window ▸ Mask Interpolation ▸ Apply)
adds in-between Mask Path keys between each pair of selected Mask Path
keys (or the existing keys at times, in seconds), giving
both ends the same vertex count with a matched correspondence. Options:
keyframeRate (number or "auto" = the comp
rate), keyframeFields, linearVertexPaths,
bendingResistance, quality,
addVertices (number or false) with
addVerticesUnit pixels|total|percent,
matchingMethod auto|curve|polyline,
oneToOne, firstVerticesMatch.
mask.interpolationOptions sets the panel's defaults.
anima-cli exec track.mask '{"layer":"#2","mask":1,"method":"perspective","wait":true}' clip.ecproj --save
anima-cli exec mask.interpolate '{"layer":"#2","mask":"Mask 1","times":[0,2],"addVertices":10}' clip.ecproj --save --json
Warp Stabilizer
Effect ▸ Distort ▸ Warp Stabilizer
(effect.apply {"effect":"Warp Stabilizer"}), Animation ▸
Warp Stabilizer VFX and the Tracker panel's button
(track.warpStabilizer, which applies the effect and starts
analysing) stabilize a layer.
warp.analyze {layer?, effect?, wait?} analyses every frame
between the layer's In and Out points in the background ("Analyzing in
background (step 1 of 2)", then "Stabilizing...";
warp.status reports progress,
banner, whether the effect is analyzed, and
the current frame's warp matrix, autoScale and
crop); warp.cancel stops it without writing
anything. The analysis is stored in the effect (saved with the project)
and is one undo step; trimming, slipping, time-remapping or replacing
the layer's source clears it and the app re-analyses automatically
(headless: run warp.analyze again). Settings are ordinary
properties under the instance's groups, e.g.
effects/#1/stabilization/result (0 Smooth Motion, 1 No
Motion), effects/#1/stabilization/smoothness,
effects/#1/stabilization/method,
effects/#1/borders/framing (0 Stabilize Only … 3 Stabilize,
Synthesize Edges),
effects/#1/borders/autoScale/maximumScale,
effects/#1/advanced/showTrackPoints. Method 3 (Subspace
Warp, the default) bends a mesh per frame to the smoothed feature
trajectories, so scenes with depth stabilise where one perspective
transform cannot; effects/#1/advanced/rollingShutterRipple
(0 Automatic Reduction, 1 Enhanced Reduction: a finer, softer mesh)
tunes it.
anima-cli run shaky.ecproj track.warpStabilizer '{"layer":"#1","wait":true}' \
prop.set '{"layer":"#1","path":"effects/#1/stabilization/result","value":1}' --save
anima-cli exec warp.analyze '{"layer":"#1","wait":true}' shaky.ecproj --save --json
anima-cli exec warp.status '{"layer":"#1"}' shaky.ecproj --json
3D Camera Tracker
Animation ▸ Track Camera and the Tracker panel's button
(track.camera {layer?, shotType?: fixed|variable|specify, aov?, solveMethod?: auto|typical|flat|tripod, detailed?, lensDistortion?, undistort?, wait?})
apply Effect ▸ Perspective ▸ 3D Camera Tracker (or reuse the layer's)
and analyse it in the background: "Analyzing in background (step 1 of
2)" tracks features, "Solving camera" solves.
camera.analyze {layer?, effect?, wait?} re-runs it,
camera.cancel stops it without writing anything.
camera.solveStatus reports
progress/banner, analyzed,
solved, methodUsed, averageError
(pixels), focalLength, horizontalAngleOfView,
the current frame's camera (position, orientation, zoom),
groundPlane and the solved lensDistortion
(k1, k2, normalised by half the frame
diagonal) when Solve Lens Distortion
(effects/#1/advanced/lensDistortion, also on with Detailed
Analysis) is on; effects/#1/advanced/undistort renders the
footage undistorted so the solved camera's 3D layers line up.
camera.points {time?} lists the solved points visible now
with their id, comp position,
depth, world position and
error.
Select points with
camera.selectPoints {points, add?, toggle?} (the viewer's
click / Shift-click / marquee), then the right-click menu's commands:
camera.setGroundPlane {points?} (Set Ground Plane and
Origin),
camera.createFromSolve {kind: text|solid|null|shadowCatcher, points? | target: {center, normal, size?}, multiple?}
(Create Text / Solid / Null / Shadow Catcher, Camera and Light, Create
Multiple …), camera.deletePoints {points?, wait?}
(re-solves; with Auto-delete Points Across Time the same feature's other
tracks go too) and camera.create (the Create Camera
button). Each is one undo step; the first create adds the one-node "3D
Tracker Camera" keyed on every frame. Changing the layer's frames clears
the analysis; changing effects/#1/shotType,
effects/#1/horizontalAngleOfView,
effects/#1/advanced/solveMethod re-solves the stored
tracks.
anima-cli run shot.ecproj track.camera '{"layer":"#1","wait":true}' \
camera.points '{}' --json
anima-cli run shot.ecproj camera.createFromSolve '{"kind":"solid","points":[12,40,77]}' --save
Roto Brush & Refine Edge
The Roto Brush tool (Alt+W cycles Roto Brush / Refine Edge) paints in
the Layer panel; agents use
roto.stroke {layer, kind: fg|bg|refine|refineErase, points: [[x, y], …], frame?, radius?}
(effect space, layer frames). The first foreground stroke applies the
Roto Brush & Refine Edge effect and sets the base frame with a span
of 20 frames each side.
roto.propagate {layer, direction?: forward|backward|both, to?, wait?}
segments the span (in the background unless wait); strokes
on any other frame correct it and propagation restarts from there.
roto.span {start?, end?}, roto.freeze {wait?}
/ roto.unfreeze,
roto.clearStrokes {frame?, kind?}, roto.cancel
and
roto.options {diameter?, refineDiameter?, view?: alphaBoundary|alpha| alphaOverlay|none}
complete the set; every edit is one undo step. If trimming the layer or
shortening the comp leaves strokes outside the layer,
roto.span returns an error without changing the strokes:
restore the timing or remove those strokes with
roto.clearStrokes first.
roto.status {layer, frame?, matte?, compute?, compareTo?}
reports the base frame, span, computed and stroked frames, frozen state,
job progress and, for a frame, the matte's area, centroid, RLE matte and
IoU against a reference. Matte settings are ordinary properties
(effects/#1/rotoBrushMatte/searchRadius,
effects/#1/refineEdgeMatte/decontaminateEdgeColors, …).
Trained model (Roto Brush 2.0 / 3.0):
roto.modelslists the built-in engine and every registered model, with its authors, licence, size, URL, SHA-256 and whether it is installed, selected and active.roto.model.download {id}fetches and verifies the official weights in the background;roto.model.install {path, id?}installs a file you already have.roto.model.select {id: classical|mobilesam}chooses which one Roto Brush uses. Loading runs in the background;"wait": true(also on download and install) returns once it is in use.roto.model.remove {id}deletes the installed weights.- The
versionproperty (effects/#1/version: 0 = 1.0, 1 = 2.0, 2 = 3.0) picks between the classic engine (1.0) and the chosen model (2.0, 3.0).
anima-cli run clip.ecproj roto.model.install '{"path":"mobile_sam.pt"}' roto.model.select '{"id":"mobilesam","wait":true}' \
roto.stroke '{"layer":"#1","points":[[300,200],[360,230]],"radius":10}' roto.propagate '{"layer":"#1","wait":true}' --save
anima-cli run clip.ecproj roto.stroke '{"layer":"#1","points":[[300,200],[360,230]],"radius":10}' \
roto.stroke '{"layer":"#1","kind":"bg","points":[[40,40],[600,40]],"radius":12}' \
roto.propagate '{"layer":"#1","wait":true}' roto.freeze '{"layer":"#1","wait":true}' --save
Object Matte
The Object Matte tool (third in the Alt+W slot) works in the
Composition viewer: click an object, or drag a box around it; Shift adds
another Object Matte instead of re-prompting the layer's. Agents use
objectMatte.apply {layer, point: [x, y], box?: [x0, y0, x1, y1], frame?, track?, before?, after?, effect?}
(layer pixels, layer frames; effect re-prompts that
instance), one undo step each. With a trained model installed (Settings
▸ Roto Brush) the box is the model's box prompt.
objectMatte.propagate {layer, effect?, direction?, wait?}
tracks the matte over the frames around the reference frame in the
background (roto.cancel stops it; edits to the prompt or
the layer's frames queue it again),
objectMatte.freeze {wait?} /
objectMatte.unfreeze store or drop the final mattes, and
objectMatte.status {layer, effect?} reports the job's
progress, the frozen state, the tracked frames and which are computed.
With the disk cache on (Settings ▸ Media & Disk Cache), computed
Object Matte and Roto Brush segmentations are also kept there, keyed by
what they are made of, so reopening a project reuses them.
anima-cli run clip.ecproj objectMatte.apply '{"layer":"#1","point":[320,200],"box":[250,120,400,300]}' \
objectMatte.propagate '{"layer":"#1","wait":true}' objectMatte.freeze '{"layer":"#1","wait":true}' --save
Text: styles and editing
Source Text holds character style runs and per-paragraph settings. CR
(\r), LF (\n) and CRLF (\r\n)
each make a paragraph break; the original string and character indices
are preserved. This also applies to scripted TextDocument
values and sourceRectAtTime() bounds.
layer.setText changes the whole layer, or only characters
range: [start, end] (character indices): character
attributes (font, size, fill,
tracking,
kerning: metrics|optical|<1/1000 em>,
tsume, baseline: superscript|subscript,
allCaps…) split the text into runs; paragraph attributes
(justify, indentLeft,
indentFirst, spaceBefore,
direction, composer,
hangingPunctuation…) apply to the paragraphs the range
touches. Editing mirrors the Type tool:
layer.newText {"text":"", "box":[100,100,600,300], "edit":true}makes paragraph text and starts editing (positioninstead ofboxmakes point text;vertical: truevertical type).text.insert {"text":"Hello world"}types at the caret (replacing the selection);text.setSelection {"start":6,"end":11}selects "world";text.moveCaret {"to":"wordLeft", "extend":true}moves like the arrow keys;text.delete {"word":true}is Alt+Backspace.layer.setText {"range":[6,11], "size":40, "fill":"#ff5500"}styles the selection (the Character panel does the same); with an empty range it sets the style the next typed text takes.edit.copy,edit.paste,edit.pasteTextMatchFormattingandedit.pasteTextFormattingOnlywork on the selected text;text.endEditcommits (an empty Type-tool layer is removed).
Fonts: text.fonts (MCP list_fonts) lists
every family a text layer can use, bundled and installed, with its
styles, origin (bundled, system,
user), variable (it has variation axes) and
own-language name ({"query":"gothic"} filters by name,
{"variable":true} keeps only variable fonts, like the font
menu's Variable Fonts filter; {"rescan":true} first picks
up fonts installed since launch). Pass a listed family and
one of its styles to
layer.setText {"font", "style"}. A variable font's named
instances (Light … Black) are styles of its
family; a style or an instance's PostScript name
("font": "Figtree-Black") sets the instance's axis values,
and variations set on the text override them axis by axis.
text.fontFeatures says which OpenType options a font draws
with its own glyphs. A tool with its own font library can hand it over
without installing it: folders listed in ANIMA_FONT_DIRS (a
path list, :-separated, ; on Windows) are
scanned like the system font folders. On Linux and the BSDs the system
font folders are /usr/share/fonts,
/usr/local/share/fonts, ~/.local/share/fonts,
~/.fonts, $XDG_DATA_HOME/fonts,
fonts in each $XDG_DATA_DIRS folder, every
<dir> the fontconfig configuration names
(/etc/fonts/fonts.conf with its includes,
~/.config/fontconfig/fonts.conf) and, in the Flatpak, the
host's fonts under /run/host. The Flatpak can't see other
folders; grant one with
flatpak override --user --filesystem=<dir>:ro --env=ANIMA_FONT_DIRS=<dir> com.ncdev.anima.
In expressions, text.sourceText.style /
getStyleAt(i, t) read styles and the setters
(setFontSize(v, start?, count?), setFillColor,
setText, setJustification…) return a styled
document.
Scripts and ScriptUI windows
Scripts that build ScriptUI windows publish them to the session; agents drive them like a user:
scriptui.list→[{window, title, kind: dialog|palette|window|panel, script, modal, size}].scriptui.get {"window": id}→ the control tree (type,name,text,value,checked,items,selection, laid-outbounds,handlers…).scriptui.click {"widget": "ok"}presses a button / toggles a checkbox / picks a radio button or tab;scriptui.set {"widget": "#4", "value": "Shot_"}types into edit text, moves a slider or picks a list item (index or text);scriptui.close {"result": 2}closes. Controls are addressed by id,#id,properties.nameor text;windowcan be omitted when one window is open.scriptui.mouse {"widget": "pad", "type": "mousedown", "x": 10, "y": 20}sends a mouse event (mousedown,mouseup,mousemove,mouseover,mouseout,click;x/yrelative to the control,button,detail,shiftKey…) to the script'saddEventListenerlisteners. Each returns the handler run's{ok, output, error}.
A dialog's show() waits for the user: the
script.run / file.runScript reply carries
"waiting": true, and the script continues (its final output
arrives in the reply of the click that closes the dialog). File ▸
Scripts: file.scripts.list,
file.runScript {"name": …}, file.installScript
/ file.installScriptUIPanel {"path": …},
window.scriptPanel {"name": …},
window.restoreScriptPanels (the panels open when the app
last quit). Scripts' app.settings:
script.settings.get {"section": …, "key": …} and
script.settings.save {…, "value": …}. Extension points and
the bundled extensions: plugins.md.
Timeline: layer order, properties and shortcuts
layer.arrange {"layers": [...], "above": "Layer Name"}moves layers to just above another one (what dragging Timeline rows does: the selected layers move together, one undo step);to: front|forward|backward|backandindexstill work.- The reveal shortcuts are UI commands (
ui.menu.invokeor the keys themselves): A Anchor Point, P Position, S Scale, R Rotation, T Opacity, M Mask Path, F Mask Feather, E Effects, L Audio Levels, U keyframed properties; pressed twice quickly AA Material Options, PP paint / Roto Brush / Puppet, SS selected properties, RR Time Remap, TT Mask Opacity, MM all mask properties, FF missing effects, EE Expressions, LL Waveform, UU everything changed from its default. Shift+key adds or removes a set; the same key again hides it; Ctrl/Cmd+` twirls the selected layers open or closed. With no layer selected they apply to every layer. - A row control clicked on one of several selected layers applies to every selected layer, as in After Effects: the A/V switches (video, audio, solo, lock), the Switches column (shy, collapse, quality, fx, frame blending, motion blur, adjustment, 3D), the label swatch and the Modes column (blending mode, Preserve Transparency), one undo step, with the layers the switch doesn't apply to left out. A control on an unselected row changes that layer alone; Alt-clicking an A/V feature still sets the selected layers apart from the rest.
keys.toggleTransform {"layers"?: [...], "prop": "position"}(Alt+Shift+A/P/S/R/T) adds or removes a keyframe at the current time on that Transform property of each layer, in one undo step (separated X/Y/Z Position; Orientation and X/Y/Z Rotation on 3D layers).- Rename a layer with Enter (or double-click a name): the whole name
is selected, Enter or a click elsewhere commits, Escape cancels;
layer.rename {layer, name}does it directly. - Ctrl/Cmd-click on the current-time display (Timeline or Composition
panel) toggles the Time Display Style between Timecode and Frames:
file.projectSettings {"timeDisplay": "frames"}. prop.pickWhip {layer, prop, target: {layer, prop, dimension?}}links a property as its expression pick whip does;dimensionpicks one of the target's values (position[0]), as a drop on that value field does.- Every property row has a property pick whip in the Parent & Link
column (
timeline.prop.<uid>.propertyPickWhip; the expression row's istimeline.prop.<uid>.pickWhip). Dragged onto another property's name or value, in the Timeline or Effect Controls (effectControls.prop.<uid>.value[.<d>]), it runsprop.pickWhip: the reference replaces the property's expression, or is added as one, and the expression is enabled. - A property's menu (right-click its name; entries
timeline.prop.<uid>.menu.<entry>): Reset (prop.reset: no expression, and the property's default value, an effect parameter's or a Transform property's; an animated property keeps its keyframes and gets the default as a keyframe at the current time), Edit Value… (prop.set {layer, prop}withoutvalueopens the value dialog), Separate Dimensions (Position:prop.separateDimensions {layer, value}), Add / Remove Expression (prop.setExpression {layer, prop}) and Add Property to Essential Graphics (essential.addProperty).
History, puppet recording, plug-ins
edit.history.listlists every undo state as a tree (undoing then editing keeps the undone states as a branch);edit.history.goto {"index": n}(orid, orsteps) jumps to any of them. MCP: thehistorytool.puppet.recordPin {"layer": "#1", "pin": "Puppet Pin 1", "samples": [[t, x, y]…]}records a drag (t = seconds since it began, effect space) into Position keys at the comp frame rate from the current time;puppet.recordOptions {speed, smoothing, useDraftDeformation, showMesh}.puppet.selectPins {"layer": "#1", "pins": ["Puppet Pin 1"], "add"?, "toggle"?}selects pins like the viewer's click / Shift-click (pins: []deselects); Edit ▸ Clear (Delete) then removes the selected pins, not their layer (puppet.removePin {pins?}). Advanced and Bend pins turn by their ring and scale by its square in the viewer (Shift: 15° / 5 % steps; automation idsviewer.puppetPin.<uid>.rotate/.scale), orpuppet.setPin {rotation, scale}. A drag outside the art (or Alt-drag) marquee-selects pins; Edit ▸ Select All with a pin selected selects every pin of that kind;puppet.recordPin {..., "pins": [...]}records several pins moving together (⌘/Ctrl-drag one of several selected pins).- Rigging pins:
paths.pointsFollowNulls {"layer": "#1", "pins"?: [...]}gives each Position / Advanced pin a null that drives it (parent the nulls to each other to build limbs);paths.nullsFollowPoints {..., "pins": [...]}makes nulls that ride on pins (parent props to them). Withoutpinsthey use the selected pins, or every pin of a layer with no path. - Follow-through (hair, cloth, tails):
puppet.follow {"leader": "Puppet Pin 1", "pins": [...], "delay": 0.1, "amount": 100, "cascade": true}makes the pins trail the leader's motion (the k-th nearest by k × delay withcascade) through Position expressions; withoutleader/pinsthe first selected pin leads the other selected pins. effect.plugins.load {"path": "x.wasm"}/effect.plugins.list: WebAssembly effect plug-ins (plugins.md), theneffect.applyby id like a built-in.
CLI
Each invocation runs a headless engine with no window. It opens the
demo project unless you pass --project F.ecproj, a
positional *.ecproj or --empty. Add
--json for one compact JSON document on stdout. Errors
print {"error": ...} and exit with status 1; usage errors,
such as an unknown option, exit with status 2 before anything runs.
anima-cli info --json
anima-cli commands --filter keys
anima-cli exec --list --schemas --json # every command with its params JSON Schema
anima-cli exec comp.new --params '{"name":"Main","width":1280,"height":720}' --empty --save-as main.ecproj
anima-cli exec layer.newSolid '{"color":"#3366ff"}' main.ecproj --save
anima-cli props Main '#1' --project main.ecproj # flat property list with paths
anima-cli set Main '#1' transform/opacity 40 main.ecproj --save
anima-cli set Main '#1' transform/position '[100,360]' --time 0 main.ecproj --save
anima-cli get Main '#1' transform/position --time 0.5 main.ecproj --json
anima-cli run main.ecproj comp.open '{"comp":"Main"}' time.set '{"time":1}' --json
anima-cli render-frame main.ecproj --time 1 --max-side 640 --out f.png --json
anima-cli exec layer.newNull --bridge 9877 # same commands, against the live app
anima-cli exec file.exportLottie '{"comp":"Main","path":"main.json","includeExpressions":true}' main.ecproj --json
anima-cli exec file.importLottie '{"path":"anim.json"}' main.ecproj --save
anima-cli render main.ecproj --comp Main --out main.mov --format prores --progress # progress lines on stderr for a program that reads them
Scripts written against After Effects' documented scripting API run
with script:
anima-cli script build.jsx --save-as main.ecproj # empty project unless one is given
anima-cli script --eval 'app.project.item(1).numLayers' main.ecproj --json
anima-cli script tweak.jsx --bridge 9877 # against the live app
writeLn/$.writeln/alert output
is printed, then the value of the last expression. A script error exits
with status 1 and file:line:col: message. Scripts may read
files only in the project's folder and may not write files or use the
network unless the user turns on Preferences ▸ Scripting &
Expressions ▸ Allow Scripts to Write Files and Access Network.
file.exportLottie returns
{path, bytes, warnings}: the warnings list every feature
Lottie cannot express (most effects, cameras and lights, layer styles,
audio, video footage…), so an agent can check what a player will not
show. A .lottie path writes a dotLottie archive.
file.importLottie returns
{comp, items, warnings} and opens the new composition.
Premiere Pro interop goes through timeline interchange (MCP: the
execute_command tool; control channel:
engine.execute with the same ids):
anima-cli exec file.importTimeline '{"path":"edit.xml"}' main.ecproj --save # FCP7 XML / .fcpxml / .otio / .edl / .aaf / .omf
anima-cli exec file.exportTimeline '{"comp":"Main","path":"Main.xml"}' main.ecproj --json
anima-cli exec file.exportTimeline '{"comp":"Main","path":"Main.otio","prerender":"none"}' main.ecproj
file.importTimeline returns
{format, folder, comps, allComps, items, missing, warnings}
(missing lists media imported as placeholders).
file.exportTimeline writes Final Cut Pro XML
(.xml, which Premiere Pro opens with File ▸ Import) unless
format or the extension says fcpxml,
otio, edl, aaf or
omf; prerender (unsupported by
default, all, none) controls which layers are
rendered to ProRes 4444 movies next to the document. It returns
{path, format, sequences, videoTracks, audioTracks, clips, prerendered, warnings}.
Native .prproj files are not read or written.
<comp> is an id or name, or - for the
active comp. <value> is JSON (50,
[960,540], "#ff0000") or a bare string.
Seeing the UI
To work on the UI, start the app with
cargo run -p anima -- --control 9877 (add
--demo to open the demo project; without it the app starts
with an empty project) and use MCP bridge mode or the raw control
channel. A good loop is: ui_elements to find an id,
ui_click or ui_drag to act, then
screenshot {"panel":"Timeline"} to check the result.
render_frame shows the composition itself at any zoom.
Effects & Presets
The native Effects & Presets panel's options menu includes
Copy Presets Folder Path. It copies the folder the
panel scans: EC_PRESETS_DIR when set, otherwise
~/Documents/Anima/Presets using HOME or
USERPROFILE. Relative paths are resolved to an absolute
path. The folder may not exist yet; this action does not create it. Over
the live UI control channel,
ui.menu.invoke {"id":"effects.copyPresetsFolderPath"}
copies the same path and returns {path}. The web app
reports that it has no local presets folder.