Last episode we got properly nerdy about the algorithms that make a plotter sing - stippling a photo into a cloud of dots, weaving all those dots into one unbroken TSP line, filling space with a Hilbert curve, cross-hatching tone the way an old engraver would. By now you can take basically any image or generative idea and turn it into a single-pen drawing. That's a real superpower.
But everything we've done so far has been in one colour, with one pen, on whatever paper happened to be in the machine. Today we leave the comfort zone. We're going to plot in full colour, layer pens on top of each other, deliberately mess with how the ink sits on the paper, and turn one algorithm into a signed, numbered edition of prints. This is the episode where plotting stops being "a printer that draws" and becomes an actual studio practice. Allez, aprons on :-).
Here's the mental shift that unlocks everything today. A colour plot is not one drawing. It's several drawings, one per pen, all sharing the same coordinate space, stacked on the same sheet of paper. You plot the red bits, you stop, you physically swap the pen for a blue one, you plot the blue bits, and so on. The paper never moves. The machine draws each layer in perfect register and your eye fuses them into one colour image.
We touched this at the very end of episode 105 with the registration problem - if the paper shifts even half a millimetre between pens, the layers don't line up. Today we build the whole workflow around it properly. And it starts with extending our little SVG plotter to think in layers.
// a multi-layer plotter: each layer is a named bucket of strokes,
// and every layer shares the SAME page coordinates so they register.
class LayeredPlot {
constructor(w, h) {
this.w = w;
this.h = h;
this.layers = {}; // { "red": [ [pt,pt,..], ... ], "blue": [...] }
}
// drop a stroke into a named pen layer (creates the layer if new)
add(layerName, points) {
if (!this.layers[layerName]) this.layers[layerName] = [];
this.layers[layerName].push(points);
}
}
Nothing fancy - it's just our old polyline list, except now there's one list per pen. The whole rest of the episode is about what you put in those buckets and how you get them onto paper without losing your mind.
Before we plot a single colour, we need a way to line up the layers. The trick that every plotter artist uses is dead simple: draw the same tiny marks in every layer, in the exact same spot. Usually little crosses or corner brackets. When you swap pens, you tell the machine to redraw those marks first - if the new pen lands exactly on the old marks, you know the paper hasn't moved. If it's off, you nudge the paper and try again.
// add identical registration crosses to EVERY layer.
// because they're in the same xy on every pen pass, they overlap perfectly
// when the paper is aligned - and visibly miss when it isn't.
function addRegistrationMarks(plot, margin = 10, size = 4) {
const corners = [
[margin, margin], // top-left
[plot.w - margin, margin], // top-right
[margin, plot.h - margin], // bottom-left
[plot.w - margin, plot.h - margin], // bottom-right
];
for (let name in plot.layers) {
for (let [cx, cy] of corners) {
plot.add(name, [[cx - size, cy], [cx + size, cy]]); // horizontal tick
plot.add(name, [[cx, cy - size], [cx, cy + size]]); // vertical tick
}
}
}
Run that once, right before you export, and every pen draws the same four crosses. It feels like a waste of ink the first time. The first time it saves a four-pen plot from being a blurry mess, you'll never skip it again. (Ask me how I know. The bin was full of magenta-and-cyan ghosts for a while.)
A plotter draws one SVG at a time, so a colour plot is really N separate files - one per pen - that you feed to the machine in sequence. We sketched this in episode 105; here's the grown-up version, with registration marks baked in and the stroke order optimised per layer (that nearest-neighbour reorder from episode 105 still applies, per pen).
// turn a LayeredPlot into one SVG string per pen colour.
// you plot file 1, swap the pen by hand, plot file 2 on the same sheet.
function exportLayers(plot) {
addRegistrationMarks(plot); // identical marks in every layer
const files = {};
for (let name in plot.layers) {
let svg = `<svg xmlns="http://www.w3.org/2000/svg" `;
svg += `width="${plot.w}mm" height="${plot.h}mm" `;
svg += `viewBox="0 0 ${plot.w} ${plot.h}">\n`;
let ordered = optimizeOrder(plot.layers[name]); // cut pen-up travel
for (let pts of ordered) {
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)}`;
}
svg += ` \n`;
}
svg += '';
files[name] = svg;
}
return files; // { red: "", blue: "", ... }
}
Notice the stroke colour in the SVG is still just black - the actual colour comes from which pen you physically load. The layer name is a note to you, the human, telling you which pen to put in the holder. The machine doesn't care; it just draws lines.
So how do you decide which strokes go in which layer? For generative work you often just design it that way from the start - this shape is red, that flow field is blue, done. But the really juicy technique is colour separation: take a full-colour photo and automatically break it into a handful of pen colours.
The idea leans on everything we learned about colour back in episode 7. You pick a small palette - say four pens - and for every pixel you ask "which of my pens is closest to this colour?" Then that pixel's tone gets drawn in that pen's layer, using stippling or hatching for density. Overlap a few translucent-ish ink layers and your eye reconstructs a startlingly full-colour image from four pens.
First, the "which pen is closest" question. It's just distance in colour space - the same distance idea we've used since the trig episode, only in three dimensions (r, g, b) instead of two.
// find the nearest pen colour to a pixel, by plain RGB distance.
// palette = [{name:"cyan", rgb:[0,170,200]}, {name:"magenta", ...}, ...]
function nearestPen(palette, r, g, b) {
let best = palette[0], bestD = Infinity;
for (let pen of palette) {
let dr = pen.rgb[0] - r;
let dg = pen.rgb[1] - g;
let db = pen.rgb[2] - b;
let d = dr*dr + dg*dg + db*db; // squared distance, no sqrt needed
if (d < bestD) { bestD = d; best = pen; }
}
return best.name;
}
Now walk the image on a grid and, for each cell, decide its pen and how dark that pen should be there. Dark, saturated cells get more dots; pale cells get fewer. We're literally reusing the stippling logic from last episode, just routed into a per-colour bucket.
// separate an image into pen layers of dots.
// each cell picks its nearest pen, then drops dots based on how strong
// that colour is there. more colour = more dots = denser ink.
function separateToLayers(plot, img, palette, cell = 4) {
img.loadPixels();
for (let y = 0; y < img.height; y += cell) {
for (let x = 0; x < img.width; x += cell) {
let i = (y * img.width + x) * 4;
let r = img.pixels[i], g = img.pixels[i+1], b = img.pixels[i+2];
let pen = nearestPen(palette, r, g, b);
// how "present" is this colour? use distance-from-white as strength
let strength = 1 - (r + g + b) / (3 * 255); // 0 = white, 1 = dark
let dots = floor(strength * 6); // up to 6 dots per cell
for (let k = 0; k < dots; k++) {
let px = x + random(cell);
let py = y + random(cell);
plot.add(pen, [[px, py], [px + 0.4, py]]); // a tiny dash = a "dot"
}
}
}
}
The result is one dot layer per pen. Plot them in order - usually lightest pen first, darkest last, so the dark ink sits on top - and a photo emerges in four colours from four single pens. The first time a recognisable colour face appeared out of four passes of dots, I actually laughed out loud. It feels like cheating the laws of physics, and it's just RGB distance and dots.
Here's a technique that sounds like a mistake and is actually a tool. Overplotting means drawing the same path more than once. Why would you? Two reasons. One, a single pen pass can look thin and pale, especially on textured paper - a second pass lays down more ink and gives you a richer, darker line. Two, and this is the fun one: if you offset each pass by a tiny random amount, the line stops looking machine-perfect and starts looking hand-drawn.
// draw a path multiple times, each pass nudged by a small random offset.
// 1 pass = crisp machine line. 3 passes with wobble = looks hand-drawn.
function overplot(plot, layer, points, passes = 3, jitter = 0.2) {
for (let p = 0; p < passes; p++) {
let wobbled = points.map(([x, y]) => [
x + random(-jitter, jitter),
y + random(-jitter, jitter),
]);
plot.add(layer, wobbled);
}
}
That jitter of 0.2mm is tiny - smaller than most people would notice consciously - but across a whole drawing it adds up to a warmth that perfectly clean lines just don't have. There's a lovely irony here: we spend ages making the machine precise, then we spend code making it look like it wasn't. The wobble is the point. A bit of imperfection reads as human, and our eyes are suckers for it.
If you want the wobble to flow instead of being pure random noise, swap random() for the Perlin noise from episode 12 - then the wiggle drifts smoothly along the line like a slightly unsteady hand, instead of buzzing randomly. Tiny change, very different feel.
// smooth wobble using noise instead of random - drifts like a real hand.
function organicOffset(points, amount = 0.3, scale = 0.05) {
return points.map(([x, y], i) => [
x + (noise(i * scale, 0) - 0.5) * 2 * amount,
y + (noise(0, i * scale) - 0.5) * 2 * amount,
]);
}
This is my favourite part, because it's where plotting connects back to how generative artists actually share (and sell) their work. Remember way back in episode 24, seed-based art - same algorithm, different seed, reproducible every time? That idea is the whole business model of plotter editions.
You write one generative system. You run it with seed 1, plot it, sign it "1/50". Run it with seed 2, plot it, sign "2/50". Every print is unique - different seed, genuinly different drawing - but they're all unmistakably from the same system. An edition of 50 unique originals. That's a completely different thing from printing 50 copies of one image, and collectors know the difference.
// generate an edition: same system, one unique drawing per seed.
// each returns a LayeredPlot you can export and plot.
function makeEdition(systemFn, count, w, h) {
const edition = [];
for (let n = 1; n <= count; n++) {
randomSeed(n); // reproducible: seed n ALWAYS gives print n
noiseSeed(n);
let plot = new LayeredPlot(w, h);
systemFn(plot); // your generative art fills the layers
edition.push({ number: n, total: count, plot });
}
return edition;
}
Because the seed fully determines the output, you can preview all fifty on screen, throw out the three boring ones, and only plot the keepers - and you can always exactly reproduce print 27 if a pen skips halfway through and you need a clean redo. That reproducibility is quietly one of the most useful things in the whole plotting world. See where episode 24 was secretly setting this up?
For the signature and numbering, a lot of artists just sign by hand in pencil under the plot - but you can also have the machine write it, since text is just more strokes. A simple approach is to keep the edition number as its own little stroke in a corner.
// stamp the edition number as plottable marks (here: simple tally ticks).
// for real lettering you'd use a single-line "hershey" font - more on
// machine-drawn text in a future episode.
function stampNumber(plot, n, total) {
let x = plot.w - 20, y = plot.h - 6;
for (let i = 0; i < n; i++) { // n little ticks = print number
plot.add("label", [[x + i * 1.2, y], [x + i * 1.2, y - 3]]);
}
}
I left lettering deliberately shallow there - drawing real text with a single continuous line is its own rabbit hole (single-line "engraving" fonts), and we'll give it the room it deserves down the line. For now, hand-signing in pencil is honestly the nicest option anyway. It's the one part of a code-made artwork that's unrepeatably you.
I'd be lying to you if I pretended this was all software. Half of advanced plotting is material choices, and no amount of clever code substitutes for them. Let me hand you what took me an embarrassing amount of wasted paper to learn.
// femdev's hard-won material cheat sheet (not code, just notes-as-code :-)
const materials = {
finePen: "Sakura Micron / technical pens - crisp, archival, great for detail",
gelPen: "metallic gel on BLACK or navy card - gold constellations, stunning",
brushPen: "thick-thin variation - same SVG, totally different soul",
fountain: "ink pools and flows - beautiful, but bleeds on cheap paper",
smoothPaper:"Bristol board - sharp lines, no bleed, best for fine detail",
roughPaper: "watercolour/textured - soft edges, organic, ink soaks in",
darkPaper: "flips everything - light pen on dark = drama, great for gifts",
};
A few things worth saying out loud. The paper is a collaborator, not a neutral surface. Rough watercolour paper drinks ink and softens every edge - lovely for organic, botanical, flowing work, terrible for crisp geometry. Smooth Bristol does the opposite. Absorbent paper saturates fast, so on those you want fewer overplot passes or the lines bloom into blobs. And if you ever want to sell prints, the words you need are archival: acid-free paper and pigment-based ink (not dye), so the piece doesn't yellow or fade in a few years. Sakura Pigma Microns are pigment and archival, which is why half the plotter world swears by them.
One more move before the exercise, because it's where a lot of the most gorgeous plotter work lives: don't let the machine do everything. Plot a precise base structure in code, then bring it to a desk and add watercolour washes, coloured pencil, or marker by hand. The precision of the code plus the warmth of the human hand - the two things are better together than either alone.
// plan a hybrid piece: machine does the precise layer, you do the rest.
const hybridPlan = {
machine: "plot fine ink linework - the precise skeleton",
thenHand: [
"watercolour wash inside the shapes (let the ink resist or bleed)",
"coloured-pencil shading in key areas for depth",
"a few hand-drawn marks the algorithm would never make",
],
why: "code gives precision, the hand gives intention. nobody can tell "
+ "exactly where one stops and the other starts - that's the magic",
};
There's no code trick here, and that's the lesson. The most interesting place to stand is the seam between digital and analog. Your code is a collaborator now, not a vending machine. (And honestly? Watching a friend try to guess which lines were machine and which were my hand is one of the small joys of showing this work in person.)
Time to put it all together into one proper piece. Build a generative system that fills three pen layers, then export and either plot it or simulate it on screen. Here's the structure I want you to aim for:
// a complete three-layer generative print.
// layer 1: big background shapes (wide pen)
// layer 2: flow-field lines (medium pen) - hello again, episode 12 & 105
// layer 3: stippled detail from an image (fine pen) - episode 106
function threeLayerArt(plot) {
// --- layer 1: bold background geometry ---
for (let i = 0; i < 8; i++) {
let x = random(plot.w), y = random(plot.h), r = random(20, 60);
let ring = [];
for (let a = 0; a <= TWO_PI + 0.1; a += 0.2) {
ring.push([x + cos(a) * r, y + sin(a) * r]); // trig from ep13 :-)
}
plot.add("background", ring);
}
// --- layer 2: flow field over the whole page ---
for (let i = 0; i < 200; i++) {
let x = random(plot.w), y = random(plot.h), line = [[x, y]];
for (let s = 0; s < 60; s++) {
let a = noise(x * 0.01, y * 0.01) * TWO_PI * 2; // noise from ep12
x += cos(a) * 2; y += sin(a) * 2;
if (x < 0 || x > plot.w || y < 0 || y > plot.h) break;
line.push([x, y]);
}
plot.add("flow", line);
}
// --- layer 3: stippled detail (reuse last episode's stipple dots) ---
// separateToLayers(plot, img, palette) // or drop your stipple here
}
Run that through exportLayers, and you'll get three SVG files plus the registration marks tying them together. If you have a plotter: load a wide pen for the background, plot it, swap to a medium pen for the flow lines, plot that, swap to a fine pen for the detail, plot that. Watch a full-colour image build up one pen at a time. If you don't have a machine: render each layer in its own colour on screen, stacked, and you'll see exactly what the plot would become.
Stretch goal: turn it into an edition. Wrap threeLayerArt in makeEdition, generate ten seeds, preview them, and pick your favourite three to "plot". Number them 1/3, 2/3, 3/3. Congratulations - you just made a generative art edition, the exact way artists sell this stuff on the wall and online.
And one last nudge: if you can, plot somewhere people can watch. The slow, patient scratch of a pen building an image over twenty minutes is wierdly mesmerising - the process becomes part of the artwork. I've seen people stand and watch a single plot finish like it's a tiny theatre show. The machine doing the work in front of you is half the magic.
LayeredPlot just keeps one stroke-list per penThat wraps the plotter chapter. Look back at what happened: we started with "a machine that drags a pen", and we've arrived at signed, numbered, multi-colour editions made of code, ink, and paper. Almost none of it was new math - it was noise, distance, seeds, and colour, all things we already had, pointed at the physical world. That's the whole theme of this part of the series: your creative coding doesn't have to stay on the screen.
And the pen is only the beginning. There are plenty of other ways to make a machine turn your code into something you can hold - cutting, lights, motion, depth. We're going to keep walking out of the screen and into the room. Bring your curiosity (and maybe some safety glasses) next time :-).
Sallukes! Thanks for reading.
X