p5.js Expert Article 5

Custom Renderers

Off-screen buffers, multi-pass compositing, SVG export, and instanced rendering for thousands of identical shapes.

⏱ 24 min read renderer createGraphics SVG instancing compositing off-screen

createGraphics() — off-screen buffers

createGraphics(w, h) creates an invisible p5 canvas you can draw into and use as a texture or composite onto the main canvas:

let pg;

function setup() {
  createCanvas(800, 600);
  pg = createGraphics(800, 600);

  // Draw something complex into the buffer (runs once)
  pg.background(0, 0);
  pg.colorMode(HSB, 360, 100, 100, 255);
  for (let i = 0; i < 2000; i++) {
    pg.fill(random(360), 70, 90, 80);
    pg.noStroke();
    pg.circle(random(800), random(600), random(2, 12));
  }
}

function draw() {
  background(20);

  // Draw the buffer onto the main canvas
  image(pg, 0, 0);

  // Overlay live elements
  fill(255, 200, 50);
  noStroke();
  circle(mouseX, mouseY, 30);
}

Multiple layers

let bgLayer, midLayer, fgLayer;

function setup() {
  createCanvas(800, 600);
  bgLayer  = createGraphics(800, 600);
  midLayer = createGraphics(800, 600);
  fgLayer  = createGraphics(800, 600);

  drawBackground(bgLayer);
}

function draw() {
  clear();
  drawMiddle(midLayer);
  drawForeground(fgLayer);

  image(bgLayer,  0, 0);
  image(midLayer, 0, 0);
  image(fgLayer,  0, 0);
}

Blending modes

Both the main canvas and createGraphics() support blend modes:

blendMode(ADD);       // additive — bright glowing overlaps
blendMode(MULTIPLY);  // darkening
blendMode(SCREEN);    // lightening
blendMode(OVERLAY);   // contrast boost
blendMode(DODGE);     // brighten highlights
blendMode(BURN);      // darken shadows
blendMode(DIFFERENCE);// invert where images differ
blendMode(EXCLUSION); // softer difference
blendMode(HARD_LIGHT);
blendMode(SOFT_LIGHT);
blendMode(NORMAL);    // reset (default)

Glow effect using ADD blending:

function draw() {
  background(10);

  // Draw glow layer
  glowLayer.clear();
  glowLayer.blendMode(ADD);
  glowLayer.noStroke();
  for (let i = 0; i < 5; i++) {
    let alpha = map(i, 0, 4, 30, 5);
    let size  = map(i, 0, 4, 20, 80);
    glowLayer.fill(100, 150, 255, alpha);
    glowLayer.circle(mouseX, mouseY, size);
  }

  blendMode(ADD);
  image(glowLayer, 0, 0);

  blendMode(NORMAL);
  fill(200, 220, 255);
  noStroke();
  circle(mouseX, mouseY, 10);
}

SVG output

p5.svg lets you export resolution-independent vector files:

<script src="https://cdn.jsdelivr.net/npm/p5.js-svg@1.5.1/dist/p5.svg.js"></script>
function setup() {
  createCanvas(600, 600, SVG);  // SVG renderer
  noLoop();
}

function draw() {
  background(255);
  noFill();
  stroke(0, 0, 0, 60);
  strokeWeight(0.5);

  for (let i = 0; i < 100; i++) {
    let x = random(width), y = random(height);
    let r = random(10, 80);
    circle(x, y, r * 2);
  }

  save('output.svg');
}

SVG files can be opened in Illustrator or Inkscape for further editing, or sent directly to a pen plotter (AxiDraw).

Instanced rendering (WebGL)

For thousands of identical shapes with different transforms, instanced rendering sends all data to the GPU in one draw call:

// p5.js doesn't expose instancing directly — use raw WebGL
let instancedShader, posBuffer, instances;

function setup() {
  let canvas = createCanvas(800, 600, WEBGL);
  let gl     = canvas.GL;

  // ... set up instanced rendering with gl.drawElementsInstanced()
  // This requires working with raw WebGL APIs alongside p5
}

For most use cases, switching to a noFill() + beginShape() batch, or grouping draw calls, achieves similar gains without raw WebGL complexity.

The drawingContext — direct Canvas 2D API access

When p5 doesn’t expose what you need, drop down to the raw Canvas API:

function draw() {
  background(20);

  let ctx = drawingContext;  // raw Canvas 2D context

  // Radial gradient — not natively in p5.js
  let grad = ctx.createRadialGradient(mouseX, mouseY, 0, mouseX, mouseY, 200);
  grad.addColorStop(0,   'rgba(200,100,255,0.8)');
  grad.addColorStop(0.5, 'rgba(100,50,200,0.4)');
  grad.addColorStop(1,   'rgba(0,0,0,0)');

  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, width, height);
}

Also available: ctx.filter, ctx.globalCompositeOperation, ctx.getTransform(), and anything else in the Canvas 2D spec.

Pre-rendering static content

Compute expensive static content once, store it in an ImageData object, and reuse it:

let staticBg;

function setup() {
  createCanvas(800, 600);
  staticBg = drawStaticContent();
}

function drawStaticContent() {
  let pg = createGraphics(width, height);
  // ... expensive drawing ...
  return pg;
}

function draw() {
  image(staticBg, 0, 0);  // fast — just blit the image
  // draw dynamic elements on top
}

Key takeaways

  • createGraphics() creates off-screen buffers — draw expensive content once, reuse each frame
  • Layered graphics buffers (image(layer, 0, 0)) give clean separation between background, mid, foreground
  • blendMode(ADD) is the go-to for glowing, emissive effects
  • p5.svg swaps the renderer for SVG output — great for plotter art and print
  • drawingContext gives direct access to the Canvas 2D API for features p5 doesn’t expose
  • Pre-render static content into a buffer to save GPU work each frame