trakodag.web
no image loaded
Load an image to start.
Write code on the right — each layer is its own program.
Hover the canvas to see the original.
Layers
Code
Images
access in code as images[i]
no extra images — use "+ Add image"
×

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.