PaintFE Scripting API
PaintFE embeds Rhai v1.24, a fast, safe, sandboxed scripting language. Scripts run on the active layer's pixels and can call built-in effects, manipulate individual pixels, perform math, and watch their progress live on the canvas.
Opening the Script Editor
Go to View β Script Editor in the menu bar. The editor opens as a floating panel. You can move, resize, or collapse it independently of the canvas.
The editor toolbar has four buttons:
- βΆ Run β Execute the current script against the active layer.
- βΉ Stop β Cancel a running script at any time.
- π Live β Toggle live canvas updates during
sleep()calls. - Save / Load β Manage saved scripts. Scripts persist to disk between sessions.
Your First Script
Let's write a script that inverts every pixel in the image:
// Invert all pixels
map_channels(|r, g, b, a| {
[255 - r, 255 - g, 255 - b, a]
});
print("Done! Canvas inverted.");
Click βΆ Run and your canvas flips to its negative. Click Ctrl+Z to undo.
Language Basics (Rhai)
Rhai syntax is close to Rust and JavaScript. Here's everything you need to write useful scripts:
// Variables β use `let`
let x = 42; // integer (i64)
let f = 3.14; // float (f64)
let name = "hello"; // string
let flag = true; // boolean
// Arithmetic
let sum = x + 10; // 52
let half = f / 2.0; // 1.57
// Type conversion (Rhai is strict!)
let as_float = x.to_float(); // 42.0
let as_int = f.to_int(); // 3 (truncates)
// if / else
if x > 10 {
print("big");
} else {
print("small");
}
// while loop
let i = 0;
while i < 5 {
print(i);
i += 1;
}
// for loop (range)
for j in 0..10 { // 0, 1, 2 ... 9
print(j);
}
// Functions
fn double(v) {
v * 2 // last expression = return value
}
print(double(7)); // 14
// Arrays
let arr = [10, 20, 30];
print(arr[0]); // 10
print(arr.len()); // 3
// Closures (used by bulk iteration functions)
let add_ten = |v| v + 10;
// Stored closures must use .call():
print(add_ten.call(5)); // 15
// String concatenation
let msg = "Width: " + width();
print(msg);
.to_float() or .to_int() to convert.
This is the most common beginner stumbling block.
Canvas API
Read-only information about the canvas. These functions never modify the image.
Returns the canvas width in pixels.
Returns the canvas height in pixels.
Returns true if pixel (x, y) falls within the current selection mask.
If there is no active selection, returns true for all pixels, meaning "the entire canvas is selected."
Pixel API
Direct read/write access to individual pixels. Coordinates are zero-based. Out-of-bounds reads return [0,0,0,0]; out-of-bounds writes are silently ignored.
Returns a 4-element array containing the RGBA values (0β255) of the pixel at (x, y).
Sets the pixel at (x, y) to the given RGBA values. Values are automatically clamped to 0β255.
set_r / set_g / set_b / set_a(x: int, y: int, v: int)
Fast single-channel read/write. When you only need one channel, these avoid the overhead of creating a full pixel array.
Iterates over every pixel. If the closure returns an array [r, g, b, a], that pixel is updated.
If the closure returns nothing (empty), the pixel is unchanged. This makes it easy to apply conditional per-pixel logic.
Same as for_each_pixel but restricted to a rectangular sub-region starting at (x, y) of size w Γ h. Much faster than iterating the whole canvas when you only need to affect part of it.
Fastest bulk path. Like for_each_pixel but without x/y coordinates. The closure receives only the channel values and must return [r, g, b, a].
Use this whenever you don't need to know a pixel's position.
Effect API
These functions call PaintFE's built-in image processing kernels. They operate on the entire active layer and are significantly faster than implementing the same effect via per-pixel scripting.
Blur & Denoise
Gaussian blur. sigma controls the blur radius. Higher = blurrier. Typical range: 0.5β20.0. GPU-accelerated at large values.
Fast box (averaging) blur. Radius in pixels. Less smooth than Gaussian but fast for large radii.
Directional blur. angle in degrees (0.0β360.0). distance in pixels.
Unsharp mask sharpening. Typical range: 0.5β5.0.
Noise reduction. Range: 0.0β1.0.
Median filter. Removes salt-and-pepper noise without blurring edges. Radius ≥ 1.
Color & Adjustment
| Function | Parameters | Description |
|---|---|---|
apply_invert() | β | Invert RGB channels (not alpha) |
apply_desaturate() | β | Convert to grayscale (BT.601 weights) |
apply_sepia() | β | Classic sepia tone |
apply_brightness_contrast(b, c) | b, c: float (β255 to 255) | Brightness + contrast adjustment |
apply_hsl(h, s, l) | h: β180β180Β°, s/l: β100β100 | Hue/Saturation/Lightness shift |
apply_exposure(ev) | ev: float (EV stops, Β±) | Exposure adjustment |
apply_levels(black, white, gamma) | black/white: 0β255, gamma: 0.1β10 | Levels input/output |
Noise
Adds noise. amount: 0.0β1.0. monochrome = true adds gray grain; false adds color noise.
Distort
| Function | Parameters | Effect |
|---|---|---|
apply_pixelate(size) | size: int (block px) | Mosaic / pixelation |
apply_crystallize(size) | size: int (cell px) | Voronoi crystal shards |
apply_bulge(amount) | amount: float (neg = pinch) | Spherical distortion |
apply_twist(angle) | angle: float (degrees) | Swirl / twist distortion |
Stylize
| Function | Parameters | Effect |
|---|---|---|
apply_glow(radius, intensity) | radius: float, intensity: float 0β1 | Soft bloom |
apply_vignette(strength, softness) | both: float 0β1 | Edge darkening |
apply_halftone(dot_size) | dot_size: float | Halftone dots |
Artistic
| Function | Parameters | Effect |
|---|---|---|
apply_ink(strength, threshold) | both: float 0β1 | Edge-detect line art |
apply_oil_painting(radius) | radius: int β₯ 1 | Oil painting simulation |
Transform API
Geometric transforms for the active layer or the entire canvas. Layer-only functions transform just the active layer. Canvas-wide functions transform every layer and may change overall canvas dimensions.
Layer Transforms
These affect only the active layer. No undo overhead beyond the standard single-layer snapshot.
Mirrors the active layer leftβright.
Mirrors the active layer topβbottom.
Rotates the active layer 180Β°.
Canvas Transforms
These transform all layers. Canvas rotation by 90Β° swaps width and height.
Mirrors every layer leftβright.
Mirrors every layer topβbottom.
Rotates every layer 90Β° clockwise. Swaps canvas width β height.
Rotates every layer 90Β° counter-clockwise. Swaps canvas width β height.
Rotates every layer 180Β°. Dimensions stay the same.
Resize Operations
Scales every layer to new dimensions. Methods: "nearest", "bilinear" (default), "bicubic", "lanczos". Max 32768Γ32768.
Extends or crops the canvas. Content stays at the given anchor position.
Anchors: "top-left", "top-center", "top-right", "center-left", "center", "center-right", "bottom-left", "bottom-center", "bottom-right".
Short aliases: "tl", "tc", "tr", "cl", "c", "cr", "bl", "bc", "br".
Utility API
Output & Progress
Prints any value to the script console. Strings, numbers, arrays, anything goes. No string conversion required.
Pauses execution for ms milliseconds (max 10,000ms). When the π Live button is active, each sleep() sends the current pixel state to the canvas. You can watch your script work in real time.
Sets the script progress bar. fraction from 0.0 to 1.0. Call this in loops to show users how far along the script is.
Random Numbers
| Function | Returns | Notes |
|---|---|---|
rand_int(min, max) | int in [min, max) | Upper bound is exclusive |
rand_float(min, max) | float in [min, max) | Upper bound is exclusive |
Math Functions
| Function | Signature | Description |
|---|---|---|
clamp | (v, lo, hi: int) β int | Clamp integer to range |
clamp_f | (v, lo, hi: float) β float | Clamp float to range |
lerp | (a, b, t: float) β float | Linear interpolation: a + (bβa)Γt |
distance | (x1, y1, x2, y2: float) β float | Euclidean distance |
abs_i | (x: int) β int | Absolute value (int) |
min_i / max_i | (a, b: int) β int | Min / max of two ints |
min_f / max_f | (a, b: float) β float | Min / max of two floats |
floor / ceil / round | (x: float) β float | Rounding functions |
sqrt | (x: float) β float | Square root |
pow | (x, y: float) β float | x raised to power y |
sin / cos / tan | (x: float) β float | Trig (radians) |
atan2 | (y, x: float) β float | Two-arg arctangent |
PI() | () β float | Returns Ο (3.14159β¦) |
Color Space Conversion
Converts RGB (0β255) to HSL. Returns hue (0β360Β°), saturation (0β100), lightness (0β100).
Converts HSL to RGB. h: 0β360, s: 0β100, l: 0β100. Returns [r, g, b] (0β255 each).
Types
| Rhai Type | Notes |
|---|---|
int | 64-bit signed integer (i64). Used for coordinates, channel values (0β255), sizes. |
float | 64-bit float (f64). Used for effect parameters, math results. |
bool | true / false |
string | UTF-8 string. Concatenate with +. |
array | Dynamic typed array: [1, 2, 3]. Used for pixel values [r,g,b,a], HSL, etc. |
10 + 3.0 fails. Convert with .to_float() or .to_int(). Similarly: let n = 10; n.to_float() + 3.0 works.Sandbox Limits
| Limit | Value |
|---|---|
| Max operations per run | 50,000,000 |
| Max function call depth | 64 levels |
| Max expression depth | 64 |
| Max string length | 10,000 characters |
| Max array size | 10,000 elements |
| Max map size | 1,000 entries |
| sleep() max | 10,000 ms per call |
Scripts can be cancelled at any time using the βΉ Stop button, even during infinite loops.
Full Script Examples
1. Dramatic B&W Photo Finish
// Classic B&W with contrast boost and vignette
apply_desaturate();
apply_brightness_contrast(10.0, 40.0);
apply_vignette(0.55, 0.3);
print("Done β dramatic B&W applied.");
2. Warm Film Look
// Warm hue shift + grain + vignette
apply_hsl(12.0, 15.0, 0.0); // warm tone
apply_noise(0.06, true); // subtle grain
apply_vignette(0.35, 0.5); // soft edges
print("Warm film look applied.");
3. Freeze Color β Selection-Aware
// Desaturate everything OUTSIDE the selection
// β keeps selected area in color, rest goes grey
for_each_pixel(|x, y, r, g, b, a| {
if !is_selected(x, y) {
let grey = (0.299 * r.to_float()
+ 0.587 * g.to_float()
+ 0.114 * b.to_float()).to_int();
[grey, grey, grey, a]
}
});
4. Animated Saturation Drain (Live Preview)
// Enable π Live in the toolbar to watch this run!
let steps = 20;
let i = 0;
while i < steps {
apply_hsl(0.0, -5.0, 0.0); // drain 5% sat
progress(i.to_float() / steps.to_float());
sleep(200); // canvas updates live
i += 1;
}
print("Colour drain complete!");
5. Radial Distance Tint
// Tint pixels based on distance from center
let cx = (width() / 2).to_float();
let cy = (height() / 2).to_float();
let max_d = distance(0.0, 0.0, cx, cy);
for_each_pixel(|x, y, r, g, b, a| {
let d = distance(x.to_float(), y.to_float(), cx, cy);
let t = clamp_f(d / max_d, 0.0, 1.0);
// interpolate from original colour to purple tint
let new_r = lerp(r.to_float(), 80.0, t).to_int();
let new_b = lerp(b.to_float(), 180.0, t).to_int();
[new_r, g, new_b, a]
});