P5.PAPER

The Only Good Kind of Paperwork: Introducing 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.
What is p5.paper?
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.
How it Works
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.
Procedural Art: p5.paper + p5.brush
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.
The Setup
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);
}
Why This Combination is Powerful
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.
