What p5.js is

The vsketch note made plotter drawings with Python. p5.js does the same with JavaScript, in the browser. Lauren Lee McCarthy created it in 2013 as a new interpretation of Processing for the web. Processing itself was started in 2001 by Casey Reas and Ben Fry at the MIT Media Lab, to teach programming through pictures. Both are free and open source and supported by the Processing Foundation; p5.js has a large community, with a rotating leadership since 2020.

  • A sketch has a setup() function that runs once and a draw() function that runs again for every frame (60 per second by default). noLoop() stops that after the first frame, which is what a drawing for a plotter wants.
  • Drawing commands such as line(), ellipse() and beginShape()/vertex()/endShape(), and helpers for generative work: random(), noise(), map().
  • The reference has an example for every function, and the web editor runs sketches without installing anything.

Pixels or lines

p5.js draws on a <canvas>: pixels, like a photo. A plotter cannot do anything with a PNG; it needs the lines themselves. The add-on p5.js-svg, by Zeno Zeng, replaces the canvas with an <svg> element. With createCanvas(w, h, SVG) every line becomes a <path>, which is exactly what the LY Drawbot's Inkscape extension reads, and save("name.svg") saves it.

What p5.js does not have, unlike vsketch, is anything made for plotters: no millimeters and no optimizing of the lines. The example below does both itself, in a few lines.

p5.js 1 and p5.js 2

p5.js 2.0 came out in April 2025; the newest version is 2.3.4 (25 September 2026). Version 1 is still maintained: 1.11.13 is from 8 April 2026. The README of p5.js-svg 1.6.0 says it is compatible with p5.js 1.11.x, so for plotter work you use p5.js 1.11.13. The script tags in this note load both with their exact version:

<script src="https://cdn.jsdelivr.net/npm/p5@1.11.13/lib/p5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/p5.js-svg@1.6.0/dist/p5.svg.js"></script>

Getting started

There are two ways, and neither installs anything.

In the web editor

Open editor.p5js.org. An account is only needed to save sketches online. Open index.html in the file list (the arrow at the top left), check that the p5.js line loads a 1.11 version or replace it with the one above, and add the p5.js-svg line under it. Paste the sketch into sketch.js and press Play.

The p5.js web editor with the flow field sketch: the code of sketch.js on the left, with SEED set to 7, and on the right the preview with the text Press S to save the drawing as flow-field.svg above the drawing of curved lines. At the top right the editor shows the version p5.js 1.11.13.
The flow field (seed 7) in the p5.js web editor, with the p5.js version at the top right. Select the image to enlarge it.

On your own computer

A p5.js sketch is an ordinary web page: an index.html that loads p5.js, p5.js-svg and your sketch.js. Put the two files in one folder and open index.html in Chrome, Edge or Firefox with a double click. No web server is needed. The two libraries come from the internet; to work offline, download the two files from the addresses above, put them in the folder, and change the src to their file names.

index.html · sketch.js

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Flow field</title>
    <script src="https://cdn.jsdelivr.net/npm/p5@1.11.13/lib/p5.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/p5.js-svg@1.6.0/dist/p5.svg.js"></script>
    <style>
      body {
        margin: 24px;
        font-family: sans-serif;
      }
      svg {
        outline: 1px solid #ccc; /* the edge of the A4 page */
      }
    </style>
  </head>
  <body>
    <p>Press S to save the drawing as flow-field.svg.</p>
    <script src="sketch.js"></script>
  </body>
</html>

The example: a flow field

A flow field fills the page with invisible arrows, like wind or a current. Each line starts at a random point and follows the arrows, one millimeter at a time. The arrows come from noise(), p5.js's Perlin noise: random numbers that change smoothly from one place to the next, so neighboring arrows point almost the same way and the lines bend together, like combed hair.

// Flow field: lines that follow Perlin noise, in millimeters on A4 landscape.
// Press S to save the drawing as flow-field.svg.

const W = 297; // page width, mm
const H = 210; // page height, mm
const MARGIN = 20; // empty border, mm
const SEED = 7; // same seed, same drawing
const COUNT = 400; // number of lines
const STEPS = 60; // points per line at most
const STEP = 1; // distance between points, mm
const ZOOM = 0.01; // smaller: wider, calmer curves

function setup() {
  createCanvas(W, H, SVG);
  const svg = document.querySelector("svg");
  svg.setAttribute("width", W + "mm");
  svg.setAttribute("height", H + "mm");
  svg.removeAttribute("xmlns"); // saving adds it again; this way never twice

  randomSeed(SEED);
  noiseSeed(SEED);
  noFill();
  stroke(0);
  strokeWeight(0.3);

  const lines = [];
  for (let i = 0; i < COUNT; i++) {
    lines.push(
      flowLine(random(MARGIN, W - MARGIN), random(MARGIN, H - MARGIN)),
    );
  }
  for (const points of sortLines(lines.filter((points) => points.length > 1))) {
    beginShape();
    for (const p of points) vertex(round(p.x, 2), round(p.y, 2)); // 0.01 mm is plenty
    endShape();
  }
  noLoop();
}

// Follow the field from (x, y) until the line leaves the margin.
function flowLine(x, y) {
  const points = [];
  for (let s = 0; s < STEPS; s++) {
    if (x < MARGIN || x > W - MARGIN || y < MARGIN || y > H - MARGIN) break;
    points.push({ x, y });
    const angle = noise(x * ZOOM, y * ZOOM) * TWO_PI * 2;
    x += cos(angle) * STEP;
    y += sin(angle) * STEP;
  }
  return points;
}

// Draw the nearest line next, from whichever end is closer,
// so the pen travels less with the pen up.
function sortLines(lines) {
  const sorted = [];
  let pen = { x: 0, y: H }; // the plotter's zero point: bottom left
  while (lines.length > 0) {
    let best = 0;
    let bestDist = Infinity;
    let flip = false;
    for (let i = 0; i < lines.length; i++) {
      const first = lines[i][0];
      const last = lines[i][lines[i].length - 1];
      const toFirst = dist(pen.x, pen.y, first.x, first.y);
      const toLast = dist(pen.x, pen.y, last.x, last.y);
      if (toFirst < bestDist) [best, bestDist, flip] = [i, toFirst, false];
      if (toLast < bestDist) [best, bestDist, flip] = [i, toLast, true];
    }
    const next = lines.splice(best, 1)[0];
    if (flip) next.reverse();
    sorted.push(next);
    pen = next[next.length - 1];
  }
  return sorted;
}

function keyPressed() {
  if (key === "s" || key === "S") save("flow-field.svg");
}

What it does:

  • createCanvas(W, H, SVG) makes a 297 by 210 canvas, so every number in the sketch is a millimeter. The two setAttribute lines put mm after the page size; without them the file says 297 by 210 pixels, and Inkscape makes it 78 by 56 millimeters. The removeAttribute line prevents a small fault: p5.js-svg also writes the xmlns attribute itself, and saving can then put it in the file twice, which makes the file invalid.
  • randomSeed() and noiseSeed() fix the randomness: the same SEED gives the same drawing, another seed a new one.
  • In flowLine(), noise(x * ZOOM, y * ZOOM) gives a number between 0 and 1 for the current point, which becomes an angle of up to two full turns. cos() and sin() turn the angle into a step of one millimeter (see sine and cosine). ZOOM decides how fast the field changes: smaller gives wider, calmer curves.
  • vertex(round(p.x, 2), round(p.y, 2)) rounds to a hundredth of a millimeter, far finer than a pen. Without rounding every coordinate has 15 decimals and the file is more than twice as large.
  • sortLines() decides the order and the direction of the lines; see below.
  • keyPressed() saves the drawing when you press S.
The flow field with seed 7: about 400 thin curved lines on an A4 landscape page, flowing in streams that bend around each other, with empty areas between the streams.
The flow field with seed 7: 397 lines, 20.3 meters of line.

Order and direction

A plotter draws the lines in the order of the file. Between two lines it lifts the pen and travels to the start of the next one. In the order the sketch makes them, the 397 lines lie all over the page, and the pen would travel 46.6 meters lifted for 20.3 meters of drawing.

The LY Drawbot's Inkscape extension already sorts a little: each time it picks the line whose start is nearest to where the pen is. That brings the 46.6 meters down to 19.4. But it never turns a line around, and in a flow field the nearest point is often the end of a line. sortLines() looks at both ends and turns a line around (reverse()) when its end is closer. With the lines pointing the right way the extension gets to 3.1 meters; that is about 16 meters, and minutes of plotting, less.

Try it yourself, step by step

  1. Get the files. Make a folder flow-field and save index.html and sketch.js in it.
  2. Open it. Double-click index.html. The drawing appears at its real size, with a thin gray edge for the page. Nothing there? Open the browser console (F12) and read the first error.
  3. Play. Change SEED, ZOOM, COUNT or STEPS in sketch.js, save, and reload the page (F5). Write down the seed of a drawing you like.
  4. Save. Press S. The file flow-field.svg lands in your downloads. Safari first shows a message from p5.js with instructions for saving; Chrome, Edge and Firefox simply download the file.
  5. Check in Inkscape. Open the SVG. Under File › Document Properties the page must be 297 by 210 mm. The lines are already <path> elements, so Object to Path is not needed (doing it anyway does no harm).
  6. Make the G-code and plot. Run the LY Drawbot extension, check in UGS that the drawing stays within 310 by 210 mm, set the zero point and start, as in the LY Drawbot note.

Easy to forget

  • No background(). With the SVG renderer it adds a white rectangle the size of the page. The extension skips rectangles, but after Object to Path it is a path, and the extension plots every path, also one without a line: a frame around the page that you did not draw.
  • noFill(). A plotter draws lines only. A filled shape comes out as its outline at best; filling an area means drawing lines inside it, as in the Paper.js note.
  • noLoop(). Without it, draw() runs 60 times a second; with random numbers in it you get a new drawing every frame, and a save() in draw() saves every frame.

The plot

The plotted flow field: hundreds of thin blue curved lines on a white A4 sheet taped to the table, flowing in streams across the page, with the pen carriage of the plotter still standing at the right edge.
The flow field with seed 7 on paper, still in the plotter. Pen: Sakura Gelly Roll Medium (0.4 mm), color 36. Paper: Fastprint Gold, 120 g/m², A4. Select the image to enlarge it.

Sources