For ninety-five episodes we have been trapped behind glass. Every single thing we made lived inside a screen - pixels, shaders, canvases, webcams feeding back into more pixels. I said at the end of the machine-learning arc that it was high time we let the work out into the physical world, and I meant it. So today we put down the screen and pick up a pen. Real ink. Real paper. Code that moves a machine.
This is the start of a whole new chapter for us, and honestly it's the part of creative coding I love most. There is something that happens the first time you watch a robot draw your code onto a sheet of paper that no amount of retina display can give you. The line wobbles a tiny bit. The ink pools where the pen changes direction. The paper texture shows through. Your perfectly clean digital math suddenly has a body, and it's a little bit alive. Allez, let me show you how this works :-).
Strip away the romance and a pen plotter is a very simple machine. It holds an ordinary pen. It moves that pen around a flat sheet of paper in two axes (left-right and up-down), and it can lift the pen off the paper or press it down. That's it. Three motors and a pen holder. No ink cartridges, no print heads, no nozzles.
What makes it special is that it draws the same way you would. A regular printer lays down a grid of tiny dots - it thinks in pixels, like a screen. A plotter thinks in strokes. It puts the pen down somewhere, drags it along a path, and lifts it up again. That is exactly how a human draws, and it's exactly how our code has been describing shapes this whole series.
// a printer thinks in dots (pixels) - it fills a grid
// a plotter thinks in strokes - it follows a path
//
// everything we have ever drawn in p5 is already a set of strokes:
line(10, 10, 100, 100); // pen down at (10,10), drag to (100,100), pen up
//
// that's a plotter instruction. we've been writing them since episode 1.
The most popular plotter in the creative coding world is the AxiDraw, made by Evil Mad Scientist. It plugs into your computer over USB, it draws anything you can describe as lines, and the basic model runs around 475 dollars. You don't need one to follow along today - I'll show you how to design and preview everything in code, and at the end I'll give you a way to get the physical experience even with no machine at all. But once you see your first plot come off the bed, fair warning, you'll want one.
Here's the thing nobody tells you up front. A plotter doesn't understand p5. It doesn't understand JavaScript. It speaks a much older, much simpler language: SVG. Scalable Vector Graphics. And SVG is not some scary binary format - it's just text. XML text, the same family as HTML.
That's the whole trick of this episode. If you can turn your creative coding output into SVG text, you can plot it. So let's actually look at what SVG is, because it's almost embarrassingly simple.
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<path d="M 10 10 L 190 190" stroke="black" fill="none"/>
</svg>
That's a complete, valid SVG file that draws a single diagonal line. Save that as drawing.svg, open it in any browser, and there's your line. The interesting part is the d attribute on that <path>. It's a tiny little language of its own, and it maps perfectly onto how a plotter moves.
// the SVG path "d" mini-language - this is the WHOLE thing:
//
// M x y -> moveTo: lift pen, jump to (x,y) (pen UP)
// L x y -> lineTo: drag pen in a straight line (pen DOWN)
// C ... -> cubic bezier curve (remember episode 14!) (pen DOWN)
// Z -> close the path back to where it started
//
// "M 10 10 L 100 100 L 100 50" = jump to (10,10),
// draw to (100,100), draw to (100,50). one continuous stroke.
See that C command? That is a cubic Bezier curve, and we spent a whole episode building those by hand back in episode 14. Everything we learned about control points and organic shapes there comes straight back here, because the plotter draws Beziers natively. Your digital curves become physical ink lines with no loss in between. That continuity is not an accident - it's why I had us learn Beziers properly in the first place.
p5.js does not export SVG out of the box, which trips a lot of people up. There are three honest ways around it, and I'll be straight with you about which one I actually use.
Option one is the p5.js-svg library, which swaps the canvas renderer for an SVG one. It works, but it can be finicky with some functions. Option two is to record every drawing call and replay it as SVG. Option three - the one I reach for - is to just build the SVG string myself. It sounds like more work. It's actually less, and you understand every byte of what comes out. Let me show you.
// a tiny SVG builder. you feed it polylines, it gives you a file.
class SvgPlot {
constructor(w, h) {
this.w = w;
this.h = h;
this.paths = []; // each path is an array of [x, y] points
}
// add one continuous stroke (a list of points the pen connects)
addPolyline(points) {
this.paths.push(points);
}
toString() {
let out = `<svg xmlns="http://www.w3.org/2000/svg" `;
out += `width="${this.w}mm" height="${this.h}mm" `;
out += `viewBox="0 0 ${this.w} ${this.h}">\n`;
for (let pts of this.paths) {
out += ' ' + this.pathData(pts) + '\n';
}
out += '';
return out;
}
}
Notice I'm using mm for the size. That matters a lot for plotting - the machine draws in real millimetres on real paper, so I think in physical units from the very start, not pixels. A path on an A4 sheet that's 210mm wide is a different beast from one that's 210 pixels wide.
Now the heart of it, turning a list of points into that d string:
// turn [[x,y],[x,y],...] into "M x y L x y L x y ..."
SvgPlot.prototype.pathData = function (pts) {
if (pts.length === 0) return '';
// first point is a moveTo (pen up), the rest are lineTo (pen down)
let d = `M ${pts[0][0].toFixed(2)} ${pts[0][1].toFixed(2)}`;
for (let i = 1; i < pts.length; i++) {
d += ` L ${pts[i][0].toFixed(2)} ${pts[i][1].toFixed(2)}`;
}
return ``;
};
And that's genuinly the entire pipeline from "list of points" to "plottable file". You give it the points your generative code already produces, and out comes SVG. Let me wire it to something we already know how to make.
Back in episodes 12 and 14 we played with noise and curves. A flow field is the perfect first plot, because it's nothing but a big pile of flowing lines - exactly what a plotter wants to eat. The idea: drop a bunch of particles on the page, and let each one wander, steered by Perlin noise, leaving a trail behind it.
// generate a flow-field plot: many curves following a noise field
function generateFlowField(plot, count, steps) {
for (let i = 0; i < count; i++) {
// each curve starts at a random spot on the page (in mm)
let x = random(plot.w);
let y = random(plot.h);
let points = [[x, y]];
for (let s = 0; s < steps; s++) {
// sample the noise field to get an angle at this position
let angle = noise(x * 0.01, y * 0.01) * TWO_PI * 2;
x += cos(angle) * 2; // step 2mm in that direction
y += sin(angle) * 2;
// stop if we wander off the page
if (x < 0 || x > plot.w || y < 0 || y > plot.h) break;
points.push([x, y]);
}
plot.addPolyline(points); // one curve = one continuous stroke
}
}
Each particle's whole journey becomes one polyline, which becomes one continuous pen stroke. Five hundred of these and you get that beautiful combed, flowing texture that looks hand-drawn but came out of a noise function. The same noise() we met in episode 12, now wandering across real paper.
// put it together and hand the result to the browser as a download
function exportPlot() {
let plot = new SvgPlot(210, 297); // A4 paper, in mm
generateFlowField(plot, 500, 80); // 500 curves, up to 80 steps each
saveStrings([plot.toString()], 'flowfield', 'svg'); // p5 download helper
}
Run that and your browser hands you a flowfield.svg. Open it - there's your art, ready to plot. We just turned a noise field into a physical-ready drawing, and we never left concepts we already knew. That's the whole magic of this arc: the generative thinking is the same, only the output device changed.
Here's where plotting starts to rewire your brain a little, and I think it's the most valuable lesson in the whole episode. On screen, you fill shapes. You call fill() and a region goes solid - the GPU paints a million pixels for free. A plotter cannot do that. It has one pen. It can only draw lines.
So how do you make something look dark, or solid, or shaded? You don't fill - you hatch. You draw lots of closely-spaced lines, and your eye blends them into a tone. Want it darker? Pack the lines tighter. This is exactly how old engravings and pen-and-ink illustrations work, and now you get to do it in code.
// you can't fill - so fake a grey tone with parallel lines (hatching)
// darkness is controlled by the GAP between lines: smaller gap = darker
function hatchRect(plot, x, y, w, h, gap) {
let points;
let lineY = y;
while (lineY < y + h) {
// each scan-line is its own little stroke
points = [[x, lineY], [x + w, lineY]];
plot.addPolyline(points);
lineY += gap; // smaller gap -> more lines -> darker patch
}
}
Once you're thinking this way, a whole world opens up. Cross-hatching (lines in two directions) makes deeper shadows. Stippling (lots of tiny dots) gives you soft gradients. Varying the gap based on an image's brightness turns a photo into a plotted engraving. The constraint of "lines only" isn't a limitation - it's a style, and a gorgeous one.
// map an image's brightness to hatching density:
// dark pixels -> tight lines, bright pixels -> sparse lines
function brightnessToGap(brightnessValue) {
// brightness 0 (black) -> gap 0.4mm (very dark hatching)
// brightness 255 (white) -> gap 4mm (almost empty)
return map(brightnessValue, 0, 255, 0.4, 4);
}
When I first started plotting I kept trying to make it behave like a screen, fighting the no-fill thing constantly. The moment I let go and started designing for lines instead of against them, my work got way better. See where this is going? The medium has opinions. Listen to them.
Now a practical headache that's secretly a beautiful little computer-science problem. Your flow field has 500 separate curves. Between each one, the pen has to lift up, fly across the page to the start of the next curve, and drop down again. That flying-around is called pen-up travel, and it is dead time - no ink, just the machine moving.
If your code spits out the curves in a random order, the pen zigzags all over the sheet like a confused bee, and a plot that should take ten minutes takes forty. The drawing is identical - but you wasted half an hour. So we sort the strokes to keep the pen close to home.
// measure total pen-up travel for a given ordering of paths
function totalTravel(paths) {
let travel = 0;
for (let i = 1; i < paths.length; i++) {
let prevEnd = paths[i - 1][paths[i - 1].length - 1]; // last point of prev
let nextStart = paths[i][0]; // first point of next
travel += dist(prevEnd[0], prevEnd[1], nextStart[0], nextStart[1]);
}
return travel; // smaller is faster and quieter
}
Finding the perfect order is the famous Travelling Salesman Problem, and solving it exactly for 500 points would take longer than the plot itself. But we don't need perfect - we need good enough, fast. A greedy nearest-neighbour pass gets us most of the way: always draw whichever remaining stroke starts closest to where the pen currently is.
// greedy nearest-neighbour reorder: cheap, fast, hugely effective
function optimizeOrder(paths) {
let remaining = paths.slice();
let ordered = [remaining.shift()]; // start with the first path
while (remaining.length > 0) {
let last = ordered[ordered.length - 1];
let penPos = last[last.length - 1]; // where the pen is now
// find the remaining stroke whose start is nearest the pen
let bestI = 0;
let bestD = Infinity;
for (let i = 0; i < remaining.length; i++) {
let d = dist(penPos[0], penPos[1], remaining[i][0][0], remaining[i][0][1]);
if (d < bestD) { bestD = d; bestI = i; }
}
ordered.push(remaining.splice(bestI, 1)[0]); // take it, repeat
}
return ordered;
}
This one function routinely cuts my plot times in half or better, and it's maybe fifteen lines. That's the kind of small, sharp algorithm that pays for itself the very first time you use it. Makes sense, right? The output looks identical; the machine just stops wasting its breath.
I'd be doing you a disservice if I made this sound purely digital, because half the art of plotting lives in the physical choices. The pen is part of the artwork. A fine-tip technical pen (a Sakura Micron 005, say) gives you crisp, precise, almost architectural lines. A brush pen gives you thick-and-thin variation that makes the same SVG look completely different. Fountain pens flow and pool. Metallic gel pens on black paper feel like magic. Same code, totally different soul - just by swapping what's in the holder.
Paper matters just as much. Smooth Bristol board gives you clean, sharp lines with no bleed. Textured watercolour paper roughens everything up and adds an organic, handmade feel. Heavier paper handles ink saturation without buckling. And dark paper with a white or metallic pen flips the whole aesthetic inside out. None of this is in your code - all of it is in the result.
And if you ever want multiple colours, you hit the registration problem: you plot one colour, swap the pen, and plot the next - but the paper must not move even a hair between passes, or the layers won't line up. Even half a millimetre of drift is visible. So we plan colours as separate files from the start.
// plan a multi-colour plot as SEPARATE files, one per pen
// (you plot file 1, swap the pen by hand, plot file 2 on the same sheet)
function exportLayers(layers) {
// layers = { red: [...paths], blue: [...paths] }
for (let color in layers) {
let plot = new SvgPlot(210, 297);
for (let path of layers[color]) plot.addPolyline(path);
// each colour is its own download; same coordinate space = they align
saveStrings([plot.toString()], 'plot-' + color, 'svg');
}
}
Keeping every layer in the exact same coordinate space is what makes them line up on the page. The machine handles the precision; you just have to not bump the paper. (Ask me how I learned that one. Oops.)
Let me zoom out and show you the whole journey, because seeing it as one flow makes it click. Every plot I make walks through these same stages.
// the creative-coding-to-paper pipeline, as one mental checklist:
const plotPipeline = [
"1. design - generate your art in code (noise, curves, whatever)",
"2. preview - render it on screen FIRST, in mm, exactly as it'll plot",
"3. export - write it out as an SVG file (M/L/C paths)",
"4. optimize - reorder strokes to cut pen-up travel time",
"5. plot - load the SVG in the plotter software and draw it",
"6. finish - swap pens for layers, sign it, let the ink dry"
];
// each stage can quietly change your drawing - so preview before you plot
That preview step in the middle is the one beginners skip and regret. Always render the SVG exactly as it will plot - same millimetre dimensions, lines only, no fills - before you send it to the machine. Paper and ink are not undo-able. A thirty-second preview saves a wasted sheet and twenty minutes of plotting every single time.
Here's your assignment, and I've built it so everyone can do it, machine or no machine. Take the flow-field generator from earlier (or honestly any generative sketch you've made in this series) and add SVG export to it. Get a real .svg file out of your own code and open it in a browser. That alone is a milestone - your generative art is now a portable, physical-ready document.
Then, the part that matters: make it real. If you have a plotter, plot it - obviously. If you don't, print the SVG on paper and trace over the lines yourself with a fine-tip pen. I'm serious. Becoming the plotter for ten minutes, following your own code's paths by hand, teaches you more about single-line thinking than anything I could write. You'll feel why hatching works, why travel order matters, why the pen is part of the art.
For a stretch goal: load a photo, sample its brightness, and hatch it into a plotted portrait using that brightnessToGap idea. Generic test image, please - no faces of real people, you know the house rules by now :-).
Either way, you will have crossed a line that most coders never cross: you turned something that only existed as math into something you can hold in your hand and pin to a wall. That shift never gets old, and it's the whole reason this new chapter exists.
<path d="..."> mini-language is four commands: M (moveTo, pen up), L (lineTo, pen down), C (cubic Bezier - straight from episode 14), and Z (close). If you can produce SVG text, you can plotfill(), you have one pen. Fake tone with hatching (closely-spaced lines), cross-hatching, or stippling. Tie line density to brightness and a photo becomes an engraving. The constraint is a style, not a limitAnd that's our first step out from behind the glass. Notice we didn't actually learn much new today - the noise, the curves, the path-thinking were all already ours from earlier in the series. What changed is the destination: instead of pixels, our code now produces instructions for a machine that touches the real world. That's the theme of this whole new chapter. Next time we'll go deeper into the algorithms that make plots genuinely sing - how to fill space with a single unbroken line, how to make curves the pen actually loves to draw. The screen was just the rehearsal. Now we perform on paper.
Sallukes! Thanks for reading.
X