Skip to content
 
 

Repository files navigation

Teaser

p5.brush

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.

▶️ Check the teaser live →


Two builds

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.


Table of Contents


Installation

Script tags

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 + bundler (Vite, Webpack, esbuild)

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 namespace

If you are using p5 as an ES module (instance mode), import both explicitly:

import p5 from 'p5';
import * as brush from 'p5.brush';

Vite example

// 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);
  };
});

Standalone build (no p5 required)

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 canvas

See docs/standalone.md for the full standalone guide.


p5 instance mode

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.


TypeScript

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.


Quick Start

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:

  1. brush.scaleBrushes(n) — scale the built-in brushes to match your canvas size
  2. brush.set(name, color, weight) — pick a brush
  3. brush.fill(color, opacity) or brush.hatch(dist, angle) — add interior texture
  4. brush.line(), brush.rect(), brush.circle(), etc. — draw geometry

Core Concepts

p5.brush follows p5's drawing model: first configure state, then draw shapes.

  • Stylebrush.set(), brush.fill(), brush.hatch() and related functions configure how upcoming shapes will look.
  • Geometrybrush.line(), brush.rect(), brush.circle(), brush.arc(), brush.beginShape(), brush.polygon() actually draw.
  • State resetbrush.noStroke(), brush.noFill(), brush.noHatch() turn parts of that state off.
  • Save / restorepush() and pop() 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.

Reference

Configuration


brush.load(buffer?)

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 from p5.Graphics) is not supported.


brush.scaleBrushes(scale)

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
}

brush.instance(p)

Required in p5 instance mode. Tells p5.brush which p5 instance to render into. See p5 instance mode above.


Vector Fields

Vector fields bend strokes across the canvas by storing a grid of directional angles. If you never activate one, p5.brush draws normally.


brush.field(name)

Activates a named vector field. Built-in fields: hand, curved, zigzag, waves, seabed, spiral, columns.

brush.field('waves');

brush.noField()

Deactivates the current vector field.


brush.wiggle(intensity)

Shorthand for activating the hand field. Adds a subtle hand-drawn wobble to strokes.

brush.wiggle(3);   // 1–10 range

brush.refreshField(t)

Updates the active field with a new time value — use in draw() for animated fields.

function draw() {
  brush.refreshField(frameCount / 10);
}

brush.listFields()

Returns an array of all registered field names (built-ins + custom).


brush.addField(name, generatorFn, options?)

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.

Design fields visually →


Brush Management


brush.box()

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.


brush.add(name, params)

Creates a custom brush. Once added, use it with brush.set() just like a built-in.

Design brushes visually →

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 function

Custom 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);
}

brush.clip(region)

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();

brush.noClip()

Removes the active clipping region.


Stroke Operations


brush.set(brushName, color, weight)

Sets the active brush, color, and weight. Activates stroke mode automatically.

brush.set('HB', '#002185', 1);
brush.set('charcoal', color(40, 30, 20), 2);

brush.pick(brushName)

Switches the brush type without changing color or weight.


brush.stroke(color)

Sets the stroke color without changing the brush type or weight.


brush.strokeWeight(weight)

Sets the stroke weight multiplier.


brush.noStroke()

Disables stroke for subsequent shapes.


Fill Operations

Fill gives shapes a watercolor or solid interior texture.


brush.fill(color, opacity?)

Enables watercolor fill.

brush.fill('#a0c4a0', 120);
brush.fill(color(160, 196, 160), 120);

brush.noFill()

Disables fill.


brush.fillBleed(intensity, direction?)

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 bleed

brush.fillTexture(texture?, border?, scatter?)

Controls the texture and border intensity of the watercolor fill.

brush.fillTexture(0.4, 0.4);
brush.fillTexture(0.8, 0.6, false);  // disable scatter noise

brush.wash(color, opacity?)

Enables a simpler solid wash fill (no bleed simulation). Faster than watercolor fill.

brush.wash('#d4c4a0', 180);

brush.noWash()

Disables wash fill.


Hatch Operations

Hatching fills shapes with scanline strokes. Stacks with fill.


brush.hatch(dist, angle, options?)

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 shape

Options:

Property Description
rand Endpoint jitter amount (0–1)
continuous Connect scanlines into a serpentine path
gradient Multiplicative spacing growth per scanline

brush.hatchStyle(brush, color?, weight?)

Sets the brush, color, and weight used exclusively for hatch strokes.

brush.hatchStyle('pen', '#1a1a1a', 0.7);

brush.noHatch()

Disables hatching.


brush.mass(brush, color, options?)

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

brush.noMass()

Disables massing.


Primitives

All primitives respect the currently active stroke, fill, and hatch state.


brush.line(x1, y1, x2, y2)

Draws a straight brush stroke from point 1 to point 2.


brush.flowLine(x, y, length, angle)

Draws a stroke that flows along the active vector field starting from (x, y).

brush.field('waves');
brush.flowLine(0, 0, 200, 45);

brush.rect(x, y, w, h, mode?)

Draws a rectangle. mode is 'center' or 'corner' (default 'corner').


brush.circle(x, y, diameter)

Draws a circle.


brush.arc(x, y, radius, startAngle, endAngle)

Draws an arc. Angles follow the current p5 angleMode().


brush.spline(points, tightness?)

Draws a smooth curve through an array of [x, y] pairs.

brush.spline([[-100, 0], [0, -80], [100, 0], [0, 80]], 0.5);

brush.polygon(x, y, radius, sides)

Draws a regular polygon.

brush.polygon(0, 0, 80, 6);   // hexagon

brush.beginShape() / brush.vertex(x, y) / brush.endShape()

Draws 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();

Exposed Classes


brush.Polygon(vertices)

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();

brush.Plot(type, ...args) and brush.Position(x, y)

Advanced classes for building programmatic paths and flow-field-aware positions. Typically used together with brush.beginStroke() / brush.endStroke().


Contributing

Contributions, bug reports, and feature requests are welcome via GitHub Issues.


License

p5.brush is licensed under the MIT License. See LICENSE.md for details.


Acknowledgements

The watercolor fill system is inspired by Tyler Hobbs' essay on simulating watercolor. The library was originally created by acamposuribe.

About

Unlock custom brushes, natural fill effects and intuitive hatching in p5.js

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages