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/[email protected]/lib/p5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></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. Coordinates are copied, so the same input array can be edited and passed again. Unchanged coordinates skip resampling. Keep the same count as curve.controlPoints. 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);
}