×
Code API
Each layer runs its own JS code as a step in a pipeline:
original → layer1 → layer2 → … → final
A layer's prev is the previous layer's output (or the original for the bottom layer).
Write to output — the result is fed as prev to the next layer.
The "strength" number on each layer is a soft mix: lerp(prev, output, strength).
Available variables
width, height // image size (after scale)
original.pixels // Uint8ClampedArray RGBA of source
original.width, .height
original.getPixel(x,y) // -> {r,g,b,a}
prev // same shape — previous layer's output
// (= original for the bottom layer)
images[i] // additional images you added
// .width .height .pixels .getPixel(x,y)
output // Uint8ClampedArray you write to
// length = width*height*4 (RGBA)
Helpers
getPixel(x, y) // reads from prev
setPixel(x, y, r, g, b, a=255)
forEach((x, y, p) => { p.r += 5; return p; })
// iterates every pixel of prev, writes back
clamp(v, lo=0, hi=255)
lerp(a, b, t)
PI, TAU, E
sin, cos, tan, asin, acos, atan, atan2,
abs, sqrt, pow, exp, log, floor, ceil, round, min, max,
random, hypot, sign
Examples — basics
// brightness +5
forEach((x,y,p) => { p.r=clamp(p.r+5); p.g=clamp(p.g+5); p.b=clamp(p.b+5); });
// invert
forEach((x,y,p) => { p.r=255-p.r; p.g=255-p.g; p.b=255-p.b; });
// grayscale
forEach((x,y,p) => {
const g = (p.r * 0.299 + p.g * 0.587 + p.b * 0.114) | 0;
p.r = p.g = p.b = g;
});
Examples — pixel coordinates / index
// max blue at column x === 50
forEach((x, y, p) => {
if (x === 50) { p.r = 0; p.g = 0; p.b = 255; }
});
// horizontal red line at y === 100, 1px tall
forEach((x, y, p) => {
if (y === 100) { p.r = 255; p.g = 0; p.b = 0; }
});
// every 500th pixel (by ordinal index): green channel * 2
forEach((x, y, p) => {
const i = y * width + x; // ordinal pixel index
if (i % 500 === 0) p.g = clamp(p.g * 2);
});
// vertical stripes every 20 px
forEach((x, y, p) => {
if (x % 20 === 0) { p.r = 255; p.g = 255; p.b = 0; }
});
// checkerboard tint
forEach((x, y, p) => {
if (((x >> 4) + (y >> 4)) & 1) p.b = clamp(p.b + 80);
});
Examples — random
// pure noise
forEach((x, y, p) => {
p.r = random256(); p.g = random256(); p.b = random256();
});
// salt & pepper: 1% pixels become white, 1% black
forEach((x, y, p) => {
const r = random1();
if (r < 0.01) { p.r = p.g = p.b = 255; }
else if (r < 0.02) { p.r = p.g = p.b = 0; }
});
// random R/B channel swap on half the pixels
forEach((x, y, p) => {
if (randomBoolean()) { const t = p.r; p.r = p.b; p.b = t; }
});
// jittered brightness ±30
forEach((x, y, p) => {
const j = randomInt(-30, 30);
p.r = clamp(p.r + j); p.g = clamp(p.g + j); p.b = clamp(p.b + j);
});
// 30% chance: take pixel from random spot in original
forEach((x, y, p) => {
if (random1() < 0.3) {
const q = original.getPixel(randomInt(0, width-1), randomInt(0, height-1));
p.r = q.r; p.g = q.g; p.b = q.b;
}
});
Examples — geometry & math
// sine wave horizontal displacement
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const dx = floor(sin(y * 0.05) * 20);
const p = prev.getPixel((x + dx + width) % width, y);
setPixel(x, y, p.r, p.g, p.b, p.a);
}
}
// radial gradient over original (darken edges)
const cx = width / 2, cy = height / 2;
const maxD = hypot(cx, cy);
forEach((x, y, p) => {
const d = hypot(x - cx, y - cy) / maxD; // 0..1
const k = 1 - d;
p.r = (p.r * k) | 0;
p.g = (p.g * k) | 0;
p.b = (p.b * k) | 0;
});
Examples — second image
// average with images[0] (added via "+ Add image")
const img = images[0];
forEach((x, y, p) => {
const q = img.getPixel(x % img.width, y % img.height);
p.r = (p.r + q.r) >> 1;
p.g = (p.g + q.g) >> 1;
p.b = (p.b + q.b) >> 1;
});
// difference (abs) — highlights what's different
const img = images[0];
forEach((x, y, p) => {
const q = img.getPixel(x % img.width, y % img.height);
p.r = abs(p.r - q.r); p.g = abs(p.g - q.g); p.b = abs(p.b - q.b);
});
// paste images[0] at coordinates (offsetX, offsetY)
const img = images[0];
const offsetX = 50, offsetY = 100;
for (let y = 0; y < img.height; y++) {
for (let x = 0; x < img.width; x++) {
const dx = x + offsetX, dy = y + offsetY;
if (dx < 0 || dx >= width || dy < 0 || dy >= height) continue;
const q = img.getPixel(x, y);
setPixel(dx, dy, q.r, q.g, q.b, q.a);
}
}
// paste with alpha blending (respects q.a as transparency)
const img = images[0];
const offsetX = 50, offsetY = 100;
for (let y = 0; y < img.height; y++) {
for (let x = 0; x < img.width; x++) {
const dx = x + offsetX, dy = y + offsetY;
if (dx < 0 || dx >= width || dy < 0 || dy >= height) continue;
const q = img.getPixel(x, y);
const p = prev.getPixel(dx, dy);
const a = q.a / 255;
setPixel(dx, dy,
p.r * (1 - a) + q.r * a,
p.g * (1 - a) + q.g * a,
p.b * (1 - a) + q.b * a,
255);
}
}
// paste centered on the canvas
const img = images[0];
const offsetX = ((width - img.width) / 2) | 0;
const offsetY = ((height - img.height) / 2) | 0;
for (let y = 0; y < img.height; y++) {
for (let x = 0; x < img.width; x++) {
const q = img.getPixel(x, y);
setPixel(x + offsetX, y + offsetY, q.r, q.g, q.b, q.a);
}
}
// paste with math: add images[0] to original at (offsetX, offsetY)
const img = images[0];
const offsetX = 50, offsetY = 100;
for (let y = 0; y < img.height; y++) {
for (let x = 0; x < img.width; x++) {
const dx = x + offsetX, dy = y + offsetY;
if (dx < 0 || dx >= width || dy < 0 || dy >= height) continue;
const q = img.getPixel(x, y);
const p = prev.getPixel(dx, dy);
setPixel(dx, dy, clamp(p.r + q.r), clamp(p.g + q.g), clamp(p.b + q.b));
}
}
Performance tip
JS per-pixel loops are slow on huge images. Use the scale selector
(e.g. 0.25x) for live editing, then bump to 1x when exporting.