home/ modules/ color-layers-and-pixel-processing

Colour, layers, and pixel-level image processing

  • learner can control colour with HSB mode, gradients, and blend modes across stacked layers
  • learner can draw into off-screen PGraphics/framebuffer layers and cross-dissolve them
  • learner can read and rewrite the pixels[] array to build image filters, palette extraction, and pixel-mapped renders

Take a source photo and produce a layered, recoloured piece: extract its palette, remap its pixels into drawn tiles or a convolution filter, and composite the result across blend-moded PGraphics layers.

This module builds the image-transformation half of a live-visuals practice: taking found imagery and reprocessing it into something that reads as yours on a projector. In a VJ or audiovisual set, raw photos and camera feeds rarely work as-is — they need recolouring to match the set’s palette, restructuring into tiles or filtered textures that move with the music, and compositing so multiple visual voices coexist on one screen. That whole pipeline — source image in, layered recoloured artwork out — is the capstone task.

The arc starts supported and on-canvas: switch to HSB so hue becomes a single controllable axis, then build gradients by lerping colours in a loop, then stack simple shapes under different blend modes and watch LIGHTEST and DIFFERENCE change the composite. Next the work moves off-screen — “PGraphics is an independent off-screen drawing layer” is the JIT pointer when your first layered sketch needs elements composed separately, and “cross-dissolving two layers per pixel with lerpColor()” shows how two buffers become one animated transition. Finally you drop to the pixel level: loadPixels() and the row-major index formula unlock palette extraction by sampling and sorting, pixel-mapping into drawn substitutes, brightness-driven tile rasterization, and convolution kernels for blur and sharpen.

Every required atom is load-bearing for the capstone: you cannot extract a palette without pixels[] access and 2D grid iteration, cannot remap into tiles without the brightness-to-size move, and cannot composite the final piece without PGraphics layers and blend modes. The supporting atoms enrich rather than gate — alpha-accumulation traces, continuous cellular automata, and frame differencing point at where this pixel toolkit goes next: generative textures and live camera input. Drill the gradient loop, pixels[] indexing, and nested grid iteration until they are automatic; everything else in the module rides on them.

Walkthrough

You’ll build the “found image in → recoloured, layered artwork out” pipeline. There’s no photo to load headless, so we generate a source image in code — the pixel-processing moves are identical to a loadImage()’d photo. Paste each sketch into the p5 web editor and press Run (▶). Each step is a complete, standalone sketch.

1 — a gradient by interpolation. lerpColor(a, b, t) blends two colours by fraction t. Loop it down the rows and you get a smooth gradient — the atom of all colour blending. Work in HSB so the endpoints are chosen by hue, not RGB guesswork ([[hsb-color-mode-p5js]], [[p5js-color-gradients-lerp]]).

function setup() { createCanvas(400, 400); colorMode(HSB, 360, 100, 100); }
function draw() {
  let a = color(200, 80, 90), b = color(340, 70, 55);
  for (let y = 0; y < height; y++) {
    stroke(lerpColor(a, b, y / height));   // interpolate per row
    line(0, y, width, y);
  }
}

2 — blend modes stack light. blendMode() changes how new pixels combine with what’s under them. ADD sums channels, so overlapping shapes get brighter where they meet — the RGB-primary Venn every VJ knows. Always reset to BLEND after ([[p5js-blend-modes]], [[processing-image-blend-modes]]).

function setup() { createCanvas(400, 400); noStroke(); }
function draw() {
  background(0);
  blendMode(ADD);                          // additive light
  fill(255, 0, 0); circle(160, 200, 210);
  fill(0, 255, 0); circle(240, 170, 210);
  fill(0, 0, 255); circle(200, 250, 210);
  blendMode(BLEND);
}

3 — an off-screen layer (PGraphics). createGraphics() is a whole independent canvas you draw into off-screen, then composite in one image() call. It’s how separate visual voices stay separate until you deliberately merge them ([[pgraphics-as-layer]], [[p5js-framebuffer-gpu-texture]]).

let layer;
function setup() {
  createCanvas(400, 400);
  layer = createGraphics(400, 400);        // an independent off-screen canvas
  layer.noStroke();
  layer.fill(255, 200, 0);
  for (let i = 0; i < 6; i++) layer.circle(70 + i * 50, 200, 60);
}
function draw() {
  background(30, 0, 60);
  blendMode(SCREEN);
  image(layer, 0, 0);                       // composite the whole layer at once
  blendMode(BLEND);
}

4 — down to the pixels. loadPixels() exposes pixels[], four entries (R,G,B,A) per pixel in row-major order: pixel (x,y) starts at index (x + y*width) * 4. Write to it directly and you’ve built an image from scratch — the same array you’d read to process a photo ([[processing-image-pixels]], [[pixel-manipulation]]).

function setup() { createCanvas(400, 400); }
function draw() {
  loadPixels();
  for (let y = 0; y < height; y++) {
    for (let x = 0; x < width; x++) {
      let v = 255 - dist(x, y, 200, 200);   // bright centre, dark edges
      let i = (x + y * width) * 4;           // the row-major index formula
      pixels[i] = v; pixels[i + 1] = v * 0.4; pixels[i + 2] = 255 - v; pixels[i + 3] = 255;
    }
  }
  updatePixels();
}

5 — brightness → tiles (rasterize). Sample a source’s brightness on a grid and draw a substitute mark whose size tracks it: bright source → big tile. That’s the whole “turn a photo into drawn dots” move, and 2D grid iteration is its engine ([[brightness-to-tile-rasterize]], [[pixel-mapping-remap]], [[processing-two-d-arrays]]).

function setup() { createCanvas(400, 400); }
function draw() {
  background(10);
  noStroke();
  let step = 20;                            // 400/20 = 20 tiles across
  for (let x = 0; x < width; x += step) {
    for (let y = 0; y < height; y += step) {
      let bright = (sin(x * 0.03) + cos(y * 0.03) + 2) / 4;  // 0–1 stand-in for photo luminance
      fill(120, 200, 255);
      let s = bright * step;                 // brighter source → bigger tile
      ellipse(x + step / 2, y + step / 2, s, s);
    }
  }
}

6 — the full pipeline (the capstone). Generate a source in an off-screen layer, read its pixels, remap each tile’s hue and size from the sampled brightness (a one-palette recolour), and composite it all over a screened glow layer. Source image in, layered recoloured artwork out ([[image-palette-extraction-sorting]], [[pgraphics-lerpcolor-blend]]):

let src, glow;
function setup() {
  createCanvas(400, 400);
  colorMode(HSB, 360, 100, 100);
  noStroke();
  src = createGraphics(400, 400);           // 1) a procedural "source" image
  src.colorMode(HSB, 360, 100, 100);
  src.noStroke();
  for (let i = 0; i < 200; i++) {
    src.fill((i * 7) % 360, 70, 90);
    src.circle((i * 53) % 400, (i * 97) % 400, 40);
  }
  src.loadPixels();                          // pixels[] is raw RGBA regardless of colorMode
  glow = createGraphics(400, 400);           // 2) a soft glow layer
  glow.noStroke();
  glow.fill(255, 40);
  for (let r = 260; r > 0; r -= 20) glow.circle(200, 200, r);
}
function draw() {
  background(230, 60, 15);
  blendMode(SCREEN);
  image(glow, 0, 0);                         // glow, screened under the tiles
  blendMode(BLEND);
  let step = 25;                             // 400/25 = 16 tiles across
  for (let x = 0; x < width; x += step) {
    for (let y = 0; y < height; y += step) {
      let idx = (x + y * width) * 4;
      let b = (src.pixels[idx] + src.pixels[idx + 1] + src.pixels[idx + 2]) / 3;  // luminance
      fill(map(b, 0, 255, 200, 320), 70, map(b, 0, 255, 40, 95));                 // recolour to one palette
      let s = map(b, 0, 255, 6, step);
      rect(x, y, s, s);
    }
  }
}

What good looks like. A processed image should read as a deliberate transformation, not a filter slapped on: a coherent recolour (all hues pulled into one range, per the palette discipline from [[generating-palettes-in-hsb]]), a legible structure from the tiling, and layers that add depth without turning to mud. The beginner failure is stacking blend modes until everything blows out to white — keep one dominant layer and let others support. Check value contrast survives on a projector. (Skill map: live-visualist Domain B2/B4 — colour discipline and texture/contrast as language.)

Now make it yours. Replace the generated source with loadImage() of a real photo (preload() it). Add a convolution kernel — a 3×3 weighted average of neighbours — for blur or edge-detect ([[convolution-kernel]]). Extract a real palette by sampling pixels and sorting by brightness. Cross-dissolve two PGraphics layers with lerpColor() per pixel for an animated transition. Swap rect tiles for rotated lines whose angle tracks brightness.

Runnable examples

Generated from the context/ instrument corpus by concept (redistributable idioms only). Do not edit — regenerate with gen-module-examples.mjs.

feedback-trail

osc(4).modulate(src(o0), 0.6).out(o0)

hydra-0022 · CC0-1.0

function draw(){ fill(0, 20); rect(0, 0, width, height); circle(mouseX, mouseY, 40) }

p5live-0003 · CC0-1.0

palette-cycle

osc(30, 0.1, 1).colorama(0.1).out()

hydra-0015 · CC0-1.0

hsvrgb [fract (ft/6.28 + 0.1*time), 1, 1] >> rgb

punctual-0024 · CC0-1.0

hue-shift

osc(30).hue(() => time * 0.1).out()

hydra-0016 · CC0-1.0

hsvrgb [fract (ft/6.28 + 0.1*time), 1, 1] >> rgb

punctual-0024 · CC0-1.0

gradient-ramp

gradient(0.3).out()

hydra-0170 · MIT

[fr, fr*0.5, 1-fr] >> rgb

punctual-0033 · CC0-1.0

grain-glitch

col += (h21(st + fract(u_time)) - 0.5) * 0.15;

glsl-0026 · public-domain

blur-soften

filter(BLUR, 4)

p5live-0073 · CC0-1.0

pixel-manipulation

loadPixels(); for(let i=0;i<pixels.length;i+=4) pixels[i]=255; updatePixels()

p5live-0030 · CC0-1.0

Atoms in this module

Required — these gate the capstone

colorMode(HSB) makes hue, saturation, and brightness independently controllable axes
Concept L2 First instrument H
lerpColor() over a for-loop of stacked lines builds a smooth gradient in p5.js
Procedure L2 First instrument H
blendMode() controls how overlapping layers combine in p5.js (e.g. LIGHTEST keeps the brighter pixel)
Concept L2 First instrument H
Processing's PGraphics is an independent off-screen drawing layer with its own coordinate system and settings
Concept L2 First instrument HL
p5.Framebuffer is an off-screen GPU surface you can draw to and then reuse as a texture
Concept L3 Craft HG
Cross-dissolving two layers per pixel with lerpColor() and a coordinate-dependent wave makes non-uniform wave transitions
Procedure L3 Craft H
Processing's pixels[] array gives direct read-write access to every pixel of the display window
Concept L3 Craft H
Sampling an image's pixels and sorting the resulting colours by hue, saturation, or brightness extracts its palette
Procedure L2 First instrument H
Pixel mapping replaces each pixel of a source image with a drawn element sized or coloured by that pixel's value
Procedure L2 First instrument H
Mapping source brightness to tile size rasterizes an image into a halftone-like grid where dark areas make smaller tiles
Procedure L3 Craft H
Image convolution applies a kernel matrix to each pixel's neighbourhood to produce blur and sharpen filters
Concept L3 Craft H
Processing's blend() and filter() apply compositing modes and pixel filters to images
Concept L3 Craft H
Two-dimensional arrays in Processing store grid data as arrays of arrays
Concept L2 First instrument H

Supporting — enrichment, not gating

Drawing with semi-transparent fill and no background clear lets shapes accumulate as a trace
Concept L1 Foundations H
Semi-transparent marks accumulate in visual density where shapes overlap most
Principle L2 First instrument H
Arrays in Processing store multiple values under one name, accessed by zero-based index
Concept L2 First instrument H
Cellular automata with continuous state values (0–255) produce fluid wave-like patterns
Concept L3 Craft H
Frame differencing detects motion by comparing pixel values between consecutive video frames
Concept L3 Craft H
A light grain pass over the final composite is the most reliable 'make it look intentional' move — it unifies layers and hides banding
Principle L2 First instrument HGL
A blurred copy of the image added back via screen or add blend is a cheap bloom/glow effect
Concept L2 First instrument HG
CPU pixel manipulation reads and writes the raw RGBA array directly, unlike a per-pixel GPU shader
Concept L2 First instrument LH
Psychedelic palettes are highly saturated complementary/triadic hues always in motion via palette-cycle — color must never settle
Concept L2 First instrument HL