Last time we lifted our heads up out of the code for once and gave the edge of chaos a proper name, and I told you the arc from here on is about the work itself, not the next algorithm. So let's actually do that. Today we build the thing that turns a folder full of sketches into a body of work: a portfolio. And I want to be honest with you right away, this is the episode I most wish someone had sat me down and given me years ago, because for the longest time I had hundreds of little pieces scattered across three laptops and no way for anyone (including me) to see them as a whole.
A portfolio is not a backup folder. It's a choice. It's you standing in front of everything you've ever made and deciding what represents you, then presenting it so a stranger can feel in thirty seconds what took you years to learn. That's a genuinly diffrent skill from making the art, and nobody teaches it. So let me show you how I actually do it - the selecting, the exporting, the presenting, and the little bits of code that make the whole thing far less painful than doing it by hand. Allez, let's build your gallery.
Here's the first hard truth, and I learned it the embarrassing way by showing someone a folder of 200 images and watching their eyes glaze over. Your portfolio should be small. Ten strong pieces beat a hundred okay ones, every single time. A curator, a client, a fellow artist - they don't have the patience to dig for your best work, and honestly it's not their job to. The kindest and smartest thing you can do is dig for them.
So the whole job starts with data, not pixels. Before I present anything I write down every candidate piece as a plain little record - a manifest. This is nothing clever, just an honest list of what exists and what I think about it:
// a portfolio is data before it's pixels. describe every candidate piece as a record.
const pieces = [
{ id: "colonies-1234", title: "Negotiated Borders", seed: 1234,
year: 2026, tags: ["agents", "flow"], keep: null, note: "the border ink really sings" },
{ id: "dejong-88", title: "Trapped Light", seed: 88,
year: 2026, tags: ["attractor"], keep: null, note: "one of maybe 40 seeds I tried" },
{ id: "reaction-07", title: "Turing Bloom", seed: 7,
year: 2025, tags: ["reaction-diffusion"], keep: null, note: "old, but still holds up" },
];
That keep: null field is deliberate. Right now nothing is decided. Every piece is a maybe. The work of building a portfolio is turning all those nulls into honest true or false answers, and that's the next bit.
I can't tell you which of your pieces are good, but I can give you the questions I ask myself, and I can turn them into a little scoring pass so I'm not just going on mood. For each piece I score three things out of five: how much I still love it, how well it shows a distinct idea (not a near-duplicate of another piece), and how finished it feels. Then I only keep pieces that clear a bar on all three:
// score each piece honestly, then keep only the ones that clear the bar on ALL axes.
// love = do I still feel something; distinct = not a near-twin of another; done = actually finished.
function curate(pieces, scores, bar = 4) {
return pieces.map((p) => {
const s = scores[p.id] || { love: 0, distinct: 0, done: 0 };
const keep = s.love >= bar && s.distinct >= bar && s.done >= bar;
return { ...p, keep, score: s.love + s.distinct + s.done };
});
}
The && matters more than a total would. A piece you adore that's basically a colour-swap of another piece still gets cut, because a portfolio full of near-twins reads as one idea, not ten. I want each surviving piece to earn its wall space by being unmistakably its own thing. Once scored, I sort what survives and take the top handful:
// keep the survivors, best-first, and cap the whole thing. small is strong.
function shortlist(scored, max = 10) {
return scored
.filter((p) => p.keep)
.sort((a, b) => b.score - a.score)
.slice(0, max);
}
When I first ran this on my own work I went from 180-something sketches to eleven, and cutting hurt. But you know what? The eleven looked like an artist. The 180 looked like a person practising. See the diffrence? Practice is private. The portfolio is the recital.
Right, we have our shortlist. Now every piece needs to exist as a real, high-resolution image - not the 800-pixel canvas you happened to have open, but something crisp enough to print or blow up on a big screen. And here's where all our seed discipline from episode 24 pays off enormously: because our systems are reproducible, we can re-render any old piece at any size, today, from just its seed. The canvas from six months ago is gone, but the seed regrows it perfectly.
The trick for hi-res is to render into a big off-screen canvas, not the one on the page:
// re-render a piece at print resolution into an off-screen canvas.
// because the art is seeded, this regrows the EXACT piece at any size we like.
function renderHiRes(drawFn, seed, size = 3000) {
const canvas = document.createElement("canvas"); // never touches the DOM
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d");
drawFn(ctx, size, size, seed); // your sketch, told to fill this size
return canvas;
}
Notice the sketch has to accept a size rather than hard-coding 800. This is the one habit that separates work you can present from work you can't: never bake the canvas dimensions into your drawing code. Pass width and height in, express everything as a fraction of them, and future-you can export a postage stamp or a poster from the same function. Then getting a file out is a two-liner:
// turn any canvas into a downloadable PNG file.
async function exportPng(canvas, filename) {
const blob = await new Promise((res) => canvas.toBlob(res, "image/png"));
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url; a.download = filename;
a.click();
URL.revokeObjectURL(url); // tidy up after ourselves
}
A gallery page can't load ten 3000-pixel monsters, your reader's browser would cry. So every full render also needs a small, fast thumbnail - and crucially, all your thumbnails should be the same size and shape, or the gallery looks like a jumble sale. I generate them by drawing the big canvas down into a small one:
// shrink a big render into a tidy, uniform thumbnail. same size for ALL pieces.
function makeThumbnail(bigCanvas, thumbSize = 400) {
const thumb = document.createElement("canvas");
thumb.width = thumbSize;
thumb.height = thumbSize;
const ctx = thumb.getContext("2d");
ctx.imageSmoothingQuality = "high"; // don't let the downscale go crunchy
ctx.drawImage(bigCanvas, 0, 0, thumbSize, thumbSize);
return thumb;
}
Downscaling from a big render, by the way, gives you a nicer thumbnail than rendering small in the first place - all that extra detail averages down into smooth, clean edges. It's basically free anti-aliasing. Little wins like that are why we bother rendering big even for the small version.
Here's a thing curators and other artists genuinly love to see, especially for generative work: not just your one chosen piece, but the family it came from. Because our art is seeded, we can lay out a grid of variations - seed 1, seed 2, seed 3 - and show that behind the single beautiful frame there's a whole system that could have produced thousands. That grid is called a contact sheet, borrowed straight from film photography:
// a contact sheet: render the same system across many seeds into one grid image.
// this shows people your piece is a SYSTEM, not a lucky one-off.
function contactSheet(drawFn, seeds, cellSize = 200) {
const cols = Math.ceil(Math.sqrt(seeds.length));
const rows = Math.ceil(seeds.length / cols);
const sheet = document.createElement("canvas");
sheet.width = cols * cellSize;
sheet.height = rows * cellSize;
const ctx = sheet.getContext("2d");
seeds.forEach((seed, i) => {
const cell = renderHiRes(drawFn, seed, cellSize); // reuse our hi-res renderer, small
ctx.drawImage(cell, (i % cols) * cellSize, Math.floor(i / cols) * cellSize);
});
return sheet;
}
I put one of these on nearly every generative project page now. It quietly tells the viewer "the thing you're looking at is one frame from a living system", and that reframes your single image from a picture into evidence of a process. For generative art specifically, that's often the most impressive thing you own.
Way back in episode 139 we talked about documenting creative work as you go, and this is the episode where that pays its dividend. A finished image is nice. The story of how it got finished is what makes people trust you and remember you. So alongside each piece I keep a tiny process log - a handful of dated snapshots with a one-line note about what changed:
// a lightweight process log: dated snapshots, each with a note about what changed.
// keeping this AS YOU WORK is the whole trick. reconstructing it later is a nightmare.
function logStep(history, note, seedOrParams) {
history.push({
step: history.length + 1,
note,
params: seedOrParams,
});
return history;
}
let history = [];
logStep(history, "first pass, borders too harsh", { seed: 1234, ink: 0.9 });
logStep(history, "softened the ink, added lightness wobble", { seed: 1234, ink: 0.55 });
logStep(history, "this is the one", { seed: 1234, ink: 0.55, palette: "warm" });
Look at that third entry - "this is the one". When you present work, showing the messy first pass next to the final is worth a thousand words of artist statement. It proves you made decisions, that the piece is the result of taste applied over time, not a single lucky roll of the dice. People connect with the struggle far more than the polish. 't Komt erop neer: keep the receipts.
Okay, we have curated pieces, hi-res exports, uniform thumbnails, contact sheets, and process notes. Now we present it, and the honest best place to do that is your own website. Not someone else's feed that changes its rules every year - your own patch of the internet that you control. And the lovely thing is, because all our pieces are just data records, we can generate the gallery HTML straight from the manifest instead of hand-writing a hundred lines of markup:
// generate the gallery markup straight from the manifest. data in, webpage out.
function galleryHtml(pieces) {
return pieces.map((p) => `
${p.title} (${p.year})
`).join("\n");
}
That loading="lazy" on the image is not a throwaway detail, it's the browser's built-in way to only fetch a thumbnail when the reader scrolls near it, so a gallery of fifty pieces still loads instantly. For anything the native attribute can't cover, you can do it yourself with an observer - watch each image and only set its real source when it's about to come into view:
// manual lazy-load for full-size images: only fetch when the piece scrolls into view.
function lazyLoadFull() {
const io = new IntersectionObserver((entries, obs) => {
for (const entry of entries) {
if (!entry.isIntersecting) continue;
const img = entry.target;
img.src = img.dataset.full; // swap placeholder -> real image
obs.unobserve(img); // load once, then stop watching
}
}, { rootMargin: "200px" }); // start a little before it's visible
document.querySelectorAll("img[data-full]").forEach((img) => io.observe(img));
}
For a creative coding portfolio you often want a piece to run, live, right there on the page - that's half the magic, right? But ten sketches all animating at once will melt a laptop. So the same idea we just used for images applies to running sketches: only let a sketch animate while it's actually on screen, and pause it the moment it scrolls away:
// only run a live sketch while it's visible. offscreen sketches must go quiet.
function runWhenVisible(el, start, stop) {
const io = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) start(); // on screen -> come alive
else stop(); // scrolled away -> shut up and save the CPU
}, { threshold: 0.2 });
io.observe(el);
}
This one habit is the diffrence between a portfolio that feels slick and one that spins up your visitor's fans until they close the tab. Be kind to their battery and they'll stay longer. We spent a whole episode on performance earlier in the series - this is that thinking applied to the gallery instead of the artwork.
One more thing that quietly marks out a professional portfolio from a hobby dump: every piece carries its own caption, generated from the same record so it can never drift out of sync. Title, year, the system it came from, and - because we're proud of it - the seed, so the piece is honestly reproducible by anyone:
// build a consistent caption from the record. one source of truth, no manual typing.
function caption(piece) {
const tags = piece.tags.join(", ");
return `${piece.title} (${piece.year}) - generative, ${tags} - seed ${piece.seed}`;
}
console.log(caption(pieces[0]));
// "Negotiated Borders (2026) - generative, agents, flow - seed 1234"
Putting the seed right in the caption is a small act of generosity that says "this isn't a trick, here's exactly how to regrow it". It also protects you - months later when someone asks "how did you make that one?", the answer is printed right under the image.
Last piece, and it's a purely practical one. A portfolio has to look right on a phone and on a big monitor, which means the number of columns should adapt to the width available. You can do most of this in CSS, but it's worth understanding the little bit of maths underneath - pick a comfortable target width per piece, then fit as many whole columns as will honestly fit:
// choose a column count that fits the screen: aim for a target width, never fewer than one.
function columnsFor(screenWidth, targetPiece = 320, gap = 16) {
const cols = Math.floor((screenWidth + gap) / (targetPiece + gap));
return Math.max(1, cols); // always at least one column
}
console.log(columnsFor(1440)); // 4 on a laptop
console.log(columnsFor(390)); // 1 on a phone
That Math.max(1, ...) guard is the same defensive habit we keep coming back to - never let a calculation hand you zero columns and a blank page on a narrow screen. Small guard, big save. Wire that up to a resize listener and your gallery breathes with the window, which is exactly what you want a stranger's first impression to do.
&&. Near-twins get cut, because a portfolio of duplicates reads as one ideaSo that's the whole machine, from a messy folder of sketches to a gallery a stranger can walk through in a minute and come away knowing exactly who you are as an artist. And I want to leave you with the bit that actually matters more than any of the code: the hard part was never the exporting or the HTML, it was the choosing. Standing in front of everything you've made and being honest about what's strong and what was just practice - that's the real work, and it's a muscle you build the same way you built all the others, by doing it and doing it again.
Here's the thread forward. A portfolio sitting quietly on your own website is a lovely thing, but it's still you, alone, in your room. At some point the work wants to go out - in front of real eyes, into rooms and screens where strangers who owe you nothing decide whether to stop and look. There are whole worlds of open calls and juried spaces for exactly that, with their own quiet rules about how to approach them, and next time we start knocking on those doors. 't Was plezant to help you build your gallery today :-).
Sallukes! Thanks for reading.
X