Bézier curves for p5.js.

Draw curves with more than four control points.
A small library for p5.js, Canvas 2D, and WebGL.

Open playground
Bézier curves drawn with the library.
Created by Peiling Jiangv0.8.1 / MIT

Playground

Edit a curve. Copy the code.

API reference
Control points
A curve with six control points.
Drag a control point to edit.6 points · degree 5
Code p5.js
sketch.js
// Select an example to view its code.
// Complete examples are also available in the API reference below.

Setup

Load p5.js and p5.bezier, then pass your canvas to initBezier().

With a bundler:

import initBezier from 'p5bezier'
Open in p5.js editor
index.html
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>p5.bezier example</title>
<script src="https://cdn.jsdelivr.net/npm/p5@2.3.3/lib/p5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5bezier@0.8.1"></script>
<script>
function setup() {
  const curves = initBezier(createCanvas(600, 400));
  background('#ffffff');
  noFill();
  stroke('#171717');
  strokeWeight(2);
  curves.draw([
    [60, 320], [60, 40], [540, 40],
    [540, 360], [240, 360], [240, 160]
  ]);
}
</script>
</html>

API

Initialization, drawing, and curve methods.

For agents

Markdown reference llms.txt
initBezier(canvas)Initialize a drawing system

Pass a renderer from createCanvas() or a buffer from createGraphics(). Returns a drawing system with draw() and new(). For WebGL, supply [x, y, z] points. Raw WebGL contexts are unsupported.

const curves = initBezier(createCanvas(600, 400));
const buffer = createGraphics(300, 300, WEBGL);
const curves3D = initBezier(buffer);

For native Canvas 2D, use the adapter in Canvas without p5.js.

curves.draw(points, closeType?, smoothness?)Draw a curve

At least two [x, y] points, or [x, y, z] in WebGL. closeType is 'OPEN' (default) or 'CLOSE'. Smoothness is an integer from 1 to 5; default 3. Higher values sample more vertices. Uses the target's current stroke, fill, and transforms. Returns processed control points, not sampled vertices. Very large point lists are reduced internally.

curves.draw([[20, 160], [100, 20], [220, 180]], 'OPEN', 3);
curves.new(points, closeType?, smoothness?)Create a reusable curve

Creates a reusable curve object with precomputed vertices. Accepts the same arguments as curves.draw(), but does not draw until you call the object's draw() method.

const curve = curves.new([[20, 160], [100, 20], [220, 180]]);
curve.draw();
curve.draw(dash?)Draw a solid or dashed curve

Omit dash for a solid curve. Pass [solidLength, gapLength] for dashes. Use positive nonzero lengths. Dashed curves draw only the stroke and restore fill afterward. Create dashed objects at smoothness 3 or higher.

curve.draw([16, 8]);
curve.update(points)Update control points

Recomputes vertices without drawing. Pass fresh coordinate arrays with the same count as curve.controlPoints; mutating stored arrays in place can skip the update. Closed curves add internal closure points, so rebuild a closed object from your original points instead.

curve.update([[20, 100], [100, 60], [220, 140]]);
curve.draw();
curve.move(x, y, z?, toDraw?, dash?)Translate a curve

Returns a translated copy, preserving the original. Draws the copy by default. Pass false as the fourth argument to defer drawing. A 3D curve requires a Z offset.

const shifted = curve.move(20, 10, null, false);
shifted.draw();
curve.shortest(x, y, z?)Find the nearest sampled point

Returns coordinates of the nearest sampled vertex. Supply Z for a 3D curve. Precision depends on smoothness; this is a sampled lookup, not an exact analytic projection.

const nearest = curve.shortest(mouseX, mouseY);
line(mouseX, mouseY, nearest[0], nearest[1]);
Canvas without p5.jsUse a native Canvas 2D context

Native canvases need a renderer-shaped adapter with a 2D drawing context and explicit stroke/fill flags. The 2D experiments on this page use this approach.

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const curves = initBezier({
  drawingContext: ctx, _doStroke: true, _doFill: false
});
ctx.strokeStyle = '#171717';
ctx.lineWidth = 3;
curves.draw([[20, 160], [100, 20], [220, 180]]);
Complete examples 7 sketches

Control points

Change the control points and update the curve.

let curves, curve;
const points = [[90,310],[650,360],[650,40],[90,40],[90,360],[650,310]];

function setup() {
  curves = initBezier(createCanvas(720, 400));
  curve = curves.new(points, 'OPEN', 3);
  noLoop();
}

function draw() {
  background('#ffffff');
  noFill();
  stroke('#171717');
  strokeWeight(3);
  curve.draw();
  stroke('#d4d4d4');
  strokeWeight(1);
  for (let i = 1; i < points.length; i++) line(...points[i - 1], ...points[i]);
  fill('#ffffff');
  stroke('#171717');
  for (const [x, y] of points) circle(x, y, 10);
}

// Drag a control point. Rebuild closed curves to regenerate closure points.
let selected = -1;
function mousePressed() {
  selected = points.findIndex(([x, y]) => dist(x, y, mouseX, mouseY) < 20);
}
function mouseDragged() {
  if (selected < 0) return;
  points[selected] = [constrain(mouseX, 0, width), constrain(mouseY, 0, height)];
  curve.update(points.map(point => [...point]));
  redraw();
}
function mouseReleased() { selected = -1; }

Freehand

Use a stroke as control points. Each stroke replaces the previous curve.

let curves;
let points = [[90,310],[650,360],[650,40],[90,40],[90,360],[650,310]];

function setup() {
  curves = initBezier(createCanvas(720, 400));
  noLoop();
}

function draw() {
  background('#ffffff');
  noFill();
  stroke('#171717');
  strokeWeight(3);
  if (points.length > 1) curves.draw(points, 'OPEN', 3);
}

function mousePressed() {
  if (mouseX < 0 || mouseX > width || mouseY < 0 || mouseY > height) return;
  points = [[mouseX, mouseY]];
  redraw();
}
function mouseDragged() {
  const next = [constrain(mouseX, 0, width), constrain(mouseY, 0, height)];
  if (points.length === 0) return;
  const last = points[points.length - 1];
  if (dist(...last, ...next) < 8) return;
  if (points.length >= 80) points = points.filter((_, i) => i % 2 === 0);
  points.push(next);
  redraw();
}

Dashed curves

Draw a cached curve with a dash and gap pattern.

let curves, curve;
const points = [[90,310],[650,360],[650,40],[90,40],[90,360],[650,310]];

function setup() {
  curves = initBezier(createCanvas(720, 400));
  curve = curves.new(points, 'OPEN', 3);
  noLoop();
}

function draw() {
  background('#ffffff');
  noFill();
  stroke('#171717');
  strokeWeight(3);
  curve.draw([16, 8]);
  stroke('#d4d4d4');
  strokeWeight(1);
  for (let i = 1; i < points.length; i++) line(...points[i - 1], ...points[i]);
  fill('#ffffff');
  stroke('#171717');
  for (const [x, y] of points) circle(x, y, 10);
}

// Drag a control point. Rebuild closed curves to regenerate closure points.
let selected = -1;
function mousePressed() {
  selected = points.findIndex(([x, y]) => dist(x, y, mouseX, mouseY) < 20);
}
function mouseDragged() {
  if (selected < 0) return;
  points[selected] = [constrain(mouseX, 0, width), constrain(mouseY, 0, height)];
  curve.update(points.map(point => [...point]));
  redraw();
}
function mouseReleased() { selected = -1; }

Closest point

Find the nearest sampled vertex to the pointer.

let curves, curve;
const points = [[90,310],[650,360],[650,40],[90,40],[90,360],[650,310]];
let probe = [440,130];

function setup() {
  curves = initBezier(createCanvas(720, 400));
  curve = curves.new(points, 'OPEN', 3);
  noLoop();
}

function draw() {
  background('#ffffff');
  noFill();
  stroke('#171717');
  strokeWeight(3);
  curve.draw();
  const nearest = curve.shortest(probe[0], probe[1]);
  stroke('#a3a3a3');
  strokeWeight(1);
  line(probe[0], probe[1], nearest[0], nearest[1]);
  fill('#171717');
  noStroke();
  circle(nearest[0], nearest[1], 9);
  noFill();
  stroke('#171717');
  circle(probe[0], probe[1], 12);
}

function mouseMoved() {
  probe = [constrain(mouseX, 0, width), constrain(mouseY, 0, height)];
  redraw();
}

Smoothness

Compare five sampling levels. The selected level is drawn in black.

let curves;
const points = [[90,310],[650,360],[650,40],[90,40],[90,360],[650,310]];

function setup() {
  curves = initBezier(createCanvas(720, 400));
  noLoop();
}

function draw() {
  background('#ffffff');
  noFill();
  stroke('#171717');
  strokeWeight(3);
  for (let level = 1; level <= 5; level++) {
    stroke(level === 3 ? '#171717' : '#a3a3a3');
    strokeWeight(level === 3 ? 3 : 1);
    const shifted = points.map(([x, y]) => [x, y * 0.6 + level * 25]);
    curves.draw(shifted, 'OPEN', level);
  }
}

Translation

Translate cached vertices to draw twelve copies of a curve.

let curves, curve;
const points = [[90,310],[650,360],[650,40],[90,40],[90,360],[650,310]];

function setup() {
  curves = initBezier(createCanvas(720, 400));
  curve = curves.new(points, 'OPEN', 3);
  noLoop();
}

function draw() {
  background('#ffffff');
  noFill();
  stroke('#171717');
  strokeWeight(3);
  for (let i = 11; i >= 0; i--) {
    stroke(i === 0 ? '#171717' : '#a3a3a3');
    strokeWeight(i === 0 ? 3 : 1);
    curve.move(i * -6, i * 6);
  }
  stroke('#d4d4d4');
  strokeWeight(1);
  for (let i = 1; i < points.length; i++) line(...points[i - 1], ...points[i]);
  fill('#ffffff');
  stroke('#171717');
  for (const [x, y] of points) circle(x, y, 10);
}

// Drag a control point. Rebuild closed curves to regenerate closure points.
let selected = -1;
function mousePressed() {
  selected = points.findIndex(([x, y]) => dist(x, y, mouseX, mouseY) < 20);
}
function mouseDragged() {
  if (selected < 0) return;
  points[selected] = [constrain(mouseX, 0, width), constrain(mouseY, 0, height)];
  curve.update(points.map(point => [...point]));
  redraw();
}
function mouseReleased() { selected = -1; }

WebGL

Draw 3D curves with a p5.js WebGL graphics buffer.

let buffer, curve;
const points = [[90,310],[650,360],[650,40],[90,40],[90,360],[650,310]].map(([x, y], i) => [x - 360, y - 200, Math.sin(i * 1.4) * 140]);

function setup() {
  createCanvas(720, 400);
  buffer = createGraphics(720, 400, WEBGL);
  const curves = initBezier(buffer);
  curve = curves.new(points, 'OPEN', 3);
  noLoop();
}

function draw() {
  buffer.background('#0a0a0a');
  buffer.push();
  buffer.scale(0.48);
  buffer.rotateY(0.45);
  buffer.rotateX(-0.25);
  buffer.noFill();
  buffer.strokeWeight(2);
  for (let i = 0; i < 14; i++) {
    buffer.stroke(i % 3 === 0 ? '#ffffff' : '#a3a3a3');
    buffer.push();
    buffer.translate(0, (i - 7) * 9, (i - 7) * 8);
    curve.draw();
    buffer.pop();
  }
  buffer.pop();
  image(buffer, 0, 0);
}