p5.brush adds natural drawing tools to p5.js — pencils, charcoal, markers, watercolor fills, hatch patterns, and vector fields that bend strokes organically. It's built for generative art and high-resolution printing.
🖌️ Try the interactive Brush Maker → Design custom brushes with live preview and generate ready-to-paste
brush.add()code.
🌊 Try the interactive Flow Field Generator → Design custom vector fields with live preview and generate ready-to-paste
brush.addField()code.
p5.brush ships in two flavors:
| p5 build | Standalone build | |
|---|---|---|
| File | dist/p5.brush.js |
dist/brush.js |
| Requires | p5.js 2.x + WEBGL canvas | Nothing — WebGL2 browser only |
| Canvas setup | createCanvas(w, h, WEBGL) |
brush.createCanvas(w, h) |
| Transforms | p5's push/pop, translate, rotate, scale |
brush.push/pop, brush.translate, etc. |
| Angle mode | Follows p5's angleMode() |
brush.angleMode(brush.DEGREES | brush.RADIANS) |
| Seeding | randomSeed() / noiseSeed() |
brush.seed() / brush.noiseSeed() |
| Frame flush | Automatic | brush.render() at end of each frame |
| Clear | p5's background() |
brush.clear(color?) |
This README covers the p5 build. For the standalone build see docs/standalone.md.
The simplest setup. Download dist/p5.brush.js from this repository (or use a CDN) and load it after p5.js. It registers itself automatically.
<script src="https://cdn.jsdelivr.net/npm/p5@2/lib/p5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5.brush@latest"></script>
<script>
function setup() {
createCanvas(800, 600, WEBGL);
brush.scaleBrushes(4);
}
function draw() {
background(245, 240, 230);
brush.set("HB", "#333", 1);
brush.line(-200, 0, 200, 0);
}
</script>p5.js 2.x required. p5.brush does not support p5.js 1.x.
npm install p5.brush
Import the p5 build as a side-effect — it auto-registers with the global p5:
import 'p5.brush'; // registers brush as a global
import * as brush from 'p5.brush'; // or import the namespaceIf you are using p5 as an ES module (instance mode), import both explicitly:
import p5 from 'p5';
import * as brush from 'p5.brush';// main.js
import p5 from 'p5';
import * as brush from 'p5.brush';
new p5((p) => {
brush.instance(p);
p.setup = () => {
p.createCanvas(800, 600, p.WEBGL);
brush.scaleBrushes(4);
};
p.draw = () => {
p.background(245, 240, 230);
brush.set('HB', '#2a2a2a', 1);
brush.line(-200, 0, 200, 0);
};
});Use the /standalone subpath:
import * as brush from 'p5.brush/standalone';
brush.createCanvas(800, 600, { parent: document.body });
brush.scaleBrushes(4);
brush.angleMode(brush.DEGREES);
brush.clear('#f5f0e0');
brush.push();
brush.translate(-400, -300); // shift origin to top-left
brush.set('HB', '#2a2a2a', 1);
brush.line(100, 100, 700, 500);
brush.pop();
brush.render(); // flush to canvasSee docs/standalone.md for the full standalone guide.
When you load p5 as an ES module, p5 functions are scoped to the sketch instance rather than the global namespace. Call brush.instance(p) before setup and draw:
import p5 from 'p5';
import * as brush from 'p5.brush';
const sketch = (p) => {
brush.instance(p); // ← required when using instance mode
p.setup = () => {
p.createCanvas(700, 410, p.WEBGL);
brush.scaleBrushes(3);
};
p.draw = () => {
p.background(240);
brush.set('HB', '#333', 1);
brush.line(100, 100, 400, 300);
};
};
new p5(sketch);After calling brush.instance(p), all brush.* calls work normally — no p. prefix is needed.
p5.brush ships with bundled type declarations. No @types/ package is needed.
import * as brush from 'p5.brush';
// or
import * as brush from 'p5.brush/standalone';Types are automatically resolved from the package's exports map — nothing extra to configure in tsconfig.json.
Create a WEBGL canvas, pick a brush, and draw:
function setup() {
createCanvas(700, 410, WEBGL);
background('#f6f1e8');
brush.scaleBrushes(3); // scale built-in brushes to canvas size
// Draw a line
brush.set('HB', '#2f2a26', 1.4);
brush.line(-220, -80, 180, 40);
// Draw a filled circle with no stroke
brush.fill('#d7c3a3', 120);
brush.noStroke();
brush.circle(120, 20, 70);
// Draw a hatched rectangle with no fill
brush.set('rotring', '#1f4b99', 0.8);
brush.noFill();
brush.hatch(7, 35);
brush.rect(-140, 40, 120, 90, 'center');
}A typical first sequence:
brush.scaleBrushes(n)— scale the built-in brushes to match your canvas sizebrush.set(name, color, weight)— pick a brushbrush.fill(color, opacity)orbrush.hatch(dist, angle)— add interior texturebrush.line(),brush.rect(),brush.circle(), etc. — draw geometry
p5.brush follows p5's drawing model: first configure state, then draw shapes.
- Style —
brush.set(),brush.fill(),brush.hatch()and related functions configure how upcoming shapes will look. - Geometry —
brush.line(),brush.rect(),brush.circle(),brush.arc(),brush.beginShape(),brush.polygon()actually draw. - State reset —
brush.noStroke(),brush.noFill(),brush.noHatch()turn parts of that state off. - Save / restore —
push()andpop()save and restore both p5 transform state and all brush state (stroke, fill, hatch, field). - Vector fields are optional. If you never call
brush.field(), everything draws normally.
Redirects brush drawing to a secondary canvas target. Not required for the main canvas — the library initialises automatically when createCanvas() is called.
Pass a p5.Graphics or an active p5.Framebuffer to draw into that target instead. Call with no argument to switch back to the main canvas.
// Draw into a p5.Graphics buffer
const pg = createGraphics(400, 300, WEBGL);
pg.background(250);
brush.load(pg);
brush.set('HB', 'black', 1);
brush.circle(200, 150, 80);
brush.load(); // restore main canvas
image(pg, 20, 20);
// Draw into a framebuffer
const fb = createFramebuffer({ width: 400, height: 300 });
fb.draw(() => {
background(250);
brush.load(fb);
brush.set('HB', 'black', 1);
brush.circle(200, 150, 80);
});
brush.load();
image(fb, 20, 340);
pg.createFramebuffer()(framebuffers created fromp5.Graphics) is not supported.
Scales all currently registered brush parameters (weight, scatter, spacing) by a multiplier.
This is almost always needed with the built-in brushes. Without it, strokes will look far too thin for most canvas sizes. For a 600×600 canvas, brush.scaleBrushes(3) is a reasonable starting point — confirm visually.
Call this before adding custom brushes if you only want the built-ins scaled. Custom brushes added afterward are not affected unless you call scaleBrushes() again.
function setup() {
createCanvas(800, 600, WEBGL);
brush.scaleBrushes(4); // call early, before drawing
}Required in p5 instance mode. Tells p5.brush which p5 instance to render into. See p5 instance mode above.
Vector fields bend strokes across the canvas by storing a grid of directional angles. If you never activate one, p5.brush draws normally.
Activates a named vector field. Built-in fields: hand, curved, zigzag, waves, seabed, spiral, columns.
brush.field('waves');Deactivates the current vector field.
Shorthand for activating the hand field. Adds a subtle hand-drawn wobble to strokes.
brush.wiggle(3); // 1–10 rangeUpdates the active field with a new time value — use in draw() for animated fields.
function draw() {
brush.refreshField(frameCount / 10);
}Returns an array of all registered field names (built-ins + custom).
Creates a custom vector field. The generator receives a time value t and a 2D grid field[col][row] — fill each cell with an angle and return the grid.
brush.addField('diagonal', (t, field) => {
for (let col = 0; col < field.length; col++)
for (let row = 0; row < field[0].length; row++)
field[col][row] = 45 + t * 10; // degrees by default
return field;
});
brush.field('diagonal');Pass { angleMode: 'radians' } if your generator writes radian values.
Returns an array of all registered brush names (built-ins + custom).
Built-in brushes: 2B, HB, 2H, cpencil, pen, rotring, spray, marker, marker2, charcoal, hatch_brush.
Creates a custom brush. Once added, use it with brush.set() just like a built-in.
Parameters:
| Property | Type | Description |
|---|---|---|
type |
string |
"default", "spray", "marker", "custom", or "image" |
weight |
number |
Stroke thickness in canvas units |
scatter |
number |
Sideways wobble amount |
sharpness |
number |
0–1, edge softness ("default" only) |
grain |
number |
Texture density ("default" and "spray" only) |
opacity |
number |
0–255 per-mark opacity |
spacing |
number |
Gap between tip stamps along the stroke |
pressure |
array|function|object |
Pressure profile (see below) |
tip |
function |
Custom tip drawing function ("custom" only) |
image |
{ src: string } |
Image path ("image" only) |
rotate |
string |
"none", "natural", or "random" |
markerTip |
boolean |
Soft buildup at stroke start/end |
noise |
number |
Per-stroke opacity variation, 0–1 |
Pressure profiles:
pressure: [0.5, 1.5, 0.5] // [start, mid, end] — thin→thick→thin
pressure: [2, 0.5] // [start, end] — thick→thin
pressure: (t) => Math.sin(t * Math.PI) // custom functionCustom tip brushes receive a p5.Graphics buffer (_m). Draw in a 100×100 unit space centered at the origin. Dark = opaque, light/white = transparent:
brush.add('diamond', {
type: 'custom',
weight: 5,
scatter: 0.08,
opacity: 23,
spacing: 0.6,
pressure: [0.5, 1.5, 0.5],
tip: (_m) => {
_m.rotate(45);
_m.rect(-1.5, -1.5, 3, 3);
},
rotate: 'natural',
markerTip: false,
});Image brushes return a Promise — await them before drawing:
async function setup() {
createCanvas(800, 800, WEBGL);
await brush.add('watercolor', {
type: 'image',
weight: 10,
scatter: 2,
opacity: 30,
spacing: 1.5,
pressure: [1, 0.5],
image: { src: './tip.jpg' },
rotate: 'random',
markerTip: false,
});
brush.set('watercolor', 'blue', 1);
brush.line(100, 100, 400, 100);
}Restricts brush strokes to a rectangular region [x1, y1, x2, y2]. Affects strokes and hatching — not fills.
brush.clip([10, 10, 250, 200]);
brush.line(0, 0, 300, 300); // clipped to the region
brush.noClip();Removes the active clipping region.
Sets the active brush, color, and weight. Activates stroke mode automatically.
brush.set('HB', '#002185', 1);
brush.set('charcoal', color(40, 30, 20), 2);Switches the brush type without changing color or weight.
Sets the stroke color without changing the brush type or weight.
Sets the stroke weight multiplier.
Disables stroke for subsequent shapes.
Fill gives shapes a watercolor or solid interior texture.
Enables watercolor fill.
brush.fill('#a0c4a0', 120);
brush.fill(color(160, 196, 160), 120);Disables fill.
Controls how much the watercolor bleeds outside the shape's edge.
brush.fillBleed(0.2); // subtle bleed
brush.fillBleed(0.5, 'out'); // stronger outward bleed
brush.fillBleed(0.3, 'in'); // inward bleedControls the texture and border intensity of the watercolor fill.
brush.fillTexture(0.4, 0.4);
brush.fillTexture(0.8, 0.6, false); // disable scatter noiseEnables a simpler solid wash fill (no bleed simulation). Faster than watercolor fill.
brush.wash('#d4c4a0', 180);Disables wash fill.
Hatching fills shapes with scanline strokes. Stacks with fill.
Enables hatching with given line spacing and angle.
brush.hatch(6, 45);
brush.hatch(8, 30, { rand: 0.05, continuous: true });
brush.hatch(5, 60, { gradient: 0.5 }); // spacing grows across shapeOptions:
| Property | Description |
|---|---|
rand |
Endpoint jitter amount (0–1) |
continuous |
Connect scanlines into a serpentine path |
gradient |
Multiplicative spacing growth per scanline |
Sets the brush, color, and weight used exclusively for hatch strokes.
brush.hatchStyle('pen', '#1a1a1a', 0.7);Disables hatching.
Enables "massing" — an expressive arc-based shading effect built on top of hatching.
brush.mass('HB', '#2a2a2a', { precision: 0.7, strength: 0.8 });Options:
| Property | Default | Description |
|---|---|---|
precision |
0.5 |
Regularity of arcs (0–1) |
strength |
1 |
Number of layers drawn |
gradient |
0.1 |
Spacing gradient |
outline |
false |
Draw the polygon outline too |
Disables massing.
All primitives respect the currently active stroke, fill, and hatch state.
Draws a straight brush stroke from point 1 to point 2.
Draws a stroke that flows along the active vector field starting from (x, y).
brush.field('waves');
brush.flowLine(0, 0, 200, 45);Draws a rectangle. mode is 'center' or 'corner' (default 'corner').
Draws a circle.
Draws an arc. Angles follow the current p5 angleMode().
Draws a smooth curve through an array of [x, y] pairs.
brush.spline([[-100, 0], [0, -80], [100, 0], [0, 80]], 0.5);Draws a regular polygon.
brush.polygon(0, 0, 80, 6); // hexagonDraws an arbitrary closed polygon through a list of vertices.
brush.beginShape();
brush.vertex(-50, -50);
brush.vertex(50, -50);
brush.vertex(50, 50);
brush.vertex(-50, 50);
brush.endShape();brush.beginStroke(brushName, color, weight) / brush.move(length, angle, pressure?) / brush.endStroke()
Builds a stroke segment by segment — useful for hand-coded paths.
brush.beginStroke('HB', '#333', 1);
brush.move(80, 0);
brush.move(80, 30);
brush.move(80, -30);
brush.endStroke();Creates a polygon object from an array of [x, y] pairs. Call .draw(), .fill(), .hatch(), .wash(), or .mass() on the result.
const poly = new brush.Polygon([[-50, -50], [50, -50], [0, 60]]);
poly.draw();
poly.fill();
poly.hatch();Advanced classes for building programmatic paths and flow-field-aware positions. Typically used together with brush.beginStroke() / brush.endStroke().
Contributions, bug reports, and feature requests are welcome via GitHub Issues.
p5.brush is licensed under the MIT License. See LICENSE.md for details.
The watercolor fill system is inspired by Tyler Hobbs' essay on simulating watercolor. The library was originally created by acamposuribe.
