Unlock custom brushes, natural fill effects and intuitive hatching in p5.js
See the code
p5.brush.js 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.
Visit the library website here! (more examples soon)
🖌️ Try the interactive Brush Maker →
Design custom brushes with live preview and generate ready-to-pastebrush.add()code.
🌊 Try the interactive Flow Field Generator → Design custom vector fields with live preview and generate ready-to-paste
brush.addField()code.
▶️ Check the teaser live → Live on the p5.js Web Editor, where you can read and edit 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.
Important note: p5.brush requires p5.js 2.x or higher.
To set up your project, add p5.min.js and p5.brush.js to your HTML file. You can download the latest version of the p5.brush.js library in the dist folder.
Place the script tags in the following order:
<script src="path_to/p5.min.js"></script>
<script src="path_to/p5.brush.js"></script>
Replace path_to with the actual path to the script in your project directory or the URL if you are using a CDN.
Alternatively, you can link to a p5.brush.js file hosted online. All versions are stored in a CDN (Content Delivery Network). You can find a history of these versions in the p5.js CDN. In this case you can change the link to:
<!-- Online version of p5.brush -->
<script src="https://cdn.jsdelivr.net/npm/p5.brush@latest"></script>
Install the npm package. p5.brush requires p5.js as a peer dependency for the p5 build.
npm install p5.brush --save
The package exposes two subpaths — pick the one that matches your build:
// p5 build (default) — requires p5.js 2.x
import * as brush from 'p5.brush'
// Standalone build — no p5.js required
import * as brush from 'p5.brush/standalone'
If you are using p5 and p5.brush as modules, you will need to use instance mode. Read below.
In instance mode, p5 functions are scoped to a variable instead of the global namespace — useful when mixing with other libraries or using p5 as an ES module.
Call brush.instance(p) inside your sketch function before setup and draw. After that, all brush.* calls work normally without any p. prefix.
const sketch = (p) => {
// Tell p5.brush which p5 instance to use
brush.instance(p);
p.setup = () => {
// Canvas must be in WEBGL mode — brush initializes automatically
p.createCanvas(700, 410, p.WEBGL);
};
p.draw = () => {
p.background(240);
brush.set("HB", "#333", 1);
brush.line(100, 100, 400, 300);
};
};
new p5(sketch);
If you already know the basics of p5, this is the shortest path to drawing with p5.brush:
WEBGL canvas.brush.set(name, color, weight).brush.line(), brush.rect(), or brush.circle().function setup() {
createCanvas(700, 410, WEBGL);
background("#f6f1e8");
brush.scaleBrushes(3);
brush.set("HB", "#2f2a26", 1.4);
brush.line(-220, -80, 180, 40);
brush.fill("#d7c3a3", 120);
brush.noStroke();
brush.circle(120, 20, 70);
brush.set("rotring", "#1f4b99", 0.8);
brush.noFill();
brush.hatch(7, 35);
brush.rect(-140, 40, 120, 90, "center");
}
If you are new to the library, a good first sequence is:
brush.scaleBrushes(...) if you are using built-in brushesbrush.set(...) to choose the stroke brushbrush.fill(...) or brush.hatch(...) if you want interior texturebrush.line(), brush.rect(), brush.circle(), or brush.polygon() to drawbrush.set(), brush.stroke(), brush.strokeWeight(), brush.fill(), brush.hatch(), and related functions configure how upcoming shapes should look.brush.line(), brush.rect(), brush.circle(), brush.arc(), brush.beginShape(), and brush.polygon() actually draw geometry.brush.noStroke(), brush.noFill(), brush.noHatch(), and brush.noWash() turn parts of that state back off.brush.field(...), your shapes still draw normally.You can think of the library in three layers:
brush.set(), brush.fill(), brush.hatch(), and related state functionsbrush.line(), brush.rect(), brush.circle(), brush.beginShape(), and morep5.brush.js enhances the p5.js framework with a set of tools that allow for sophisticated drawing and rendering techniques.
With p5.brush.js, your digital canvas becomes a playground for innovation and expression, where each tool is fine-tuned to complement your unique creative process.
p5.brush.js provides a comprehensive API for creating complex drawings and effects. Below are the categorized functions and classes available in the library.
| Section | Functions | Section | Functions |
|---|---|---|---|
| Configuration | brush.load(), brush.scaleBrushes(), brush.instance() | Fill Operations | brush.fill(), brush.noFill(), brush.wash(), brush.noWash(), brush.fillBleed(), brush.fillTexture() |
| Vector Fields | brush.field(), brush.noField(), brush.refreshField(), brush.listFields(), brush.addField(), brush.wiggle() | Hatch Operations | brush.hatch(), brush.noHatch(), brush.hatchStyle(), brush.mass(), brush.noMass() |
| Brush Management | brush.box(), brush.add(), brush.clip(), brush.noClip() | Primitives | brush.line(), brush.flowLine(), brush.beginStroke(), brush.move(), brush.endStroke(), brush.spline(), brush.rect(), brush.circle(), brush.arc(), brush.beginShape(), brush.vertex(), brush.endShape(), brush.polygon() |
| Stroke Operations | brush.set(), brush.pick(), brush.stroke(), brush.noStroke(), brush.strokeWeight() | Exposed Classes | brush.Polygon(), brush.Plot(), brush.Position() |
| Utility Functions | upgrade notes, angle behavior |
This section covers functions for initializing the drawing system and configuring how the library behaves in your sketch. In particular, brush.scaleBrushes() is usually important when you use the built-in brushes.
Seeding: p5.brush automatically hooks into p5's
randomSeed()andnoiseSeed(). Calling either of those functions seeds both p5 and the library simultaneously — no separatebrush.seed()call is needed.
brush.load(buffer)
Description: Redirects brush drawing to a secondary canvas target. Not needed for the main canvas — the library initializes on the first brush drawing operation after createCanvas(..., WEBGL). Pass a p5.Graphics buffer or an active p5.Framebuffer to draw into that target instead; call brush.load() with no argument to switch back to the main canvas. Framebuffers created from p5.Graphics are not supported.
Parameters:
buffer (p5.Graphics | p5.Framebuffer): Optional. An offscreen target to draw into.How to use it:
p5.Graphics: create it with WEBGL, call brush.load(pg), draw with brush functions, then call brush.load() to restore the main canvas before presenting it with image(pg, ...).p5.Framebuffer: create it from the main sketch with createFramebuffer(...), enter its draw() or begin() / end() scope, call brush.load(fb) while it is active, draw with brush functions, then call brush.load() again after leaving the framebuffer scope.brush.load(...) only changes the target used by p5.brush. If you also use native p5 drawing calls, call them on the same target yourself, for example pg.background(...) for p5.Graphics.pg.createFramebuffer() is not supported.Example (draw into a buffer):
function setup() {
createCanvas(400, 400, WEBGL);
brush.load();
brush.set("HB", "black", 1);
brush.rect(40, 40, 150, 100);
// Switch to a buffer
let buffer = createGraphics(200, 300, WEBGL);
brush.load(buffer);
brush.set("HB", "black", 1);
brush.rect(40, 40, 150, 100);
image(buffer, 20, 40);
// Switch back to main canvas
brush.load();
}
Example (draw into a framebuffer):
function setup() {
createCanvas(400, 400, WEBGL);
brush.load();
const layer = createFramebuffer({ width: 200, height: 200 });
layer.draw(() => {
brush.load(layer);
brush.set("HB", "black", 1);
brush.circle(100, 100, 80);
});
brush.load();
image(layer, 20, 20);
}
Typical workflows:
// p5.Graphics
const pg = createGraphics(300, 200, WEBGL);
pg.background(250);
brush.load(pg);
brush.set("HB", "black", 1);
brush.circle(150, 100, 70);
brush.load();
image(pg, 20, 20);
// p5.Framebuffer
const fb = createFramebuffer({ width: 300, height: 200 });
fb.draw(() => {
background(250);
brush.load(fb);
brush.set("HB", "black", 1);
brush.circle(150, 100, 70);
});
brush.load();
image(fb, 20, 240);
brush.scaleBrushes(scale)
scale (Number): The scaling factor to be applied to the brush parameters.brush.scaleBrushes() is usually not optional in practice. Without it, the built-in brushes will often look much too small or otherwise wrong for your canvas size. For a 600x600 canvas, brush.scaleBrushes(3) is a good starting point, but the final value should still be confirmed visually.brush.scaleBrushes() again to scale them too.// Usually needed when working with built-in brushes
brush.scaleBrushes(3);
Use brush.scaleBrushes() early in setup() so the built-in brushes match your canvas scale before you start drawing.brush.instance(p)
setup and draw when using p5 in instance mode. Tells p5.brush which p5 instance to render into. After calling this, all brush.* functions work normally — no need to prefix them with p..p (p5): The p5 instance passed as the argument to your sketch function.const sketch = (p) => {
// Must be called before setup/draw
brush.instance(p);
p.setup = () => {
// Canvas must be WEBGL
p.createCanvas(700, 410, p.WEBGL);
brush.load();
};
p.draw = () => {
p.background(240);
brush.set("HB", "#333", 1);
brush.line(100, 100, 400, 300);
};
};
new p5(sketch);
Vector fields let you bend and redirect strokes so they follow a flow across the canvas. If you do not use them, p5.brush still works like a normal drawing library.
brush.field(name)
name (String): The identifier for the vector field to be activated. This can be a name of one of the predefined fields or a custom field created with brush.addField().hand, curved, zigzag, waves, seabed, spiral, and columns. These, as well as any custom fields added, can be activated using this function.// To activate the "waves" vector field
brush.field("waves");
// To activate a custom vector field named "myCustomField"
brush.field("myCustomField");
Once a vector field is activated, it affects how the subsequent shapes are drawn, aligning them with its directional flow, unless stated otherwise in the documentationbrush.noField()
brush.noField();
brush.wiggle(wiggle)
"hand" vector field with a given wiggle intensity. A shorthand for adding a subtle hand-drawn wobble to strokes without calling brush.field("hand") directly.wiggle (Number): Intensity of the wobble, e.g. 1–10.brush.wiggle(3);
brush.refreshField(time)
time (Number): The time input for the vector field generator function, typically related to the frame count.// In the draw loop, refresh the vector field based on the frame count
function draw() {
brush.refreshField(frameCount / 10);
// Additional drawing code
}
This function will invoke the generator function of the active vector field with the provided time argument, allowing the field to evolve and create fluid, dynamic animations in the rendering.brush.listFields()
brush.addField().Array<string> - An array of vector field names.let fieldNames = brush.listFields();
for (let name of fieldNames) {
console.log(name);
}
brush.addField(name, generatorFunction, options)
brush.field(name) just like any built-in field.name (String): Any name you want, e.g. "myField".generatorFunction (Function): A function (t, field) => field that fills every cell of the grid with an angle and returns it. t is a time value you can use for animation (pass it via brush.refreshField(t)).options (Object): Optional configuration for custom field angles.
angleMode: "degrees" (default) or "radians". This controls how the values written into field[column][row] are interpreted.field is a 2D array — field[column][row]. You loop through every column and row and set each cell to an angle. By default those angles are interpreted as degrees. Small angles (±10°) make subtle curves; large ones (±90° or more) make dramatic bends.brush.addField("diagonal", function(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; // 45° angle, changes with time
}
}
return field; // always return field at the end!
});
brush.field("diagonal"); // activate it
t at all if you don't need animation. Just fill the grid and return it.This section covers choosing brushes, adding your own brushes, and clipping where strokes are allowed to appear.
brush.box()
Array<string> - An array listing the names of all brushes.2B, HB, 2H, cpencil, pen, rotring, spray, marker, marker2, charcoal, and hatch_brush (for clean hatching).// Retrieve a list of all available brush names
let brushes = brush.box();
console.log(brushes); // Logs the array of brush names
brush.box() allows you to explore and select from the various brushes, facilitating the choice of the appropriate brush for different artistic needs.brush.add(name, params) — Design your brush visually with the Brush Maker ↗
Description: Creates a new brush with your own settings. Once added, you use it just like any built-in brush with brush.set("myBrush", color, size).
Parameters:
name (String): A name for your brush — pick anything you like.params (Object): An object with the brush settings:
type: The kind of brush tip. Choose one: "default" (pencil-like), "spray" (scattered dots), "marker" (flat solid), "custom" (you draw the tip shape), "image" (use a photo as the tip).weight: How thick the brush is, in canvas units.scatter: How much the stroke wobbles sideways. Higher = more spread. (in canvas units)sharpness: A number from 0 to 1. Lower = softer/fuzzier edge. Only matters for "default" type.grain: Controls how dense the texture is. Higher = smoother, more continuous line. Only matters for "default" and "spray" types.opacity: How opaque each mark is, from 0 to 255. Pressure also affects this.spacing: Gap between brush tip stamps along the stroke. 1 means no overlap, lower values create denser strokes.pressure: Controls how the brush size changes from start to end of a stroke. Use a simple array — the easiest way:
[start, end] — e.g. [2, 0.5] starts thick and gets thin.[start, middle, end] — e.g. [0.5, 2, 0.5] starts thin, swells in the middle, ends thin.(t) => value where t goes from 0 (start) to 1 (end), for full control.{ mode: "gaussian", curve: [0.15, 0.2], min_max: [1.1, 0.9] }.
curve adjusts the wobble and asymmetry of the pressure envelope, while min_max sets the mapped pressure range. This is the same family of pressure logic used by the built-in brushes.tip: Only for "custom" type. A function that draws the tip shape — see Custom tip brushes note below.image: Only for "image" type. An object with a src property pointing to your image file: { src: "./tip.jpg" }.rotate: How the tip rotates as it moves. "none" keeps it fixed, "natural" follows the stroke direction, "random" spins randomly.markerTip: Only for "marker", "custom", and "image" types. Set to false to disable the extra soft tip buildup those brushes add at the start and end of each stroke. Defaults to true.noise: Controls per-stroke opacity variation. Each time a stroke is drawn, the brush samples a Gaussian to randomly shift the whole stroke slightly lighter or darker than its base alpha — giving repeated strokes an organic, non-mechanical feel. 0 disables the variation (every stroke is identical in opacity). 1 is the maximum variation. Defaults to 0.3.Usage:
// Image brush — use a photo as the brush tip.
// IMPORTANT: use "await" so the image loads before drawing,
// and make sure your setup() function is declared "async"!
async function setup() {
createCanvas(800, 800, WEBGL);
brush.load();
await brush.add("watercolor", {
type: "image",
weight: 10,
scatter: 2,
opacity: 30,
spacing: 1.5,
pressure: [1, 0.5], // starts thick, ends thin
image: { src: "./brush_tips/brush.jpg" },
rotate: "random",
markerTip: false,
});
// Now you can draw with it!
brush.set("watercolor", "blue", 1);
brush.line(100, 100, 400, 100);
}
// Advanced: explicitly use the built-in Gaussian pressure mode
brush.add("soft-pencil", {
type: "default",
weight: 0.3,
scatter: 0.6,
sharpness: 0.3,
grain: 10,
opacity: 170,
spacing: 0.1,
pressure: {
mode: "gaussian",
curve: [0.15, 0.2],
min_max: [1.1, 0.9],
},
rotate: "none",
});
Image brushes only: brush.add() returns a Promise when type is "image" — you must await it so the image finishes loading before you start drawing. For this to work, your setup() function must be declared async. For all other brush types, no await is needed.
Custom tip brushes: The tip function receives _m, a p5.Graphics buffer. You draw inside it using normal p5 commands. The coordinate space is 100×100 units with the origin at the centre, so shapes are drawn around (0, 0) and edges fall around ±50 units. The library converts the result to a mask: dark fills become opaque, light or white becomes transparent — so you define the shape in dark tones, and the color from brush.set() or brush.stroke() is applied at render time. Any p5 drawing command works: rect, circle, ellipse, line, triangle, beginShape/vertex/endShape, push/pop, translate, rotate, scale, and so on.
brush.add("diamond", {
type: "custom",
weight: 5,
scatter: 0.08,
opacity: 23,
spacing: 0.6,
pressure: [0.5, 1.5, 0.5], // thin → thick → thin
tip: (_m) => {
_m.rotate(45); // degrees, follows p5 angleMode
_m.rect(-1.5, -1.5, 3, 3);
},
rotate: "natural",
markerTip: false,
});
Using the standalone build? The tip function works differently there — _m is not a p5.Graphics. See docs/standalone.md → Custom tip brushes.
brush.clip(clippingRegion)
brush.clip() is called. The clipping remains in effect for all strokes drawn after the call to brush.clip() until brush.noClip() is used.clippingRegion (Array): An array defining the clipping region as [x1, y1, x2, y2], with (x1, y1) and (x2, y2) being the corners of the clipping rectangle.// Set a clipping region
brush.clip([10, 10, 250, 200]);
// Draw a line - it will be clipped according to the region
brush.line(100, 90, 300, 40);
// Remove the clipping region
brush.noClip();
// Draw another line - it will not be clipped
brush.line(0, 0, 200, 300);
brush.noClip()
// Disable the clipping region
brush.noClip();
These functions control stroke appearance: which brush is active, what color it uses, and how thick it should be.
brush.set(brushName, color, weight)
brushName (String): The name of the brush to be used.color (String|p5.Color): The color for the brush, which can be specified as a HEX string or a p5.Color object.weight (Number): The weight or size of the brush.// Set the "HB" brush with a specific blue color and weight factor 1
brush.set("HB", "#002185", 1);
By using brush.set(), you can quickly switch between different brushes, colors, and sizes, allowing for dynamic and varied stroke applications in your artwork.brush.pick(brushName)
brushName (String): The name of the brush to set as current.// Set the current brush to "charcoal"
brush.pick("charcoal");
Use brush.pick() to switch between different brush types while maintaining the current color and weight settings.brush.stroke(r, g, b) or brush.stroke(color)
r (Number|String|p5.Color): The red component of the color, a CSS color string, or a p5.Color object.g (Number): Optional. The green component of the color.b (Number): Optional. The blue component of the color.// Set the brush color using RGB values
brush.stroke(105, 111, 34);
// Or set the brush color using a HEX string
brush.stroke("#002185");
Use brush.stroke() to define the color of your brush strokes, enabling a diverse palette for your artwork.brush.noStroke()
// Disable stroke for the upcoming shapes
brush.noStroke();
brush.noStroke() is essential for creating drawings where only fill and no outline is desired.brush.strokeWeight(weight)
weight (Number): The weight to set for the brush.// Set the brush stroke weight to 2.3 times the base size
brush.strokeWeight(2.3);
brush.strokeWeight() provides the flexibility to easily adjust the thickness of your brush strokes, enhancing the expressiveness of your drawing tools.These functions control how closed shapes are filled. You can use watercolor-style fills, flat wash fills, and adjust bleed or texture.
brush.fill(a, b, c, d) or brush.fill(color, opacity)
a (Number|String|p5.Color): The red component of the color or grayscale value, a CSS color string, or a p5.Color object.b (Number): Optional. The green component of the color or grayscale opacity if two arguments are used.c (Number): Optional. The blue component of the color.d (Number): Optional. The opacity of the color, 0 - 255.// Set the fill color using RGB values and opacity
brush.fill(244, 15, 24, 75);
// Or set the fill color using a HEX string and opacity
brush.fill("#002185", 110);
brush.fill() allows for detailed control over the color and transparency of the fill.brush.noFill()
// Disable fill for the upcoming shapes
brush.noFill();
brush.wash(color, opacity)
fill(), wash() does not simulate watercolor bleed or texture; it draws a direct flat color with the given opacity.color (String|p5.Color): The wash color.opacity (Number): Opacity from 0 to 255.brush.wash("#f7e4a0", 255);
brush.circle(width / 2, height / 2, 180);
brush.noWash();
brush.noWash()
brush.noWash();
brush.fillBleed(strength, direction, angle)
strength (Number): The intensity of the bleed effect, ranging from 0 to 1.direction (String): Optional. "out" or "in". Defines the direction of the bleed effect.angle (Number): Optional. Sets the watercolor wash direction in the current angle mode; omit for random direction.brush.fillBleed(0.3, "out", 0); // left to right
brush.fillTexture(textureStrength, borderIntensity, scatter?)
textureStrength (Number): The texture of the fill effect, ranging from 0 to 1.borderIntensity (Number): The intensity of the border watercolor effect, ranging from 0 to 1.scatter (Boolean, optional, default true): Whether to draw the sparse scattered polygon layers that add edge texture noise. Set to false for a cleaner gradient trim effect.// Set the fill texture and border intensity
brush.fillTexture(0.6, 0.4);
// Disable scatter layers for a cleaner gradient
brush.fillTexture(0.6, 0.4, false);
These functions control hatching and massing inside shapes, using repeated lines or layered marks to build tone and texture.
brush.hatch(dist, angle, options)
dist (Number): The distance between hatching lines, in canvas units.angle (Number): The angle at which hatching lines are drawn. It is interpreted using p5's current angleMode() when brush.hatch() is called.options (Object): Optional settings to affect the hatching style, including:
rand: Randomness in line placement (0 to 1 or false).continuous: Whether to connect the end of a line with the start of the next.gradient: Modifies the distance between lines to create a gradient effect (0 to 1 or false).{rand: false, continuous: false, gradient: false}.// Set hatching with specific distance, angle, and options
brush.hatch(5, 30, {rand: 0.1, continuous: true, gradient: 0.3});
brush.noHatch()
// Disable hatching for upcoming shapes
brush.noHatch();
brush.hatchStyle(brushName, color, weight)
brushName (String): The name of the brush to use for hatching.color (String|p5.Color): The color for the brush, either as a CSS string or a p5.Color object.weight (Number): The weight or size of the brush for hatching.brush.hatchStyle("rotring", "green", 1.3);
brush.mass(brushName, color, options)
brushName (String): Brush used for the mass strokes.color (String|p5.Color): Color of the mass.options (Object): Optional settings:
precision: 0 to 1. Higher values reduce jitter and randomness.strength: 0 to 1. Controls how many of the three internal layers are drawn.gradient: 0 to 1. Passed through to hatch generation for spacing variation.outline: true or false. If true, the first polygon is also outlined.brush.mass("pastel", "#4b6cb7", {
precision: 0.55,
strength: 0.9,
gradient: 0.35,
outline: true,
});
brush.circle(width / 2, height / 2, 180);
brush.noMass()
brush.noMass();
In essence, the hatching system activates hatches for subsequent shapes, similarly to stroke and fill operations. However, you can also directly hatch multiple objects at once, including their intersections, as described below.
brush.hatchArray(polygons)
polygons (Array|Object): The polygon(s) to apply the hatching. Can be a single polygon object or an array of polygon objects.// Define an array of polygons (reference in the classes section)
let myPolygons = []
for (let i = 0; i < 10; i++) {
// We're creating 10 random triangles here
let p = new brush.Polygon([
[random(width), random(height)],
[random(width), random(height)],
[random(width), random(height)],
])
myPolygons.push(p)
}
// Create hatching across specified polygons
brush.hatchArray(myPolygons);
brush.hatchArray() provides an efficient way to apply complex hatching patterns to a set of defined shapes.brush.massArray(polygons)
mass() state to a single brush.Polygon or an array of brush.Polygon objects as one combined multi-layer gesture. The array is treated as combined layers: the first polygon defines the outer boundary, additional polygons act as holes or inner layers via even-odd logic.polygons (Array|Object): A single brush.Polygon or an array of brush.Polygon objects to mass.hatchArray(), this is an alternative to letting shapes pick up mass state automatically — use it when you need to treat multiple polygons as a single gesture.// Define two nested polygons — outer boundary and inner hole
let outer = new brush.Polygon([[50,50],[350,50],[350,350],[50,350]]);
let inner = new brush.Polygon([[100,100],[300,100],[300,300],[100,300]]);
let myPolygons = [outer, inner];
// Apply mass to both as one combined gesture
brush.mass("HB", "#002185", { strength: 1 });
brush.massArray(myPolygons);
brush.massArray() treats the array as layered geometry, so the inner polygon creates a hole via even-odd logic rather than being massed independently.These are the functions that actually draw lines, paths, and shapes using the current stroke, fill, and hatch settings.
The following functions are only affected by stroke() operations, completely ignoring fill() and hatch().
brush.line(x1, y1, x2, y2)
noStroke() has been called.x1 (Number): The x-coordinate of the start point.y1 (Number): The y-coordinate of the start point.x2 (Number): The x-coordinate of the end point.y2 (Number): The y-coordinate of the end point.// Set the brush color and draw a line
brush.stroke("red");
brush.line(15, 10, 200, 10);
brush.flowLine(x, y, length, dir)
x (Number): The x-coordinate of the starting point.y (Number): The y-coordinate of the starting point.length (Number): The length of the line.dir (Number): The direction in which to draw the line, measured anticlockwise from the x-axis and interpreted using the current p5 angleMode().// Set a vector field and draw a flow line
brush.field("seabed");
brush.flowLine(15, 10, 185, 0);
These three functions provide advanced control over the creation of strokes/paths, allowing for custom pressure and direction at different points along the path. Think of them as bodily movements performed with the hands. You can create two types of strokes: "curve" or "segments". For curved strokes, the curvature at any point is interpolated between the nearest control points.
These functions allow for the creation of strokes with varied pressures and directions, mimicking the organic nature of hand-drawn strokes. For an application of these principles, see: Enfantines II
brush.beginStroke(type, x, y)
type (String): The type of the stroke, either "curve" or "segments".x (Number): The x-coordinate of the starting point of the stroke.y (Number): The y-coordinate of the starting point of the stroke.// Begin a new curve stroke
brush.beginStroke("curve", 15, 30);
brush.move(angle, length, pressure)
brush.beginStroke() and brush.endStroke() to outline the stroke's trajectory and characteristics.angle (Number): The initial angle of the segment, relative to the canvas, measured anticlockwise from the x-axis.length (Number): The length of the segment.pressure (Number): The pressure at the start of the segment, influencing properties like width.brush.move(30, 150, 0.6);
brush.move(75, 40, 1.1);
brush.endStroke(angle, pressure)
angle (Number): The angle of the curve at the end point of the stroke path.pressure (Number): The pressure at the end of the stroke.// Complete the stroke with a specific angle and pressure
brush.endStroke(-45, 0.8);
brush.spline(array_points, curvature)
array_points (Array<Array>): An array of points, where each point is [x, y] or [x, y, pressure]. The optional pressure value at each point influences brush width along the curve.curvature (Number): Optional. The curvature of the spline, ranging from 0 to 1. A value of 0 produces straight segments.brush.Plot — the rendered plot. Store it to apply .hatch(), .mass(), .fill(), or .draw() later at a different position.let points = [[30, 70], [85, 20, 1.5], [130, 100], [180, 50]];
brush.spline(points, 0.5);
The following functions are affected by stroke(), fill() and hatch() operations.
brush.rect(x, y, w, h, mode)
x (Number): The x-coordinate of the rectangle.y (Number): The y-coordinate of the rectangle.w (Number): The width of the rectangle.h (Number): The height of the rectangle.mode (String): Optional. "corner" (default) positions (x, y) at the top-left corner. "center" draws the rectangle centered at (x, y).brush.noStroke();
brush.noHatch();
brush.fill("#002185", 75);
brush.rect(150, 100, 50, 35, "center");
brush.circle(x, y, radius, r)
x (Number): The x-coordinate of the circle's center.y (Number): The y-coordinate of the circle's center.radius (Number): The radius of the circle.r (Number|Boolean): Optional. Hand-drawn irregularity amount. Values around 0 to 1 give subtle variation; true behaves like a strong irregularity.[plot, offsetX, offsetY] — the brush.Plot and the top-left drawing offset. Pass these to plot.hatch(offsetX, offsetY), plot.mass(offsetX, offsetY), etc. to reuse the geometry.brush.circle(100, 150, 75, true);
brush.arc(x, y, radius, start, end)
fill() is not applied.x (Number): The x-coordinate of the center.y (Number): The y-coordinate of the center.radius (Number): The radius of the arc.start (Number): Start angle interpreted using the current p5 angleMode().end (Number): End angle interpreted using the current p5 angleMode().brush.Plot — the rendered plot, or null when start equals end (zero sweep). Store it to redraw or apply effects at a different position.brush.arc(200, 200, 50, 0, Math.PI);
These three functions perform similarly to the p5.js beginShape(), vertex(), and endShape() functions, although their curvature calculation is very different. They allow you to draw custom shapes with fine control over brush pressure at different points of the perimeter.
brush.beginShape(curvature)
curvature (Number): Optional. A value from 0 to 1 that defines the curvature of the shape's edges.// Begin defining a custom shape with a specified curvature
brush.beginShape(0.3);
brush.vertex(x, y, pressure)
brush.beginShape() and brush.endShape() calls. An optional pressure parameter can be applied at each vertex.x (Number): The x-coordinate of the vertex.y (Number): The y-coordinate of the vertex.pressure (Number): Optional. The pressure at the vertex, affecting properties like width.// Add vertices to the custom shape
brush.vertex(50, 100);
brush.vertex(100, 150, 0.5);
brush.vertex(150, 100);
brush.endShape(a)
a (Boolean): Optional. Pass true to close the shape (connect the last vertex back to the first). Pass false or omit to leave the shape open.brush.Plot — the rendered plot for the completed shape. Store it to apply .hatch(), .mass(), .fill(), or .draw() later at a different position.// Finish the custom shape and close it
brush.endShape(true);
// Or leave it open
brush.endShape(false);
brush.polygon(pointsArray)
beginShape() and endShape() approach.pointsArray (Array): An array of points, where each point is an array of two numbers [x, y].brush.Polygon — the polygon instance. Store it to call .hatch(), .mass(), .fill(), or .draw() later, or pass it to brush.hatchArray() / brush.massArray().// Define a polygon using an array of points
let points = [[x1, y1], [x2, y2], ...];
brush.polygon(points);
brush.polygon() is ideal for drawing fixed shapes that remain unaffected by vector fields, providing precise control over their form and appearance.The drawing primitives return their geometry — a brush.Polygon from brush.polygon(), a brush.Plot from brush.arc() / brush.spline() / brush.endShape(), or [plot, x, y] from brush.circle(). Store the return value to transform, redraw, or apply effects to the same geometry later, independently of when it was drawn.
// Draw once; reuse the geometry for hatching and massing
const frame = brush.polygon([[50,50],[350,50],[350,350],[50,350]]);
// Hatch the stored polygon with the current hatch state
brush.hatch(8, Math.PI / 4);
frame.hatch();
// Mass the same geometry with the current mass state
brush.mass("pastel", "#4b6cb7", { strength: 0.8 });
frame.mass();
To apply effects to multiple polygons as a single gesture (with even-odd hole logic), pass an array to brush.hatchArray() or brush.massArray().
Exposed Classes provide foundational elements for creating and manipulating shapes and paths, as well as interacting with vector-fields in a more advanced manner.
brush.PolygonDescription: Represents a polygon defined by a set of vertices. The Polygon class is essential for creating and working with multi-sided shapes, offering various methods to manipulate and render these shapes.
Constructor:
brush.Polygon(pointsArray)
pointsArray (Array): An array of points, where each point is an array of two numbers [x, y].Methods:
.intersect(line)
line (Object): A line object with properties point1 and point2.Array of objects, each with x and y properties, representing the intersection points..draw(brush, color, weight)
.fill(color, opacity, bleed, texture, border, direction, angle)
.wash(color, opacity)
.hatch(distance, angle, options)
.mass()
Attributes:
.vertices: An array of the polygon's vertices, each vertex being an object with x and y properties..sides: An array representing the different segments that make up the polygon.brush.PlotDescription: The Plot class is crucial for the plot system, managing a collection of segments to create a variety of shapes and paths. It enables intricate designs, such as curves and custom strokes, by defining each segment with an angle, length, and pressure. Plot instances can be transformed through rotation, and their appearance controlled via pressure and angle calculations.
Constructor:
brush.Plot(_type)
_type (String): The type of plot, either "curve" or "segments".Methods:
.addSegment(_a, _length, _pres)
_a (Number): The angle of the segment._length (Number): The length of the segment._pres (Number): The pressure of the segment..endPlot(_a, _pres)
_a (Number): The final angle of the plot._pres (Number): The final pressure of the plot..rotate(_a)
_a (Number): The angle for rotation..genPol(_x, _y)
_x (Number): The x-coordinate for the starting point._y (Number): The y-coordinate for the starting point.Polygon - The generated polygon..draw(x, y)
x (Number): The x-coordinate to draw at.y (Number): The y-coordinate to draw at..fill(x, y)
x (Number): The x-coordinate to fill at.y (Number): The y-coordinate to fill at..wash(x, y)
x (Number): The x-coordinate to wash at.y (Number): The y-coordinate to wash at..hatch(x, y)
x (Number): The x-coordinate to hatch at.y (Number): The y-coordinate to hatch at..mass(x, y)
x (Number): The x-coordinate to mass at.y (Number): The y-coordinate to mass at.Attributes:
.segments: An array containing the lengths of all segments..angles: An array of angles at the different control points..press: An array with custom brush pressures at the various control points..type: The type of the plot, either "curve" or "segments"..pol: Stores the generated polygon object after executing the .genPol() method.brush.PositionDescription: The Position class represents a point within a two-dimensional space, capable of interacting with a vector field. It includes methods for updating the position based on the field's flow, allowing for movement through the vector field in various ways.
Constructor:
brush.Position(x, y)
x (Number): The initial x-coordinate.y (Number): The initial y-coordinate.Methods:
.moveTo(_dir, _length, _step_length)
_dir (Number): The direction of movement, with angles measured anticlockwise from the x-axis.
It is interpreted using the current p5 angleMode()._length (Number): The length to move along the field._step_length (Number): The length of each step..plotTo(_plot, _length, _step_length, _scale)
Plot object._plot (Position): The Plot path object._length (Number): The length to move towards the target position._step_length (Number): The length of each step._scale (Number): The scaling factor for the plotting path..angle()
.reset()
plotted property to 0. This property tracks the distance moved since the last reset or the creation of the position. Important for consecutive different Plot paths.Attributes:
.x: The current x-coordinate..y: The current y-coordinate..plotted: Stores the distance moved since the last reset or the creation of the position.Note for users upgrading from older versions:
brush.push(),brush.pop(),brush.translate(),brush.rotate(), andbrush.scale()are no longer needed. The library now automatically hooks into p5'spush()andpop(), so brush state (stroke, fill, hatch settings) is saved and restored alongside p5's own state. Likewise, p5'stranslate(),rotate(), andscale()are automatically inherited by all brush strokes and fills.
Public brush APIs that accept angles also inherit p5's current
angleMode(). With noangleMode()call, that means radians by default, matching p5 itself. The main exception isbrush.addField(name, fn, { angleMode }), which lets custom field generators declare how returned angles should be interpreted before they are stored internally in degrees.
We welcome contributions from the community. If you find a bug or have a feature request, please open an issue on Github.
p5.brush.js is released under the MIT License. See the LICENSE file for details.
1,314 followers · starred Nov 2023
12,492 followers · starred Aug 2026
166 followers · starred Aug 2026
273 followers · starred Sep 2026
Unlock custom brushes, natural fill effects and intuitive hatching in p5.js
See the code
p5.brush.js 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.
Visit the library website here! (more examples soon)
🖌️ Try the interactive Brush Maker →
Design custom brushes with live preview and generate ready-to-pastebrush.add()code.
🌊 Try the interactive Flow Field Generator → Design custom vector fields with live preview and generate ready-to-paste
brush.addField()code.
▶️ Check the teaser live → Live on the p5.js Web Editor, where you can read and edit 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.
Important note: p5.brush requires p5.js 2.x or higher.
To set up your project, add p5.min.js and p5.brush.js to your HTML file. You can download the latest version of the p5.brush.js library in the dist folder.
Place the script tags in the following order:
<script src="path_to/p5.min.js"></script>
<script src="path_to/p5.brush.js"></script>
Replace path_to with the actual path to the script in your project directory or the URL if you are using a CDN.
Alternatively, you can link to a p5.brush.js file hosted online. All versions are stored in a CDN (Content Delivery Network). You can find a history of these versions in the p5.js CDN. In this case you can change the link to:
<!-- Online version of p5.brush -->
<script src="https://cdn.jsdelivr.net/npm/p5.brush@latest"></script>
Install the npm package. p5.brush requires p5.js as a peer dependency for the p5 build.
npm install p5.brush --save
The package exposes two subpaths — pick the one that matches your build:
// p5 build (default) — requires p5.js 2.x
import * as brush from 'p5.brush'
// Standalone build — no p5.js required
import * as brush from 'p5.brush/standalone'
If you are using p5 and p5.brush as modules, you will need to use instance mode. Read below.
In instance mode, p5 functions are scoped to a variable instead of the global namespace — useful when mixing with other libraries or using p5 as an ES module.
Call brush.instance(p) inside your sketch function before setup and draw. After that, all brush.* calls work normally without any p. prefix.
const sketch = (p) => {
// Tell p5.brush which p5 instance to use
brush.instance(p);
p.setup = () => {
// Canvas must be in WEBGL mode — brush initializes automatically
p.createCanvas(700, 410, p.WEBGL);
};
p.draw = () => {
p.background(240);
brush.set("HB", "#333", 1);
brush.line(100, 100, 400, 300);
};
};
new p5(sketch);
If you already know the basics of p5, this is the shortest path to drawing with p5.brush:
WEBGL canvas.brush.set(name, color, weight).brush.line(), brush.rect(), or brush.circle().function setup() {
createCanvas(700, 410, WEBGL);
background("#f6f1e8");
brush.scaleBrushes(3);
brush.set("HB", "#2f2a26", 1.4);
brush.line(-220, -80, 180, 40);
brush.fill("#d7c3a3", 120);
brush.noStroke();
brush.circle(120, 20, 70);
brush.set("rotring", "#1f4b99", 0.8);
brush.noFill();
brush.hatch(7, 35);
brush.rect(-140, 40, 120, 90, "center");
}
If you are new to the library, a good first sequence is:
brush.scaleBrushes(...) if you are using built-in brushesbrush.set(...) to choose the stroke brushbrush.fill(...) or brush.hatch(...) if you want interior texturebrush.line(), brush.rect(), brush.circle(), or brush.polygon() to drawbrush.set(), brush.stroke(), brush.strokeWeight(), brush.fill(), brush.hatch(), and related functions configure how upcoming shapes should look.brush.line(), brush.rect(), brush.circle(), brush.arc(), brush.beginShape(), and brush.polygon() actually draw geometry.brush.noStroke(), brush.noFill(), brush.noHatch(), and brush.noWash() turn parts of that state back off.brush.field(...), your shapes still draw normally.You can think of the library in three layers:
brush.set(), brush.fill(), brush.hatch(), and related state functionsbrush.line(), brush.rect(), brush.circle(), brush.beginShape(), and morep5.brush.js enhances the p5.js framework with a set of tools that allow for sophisticated drawing and rendering techniques.
With p5.brush.js, your digital canvas becomes a playground for innovation and expression, where each tool is fine-tuned to complement your unique creative process.
p5.brush.js provides a comprehensive API for creating complex drawings and effects. Below are the categorized functions and classes available in the library.
| Section | Functions | Section | Functions |
|---|---|---|---|
| Configuration | brush.load(), brush.scaleBrushes(), brush.instance() | Fill Operations | brush.fill(), brush.noFill(), brush.wash(), brush.noWash(), brush.fillBleed(), brush.fillTexture() |
| Vector Fields | brush.field(), brush.noField(), brush.refreshField(), brush.listFields(), brush.addField(), brush.wiggle() | Hatch Operations | brush.hatch(), brush.noHatch(), brush.hatchStyle(), brush.mass(), brush.noMass() |
| Brush Management | brush.box(), brush.add(), brush.clip(), brush.noClip() | Primitives | brush.line(), brush.flowLine(), brush.beginStroke(), brush.move(), brush.endStroke(), brush.spline(), brush.rect(), brush.circle(), brush.arc(), brush.beginShape(), brush.vertex(), brush.endShape(), brush.polygon() |
| Stroke Operations | brush.set(), brush.pick(), brush.stroke(), brush.noStroke(), brush.strokeWeight() | Exposed Classes | brush.Polygon(), brush.Plot(), brush.Position() |
| Utility Functions | upgrade notes, angle behavior |
This section covers functions for initializing the drawing system and configuring how the library behaves in your sketch. In particular, brush.scaleBrushes() is usually important when you use the built-in brushes.
Seeding: p5.brush automatically hooks into p5's
randomSeed()andnoiseSeed(). Calling either of those functions seeds both p5 and the library simultaneously — no separatebrush.seed()call is needed.
brush.load(buffer)
Description: Redirects brush drawing to a secondary canvas target. Not needed for the main canvas — the library initializes on the first brush drawing operation after createCanvas(..., WEBGL). Pass a p5.Graphics buffer or an active p5.Framebuffer to draw into that target instead; call brush.load() with no argument to switch back to the main canvas. Framebuffers created from p5.Graphics are not supported.
Parameters:
buffer (p5.Graphics | p5.Framebuffer): Optional. An offscreen target to draw into.How to use it:
p5.Graphics: create it with WEBGL, call brush.load(pg), draw with brush functions, then call brush.load() to restore the main canvas before presenting it with image(pg, ...).p5.Framebuffer: create it from the main sketch with createFramebuffer(...), enter its draw() or begin() / end() scope, call brush.load(fb) while it is active, draw with brush functions, then call brush.load() again after leaving the framebuffer scope.brush.load(...) only changes the target used by p5.brush. If you also use native p5 drawing calls, call them on the same target yourself, for example pg.background(...) for p5.Graphics.pg.createFramebuffer() is not supported.Example (draw into a buffer):
function setup() {
createCanvas(400, 400, WEBGL);
brush.load();
brush.set("HB", "black", 1);
brush.rect(40, 40, 150, 100);
// Switch to a buffer
let buffer = createGraphics(200, 300, WEBGL);
brush.load(buffer);
brush.set("HB", "black", 1);
brush.rect(40, 40, 150, 100);
image(buffer, 20, 40);
// Switch back to main canvas
brush.load();
}
Example (draw into a framebuffer):
function setup() {
createCanvas(400, 400, WEBGL);
brush.load();
const layer = createFramebuffer({ width: 200, height: 200 });
layer.draw(() => {
brush.load(layer);
brush.set("HB", "black", 1);
brush.circle(100, 100, 80);
});
brush.load();
image(layer, 20, 20);
}
Typical workflows:
// p5.Graphics
const pg = createGraphics(300, 200, WEBGL);
pg.background(250);
brush.load(pg);
brush.set("HB", "black", 1);
brush.circle(150, 100, 70);
brush.load();
image(pg, 20, 20);
// p5.Framebuffer
const fb = createFramebuffer({ width: 300, height: 200 });
fb.draw(() => {
background(250);
brush.load(fb);
brush.set("HB", "black", 1);
brush.circle(150, 100, 70);
});
brush.load();
image(fb, 20, 240);
brush.scaleBrushes(scale)
scale (Number): The scaling factor to be applied to the brush parameters.brush.scaleBrushes() is usually not optional in practice. Without it, the built-in brushes will often look much too small or otherwise wrong for your canvas size. For a 600x600 canvas, brush.scaleBrushes(3) is a good starting point, but the final value should still be confirmed visually.brush.scaleBrushes() again to scale them too.// Usually needed when working with built-in brushes
brush.scaleBrushes(3);
Use brush.scaleBrushes() early in setup() so the built-in brushes match your canvas scale before you start drawing.brush.instance(p)
setup and draw when using p5 in instance mode. Tells p5.brush which p5 instance to render into. After calling this, all brush.* functions work normally — no need to prefix them with p..p (p5): The p5 instance passed as the argument to your sketch function.const sketch = (p) => {
// Must be called before setup/draw
brush.instance(p);
p.setup = () => {
// Canvas must be WEBGL
p.createCanvas(700, 410, p.WEBGL);
brush.load();
};
p.draw = () => {
p.background(240);
brush.set("HB", "#333", 1);
brush.line(100, 100, 400, 300);
};
};
new p5(sketch);
Vector fields let you bend and redirect strokes so they follow a flow across the canvas. If you do not use them, p5.brush still works like a normal drawing library.
brush.field(name)
name (String): The identifier for the vector field to be activated. This can be a name of one of the predefined fields or a custom field created with brush.addField().hand, curved, zigzag, waves, seabed, spiral, and columns. These, as well as any custom fields added, can be activated using this function.// To activate the "waves" vector field
brush.field("waves");
// To activate a custom vector field named "myCustomField"
brush.field("myCustomField");
Once a vector field is activated, it affects how the subsequent shapes are drawn, aligning them with its directional flow, unless stated otherwise in the documentationbrush.noField()
brush.noField();
brush.wiggle(wiggle)
"hand" vector field with a given wiggle intensity. A shorthand for adding a subtle hand-drawn wobble to strokes without calling brush.field("hand") directly.wiggle (Number): Intensity of the wobble, e.g. 1–10.brush.wiggle(3);
brush.refreshField(time)
time (Number): The time input for the vector field generator function, typically related to the frame count.// In the draw loop, refresh the vector field based on the frame count
function draw() {
brush.refreshField(frameCount / 10);
// Additional drawing code
}
This function will invoke the generator function of the active vector field with the provided time argument, allowing the field to evolve and create fluid, dynamic animations in the rendering.brush.listFields()
brush.addField().Array<string> - An array of vector field names.let fieldNames = brush.listFields();
for (let name of fieldNames) {
console.log(name);
}
brush.addField(name, generatorFunction, options)
brush.field(name) just like any built-in field.name (String): Any name you want, e.g. "myField".generatorFunction (Function): A function (t, field) => field that fills every cell of the grid with an angle and returns it. t is a time value you can use for animation (pass it via brush.refreshField(t)).options (Object): Optional configuration for custom field angles.
angleMode: "degrees" (default) or "radians". This controls how the values written into field[column][row] are interpreted.field is a 2D array — field[column][row]. You loop through every column and row and set each cell to an angle. By default those angles are interpreted as degrees. Small angles (±10°) make subtle curves; large ones (±90° or more) make dramatic bends.brush.addField("diagonal", function(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; // 45° angle, changes with time
}
}
return field; // always return field at the end!
});
brush.field("diagonal"); // activate it
t at all if you don't need animation. Just fill the grid and return it.This section covers choosing brushes, adding your own brushes, and clipping where strokes are allowed to appear.
brush.box()
Array<string> - An array listing the names of all brushes.2B, HB, 2H, cpencil, pen, rotring, spray, marker, marker2, charcoal, and hatch_brush (for clean hatching).// Retrieve a list of all available brush names
let brushes = brush.box();
console.log(brushes); // Logs the array of brush names
brush.box() allows you to explore and select from the various brushes, facilitating the choice of the appropriate brush for different artistic needs.brush.add(name, params) — Design your brush visually with the Brush Maker ↗
Description: Creates a new brush with your own settings. Once added, you use it just like any built-in brush with brush.set("myBrush", color, size).
Parameters:
name (String): A name for your brush — pick anything you like.params (Object): An object with the brush settings:
type: The kind of brush tip. Choose one: "default" (pencil-like), "spray" (scattered dots), "marker" (flat solid), "custom" (you draw the tip shape), "image" (use a photo as the tip).weight: How thick the brush is, in canvas units.scatter: How much the stroke wobbles sideways. Higher = more spread. (in canvas units)sharpness: A number from 0 to 1. Lower = softer/fuzzier edge. Only matters for "default" type.grain: Controls how dense the texture is. Higher = smoother, more continuous line. Only matters for "default" and "spray" types.opacity: How opaque each mark is, from 0 to 255. Pressure also affects this.spacing: Gap between brush tip stamps along the stroke. 1 means no overlap, lower values create denser strokes.pressure: Controls how the brush size changes from start to end of a stroke. Use a simple array — the easiest way:
[start, end] — e.g. [2, 0.5] starts thick and gets thin.[start, middle, end] — e.g. [0.5, 2, 0.5] starts thin, swells in the middle, ends thin.(t) => value where t goes from 0 (start) to 1 (end), for full control.{ mode: "gaussian", curve: [0.15, 0.2], min_max: [1.1, 0.9] }.
curve adjusts the wobble and asymmetry of the pressure envelope, while min_max sets the mapped pressure range. This is the same family of pressure logic used by the built-in brushes.tip: Only for "custom" type. A function that draws the tip shape — see Custom tip brushes note below.image: Only for "image" type. An object with a src property pointing to your image file: { src: "./tip.jpg" }.rotate: How the tip rotates as it moves. "none" keeps it fixed, "natural" follows the stroke direction, "random" spins randomly.markerTip: Only for "marker", "custom", and "image" types. Set to false to disable the extra soft tip buildup those brushes add at the start and end of each stroke. Defaults to true.noise: Controls per-stroke opacity variation. Each time a stroke is drawn, the brush samples a Gaussian to randomly shift the whole stroke slightly lighter or darker than its base alpha — giving repeated strokes an organic, non-mechanical feel. 0 disables the variation (every stroke is identical in opacity). 1 is the maximum variation. Defaults to 0.3.Usage:
// Image brush — use a photo as the brush tip.
// IMPORTANT: use "await" so the image loads before drawing,
// and make sure your setup() function is declared "async"!
async function setup() {
createCanvas(800, 800, WEBGL);
brush.load();
await brush.add("watercolor", {
type: "image",
weight: 10,
scatter: 2,
opacity: 30,
spacing: 1.5,
pressure: [1, 0.5], // starts thick, ends thin
image: { src: "./brush_tips/brush.jpg" },
rotate: "random",
markerTip: false,
});
// Now you can draw with it!
brush.set("watercolor", "blue", 1);
brush.line(100, 100, 400, 100);
}
// Advanced: explicitly use the built-in Gaussian pressure mode
brush.add("soft-pencil", {
type: "default",
weight: 0.3,
scatter: 0.6,
sharpness: 0.3,
grain: 10,
opacity: 170,
spacing: 0.1,
pressure: {
mode: "gaussian",
curve: [0.15, 0.2],
min_max: [1.1, 0.9],
},
rotate: "none",
});
Image brushes only: brush.add() returns a Promise when type is "image" — you must await it so the image finishes loading before you start drawing. For this to work, your setup() function must be declared async. For all other brush types, no await is needed.
Custom tip brushes: The tip function receives _m, a p5.Graphics buffer. You draw inside it using normal p5 commands. The coordinate space is 100×100 units with the origin at the centre, so shapes are drawn around (0, 0) and edges fall around ±50 units. The library converts the result to a mask: dark fills become opaque, light or white becomes transparent — so you define the shape in dark tones, and the color from brush.set() or brush.stroke() is applied at render time. Any p5 drawing command works: rect, circle, ellipse, line, triangle, beginShape/vertex/endShape, push/pop, translate, rotate, scale, and so on.
brush.add("diamond", {
type: "custom",
weight: 5,
scatter: 0.08,
opacity: 23,
spacing: 0.6,
pressure: [0.5, 1.5, 0.5], // thin → thick → thin
tip: (_m) => {
_m.rotate(45); // degrees, follows p5 angleMode
_m.rect(-1.5, -1.5, 3, 3);
},
rotate: "natural",
markerTip: false,
});
Using the standalone build? The tip function works differently there — _m is not a p5.Graphics. See docs/standalone.md → Custom tip brushes.
brush.clip(clippingRegion)
brush.clip() is called. The clipping remains in effect for all strokes drawn after the call to brush.clip() until brush.noClip() is used.clippingRegion (Array): An array defining the clipping region as [x1, y1, x2, y2], with (x1, y1) and (x2, y2) being the corners of the clipping rectangle.// Set a clipping region
brush.clip([10, 10, 250, 200]);
// Draw a line - it will be clipped according to the region
brush.line(100, 90, 300, 40);
// Remove the clipping region
brush.noClip();
// Draw another line - it will not be clipped
brush.line(0, 0, 200, 300);
brush.noClip()
// Disable the clipping region
brush.noClip();
These functions control stroke appearance: which brush is active, what color it uses, and how thick it should be.
brush.set(brushName, color, weight)
brushName (String): The name of the brush to be used.color (String|p5.Color): The color for the brush, which can be specified as a HEX string or a p5.Color object.weight (Number): The weight or size of the brush.// Set the "HB" brush with a specific blue color and weight factor 1
brush.set("HB", "#002185", 1);
By using brush.set(), you can quickly switch between different brushes, colors, and sizes, allowing for dynamic and varied stroke applications in your artwork.brush.pick(brushName)
brushName (String): The name of the brush to set as current.// Set the current brush to "charcoal"
brush.pick("charcoal");
Use brush.pick() to switch between different brush types while maintaining the current color and weight settings.brush.stroke(r, g, b) or brush.stroke(color)
r (Number|String|p5.Color): The red component of the color, a CSS color string, or a p5.Color object.g (Number): Optional. The green component of the color.b (Number): Optional. The blue component of the color.// Set the brush color using RGB values
brush.stroke(105, 111, 34);
// Or set the brush color using a HEX string
brush.stroke("#002185");
Use brush.stroke() to define the color of your brush strokes, enabling a diverse palette for your artwork.brush.noStroke()
// Disable stroke for the upcoming shapes
brush.noStroke();
brush.noStroke() is essential for creating drawings where only fill and no outline is desired.brush.strokeWeight(weight)
weight (Number): The weight to set for the brush.// Set the brush stroke weight to 2.3 times the base size
brush.strokeWeight(2.3);
brush.strokeWeight() provides the flexibility to easily adjust the thickness of your brush strokes, enhancing the expressiveness of your drawing tools.These functions control how closed shapes are filled. You can use watercolor-style fills, flat wash fills, and adjust bleed or texture.
brush.fill(a, b, c, d) or brush.fill(color, opacity)
a (Number|String|p5.Color): The red component of the color or grayscale value, a CSS color string, or a p5.Color object.b (Number): Optional. The green component of the color or grayscale opacity if two arguments are used.c (Number): Optional. The blue component of the color.d (Number): Optional. The opacity of the color, 0 - 255.// Set the fill color using RGB values and opacity
brush.fill(244, 15, 24, 75);
// Or set the fill color using a HEX string and opacity
brush.fill("#002185", 110);
brush.fill() allows for detailed control over the color and transparency of the fill.brush.noFill()
// Disable fill for the upcoming shapes
brush.noFill();
brush.wash(color, opacity)
fill(), wash() does not simulate watercolor bleed or texture; it draws a direct flat color with the given opacity.color (String|p5.Color): The wash color.opacity (Number): Opacity from 0 to 255.brush.wash("#f7e4a0", 255);
brush.circle(width / 2, height / 2, 180);
brush.noWash();
brush.noWash()
brush.noWash();
brush.fillBleed(strength, direction, angle)
strength (Number): The intensity of the bleed effect, ranging from 0 to 1.direction (String): Optional. "out" or "in". Defines the direction of the bleed effect.angle (Number): Optional. Sets the watercolor wash direction in the current angle mode; omit for random direction.brush.fillBleed(0.3, "out", 0); // left to right
brush.fillTexture(textureStrength, borderIntensity, scatter?)
textureStrength (Number): The texture of the fill effect, ranging from 0 to 1.borderIntensity (Number): The intensity of the border watercolor effect, ranging from 0 to 1.scatter (Boolean, optional, default true): Whether to draw the sparse scattered polygon layers that add edge texture noise. Set to false for a cleaner gradient trim effect.// Set the fill texture and border intensity
brush.fillTexture(0.6, 0.4);
// Disable scatter layers for a cleaner gradient
brush.fillTexture(0.6, 0.4, false);
These functions control hatching and massing inside shapes, using repeated lines or layered marks to build tone and texture.
brush.hatch(dist, angle, options)
dist (Number): The distance between hatching lines, in canvas units.angle (Number): The angle at which hatching lines are drawn. It is interpreted using p5's current angleMode() when brush.hatch() is called.options (Object): Optional settings to affect the hatching style, including:
rand: Randomness in line placement (0 to 1 or false).continuous: Whether to connect the end of a line with the start of the next.gradient: Modifies the distance between lines to create a gradient effect (0 to 1 or false).{rand: false, continuous: false, gradient: false}.// Set hatching with specific distance, angle, and options
brush.hatch(5, 30, {rand: 0.1, continuous: true, gradient: 0.3});
brush.noHatch()
// Disable hatching for upcoming shapes
brush.noHatch();
brush.hatchStyle(brushName, color, weight)
brushName (String): The name of the brush to use for hatching.color (String|p5.Color): The color for the brush, either as a CSS string or a p5.Color object.weight (Number): The weight or size of the brush for hatching.brush.hatchStyle("rotring", "green", 1.3);
brush.mass(brushName, color, options)
brushName (String): Brush used for the mass strokes.color (String|p5.Color): Color of the mass.options (Object): Optional settings:
precision: 0 to 1. Higher values reduce jitter and randomness.strength: 0 to 1. Controls how many of the three internal layers are drawn.gradient: 0 to 1. Passed through to hatch generation for spacing variation.outline: true or false. If true, the first polygon is also outlined.brush.mass("pastel", "#4b6cb7", {
precision: 0.55,
strength: 0.9,
gradient: 0.35,
outline: true,
});
brush.circle(width / 2, height / 2, 180);
brush.noMass()
brush.noMass();
In essence, the hatching system activates hatches for subsequent shapes, similarly to stroke and fill operations. However, you can also directly hatch multiple objects at once, including their intersections, as described below.
brush.hatchArray(polygons)
polygons (Array|Object): The polygon(s) to apply the hatching. Can be a single polygon object or an array of polygon objects.// Define an array of polygons (reference in the classes section)
let myPolygons = []
for (let i = 0; i < 10; i++) {
// We're creating 10 random triangles here
let p = new brush.Polygon([
[random(width), random(height)],
[random(width), random(height)],
[random(width), random(height)],
])
myPolygons.push(p)
}
// Create hatching across specified polygons
brush.hatchArray(myPolygons);
brush.hatchArray() provides an efficient way to apply complex hatching patterns to a set of defined shapes.brush.massArray(polygons)
mass() state to a single brush.Polygon or an array of brush.Polygon objects as one combined multi-layer gesture. The array is treated as combined layers: the first polygon defines the outer boundary, additional polygons act as holes or inner layers via even-odd logic.polygons (Array|Object): A single brush.Polygon or an array of brush.Polygon objects to mass.hatchArray(), this is an alternative to letting shapes pick up mass state automatically — use it when you need to treat multiple polygons as a single gesture.// Define two nested polygons — outer boundary and inner hole
let outer = new brush.Polygon([[50,50],[350,50],[350,350],[50,350]]);
let inner = new brush.Polygon([[100,100],[300,100],[300,300],[100,300]]);
let myPolygons = [outer, inner];
// Apply mass to both as one combined gesture
brush.mass("HB", "#002185", { strength: 1 });
brush.massArray(myPolygons);
brush.massArray() treats the array as layered geometry, so the inner polygon creates a hole via even-odd logic rather than being massed independently.These are the functions that actually draw lines, paths, and shapes using the current stroke, fill, and hatch settings.
The following functions are only affected by stroke() operations, completely ignoring fill() and hatch().
brush.line(x1, y1, x2, y2)
noStroke() has been called.x1 (Number): The x-coordinate of the start point.y1 (Number): The y-coordinate of the start point.x2 (Number): The x-coordinate of the end point.y2 (Number): The y-coordinate of the end point.// Set the brush color and draw a line
brush.stroke("red");
brush.line(15, 10, 200, 10);
brush.flowLine(x, y, length, dir)
x (Number): The x-coordinate of the starting point.y (Number): The y-coordinate of the starting point.length (Number): The length of the line.dir (Number): The direction in which to draw the line, measured anticlockwise from the x-axis and interpreted using the current p5 angleMode().// Set a vector field and draw a flow line
brush.field("seabed");
brush.flowLine(15, 10, 185, 0);
These three functions provide advanced control over the creation of strokes/paths, allowing for custom pressure and direction at different points along the path. Think of them as bodily movements performed with the hands. You can create two types of strokes: "curve" or "segments". For curved strokes, the curvature at any point is interpolated between the nearest control points.
These functions allow for the creation of strokes with varied pressures and directions, mimicking the organic nature of hand-drawn strokes. For an application of these principles, see: Enfantines II
brush.beginStroke(type, x, y)
type (String): The type of the stroke, either "curve" or "segments".x (Number): The x-coordinate of the starting point of the stroke.y (Number): The y-coordinate of the starting point of the stroke.// Begin a new curve stroke
brush.beginStroke("curve", 15, 30);
brush.move(angle, length, pressure)
brush.beginStroke() and brush.endStroke() to outline the stroke's trajectory and characteristics.angle (Number): The initial angle of the segment, relative to the canvas, measured anticlockwise from the x-axis.length (Number): The length of the segment.pressure (Number): The pressure at the start of the segment, influencing properties like width.brush.move(30, 150, 0.6);
brush.move(75, 40, 1.1);
brush.endStroke(angle, pressure)
angle (Number): The angle of the curve at the end point of the stroke path.pressure (Number): The pressure at the end of the stroke.// Complete the stroke with a specific angle and pressure
brush.endStroke(-45, 0.8);
brush.spline(array_points, curvature)
array_points (Array<Array>): An array of points, where each point is [x, y] or [x, y, pressure]. The optional pressure value at each point influences brush width along the curve.curvature (Number): Optional. The curvature of the spline, ranging from 0 to 1. A value of 0 produces straight segments.brush.Plot — the rendered plot. Store it to apply .hatch(), .mass(), .fill(), or .draw() later at a different position.let points = [[30, 70], [85, 20, 1.5], [130, 100], [180, 50]];
brush.spline(points, 0.5);
The following functions are affected by stroke(), fill() and hatch() operations.
brush.rect(x, y, w, h, mode)
x (Number): The x-coordinate of the rectangle.y (Number): The y-coordinate of the rectangle.w (Number): The width of the rectangle.h (Number): The height of the rectangle.mode (String): Optional. "corner" (default) positions (x, y) at the top-left corner. "center" draws the rectangle centered at (x, y).brush.noStroke();
brush.noHatch();
brush.fill("#002185", 75);
brush.rect(150, 100, 50, 35, "center");
brush.circle(x, y, radius, r)
x (Number): The x-coordinate of the circle's center.y (Number): The y-coordinate of the circle's center.radius (Number): The radius of the circle.r (Number|Boolean): Optional. Hand-drawn irregularity amount. Values around 0 to 1 give subtle variation; true behaves like a strong irregularity.[plot, offsetX, offsetY] — the brush.Plot and the top-left drawing offset. Pass these to plot.hatch(offsetX, offsetY), plot.mass(offsetX, offsetY), etc. to reuse the geometry.brush.circle(100, 150, 75, true);
brush.arc(x, y, radius, start, end)
fill() is not applied.x (Number): The x-coordinate of the center.y (Number): The y-coordinate of the center.radius (Number): The radius of the arc.start (Number): Start angle interpreted using the current p5 angleMode().end (Number): End angle interpreted using the current p5 angleMode().brush.Plot — the rendered plot, or null when start equals end (zero sweep). Store it to redraw or apply effects at a different position.brush.arc(200, 200, 50, 0, Math.PI);
These three functions perform similarly to the p5.js beginShape(), vertex(), and endShape() functions, although their curvature calculation is very different. They allow you to draw custom shapes with fine control over brush pressure at different points of the perimeter.
brush.beginShape(curvature)
curvature (Number): Optional. A value from 0 to 1 that defines the curvature of the shape's edges.// Begin defining a custom shape with a specified curvature
brush.beginShape(0.3);
brush.vertex(x, y, pressure)
brush.beginShape() and brush.endShape() calls. An optional pressure parameter can be applied at each vertex.x (Number): The x-coordinate of the vertex.y (Number): The y-coordinate of the vertex.pressure (Number): Optional. The pressure at the vertex, affecting properties like width.// Add vertices to the custom shape
brush.vertex(50, 100);
brush.vertex(100, 150, 0.5);
brush.vertex(150, 100);
brush.endShape(a)
a (Boolean): Optional. Pass true to close the shape (connect the last vertex back to the first). Pass false or omit to leave the shape open.brush.Plot — the rendered plot for the completed shape. Store it to apply .hatch(), .mass(), .fill(), or .draw() later at a different position.// Finish the custom shape and close it
brush.endShape(true);
// Or leave it open
brush.endShape(false);
brush.polygon(pointsArray)
beginShape() and endShape() approach.pointsArray (Array): An array of points, where each point is an array of two numbers [x, y].brush.Polygon — the polygon instance. Store it to call .hatch(), .mass(), .fill(), or .draw() later, or pass it to brush.hatchArray() / brush.massArray().// Define a polygon using an array of points
let points = [[x1, y1], [x2, y2], ...];
brush.polygon(points);
brush.polygon() is ideal for drawing fixed shapes that remain unaffected by vector fields, providing precise control over their form and appearance.The drawing primitives return their geometry — a brush.Polygon from brush.polygon(), a brush.Plot from brush.arc() / brush.spline() / brush.endShape(), or [plot, x, y] from brush.circle(). Store the return value to transform, redraw, or apply effects to the same geometry later, independently of when it was drawn.
// Draw once; reuse the geometry for hatching and massing
const frame = brush.polygon([[50,50],[350,50],[350,350],[50,350]]);
// Hatch the stored polygon with the current hatch state
brush.hatch(8, Math.PI / 4);
frame.hatch();
// Mass the same geometry with the current mass state
brush.mass("pastel", "#4b6cb7", { strength: 0.8 });
frame.mass();
To apply effects to multiple polygons as a single gesture (with even-odd hole logic), pass an array to brush.hatchArray() or brush.massArray().
Exposed Classes provide foundational elements for creating and manipulating shapes and paths, as well as interacting with vector-fields in a more advanced manner.
brush.PolygonDescription: Represents a polygon defined by a set of vertices. The Polygon class is essential for creating and working with multi-sided shapes, offering various methods to manipulate and render these shapes.
Constructor:
brush.Polygon(pointsArray)
pointsArray (Array): An array of points, where each point is an array of two numbers [x, y].Methods:
.intersect(line)
line (Object): A line object with properties point1 and point2.Array of objects, each with x and y properties, representing the intersection points..draw(brush, color, weight)
.fill(color, opacity, bleed, texture, border, direction, angle)
.wash(color, opacity)
.hatch(distance, angle, options)
.mass()
Attributes:
.vertices: An array of the polygon's vertices, each vertex being an object with x and y properties..sides: An array representing the different segments that make up the polygon.brush.PlotDescription: The Plot class is crucial for the plot system, managing a collection of segments to create a variety of shapes and paths. It enables intricate designs, such as curves and custom strokes, by defining each segment with an angle, length, and pressure. Plot instances can be transformed through rotation, and their appearance controlled via pressure and angle calculations.
Constructor:
brush.Plot(_type)
_type (String): The type of plot, either "curve" or "segments".Methods:
.addSegment(_a, _length, _pres)
_a (Number): The angle of the segment._length (Number): The length of the segment._pres (Number): The pressure of the segment..endPlot(_a, _pres)
_a (Number): The final angle of the plot._pres (Number): The final pressure of the plot..rotate(_a)
_a (Number): The angle for rotation..genPol(_x, _y)
_x (Number): The x-coordinate for the starting point._y (Number): The y-coordinate for the starting point.Polygon - The generated polygon..draw(x, y)
x (Number): The x-coordinate to draw at.y (Number): The y-coordinate to draw at..fill(x, y)
x (Number): The x-coordinate to fill at.y (Number): The y-coordinate to fill at..wash(x, y)
x (Number): The x-coordinate to wash at.y (Number): The y-coordinate to wash at..hatch(x, y)
x (Number): The x-coordinate to hatch at.y (Number): The y-coordinate to hatch at..mass(x, y)
x (Number): The x-coordinate to mass at.y (Number): The y-coordinate to mass at.Attributes:
.segments: An array containing the lengths of all segments..angles: An array of angles at the different control points..press: An array with custom brush pressures at the various control points..type: The type of the plot, either "curve" or "segments"..pol: Stores the generated polygon object after executing the .genPol() method.brush.PositionDescription: The Position class represents a point within a two-dimensional space, capable of interacting with a vector field. It includes methods for updating the position based on the field's flow, allowing for movement through the vector field in various ways.
Constructor:
brush.Position(x, y)
x (Number): The initial x-coordinate.y (Number): The initial y-coordinate.Methods:
.moveTo(_dir, _length, _step_length)
_dir (Number): The direction of movement, with angles measured anticlockwise from the x-axis.
It is interpreted using the current p5 angleMode()._length (Number): The length to move along the field._step_length (Number): The length of each step..plotTo(_plot, _length, _step_length, _scale)
Plot object._plot (Position): The Plot path object._length (Number): The length to move towards the target position._step_length (Number): The length of each step._scale (Number): The scaling factor for the plotting path..angle()
.reset()
plotted property to 0. This property tracks the distance moved since the last reset or the creation of the position. Important for consecutive different Plot paths.Attributes:
.x: The current x-coordinate..y: The current y-coordinate..plotted: Stores the distance moved since the last reset or the creation of the position.Note for users upgrading from older versions:
brush.push(),brush.pop(),brush.translate(),brush.rotate(), andbrush.scale()are no longer needed. The library now automatically hooks into p5'spush()andpop(), so brush state (stroke, fill, hatch settings) is saved and restored alongside p5's own state. Likewise, p5'stranslate(),rotate(), andscale()are automatically inherited by all brush strokes and fills.
Public brush APIs that accept angles also inherit p5's current
angleMode(). With noangleMode()call, that means radians by default, matching p5 itself. The main exception isbrush.addField(name, fn, { angleMode }), which lets custom field generators declare how returned angles should be interpreted before they are stored internally in degrees.
We welcome contributions from the community. If you find a bug or have a feature request, please open an issue on Github.
p5.brush.js is released under the MIT License. See the LICENSE file for details.
1,314 followers · starred Nov 2023
12,492 followers · starred Aug 2026
166 followers · starred Aug 2026
273 followers · starred Sep 2026