Learn Creative Coding (#108) - Laser Cutting: Code to Material

in StemSocial16 hours ago

Learn Creative Coding (#108) - Laser Cutting: Code to Material

cc-banner

Last episode we ended the plotter chapter holding signed, numbered prints in our hands, and I promised we'd keep walking out of the screen and into the room. So here we are. Today we swap the pen for a laser, and instead of dragging ink across paper we're going to slice clean through wood, acrylic, and card. Same SVG pipeline we already built. Same "code makes a file, machine makes a thing" idea. Only now the thing is three-dimensional, snaps together without glue, and you can put a candle next to it.

I want to be honest up front: this is the episode where our little p5 sketches start becoming objects. Coasters, boxes, lamps, jewellery, topographic maps you can stack on a shelf. It's genuinly one of my favourite leaps in the whole series, because a laser cutter takes the exact same vector thinking we've used since episode 1 and turns it into something physical and useful. Allez, safety glasses on (for real this time) :-).

What a laser cutter actually does

Strip the romance away and a laser cutter is almost as simple as the plotter was. It's a flat bed, a moving head, and instead of a pen the head focuses an intense beam of light onto a tiny spot. That spot gets hot enough to vaporise material. Move the head along a path and the beam burns a line. Move it slowly and powerfully and the line goes all the way through - that's a cut. Move it fast and gentle and it only scorches the surface - that's an engrave.

That single difference - cut versus engrave - is the whole mental model, and the lovely part is that your code decides which is which.

// the two things a laser does, and how your file controls them:
//
// CUT      -> the beam follows a VECTOR PATH and slices all the way through.
//             slow + high power. this is just a <path>, exactly like a plotter line.
//
// ENGRAVE  -> the beam scans back and forth over a FILLED AREA, burning the
//             surface only. fast + low power. this is a filled shape or a raster image.
//
// so: lines = cut, fills = engrave. you already know how to make both.

If that feels familiar, it should. Back in episode 105 we learned that a plotter only understands strokes, and we spent ages turning our generative art into SVG <path> elements. A laser eats the exact same SVG. The skills transfer one-to-one. A cut path is literally a plotter stroke that happens to go all the way through the material instead of laying down ink.

The colour convention: how the machine knows your intent

Here's the one new wrinkle. A plotter draws everything the same way - a line is a line. But a laser needs to know, per shape, whether to cut, score, or engrave, and each of those needs different power and speed. The universal trick the whole laser world uses is colour as instruction. You assign a colour to each operation, and the laser software maps colours to machine settings.

The most common convention (it varies shop to shop, so always check) goes like this:

// the classic laser colour code (RGB stroke colours in your SVG):
const LASER = {
  cut:     "#FF0000",   // pure red   = cut all the way through
  score:   "#0000FF",   // pure blue  = score / partial cut (a fold line)
  engrave: "#000000",   // black fill = engrave the surface
};
// the machine operator maps each colour to power/speed. your job is just
// to make sure every shape wears the right colour for what you want.

So our job in code is barely different from plotting. We still emit SVG paths. We just paint them red for cut, blue for score, and use black fills for engrave. Let me extend the tiny SVG builder we wrote back in episode 105 so it speaks laser.

// a laser-aware SVG builder. same bones as our SvgPlot from episode 105,
// but every shape carries an operation (cut / score / engrave).
class LaserDoc {
  constructor(w, h) {       // w, h in millimetres - real material size
    this.w = w;
    this.h = h;
    this.shapes = [];       // { op: "cut", points: [[x,y],...] }
  }

  cut(points)   { this.shapes.push({ op: "cut",   points }); }
  score(points) { this.shapes.push({ op: "score", points }); }
  // engrave takes a closed shape we'll FILL rather than stroke
  engrave(points) { this.shapes.push({ op: "engrave", points }); }
}

Notice the dimensions are in mm again, just like the plotter. This matters even more here - you're cutting a real 3mm sheet of birch plywood, and if your code thinks in pixels you'll end up with a coaster the size of a postage stamp or a dinner plate. Physical units from the very first line.

// turn a LaserDoc into a real SVG string. cut/score become coloured strokes,
// engrave becomes a black-filled path. this file goes straight to the laser.
LaserDoc.prototype.toSVG = function () {
  const colour = { cut: "#FF0000", score: "#0000FF", engrave: "#000000" };
  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 s of this.shapes) {
    let d = `M ${s.points[0][0].toFixed(2)} ${s.points[0][1].toFixed(2)}`;
    for (let i = 1; i < s.points.length; i++) {
      d += ` L ${s.points[i][0].toFixed(2)} ${s.points[i][1].toFixed(2)}`;
    }
    d += " Z";                                   // close the shape
    if (s.op === "engrave") {
      out += `  <path d="${d}" fill="#000000" stroke="none"/>\n`;
    } else {
      // cut/score: NO fill, thin coloured stroke. width is irrelevant to the
      // laser - only the path matters - but 0.1 keeps previews honest.
      out += `  <path d="${d}" fill="none" stroke="${colour[s.op]}" stroke-width="0.1"/>\n`;
    }
  }
  out += "</svg>";
  return out;
};

And that's the entire bridge from code to laser. Everything from here on is just deciding what shapes to put in the document - which is the fun, generative part we've been doing all series.

Kerf: the millimetre that ruins everything

Right, time for the single most important thing in laser cutting, the thing that separates "my box fits together beautifully" from "why is everything loose and falling apart". It's called kerf.

The laser beam has width. It's thin - usually somewhere between 0.1 and 0.2mm - but it's not zero. When the beam cuts along your path, it vaporises a strip of material that wide, centred on your line. So a 50mm square you draw comes out about 49.8mm across, because 0.1mm got burned away on each side.

For a single decorative shape, who cares. But the moment you want two parts to fit together - a tab into a slot, a lid onto a box - that missing fraction of a millimetre is the difference between a satisfying snap and a sloppy wobble.

// kerf compensation. the beam eats ~half its width off each edge of a cut.
// so to END UP with a given size, we adjust the path we DRAW:
//   - things that should be SOLID (tabs)  -> draw them BIGGER by half-kerf
//   - things that should be HOLES (slots) -> draw them SMALLER by half-kerf
// the two adjustments meet in the middle and you get a tight friction fit.
const KERF = 0.15;            // measure YOUR machine+material, this is a guess

function growTab(size)  { return size + KERF / 2; }   // outer parts grow
function shrinkSlot(size) { return size - KERF / 2; }  // inner parts shrink

The honest truth is you don't guess kerf, you measure it. Every laser plus material combination is a little different - 3mm plywood on one machine cuts a different kerf than acrylic on another. So the very first thing every laser person makes is a kerf test: cut a row of slots from, say, 4.8mm up to 5.2mm in 0.05mm steps, then see which one your 5mm tab presses into with a nice firm fit. That winning number is your kerf for that material. Boring? A bit. But it's the five minutes that makes everything afterwards just work.

// generate a kerf test strip: a row of slots in tiny size increments.
// cut it, then find which slot grips a known-width tab perfectly.
function kerfTest(doc, tabWidth = 5, start = 4.7, step = 0.05, count = 8) {
  let x = 10;
  for (let i = 0; i < count; i++) {
    let slot = start + i * step;        // this slot's target width
    // a thin rectangular slot, centred at x
    doc.cut([
      [x, 10], [x + slot, 10],
      [x + slot, 14], [x, 14],
    ]);
    x += 12;                            // space the slots out along the strip
  }
}

Tabs and slots: snapping parts together with code

Now the genuinely magic part, the reason code-generated laser files crush hand-drawn ones. We're going to make two flat pieces of wood lock together at a right angle, no glue, just friction - a finger joint (also called a box joint). You see these on every laser-cut box, pen holder, and enclosure on the internet.

The idea: along the edge where two panels meet, one panel has a row of sticking-out tabs, and the other has a matching row of slots. Press them together and they interlock like fingers. The kerf-tight fit holds it. And because it's all parametric, you describe it once and the code lays out every finger perfectly spaced.

// generate a tabbed edge: a zig-zag path that alternates OUT (tab) and
// flat, walking along a straight edge of given length. `thickness` is the
// material depth - tabs stick out exactly one material-thickness so they
// poke through the matching slot and sit flush.
function tabbedEdge(length, thickness, fingers = 5) {
  const seg = length / (fingers * 2 - 1);   // width of one finger/gap
  let pts = [[0, 0]];
  let x = 0, out = false;
  for (let i = 0; i < fingers * 2 - 1; i++) {
    x += seg;
    pts.push([x, out ? 0 : thickness]);     // step in/out by material thickness
    pts.push([x, out ? thickness : 0]);     // wait - tidy this into clean steps
    out = !out;
  }
  return pts;
}

I deliberately left that one a touch rough so you can feel the shape of the problem - it's a square wave walking along an edge, where the "height" of each step is exactly the material thickness so a tab pokes through the opposing panel and ends flush with its surface. Here's the cleaner version I actually use, which returns the slots for the mating panel too, with kerf already baked in:

// the grown-up version: returns BOTH the tab path and the slot rectangles
// for the panel it mates with. kerf compensation applied so they snap.
function fingerJoint(length, thickness, fingers = 5) {
  const seg = length / (fingers * 2 - 1);
  const tabPath = [[0, 0]];
  const slots = [];
  let x = 0;
  for (let i = 0; i < fingers * 2 - 1; i++) {
    const isTab = i % 2 === 0;              // tab, gap, tab, gap...
    if (isTab) {
      tabPath.push([x, thickness], [x + seg, thickness]);  // push out
      // matching slot in the other panel, shrunk by kerf for a tight grip
      slots.push([
        [x + KERF/2, 0], [x + seg - KERF/2, 0],
        [x + seg - KERF/2, thickness], [x + KERF/2, thickness],
      ]);
    } else {
      tabPath.push([x, 0], [x + seg, 0]);                  // stay flat
    }
    x += seg;
  }
  return { tabPath, slots };
}

Change fingers from 5 to 9 and the whole joint re-spaces itself instantly. Change thickness from 3 to 6mm because you switched to thicker ply and every tab deepens to match. That is why we generate these in code. A human drawing this by hand in Inkscape would cry; we change one number. Remember way back in episode 24 when seed-based art taught us that one parameter can drive a whole system? Same lesson, now it's holding a box together.

Living hinges: making rigid wood bend

This one made my jaw drop the first time I saw it, and it's pure code-art. A living hinge is a pattern of thin cuts that lets a stiff, flat sheet of plywood bend like it's flexible. You cut a field of short parallel slits, leaving little bridges of material between them, and suddenly your rigid board curls into a curve. People make cylindrical lamps, curved boxes, and bracelets this way.

It's also just a beautiful generative pattern in its own right - rows of offset slits, like brickwork.

// a living hinge: rows of staggered slits that let flat material flex.
// the closer the slits and the thinner the bridges, the more it bends.
function livingHinge(doc, w, h, rows = 20, gap = 3) {
  const rowH = h / rows;
  for (let r = 0; r < rows; r++) {
    const y = r * rowH;
    // offset every other row by half a slit, like bricks (this is the trick)
    const offset = (r % 2 === 0) ? 0 : gap / 2;
    for (let x = offset; x < w - gap; x += gap) {
      // each slit is a short vertical cut leaving bridges above and below
      doc.cut([
        [x, y + rowH * 0.15],
        [x, y + rowH * 0.85],
      ]);
    }
  }
}

Tighten gap, add rows, and the board goes from "barely bends" to "flops like fabric". It's worth playing with - the bend radius you can achieve is a direct function of how much material you leave between cuts, which is a delightfully tangible way to feel a parameter. And there's no reason the slits have to be straight; swap them for wavy noise-driven lines (hello again, episode 12) and the hinge becomes decorative as well as functional.

Engraving a photo: dithering returns

Cutting is only half the laser's job. The other half is engraving - burning a picture into the surface. And here's a friend we've met before: a laser engraves in dots of "burned" or "not burned", which means we need to turn a smooth greyscale photo into pure black-and-white dots. That's dithering, the exact same Floyd-Steinberg idea we used for image work earlier in the series. The burned dots, seen from a distance, read as continuous tone. On wood it looks ghostly and gorgeous.

// Floyd-Steinberg dithering: turn greyscale into pure black/white dots
// by pushing each pixel's rounding error onto its neighbours. the laser
// burns the black dots; your eye reassembles the photo from a metre away.
function dither(gray, w, h) {
  const px = gray.slice();                 // copy so we can spread error
  for (let y = 0; y < h; y++) {
    for (let x = 0; x < w; x++) {
      const i = y * w + x;
      const old = px[i];
      const next = old < 128 ? 0 : 255;    // snap to black or white
      px[i] = next;
      const err = old - next;              // the error we just introduced
      // spread it to neighbours we haven't visited yet (the classic weights)
      if (x + 1 < w)            px[i + 1]     += err * 7 / 16;
      if (y + 1 < h && x > 0)   px[i + w - 1] += err * 3 / 16;
      if (y + 1 < h)            px[i + w]     += err * 5 / 16;
      if (y + 1 < h && x + 1 < w) px[i + w + 1] += err * 1 / 16;
    }
  }
  return px;                                // now only 0s and 255s
}

You hand that black-and-white field to the laser as a raster engrave, and it scans across burning a dot wherever the value is 0. The same algorithm that made plotted stippling work now scorches a portrait into a chunk of birch. I love how often this happens in creative coding - you learn one idea properly and it keeps coming back wearing a new coat.

Materials: the part the code can't choose for you

Just like with pens and paper last episode, half the art here is material, and no clever code substitutes for it. Let me hand you my hard-won cheat sheet.

// femdev's laser material notes (not code, just notes-as-code :-)
const materials = {
  birchPly3mm: "the workhorse - cheap, reliable, cuts clean, lovely for boxes",
  acrylicClear:"cast acrylic - cuts to a glassy polished edge that GLOWS on light",
  cardstock:   "intricate paper-cut art, layered shadow boxes - cuts in a flash",
  mdf:         "dirt cheap, engraves to a smooth dark contrast, but smelly smoke",
  leather:     "classy keyfobs and patches - engraves beautifully, smells amazing",
  // DO NOT cut: PVC/vinyl (releases chlorine gas - dangerous), anything you're
  // unsure of. when in doubt, ask the makerspace. seriously.
};

A couple of things worth saying out loud. Cast acrylic is the diva of the laser world - cut it and the edge comes out flame-polished and clear, so if you light it from below the whole edge glows. It's the secret behind all those "floating" LED signs. And that last comment is not a joke: never cut PVC or unknown plastics, because some release chlorine gas that wrecks both your lungs and the machine. This is the one place in the whole series where getting it wrong actually hurts you, so when in doubt, ask.

Layered assemblies: depth from flat sheets

One more technique before the exercise, because it's where laser work gets really painterly. You're cutting flat sheets - but you can stack them. Cut the same outline from several sheets of different-coloured acrylic, glue them in a pile, and you get depth and colour from 2D parts. The classic example is a topographic map: each layer is one contour line of a hill, stacked up into a little 3D landscape. Each layer is just a separate path in the same coordinate space - exactly like the colour separation layers from last episode.

// build a stacked assembly: each "layer" is a closed outline cut from its
// own sheet. same coordinate space = they line up when you stack them.
// here: nested rings -> a stepped topographic mound.
function topoLayers(doc, cx, cy, layers = 5, maxR = 40) {
  for (let L = 0; L < layers; L++) {
    const r = maxR * (1 - L / layers);    // each layer a bit smaller
    const ring = [];
    for (let a = 0; a <= TWO_PI + 0.1; a += 0.2) {
      // wobble the radius with noise so contours look natural, not robotic
      const rr = r + noise(cos(a) + L, sin(a) + L) * 6;   // noise, ep12 again
      ring.push([cx + cos(a) * rr, cy + sin(a) * rr]);
    }
    doc.cut(ring);    // in practice each layer is its OWN file / own sheet
  }
}

In real use you'd export each ring as its own SVG (one per sheet), cut them from different colours, and stack. But generating the whole nested set from one noise field, in a dozen lines, is the sort of thing that would take forever by hand and falls right out of code.

Your exercise: a generative coaster you can hold

Time to make something real. Your mission: design a generative coaster - a 90mm circle - with an intricate engraved or cut-through pattern, and export it as a laser-ready SVG. Here's a starting frame that cuts the outline and fills it with a noise-driven pattern of little holes:

// a complete generative coaster: a round cut outline + a generative
// pattern of small cut-through holes. swap the pattern for Voronoi,
// Islamic geometry, or hatching - whatever you fancy.
function coaster(doc, cx = 45, cy = 45, R = 45) {
  // 1) the outer circle - this is the CUT that frees the coaster
  const outline = [];
  for (let a = 0; a <= TWO_PI + 0.1; a += 0.05) {
    outline.push([cx + cos(a) * R, cy + sin(a) * R]);
  }
  doc.cut(outline);

  // 2) a generative field of holes inside, gated by noise so it has structure
  for (let y = -R; y < R; y += 4) {
    for (let x = -R; x < R; x += 4) {
      if (x * x + y * y > (R - 6) * (R - 6)) continue;   // stay inside, leave a rim
      const n = noise((x + R) * 0.06, (y + R) * 0.06);   // ep12 noise field
      if (n > 0.55) {
        // a tiny square hole - small enough to keep the coaster strong
        const s = 1.2;
        doc.cut([
          [cx + x - s, cy + y - s], [cx + x + s, cy + y - s],
          [cx + x + s, cy + y + s], [cx + x - s, cy + y + s],
        ]);
      }
    }
  }
}

Run coaster(doc), call doc.toSVG(), and you've got a file ready to cut. Want it fancier? Replace the hole grid with a Voronoi pattern (those organic cells from the data-art arc), or a repeating Islamic geometric tiling, or hatching that gets denser toward the middle. Add a snap-fit notch and a felt backing if you're feeling ambitious. Then - and this is the real assignment - actually get it cut.

Because here's the thing: you almost certainly don't own a laser cutter, and that's completely fine. This is where makerspaces and FabLabs come in - most cities have one, you bring your SVG and a sheet of ply, and you walk out with your coaster the same afternoon. Or use an online service: upload the file, pick a material, and they post the cut parts to your door. The barrier to turning your code into a held object is genuinly lower than people think.

// your path to a physical object, no machine of your own required:
const access = [
  "makerspace / FabLab  - bring your SVG + material, cut it yourself (cheap, social)",
  "online cutting service - upload SVG, pick material, parts arrive by post",
  "uni / library         - tons have a laser now, often free for members",
  "a mate with a machine - the oldest trick. bring snacks.",
];
// the SVG you exported above works at ALL of these. it's a portable object-recipe.

I really want you to do the physical step if you possibly can. There's a moment when you pick up a thing your code made - it has weight, an edge, a little smell of cut wood - and it lands differently than any screen ever could. Just like holding your first plot last episode, only now it's an object, not a picture.

't Komt erop neer...

  • A laser cutter does two things, and your code decides which: a vector PATH gets CUT all the way through (slow, high power), and a filled AREA or raster image gets ENGRAVED on the surface (fast, low power). Lines cut, fills engrave
  • The machine reads your intent from COLOUR: red = cut, blue = score/fold, black = engrave (varies by shop, always check). So our LaserDoc is just the SvgPlot from episode 105 with an operation tag per shape, still emitting plain SVG in millimetres
  • Kerf is the width the beam burns away (~0.1-0.2mm). It's the difference between a tight snap and a loose wobble. You MEASURE it with a kerf-test strip, then grow tabs by half-kerf and shrink slots by half-kerf so parts meet in a friction fit
  • Finger joints (tabs + slots) let flat panels lock together at right angles with no glue. Generating them in code is the whole point - change fingers or thickness and the joint re-spaces itself instantly. One parameter, whole system, exactly the seed-based lesson from episode 24
  • Living hinges are fields of staggered slits that let rigid plywood bend like fabric. Tighter slits and thinner bridges = more flex. It's a functional joint AND a generative pattern (go wavy with noise for decoration)
  • Engraving a photo is just Floyd-Steinberg dithering again - turn greyscale into burn/no-burn dots, and the laser scorches the picture into wood. The same dithering idea keeps coming back through the series
  • Material is half the art and the code can't pick it for you: 3mm birch ply is the workhorse, cast acrylic glows at the edge, cardstock and leather each have their magic. NEVER cut PVC or unknown plastics - chlorine gas is no joke
  • Layered assemblies stack flat sheets (topographic maps, shadow boxes) for depth and colour from 2D parts - same coordinate space, one file per sheet, just like last episode's colour layers
  • You don't need to own the machine. Makerspaces, FabLabs, online cutting services, and that one friend will all happily cut your exported SVG. The file is a portable recipe for an object

So that's our second machine out in the physical world, and notice the pattern: almost nothing today was new math. Noise, closed paths, dithering, parametric thinking, layers - all things we already owned, pointed at light and wood instead of ink and paper. The plotter taught us strokes; the laser taught us that the same strokes can cut, fold, and stack into real objects. The screen keeps shrinking in the rear-view mirror.

And we're not done leaving it behind. So far everything we've made just sits there, perfectly still. But code can drive things that light up, that move, that respond when you walk past. Next time we start giving our physical work a pulse. Bring your curiosity - and maybe a spare USB cable :-).

Sallukes! Thanks for reading.

X

@femdev