Last time I left you with a thread I promised we'd pull today, and here it is. We spent the accessibility episode making sure that when your art leaves your screen, anyone can get into the room with it. But there's a quieter door I skipped right past, and it's the one that trips up almost every creative coder I know: the moment someone else picks up your sketch to run it, to build on it, to show it - they have to reverse-engineer everything that was living in your head. All the knowledge just... evaporates the second you close the tab. Allez, that's the gap we close now :-).
I have to tell you about the piece I lost, because it's why I take this seriously. Years ago I made the best thing I'd made all year - a slow drifting field of light, this warm particular gold I'd stumbled into by fiddling with numbers at midnight. I screenshotted it, posted the screenshot, went to bed dead proud. And the next morning I opened the sketch to make a bigger version for print... and it was different. Not a little different. A completely other picture. Because I hadn't saved the seed, hadn't saved the parameters, hadn't written down a single thing about how I got there. The recipe was gone. That exact gold has never come back, and it never will. That morning taught me the whole lesson of this episode: an undocumented piece is a piece you can only make once, by accident.
Here's the mindset shift that fixed this for me, and it took an embarrassingly long time to land. When you make generative work, the image is not really the artwork - it's one output of the artwork. The real artwork is the recipe: the code, plus the exact inputs that produced this particular result. And back in episode 24, when we made randomness reproducible with a seed, we accidentally built the single most important documentation tool in the whole series. A seed plus a set of parameters is the piece, compressed down to a few numbers you can write on a napkin.
So the very first documentation habit, the one that would have saved my gold field, is to capture the complete recipe of every render.
// the complete recipe of a piece: everything needed to reproduce it EXACTLY.
// this is the artwork. the pixels are just what it looks like today.
function captureRecipe(state) {
return {
seed: state.seed, // the reproducible randomness from episode 24
params: { ...state.params }, // every knob: counts, speeds, palette name
width: state.width,
height: state.height,
version: "1.4.0", // which version of your sketch made this
captured: new Date().toISOString(),
};
}
const recipe = captureRecipe(currentState);
console.log(JSON.stringify(recipe, null, 2));
// paste that into a file and you can rebuild this exact image in a year.
If I'd had that one function that midnight, I'd type the seed back in the next morning and the gold would still exist. Everything else in this episode is really just variations on this one idea: never let a result exist without its recipe attached. Makes sense, right?
The trouble with "remember to save the recipe" is the remember part. You won't. I didn't. So the trick is to make the sketch do it for you - to bake the documentation into the export so it's physically impossible to save a picture without also saving how you made it. Remember the "one settings object drives everything" pattern we built the GUI around in episode 133? We're going to lean on that same tidy state and let it narrate itself.
// wire "save" so it can NEVER save a picture without its recipe.
// one keypress gives you the image AND the file that rebuilds it.
function saveArtwork(canvas, state) {
const recipe = captureRecipe(state);
const stem = `piece-seed${recipe.seed}-${Date.now()}`;
// 1. the picture
downloadDataURL(canvas.toDataURL("image/png"), stem + ".png");
// 2. its recipe, right next to it, same filename stem
downloadDataURL(
"data:application/json," + encodeURIComponent(JSON.stringify(recipe, null, 2)),
stem + ".json"
);
}
// tiny helper: trigger a browser download from a data URL.
function downloadDataURL(url, filename) {
const a = document.createElement("a");
a.href = url;
a.download = filename;
a.click();
}
Now the picture and its recipe come out of the machine together, sharing a filename stem so they can never drift apart. The .png is what you show people; the .json is what lets future-you rebuild it. That pairing is the whole game. I bind this to the s key on every sketch I make now, and it has saved me more times than I can count.
And loading is the mirror image - you feed a recipe back in and the sketch reconstitutes that exact piece. This is what makes a "gallery of saved states" possible at all.
// feed a saved recipe back in and the sketch becomes that exact piece again.
function loadRecipe(recipe) {
return {
seed: recipe.seed,
params: { ...recipe.params },
width: recipe.width,
height: recipe.height,
};
}
const state = loadRecipe(savedRecipe); // and the gold field lives again
render(state);
Right, let's talk about the image itself, because "just take a screenshot" is where most people's documentation begins and ends, and they do it badly. A screenshot grabbed off your monitor is whatever size your window happened to be, with your browser chrome and your messy desktop bleeding in at the edges. That's fine for a text to a friend. It is not fine for a portfolio, where every piece should be framed consistently so the collection reads as one body of work and not a pile of random snaps.
The fix is to render your thumbnail deliberately, at a fixed size, off-screen, seeded - so every piece in your portfolio gets the exact same treatment.
// render a clean, consistent thumbnail off-screen - not a messy monitor grab.
// every piece framed identically = the collection reads as ONE body of work.
function makeThumbnail(recipe, size = 600) {
const off = document.createElement("canvas");
off.width = size;
off.height = size;
const ctx = off.getContext("2d");
const state = loadRecipe(recipe); // rebuild the exact piece
state.width = size; state.height = size;
render(state, ctx); // draw it into the off-screen canvas
return off.toDataURL("image/png"); // a clean, uniform thumbnail
}
Because it's seeded and off-screen, this is repeatable: run it over fifty saved recipes and you get fifty thumbnails that all match in size and framing, generated in one loop while you make a coffee. That consistency is the difference between a portfolio that looks intentional and one that looks like a camera roll. And you'll notice this is just episode 20's exporting idea, grown up and pointed at documentation instead of at a single hero image.
Here's a little move I love, because it solves a real annoyance: the image and its .json recipe do get separated eventually. Someone reposts just the PNG, or you find an old render in your downloads with no idea which seed made it. So for anything I might share loose, I stamp a tiny, honest strip of metadata onto the image itself - seed, key params, right there in the pixels where it can't get lost.
// stamp the recipe INTO the picture, so image and recipe can never be separated.
function stampRecipe(ctx, recipe, w, h) {
const line = `seed ${recipe.seed} | ${recipe.params.count} particles | v${recipe.version}`;
ctx.save();
ctx.font = "13px monospace";
ctx.textBaseline = "bottom";
// a soft dark bar so the text reads over any artwork underneath
const pad = 8;
const tw = ctx.measureText(line).width;
ctx.fillStyle = "rgba(0,0,0,0.55)";
ctx.fillRect(0, h - 22, tw + pad * 2, 22);
ctx.fillStyle = "rgba(255,255,255,0.9)";
ctx.fillText(line, pad, h - 4);
ctx.restore();
}
It's small, it's unobtrusive, and it means a stray PNG is still self-describing. I keep it subtle - a whisper in the corner, not a watermark shouting across the piece. And crucially it's truthful: it's built straight from the recipe, the same numbers that drew the art, so it can never claim a seed the picture didn't actually use. That honesty matters more than you'd think when someone asks "can you make a bigger one" and you can read the answer right off the image.
Now the part I most want you to take away, because it changed how I share work entirely: the process is not the boring bit before the art. The process IS content. People are far more moved by watching a thing become than by seeing the finished frame. The wobbly early versions, the happy accident, the moment it clicked - that's the story, and it's the part only you have. A finished image says "look what I made". A process says "here's how, and you could too".
So the habit is to capture yourself along the way instead of only at the end. The simplest version: snapshot the canvas at intervals while you work, automatically.
// grab a snapshot of the work-in-progress every so often, automatically.
// you end up with a visual diary of the piece becoming itself.
const processFrames = [];
function maybeSnapshot(canvas, everyMs = 30000) {
const now = performance.now();
if (now - (maybeSnapshot.last || 0) > everyMs) {
processFrames.push(canvas.toDataURL("image/png"));
maybeSnapshot.last = now;
console.log(`process snapshot #${processFrames.length} captured`);
}
}
// call maybeSnapshot(canvas) inside your loop - a snapshot every 30s of tinkering.
After an evening of fiddling you've got a stack of frames showing the piece evolving - your own little time-lapse, captured without lifting a finger. And the lovely way to present that isn't a video, it's a contact sheet: all the process frames tiled into one image, the whole journey readable at a glance.
// tile the process frames into ONE contact-sheet image - the journey in a glance.
async function buildContactSheet(frames, cols = 4, cell = 200) {
const rows = Math.ceil(frames.length / cols);
const sheet = document.createElement("canvas");
sheet.width = cols * cell;
sheet.height = rows * cell;
const ctx = sheet.getContext("2d");
for (let i = 0; i < frames.length; i++) {
const img = await loadImage(frames[i]); // decode the data URL
const x = (i % cols) * cell;
const y = Math.floor(i / cols) * cell;
ctx.drawImage(img, x, y, cell, cell); // drop it in its grid slot
ctx.strokeStyle = "#222";
ctx.strokeRect(x, y, cell, cell); // a thin frame per step
}
return sheet.toDataURL("image/png");
}
// promise-wrap image decoding so we can await each frame
function loadImage(src) {
return new Promise(res => {
const img = new Image();
img.onload = () => res(img);
img.src = src;
});
}
That contact sheet is genuinly the most-liked thing I post, every single time, more than the finished pieces. There's a whole episode about this feeling much later in the series, but the seed of it is here: people don't just want your destination, they want the walk. Give them the walk.
Okay, put the code down for a second, because the hardest documentation isn't technical at all - it's writing about your work for people who don't code. Your aunt, a gallery curator, someone scrolling past. And creative coders are terrible at this. We write "a particle system driven by curl noise with additive blending" and the reader's eyes glaze and they scroll on. That sentence is true, and it is useless to 95% of humans. The artist statement is where you translate the machine back into feeling.
The trick that unlocked it for me is to write it in layers, widest audience first, and let each layer earn the next. I keep a little scaffold - not code exactly, but a structure I fill in every time:
ARTIST STATEMENT - three layers, widest first:
1. THE FEELING (one sentence, no jargon, for anyone)
"Slow drifting light, like watching dust in a sunbeam."
2. THE IDEA (why it exists, what you were chasing)
"I wanted stillness that never quite repeats - calm, but alive."
3. THE HOW (the technical bit, for the curious - kept SHORT)
"Thousands of points follow an invisible flow field, seeded so
each run is a different but reproducible weather."
Notice the order. You lead with the feeling, because that's the only part everyone shares. The tech goes last and stays short, a door left open for the curious rather than a wall built in front of everyone else. And here's a fun move - you can generate the raw material for that "how" layer straight from your parameters, then soften it into human language by hand.
// turn the cold parameters into a first draft of plain-English description.
// you don't ship this raw - you use it as a starting point and warm it up.
function describeParams(recipe) {
const p = recipe.params;
const density = p.count > 2000 ? "a dense swarm" : "a sparse scattering";
const mood = p.speed < 0.5 ? "drifting slowly" : "moving with energy";
return `${density} of ${p.count} points, ${mood}, in the "${p.palette}" palette.`;
}
console.log(describeParams(recipe));
// -> "a dense swarm of 3000 points, drifting slowly, in the "warm-ink" palette."
// now rewrite THAT by hand into something a person would actually feel.
The generator gives you the honest skeleton; your job is to put skin on it. Don't ship the robot sentence - use it as the true thing you then make warm. That little two-step, machine-draft then human-rewrite, is how I get statements written on days I'd otherwise stare at a blank box forever.
Right, now let's gather it all up, because a pile of pieces isn't a portfolio - a portfolio is a collection presented as one thing. And this is where all our discipline pays off beautifully. Because every piece already carries a recipe and a thumbnail, we can generate the whole gallery from data, automatically. No hand-editing HTML per piece, no broken links when you add number fifty. You keep a folder of recipe files; the gallery builds itself from them.
// a portfolio is just a list of recipes rendered as a page. build it from DATA.
function buildGallery(recipes) {
const cards = recipes.map(r => {
const thumb = makeThumbnail(r, 400); // consistent thumbnail per piece
const caption = describeParams(r);
return `
seed ${r.seed} · ${caption}
`;
});
return `
Works
Works
${cards.join("")}`;
}
Feed that a list of recipe files and out comes a complete, self-contained gallery page - every thumbnail uniformly framed, every caption honest, every seed on show so a curious visitor can see the work is genuinely generative. Add a new piece? Drop its recipe in the folder and rebuild. The portfolio grows without you ever touching layout again. That auto-generation is exactly the sort of chore-killing tool we're about to spend the next stretch of the series building on purpose - hold that thought.
One detail worth adding for grown-up portfolios: a single manifest that indexes the whole collection, so the page isn't the only place the data lives.
// a manifest: the whole collection as one queryable list. the source of truth.
function buildManifest(recipes) {
return {
updated: new Date().toISOString(),
count: recipes.length,
works: recipes.map(r => ({
seed: r.seed,
palette: r.params.palette,
version: r.version,
captured: r.captured,
})),
};
}
// save this as works.json - now you (or a tool, or a site) can sort, filter,
// and count your entire body of work without opening a single image.
Last piece, quick but it matters. When you post a link to your work, the platform tries to make a little preview card, and if you don't hand it a picture it grabs something ugly or nothing at all. You've seen those sad blank link previews. Since we already know how to render a clean thumbnail, we can render a proper share card - your best frame plus a title - and point the page's metadata at it.
<meta property="og:title" content="Slow Light - generative works">
<meta property="og:image" content="https://your-site/cards/slow-light.png">
<meta property="og:description" content="Drifting light that never quite repeats.">
And the card itself is just one more deliberate render - your thumbnail, composed with a title, at the size social platforms like (roughly 1200 by 630).
// compose a share card: your art + a title, at the size social platforms want.
function makeShareCard(recipe, title) {
const c = document.createElement("canvas");
c.width = 1200; c.height = 630;
const ctx = c.getContext("2d");
ctx.fillStyle = "#0d0d0d";
ctx.fillRect(0, 0, 1200, 630); // clean dark ground
const art = loadRecipe(recipe);
art.width = 630; art.height = 630;
render(art, ctx); // the piece, filling the left
ctx.fillStyle = "#fff";
ctx.font = "600 44px system-ui";
ctx.fillText(title, 680, 320); // title on the right
ctx.font = "20px system-ui";
ctx.fillStyle = "rgba(255,255,255,0.6)";
ctx.fillText(`seed ${recipe.seed}`, 680, 370); // honest, always
return c.toDataURL("image/png");
}
Same building blocks as everything else - a seeded render, a bit of text, an honest seed in the corner. You're not doing anything new here, you're just pointing the tools you already have at the moment of sharing so your work shows up looking like you meant it to.
Step back and notice what we've really been doing this whole late stretch. We gave your work a memory (Git), a conscience (tests), an open door (accessibility), and now a voice - a way to describe and present itself so it survives being handed to someone else. And that word, handed, is the hinge. Because up to now everything we've built, you built for you. The next thing we do is cross a line: we start making work that other people pick up and use - and the only reason that's even possible is that it's now documented well enough to hand over. Documentation was never the boring admin at the end. It was the bridge to your work having a life beyond your own screen. So this week, one small assignment: take a piece you're proud of, and give it the full treatment - capture its recipe, render one clean thumbnail, and write the three-layer statement, feeling first. Then imagine handing the whole bundle to a stranger and ask yourself honestly - could they rebuild it, understand it, and feel it? If yes, you've documented it. If not, you've found exactly what to write down next :-).
og:image at it, so link previews look the way you meant instead of grabbing something uglySo that's the door I skipped past last time, finally opened. Your work is only as durable as your ability to explain it - to a stranger, to a curator, to yourself in a year when the midnight magic has faded and all that's left is what you wrote down. Capture the recipe, show the process, say the feeling first. Do that for one piece this week and it'll outlive the evening you made it. Merci for reading, and go write down how you did the thing :-).
Sallukes! Thanks for reading.
X