mcpField
R
rebelle

rebelle

github.com/jablonkaiCreative & media443 installsUpdated today
Repository

About rebelle

rebelle is a skill for agents automating artwork in Rebelle. It supports live WebSocket control and Motion IO batch rendering, with guidance for validating output and handling operational risks.

Key features

  1. 01Send painting events over WebSocket
  2. 02Render batches with Motion IO
  3. 03Build stroke and animation-frame events
  4. 04Export canvases and inspect PNG output

Use cases

  1. 01Automate painting in an open Rebelle canvas
  2. 02Generate reproducible artwork in batches
  3. 03Create frame-by-frame animation sequences

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/jablonkai/skills --skill rebelle

After installing, type/rebellein Claude Code to confirm it loaded.

Bundled files

References and scripts installed along with SKILL.md.

references/
assets.md
batch.md
json-events.md
websocket.md
scripts/
rebelle_events.py
rebelle_ws.py
rebelle-batch.sh

Details

From the project README

Rebelle Control

Rebelle simulates real paint — water flows, pigments granulate, impasto catches light — and both of its scriptable builds take the same JSON event vocabulary:

BuildHow you drive itBest for
/Applications/Rebelle 8.app (Pro)WebSocket server, livepainting into the app the user is watching, exploring, one-off artwork — and animation when Motion IO is absent
/Applications/Rebelle 8 Motion IO.app-batch-json file, headless-ishreproducible renders, animation frame sequences, data-layer in/out

Check which are installed first (ls -d /Applications/Rebelle*) — often only Pro is. The Pro binary accepts -batch-json and then silently ignores it (blank artwork, no frames, verified 8.3.4), so Path B is Motion IO only.

Start live unless the task needs data layers, 16-bit output or a headless render: the round-trip is seconds instead of a minute, you can look at the canvas whenever you want, and animations work live too (see Path B's last paragraph).

Path A — live, over the WebSocket

Rebelle only listens when it was launched with the flag, and it does not have to be restarted for each task — check first, ask second:

python3 scripts/rebelle_ws.py --ping     # prints e.g. "Rebelle 8 Pro here"

If nothing answers, ask the user before launching: a fresh instance is a new blank document, and painting goes into whatever artwork is open, with no undo grouping.

"/Applications/Rebelle 8.app/Contents/MacOS/Rebelle 8" \
  -websocket-server-enable \
  -websocket-allowed-ip-addresses "::ffff:127.0.0.1,127.0.0.1" &

The allowlist is matched against the socket's literal peer address, and a local client arrives as the IPv6-mapped ::ffff:127.0.0.1 — listing plain 127.0.0.1 alone refuses your own connection (... refused in Rebelle's output).

Then drive it from Python — the helpers are plain modules in this skill's scripts/, so put that directory on the path (or run from it):

import sys; sys.path.insert(0, "<this skill>/scripts")
from rebelle_ws import Rebelle
from rebelle_events import catmull_rom, taper

with Rebelle() as r:
    r.event({"event_type": "NEW_ARTWORK", "width": 1200, "height": 800, "units": "px"})
    r.event({"event_type": "SET_BRUSH", "tool": "WATERCOLOR", "preset": "Watercolor/Round",
             "size": 55, "water": 70, "opacity": 90, "paint_type": "PAINT",
             "color": {"r": 30, "g": 90, "b": 200}})
    r.stroke(catmull_rom([(150, 450), (400, 250), (800, 600), (1050, 320)]),
             pressure=taper(0.95))
    r.event({"event_type": "SIMULATION", "repeats": 20})   # let the water spread
    r.sync()                                   # wait until Rebelle is really finished
    r.export("/tmp/check.png")                 # then Read the PNG

sync() sends a BOOKMARK and waits for the echo. Nothing else acknowledges anything, so without it you are exporting a canvas that is still mid-stroke.

NEW_ARTWORK is the one event you must not race: while the canvas re-initialises, Rebelle drops the next message — typically your SET_BRUSH — and a stroke without a brush crashes a freshly launched app (bad_optional_access, 8.3.4). Rebelle.event() blocks after NEW_ARTWORK until a bookmark comes back (ready()), so go through it rather than raw send_text(); a hand-rolled client has to poll the same way.

SAVE/LOAD events are rejected live — export() (cmd: export_canvas) is the only way out, and it writes the composited canvas exactly as the user sees it.

Path B — batch rendering with Motion IO

python3 build_painting.py                  # writes events.json via rebelle_events.Doc
bash scripts/rebelle-batch.sh events.json out/

Motion IO never exits by itself on macOS and reports progress only as batch frame end: i/n on stdout, so always go through rebelle-batch.sh — it waits for i == n, surfaces the ERROR: lines, then stops the app.

One frame = one output image + one implicit fluid-simulation step. That makes animation natural: paint a bit, close the frame, repeat.

from rebelle_events import Doc, catmull_rom, taper

d = Doc(1200, 800, paper={"preset": "Handmade/HM01 Handmade", "deckled_edges": True})
d.set_brush("WATERCOLOR", "Watercolor/Round", size=55, water=70, opacity=90,
            paint_type="PAINT", color=(30, 90, 200))
d.stroke(catmull_rom([(150, 450), (400, 250), (800, 600), (1050, 320)]), pressure=taper())
d.frame()                    # one animation frame ends here
d.simulation(15, frames=20)  # 20 frames of the paint spreading and drying
d.write("events.json")
The frame rules that make batch work

Doc applies these already; hand-written JSON must too, or the run wedges forever or loses work silently. Motion IO is still finishing its own startup while it reads the first frames, and these are the shapes that survive it:

  1. Frame 0 is a warm-up that must load a brush. A SET_BRUSH there is enough real work to let startup complete. A frame with only a BOOKMARK — or an empty frame — deadlocks before a single frame is processed.
  2. NEW_ARTWORK goes in frame 1, never frame 0. In frame 0 it is dropped without a word and you silently get Rebelle's default A4 canvas (2339×1654 at 200 dpi).
  3. Something must follow NEW_ARTWORK in its frame (another SET_BRUSH does). A frame that ends right after NEW_ARTWORK deadlocks.
  4. Don't paint in the NEW_ARTWORK frame — the canvas re-init wipes it. Start painting in the next frame (Doc.first_content_frame is where real content begins; pass it to ffmpeg -start_number).

If you must skip NEW_ARTWORK entirely, -input artwork.reb opens an existing artwork and is the sturdiest way to fix a canvas size — but the user has to have saved one.

No Motion IO? Render the same Doc live. rebelle_ws.py --send events.json --frames-out out/ replays it frame by frame over the WebSocket and exports frame_0000.png… with batch numbering, so first_content_frame and the ffmpeg recipe in batch.md apply unchanged. Live, the fluid simulation runs in real time rather than one step per frame, so give wet passages explicit SIMULATION repeats instead of relying on frame count.

Painting that looks painted

  • Coordinates are canvas pixels, y down from the top-left, and may go outside the canvas — start and end strokes off-canvas for edge-to-edge washes.
  • A POINTER_MOVE draws the previous segment, so a stroke must end with a release at the last move's position. stroke()/stroke_events() handle it; hand-written events routinely lose their final segment to this.
  • Calibrate brush width per preset. Neither size (the UI slider) nor size_px predicts the painted width: on 8.3.4, size_px 200 gave ~150 px with Round, ~50 px with Mop and ~200 px with Flat, and size_px 50 on Round only ~6 px. Paint one test stroke per preset, export, measure, then scale — before committing to a composition.
  • Sample paths densely. Rebelle interpolates between points, so four points give a smooth-but-generic curve; catmull_rom() at ~16 points per segment lets the brush texture, spacing and pressure actually show.
  • Vary pressure. A constant 0.9 reads as machine-drawn. taper() thins both ends, ramp() builds up — that alone is most of the difference between a plot and a stroke.
  • Wet media need simulation time. Water only spreads on simulation steps: paint, then spend frames (or {"event_type":"SIMULATION","repeats":N}) letting it bloom, and DRY/FAST_DRY before painting a layer that should not bleed into the one below.
  • Build in layers of colour, as in real painting: pale washes first, DRY, then darker glazes over them. SET_ENGINE_PARAMS controls absorbency, drips, granulation.
  • Pick brushes by simulation, not by name: WATERCOLOR for washes and bleeds, INK_PEN for line work that can be re-wetted, OIL_AND_ACRYLIC for impasto and colour mixing on the canvas, PENCIL/PASTEL for dry texture, AIRBRUSH for soft gradients.

Always look at the result

Rebelle answers almost nothing, and a wrong paper or preset produces a plausible, completely wrong image. Export and Read the PNG after each meaningful step, the same way you would glance at the canvas: live via r.export(path), in batch by reading the last rendered frame. Judge it as a picture — coverage, colour, whether the water did anything — and iterate. Then, when the user wants the finished artwork, export at full size (rgba_canvas, or SAVE with scaling for a NanoPixel-scale export).

Gotchas worth knowing before you hit them

  • Wrong preset name → loud error; wrong paper name → silence. Resolve both against the filesystem first (assets.md); the official quickstart's "Default/HM01 Handmade" is itself wrong for Rebelle 8.
  • SET_BRUSH without preset paints nothing and logs Preset for brush was not set.
  • Only one Rebelle at a time. Running the GUI build and Motion IO together is asking for trouble; stop one before starting the other.
  • Force-stopping the app leaves a Data/Sessions/<pid>/ folder behind in ~/Library/Application Support/Escape Motions/Rebelle 8/. Harmless, but they pile up at tens of MB each — worth mentioning to the user rather than deleting behind their back.
  • Colours are 8-bit RGB objects ({"r":…,"g":…,"b":…}) even though the engine mixes in 16-bit; 16-bit output only exists in Motion IO .exr exports.
  • The docs run ahead of the build. The 8.3 docs promise SAVE/LOAD over WebSockets; the shipped 8.3.x builds (checked 8.3.0 and 8.3.4) reject them. Trust an export you have looked at over any documented behaviour.
  • Animation licensing has strings attached — see the note at the end of batch.md before helping with a commercial production.

Security

Path B opens nothing — Motion IO reads a file and renders it. Path A does: launching Rebelle with -websocket-server-enable turns it into a code-execution server for its event vocabulary. Say so before asking the user to relaunch.

  • The server listens on 127.0.0.1:8265 (REBELLE_WS_PORT) and applies any event JSON it receives to the artwork the user has open, and export() writes files with their privileges. There is no authentication: every local process, and every other user on a shared machine, can paint into their canvas.
  • -websocket-allowed-ip-addresses is the only access control, and it matches the socket's peer address. Keep it to loopback (::ffff:127.0.0.1,127.0.0.1) — never add a LAN address, which would expose the canvas to the network. Note that a web page in the user's own browser is also a loopback peer, and WebSocket connections aren't subject to CORS, so don't count on the allowlist to exclude one.
  • To stop it, quit Rebelle and relaunch without the flag. The flag is per-launch, so a normally started Rebelle listens on nothing.

Comments

Markdown supported