P5.PAPER

I’ve been using p5.brush for some of my abstract pieces. It has been a massive source of inspiration for me. For years, I’ve used it to bring my digital strokes to life with stunning watercolor, charcoal, and marker simulations.

But as I kept creating, I realized something was missing. A beautiful, realistic watercolor stroke floating on top of a perfectly flat, mathematically flawless screen feels disjointed. I wanted the surface to feel as real as the medium. I looked everywhere for a lightweight, drop-in solution to simulate physical paper, and when I couldn’t find one that hit the mark, I decided to build it myself.

p5.paper is a high-performance WebGL post-processing library for p5.js. With just a few lines of code, it applies a shader pipeline over your artwork that simulates:

  • Procedural Grooves: The physical “tooth” and bumps of watercolor or raw cotton paper.
  • Grit & Blemishes: The microscopic dirt and imperfections found in real physical pulp.
  • Micro Grain: Film-like noise that binds the image together.
  • Color Bleed: A subtle chromatic separation simulating ink absorbing into the page.
  • Edge Vignettes: A natural darkening around the canvas edges.

The secret to p5.paper is that it acts as a post-processing filter. Instead of drawing directly to your main canvas, you draw your artwork onto an invisible, off-screen buffer (using p5’s built-in createGraphics). Once your art is finished, you pass that buffer to p5.paper, which magically bakes all the texture and grit into it, and hands you back a beautifully textured image to display.

Because of this off-screen buffer architecture, p5.paper and p5.brush are a match made in heaven. You can use p5.brush to render realistic strokes onto the buffer, and then use p5.paper to make the canvas itself feel real.

Let’s look at a procedural example. Instead of a standard interactive drawing app, we’ll write an algorithm that generates a series of abstract, flowing watercolor splines, and then processes them through the paper shader.

First, make sure you have both libraries loaded into your project (via npm or CDN).

// A completely procedural, generative art piece using p5.brush and p5.paper
let drawBuffer;
let paper;

function setup() {
  createCanvas(800, 800);
  
  // 1. Create our invisible drawing buffer
  drawBuffer = createGraphics(width, height);
  drawBuffer.background('#f4f1ea'); // A nice off-white paper base color

  // 2. Initialize the p5.paper library
  paper = new p5Paper(width, height);

  // 3. Tell p5.brush to target our buffer instead of the main screen!
  brush.load(drawBuffer);

  // We only want to generate the art once
  noLoop(); 
}

function draw() {
  // --- PART 1: GENERATIVE ART WITH P5.BRUSH ---
  
  // Set our brush to a dark, inky watercolor
  brush.set('watercolor', '#141414', 1.5);
  
  // Generate 30 procedural, sweeping splines
  for(let i = 0; i < 30; i++) {
    // Pick a random starting point
    let startX = random(width * 0.1, width * 0.9);
    let startY = random(height * 0.1, height * 0.9);
    
    // Create an array of points for the spline to follow
    let points = [[startX, startY]];
    
    // Add 3 more wandering points to create a flowing shape
    for(let j = 0; j < 3; j++) {
      points.push([
        points[j][0] + random(-150, 150),
        points[j][1] + random(-150, 150)
      ]);
    }
    
    // Draw the spline onto our buffer
    brush.spline(points, 1);
  }

  // FORCE p5.brush to finish all its internal blending math on the buffer
  brush.reDraw();


  // --- PART 2: THE MAGIC OF P5.PAPER ---

  // Define our paper texture settings
  const options = {
    tex: 0.18,       // Depth of the paper grooves
    grit: 0.35,      // Amount of dirt/blemishes
    grain: 0.10,     // Fine micro-noise
    vignette: 0.70,  // Edge shadowing
    bleed: 0.005,    // Ink absorption bleed
    blendMode: 0     // 0 = Multiply (Darkens like real ink)
  };

  // Apply the shader to our buffer, and draw the final result to the screen!
  const finalImage = paper.apply(drawBuffer, options);
  image(finalImage, 0, 0);
}

By linking these two libraries, you’re effectively simulating both the paint and the canvas.

The brush.load(drawBuffer) command is the bridge. It intercepts all of p5.brush‘s complex stroke generation and maps it to your off-screen graphics object. Then, paper.apply() grabs that graphics object and runs it through a highly optimized WebGL fragment shader, applying physical textures at 60 frames per second.

If you are a creative coder looking to escape the “digital aesthetic” and give your algorithmic art a tactile, physical presence, I highly encourage you to try combining these tools.

You can find the installation instructions, source code, and NPM package details on the p5.paper GitHub repository.