Rhai scripting

Pixel Stroke embeds Rhai, a small scripting language written in Rust that runs inside the editor's WebAssembly bundle. Every function in the API reference is registered on the Rhai engine, so your scripts can call it.

Scripts live as .rhai files on disk, which the desktop app reads live, or you can paste them into the in-editor console. The same script runs in the web and desktop builds.

Hello, retro stable

A first script. It defines a macro that applies two filters in a row, then runs it on the active layer. Each filter returns the number of pixels it changed.

// Save as scripts/examples/retro.rhai

fn retro(levels, block) {
  ps_filter_posterize(levels);
  ps_filter_pixelate(block);
}

retro(4, 2);

Run the file from the in-editor console, or evaluate a single line with script:

run scripts/examples/retro.rhai
script ps_filter_invert()

Want to see more first? The scripting demo on the home page runs six longer scripts in your browser with the same bindings, one line at a time. Take them apart: landscape.rhai paints a night scene from a seed, gameboy.rhai converts art to a four-shade palette with ordered dithering, team_colors.rhai palette-swaps a sprite sheet, dungeon.rhai generates a roguelike floor, life.rhai runs Conway's Game of Life, and plasma.rhai draws a demoscene plasma.

Macros stable

Any fn you define in Rhai is a macro you can call. Macros are ordinary functions: they take arguments and can call any registered binding. A function can't read the variables around it, so pass in whatever it needs.

Rhai never turns an integer into a float for you. Where a binding takes a float, write the decimal point: ps_filter_hue_shift(30.0) runs, ps_filter_hue_shift(30) fails with Function not found. The API reference marks float parameters.

Example scripts ship with the editor, under scripts/examples/.

Panel scripts stable

The editor's side-panels (toolbar, layer list, timeline, color picker, palette manager, tilemap, atlas, bones, navigator, artboard) are all Rhai scripts under scripts/panels/. The same Rhai engine drives them.

So you can fork a panel, change how it behaves, or write a new one without touching the Rust core.

Layers and frames stable

Working with layers and frames from a script:

// add a highlights layer at 80% opacity
let hi = ps.layer_add("Highlights");
ps.layer_opacity(hi, 0.8);

// duplicate the current frame; subsequent edits affect the new frame
ps_timeline_duplicate_frame(ps_timeline_current_frame());

// pixel-perfect mode for crisp 1px strokes
ps_brush_pixel_perfect(true);

State machines experimental

Animation states and the transitions between them, driven by named inputs. A transition's condition is JSON: an input plus one of bool_eq, float_gt, float_lt, float_eq or trigger (an array of them means all must hold).

// idle until the character moves, then run
ps_state_machine_add_state("idle", "clip_idle", "loop", 1.0);
ps_state_machine_add_state("run", "clip_run", "loop", 1.0);
ps_state_machine_add_transition("idle", "run",
    "{\"input\":\"speed\",\"float_gt\":0.1}", 0.0);

ps_state_machine_set_input("speed", 1.0);
print(ps_state_machine_current_state()); // run

Building a custom palette stable

// build a 4-stop custom palette
ps_palette_new("Aurora");
ps_palette_add_color("Aurora", "#0a0a1e");
ps_palette_add_color("Aurora", "#3a8fff");
ps_palette_add_color("Aurora", "#ec2a2a");
ps_palette_set_active("Aurora");

The same approach loads GIMP .gpl files or plain hex lists. The API reference lists the full palette family.

Coming next soon

  • Keybinding registration: keybind.set("B", "retro")
  • Command palette entries: cmd.register(name, fn, hint)
  • Stamp-brush capture from a selection (the editor exposes the binding once the underlying tool lands).