jablonkai/skills

cavalry

remote-control Cavalry (Scene Group's 2D motion-design app) via a scriptable bridge — build scenes procedurally, animate with keyframes and per-letter text effects, then render PNG frames or alpha overlay videos

First seen Jul 31, 2026

Installation

$ npx skills add jablonkai/skills --skill cavalry

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from jablonkai/skills · top by installs.

npx skills add jablonkai/skills

Browse all from jablonkai/skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 1
License LICENSE
Default branch main
Open issues 4
Status Active

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 12,902 B
  • docs SUMMARY.md 708 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 40 installs

SKILL.md

Cavalry Control

Cavalry (/Applications/Cavalry.app) is scriptable in JavaScript (ES6, api. / ui. / cavalry.* modules). Drive it through the Cavalry Bridge — a small script UI running inside Cavalry that executes whatever is POSTed to 127.0.0.1:8731. Everything below was proven on a real project (EMU logo sting + race animation); the reference file covers the rest of the API surface.

  • [references/api-reference.md](references/api-reference.md) — condensed module reference (api, cavalry, ui, WebClient/WebServer, render scripts, CLI). Read it when you need something outside the cheatsheet below.
  • [references/cavalry-types/](references/cavalry-types/README.md) — the complete API as vendored TypeScript definitions with JSDoc examples; grep it for any function the reference doesn't cover.
  • [scripts/cavalry-bridge.js](scripts/cavalry-bridge.js) — the bridge to install into Cavalry.
  • [scripts/cavalry-send.sh](scripts/cavalry-send.sh) — send a JS file (or -c 'inline code') to the bridge and wait for completion. --ping checks the bridge is reachable first.

The control loop

  1. User starts the bridge (one-time per session): Cavalry must be running with

Cavalry Bridge.js started from its Scripts menu. This cannot be done remotely — if curl -s -m 2 http://127.0.0.1:8731/ gets no answer, ask the user to open Cavalry and run Scripts ▸ Cavalry Bridge. To install the bridge the first time, copy [scripts/cavalry-bridge.js](scripts/cavalry-bridge.js) into the Cavalry Scripts folder (~/Library/Application Support/Cavalry/Scripts/ — verify via Help ▸ Show Scripts Folder).

  1. Write a build script to the scratchpad and send it:

bash scripts/cavalry-send.sh /path/to/build.js. The helper waits for the bridge's status file to change — it discovers the path from the bridge's GET /get reply, so it lives in the user's own Cavalry preferences folder rather than a guessable /tmp name (CAVALRYBRIDGESTATUS overrides). Long renders need CAVALRYSENDTIMEOUT=600 (seconds).

  1. Feedback comes back through files, not HTTP. The bridge only reports ok/failed;

console.log output stays in Cavalry's Log window. So every script must write its results to disk to be read back: - visual state → api.setFrame(f); api.renderPNGFrame(dir + "/check_" + f, 50) at a few key frames (50 = 50% scale preview), then Read the PNGs; - data (ids, bounding boxes, attribute values) → api.writeToFile(path, JSON.stringify(x), true).

  1. Iterate: inspect previews, fix the script, resend. Build scripts should be re-runnable —

create a fresh comp per version (api.createComp("Thing v2")) so reruns don't stack layers into the old comp.

  1. Final render: PNG frame loop + ffmpeg (works on every licence tier — see Rendering below).

A failed send (ok: false) usually means a JS exception; there is no stack trace over HTTP. Re-send the script wrapped so the error lands in a file: try { / body / } catch (e) { api.writeToFile("/tmp/cavalry-error.txt", String(e), true); }

Scene-building cheatsheet (proven patterns)

Comp setup — attribute names vary between Cavalry versions

Wrap api.set in a try/catch fallback chain rather than trusting one attribute name:

function trySet(id, obj) { try { api.set(id, obj); return true; } catch (e) { return false; } }
var comp = api.createComp("My animation v1");   // or api.getActiveComp()
api.setActiveComp(comp);
trySet(comp, {"resolution": [1920, 1080]});
trySet(comp, {"frameRate": 30}) || trySet(comp, {"fps": 30});
trySet(comp, {"frameRangeEnd": 240}) || trySet(comp, {"endFrame": 240}) || trySet(comp, {"outFrame": 240});

Shapes and attributes

var rect = api.primitive("rectangle", "BG");         // also: "ellipse", "polygon", "star"...
api.set(rect, {"generator.dimensions": [1980, 1120],  // oversize the BG ~40px past frame edges
               "position": [0, 0],                    // origin = comp centre, +y = up
               "material.materialColor": "#121212"});

Common attribute paths: position [x,y], position.x, scale.x/scale.y, rotation, material.materialColor (hex string), material.alpha (0–100 scale — values ≤ 1 are near-invisible), generator.dimensions [w,h], generator.radius [rx,ry] (ellipse). To discover names: right-click a layer → Copy Layer Id, right-click an attribute → Copy Scripting Path, or api.getAttributes(id) / api.getAttributeDefinition(id, attr).

Sizing you can trust: measure, don't assume

Generator attrs also differ between versions (radius vs dimensions), so set what works, then measure the real size and normalise with scale:

var e = api.primitive("ellipse", "Dot");
trySet(e, {"generator.radius": [px/2, px/2]}) || trySet(e, {"generator.dimensions": [px, px]});
var bb = api.getBoundingBox(e, true);              // {x,y,width,height,centre,...} world space
var s = (bb.width > 1) ? (px / bb.width) : 1;      // multiply into every later scale keyframe

The same trick sizes imported images: load, measure height, scale to target pixel height.

Keyframes and easing

api.keyframe(layer, 22, {"position.y": 940});
api.keyframe(layer, 50, {"position.y": 150});
api.magicEasing(layer, "position.y", 22, "BounceOut");   // easing is applied per-attr AT a keyframe

Magic easing presets: SlowIn/SlowOut/SlowInSlowOut, VerySlowIn/VerySlowOut/VerySlowInVerySlowOut, SpringIn/SpringOut/SpringInSpringOut, SmallSpring… variants, AnticipateIn, OvershootOut, AnticipateInOvershootOut, BounceIn/BounceOut/BounceInBounceOut, None. Taste notes from production: OvershootOut for pop-in text, BounceOut for drops/landings, SlowOut for bursts (particles, dust), SlowInSlowOut for drifts and pulses.

Cheap particle burst (no particle system needed): N small ellipses keyframed from one origin outward on Math.cos/sin(angle) * dist, scaling to 0, SlowOut — see the reference for the full confetti/spark snippet.

Text — and the per-letter animation recipe

var t = api.create("textShape", "Title");
api.set(t, {"horizontalAlignment": 1, "verticalAlignment": 1,   // 1 = centre
            "fontSize": 150, "font.font": "Nebula Sans", "font.style": "Black",
            "material.materialColor": "#FFFFFF", "autoWidth": true, "autoHeight": true,
            "position": [0, -190]});
trySet(t, {"text": "EMU 6-DAY RACE"}) || trySet(t, {"string": "EMU 6-DAY RACE"});  // attr name varies

Per-letter bounce-in (each glyph drops with a stagger) — the proven wiring is subMesh deformer + stagger driving its time offset:

var subMesh = api.create("subMesh", "Title bounce");
api.keyframe(subMesh, 34, {"shapePosition.y": 990});   // start above frame
api.keyframe(subMesh, 64, {"shapePosition.y": 0});
api.magicEasing(subMesh, "shapePosition.y", 34, "BounceOut");
api.connect(subMesh, "id", t, "deformers");
api.parent(subMesh, t);

var stagger = api.create("stagger", "Title stagger");
api.set(stagger, {"minimum": -12, "maximum": 0});      // frames of per-letter offset
api.flipGraph(stagger, "graph", "vertical");           // so the FIRST letter lands first
api.connect(stagger, "id", subMesh, "shapeTimeOffset");
api.parent(stagger, t);

Check fonts before using them: cavalry.fontExists(family), cavalry.getFontStyles(family).

Images and other assets

var asset = api.loadAsset("/abs/path/logo.png", false);   // false = not an image sequence
var layer = api.addAssetToComp(asset);
var bb = api.getBoundingBox(layer, true);
var s = 470 / bb.height;                                  // scale to ~470px tall
api.set(layer, {"scale.x": s, "scale.y": s});

Data-driven scenes: api.jsonFromAsset(assetId) parses a JSON/CSV asset, api.loadGoogleSheet(spreadsheetId, sheetId) pulls a live sheet, and api.WebClient fetches from any HTTP API — see the reference.

Rendering

Default: PNG frame loop + ffmpeg. Works on every licence tier and gives you the frames to inspect:

function pad(n) { return ("0000" + n).slice(-4); }
api.stop();
for (var f = 0; f <= END; f++) { api.setFrame(f); api.renderPNGFrame(OUT + "/name_" + pad(f), 100); }

renderPNGFrame(pathWithoutExtension, scalePercent) renders the current frame; .png is appended. A 240-frame 1080p render takes a while — raise CAVALRYSENDTIMEOUT. Then assemble:

ffmpeg -framerate 30 -i name_%04d.png -c:v libx264 -pix_fmt yuv420p out.mp4       # opaque
ffmpeg -framerate 30 -i name_%04d.png -c:v qtrle out.mov                          # alpha overlay

For alpha output leave the comp background transparent (add no BG rectangle) — the PNGs then carry alpha, and the qtrle .mov drops straight into DaVinci Resolve as an overlay. api.renderSVGFrame(path, scale, skipComps) exists for vector snapshots.

Render Queue (in-app, all tiers): api.addRenderQueueItem(compId) → configure → api.render(itemId) or api.renderAll(). Render Queue Items can carry Setup/Pre/Post render scripts (e.g. swap assets or text per render) — see the reference.

Cavalry CLI (/Applications/Cavalry.app/Contents/Applications/CavalryCLI.app/Contents/MacOS/cavalry-cli): headless render/list and the interactive --prompt REPL are Enterprise-licence only, so don't plan around them unless the user confirms an Enterprise licence; version/auth work everywhere. Formats include png, svg, gif, webm, webp, mp4, quicktime; --assetSwap replaces assets at render time.

Gotchas (hard-won)

  • The bridge is fire-and-forget — design every script around file-based feedback (status

JSON, preview PNGs, error catch-files). Never assume you'll see console.log.

  • api.exec code is wrapped in an IIFE by the bridge, so bare var at top level is fine but

nothing persists between sends. Persist state in the scene or on disk.

  • Version drift: comp attrs (frameRate vs fps), text (text vs string), generators

(radius vs dimensions) all changed names across releases — always use the trySet chain.

  • material.alpha is 0–100, not 0–1.
  • Preview cheaply: render check frames at 50% scale at a handful of story beats, not every

frame; full-res full-range renders only once the previews look right.

  • Re-runs: api.createComp a fresh versioned comp per attempt; reruns into the same comp

duplicate every layer.

  • Save the scene from script — api.saveSceneAs("/path/scene.cv") — so work survives a crash;

wrap in try/catch (fails if a modal dialog is open).

  • The vendored type definitions double as offline docs — nearly every function has a JSDoc

example; grep before guessing a signature.

Security

Running the bridge means running a code-execution server on the user's machine. Say so before asking them to start it.

  • The bridge binds 127.0.0.1:8731 and executes any JavaScript POSTed to it inside the

live session — the user's privileges, the user's open scene. Raw non-JSON bodies are run as JS directly. Requests carry no authentication: every local process, and every other user on a shared machine, can drive Cavalry through it.

  • Web pages cannot. Requests carrying an Origin header or a cross-site

Sec-Fetch-Site are rejected with 403, so a page in the user's browser can't reach the bridge. That check is the only gate — there is no token.

  • Nothing listens until the user runs it. Copying cavalry-bridge.js into the Scripts

folder only puts it in the menu; the port opens when they pick Scripts ▸ Cavalry Bridge and stays open for that session.

  • To stop the bridge, quit Cavalry. There is no remote shutdown; the port is released

with the process.