Last time we walked off the edge of music completely and made sound art - noise, drones, a place you could sit inside. And right at the end I admitted the thing that's been quietly nagging me for about a hundred episodes now: I am so, so tired of rewriting my own lerp and my own noise helper and my own "set up a canvas and start a loop" boilerplate for the thousandth time. Every single sketch we've made started with the same twenty lines of plumbing before we got to the fun part. Allez, today we fix that. Today we build our own little creative framework - a personal toolkit, shaped exactly the way our hands work, that we reach for every time we start something new :-).
I want to be really honest about what this episode is and isn't, because it's a bit of a turning point in the series. So far you've been a user of tools - p5.js, Three.js, Tone.js, the Canvas API. Today you cross a line and become a maker of tools. That's a genuinly different headspace. Instead of asking "how do I draw a circle", you start asking "what's the nicest possible way for me to draw a circle, every time, forever". It's the difference between cooking a meal and designing your own kitchen. And like designing a kitchen, it's equal parts deeply satisfying and a giant trap you can fall into for months. So we're going to do both - build a real, useful little framework and talk frankly about when building one is a mistake.
Let me get the warning out of the way before we write a single line, because it's the most important thing in the episode. There is a famous failure mode among creative coders, and it goes like this: you sit down to make some art, you notice a bit of repeated code, you think "I'll just abstract that", and three weeks later you have a beautiful, flexible, well-documented framework and zero pieces of art. You built the kitchen and never cooked. I have done this. More than once. It feels productive because you're writing code, but you made nothing.
So here's my rule, and I'd tattoo it on your keyboard if I could: build the framework out of your sketches, never instead of them. You don't design a toolkit up front. You make ten pieces, notice that you copy-pasted the same helper into all ten, and then - only then - you lift that helper out into your toolkit. The framework is the sediment left behind by real work, not a thing you plan. If you catch yourself adding a feature "because someone might want it", stop. You are not someone. You are you. Build only what you reached for and didn't have.
Makes sense, right? Right. Now that we've agreed not to over-build it, let's build it.
Look back at literally every Canvas sketch in this series - way back to episode 1, and every animation since. They all start the same way. Grab a canvas, get its 2D context, size it, and start a requestAnimationFrame loop. Here's that boilerplate, the thing we've retyped forever.
// the boilerplate we have written a HUNDRED times. grab canvas, get context, loop forever.
const canvas = document.querySelector("#c");
const ctx = canvas.getContext("2d");
canvas.width = 800;
canvas.height = 800;
function frame() {
// ... draw something ...
requestAnimationFrame(frame); // ask the browser to call us again next repaint
}
requestAnimationFrame(frame);
That's fine, but we type it every time, and it's missing all the things we always end up adding - a frame counter, the elapsed time, handling for high-DPI screens so it isn't blurry on a retina display. So the very first brick of our framework is a function that does all of that and just hands us back a clean place to draw. I'm going to call it sketch, and it takes a single draw function that we write.
// brick #1 of our framework: sketch() sets everything up and calls YOUR draw() every frame.
// it hands you (ctx, info) so you never touch the boilerplate again.
function sketch(draw, { size = 800, id = "#c" } = {}) {
const canvas = document.querySelector(id);
const ctx = canvas.getContext("2d");
const dpr = window.devicePixelRatio || 1; // 2 on a retina screen, 1 on a normal one
canvas.width = size * dpr; // real pixels (crisp on high-DPI)
canvas.height = size * dpr;
canvas.style.width = size + "px"; // CSS pixels (the display size)
canvas.style.height = size + "px";
ctx.scale(dpr, dpr); // so we can draw in nice round numbers
let frame = 0;
const start = performance.now();
function loop(now) {
const t = (now - start) / 1000; // seconds since we started
draw(ctx, { frame, t, size }); // <-- YOUR code runs here
frame++;
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
}
Feel how much that quietly does for you. It handles the blurry-retina problem (that dpr dance) that bit us in earlier sketches, it counts frames, it measures elapsed time in seconds, and it passes all of that into your draw function as a tidy little info object. Now watch what a whole animated sketch looks like using it.
// a COMPLETE animated sketch, using our framework. this is the whole thing now.
sketch((ctx, { t, size }) => {
ctx.fillStyle = "#111";
ctx.fillRect(0, 0, size, size); // clear each frame
const x = size / 2 + Math.cos(t) * 200; // t is seconds - moves on its own
const y = size / 2 + Math.sin(t * 1.3) * 200;
ctx.fillStyle = "#e91e63";
ctx.beginPath();
ctx.arc(x, y, 30, 0, Math.PI * 2);
ctx.fill();
});
Six lines of actual art and not one line of plumbing. That's the whole promise of a framework in one screen: the boring stuff vanishes, and every new idea starts from the interesting part. This alone, honestly, is worth building. Everything else today is just more bricks like this one.
Next, the helpers. Every single piece we've made leaned on the same handful of little maths functions: lerp to blend between two values (episode 16), map to rescale a number from one range to another, clamp to keep it in bounds, and a random that can go between a min and a max. We keep rewriting these. Let's collect them into one little object so they live in one place, forever.
// brick #2: the maths we ALWAYS need. lift them out of every sketch into one home.
const M = {
lerp: (a, b, t) => a + (b - a) * t, // blend a->b by t (0..1). episode 16.
map: (v, a, b, c, d) => c + (d - c) * ((v - a) / (b - a)), // rescale v from [a,b] to [c,d]
clamp: (v, lo, hi) => Math.max(lo, Math.min(hi, v)), // keep v inside [lo, hi]
rand: (lo = 0, hi = 1) => lo + Math.random() * (hi - lo), // random float in a range
randInt: (lo, hi) => Math.floor(lo + Math.random() * (hi - lo + 1)),
TAU: Math.PI * 2, // a full turn. nicer than typing 2*PI.
};
None of this is clever - that's the point. It's yours, it's consistent, and M.map(mouseX, 0, width, 0, 360) reads the same way in every project you ever write. That consistency is a real, underrated kind of power. When your tools always behave the same, you stop thinking about them and think about the art instead. I use M.TAU about forty times a day and I will never type 2 * Math.PI again as long as I live :-).
Remember episode 24, where we made art reproducible by seeding the randomness so the same seed always gave the same picture? That was one of the most useful ideas in the whole generative-art arc, and it's exactly the kind of thing that belongs in the framework rather than being re-bolted-on each time. Let me fold a tiny seeded generator right into our toolbox so every sketch gets reproducibility for free.
// brick #3: seeded random (episode 24). same seed -> same art, every single time.
// this is "mulberry32", a tiny, good-enough PRNG. you don't need to understand the bit-twiddling.
function makeRng(seed) {
let a = seed >>> 0; // force to a 32-bit unsigned int
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296; // a float in [0, 1), just like Math.random()
};
}
The beauty is in how it plugs in. Because our M.rand just calls Math.random under the hood, we can let the framework swap in a seeded source instead, and suddenly every helper - rand, randInt, all of it - becomes reproducible without changing a single sketch. That's the payoff of having built the toolbox as one object: you change the source in one spot and the whole thing follows.
// let the sketch choose a seed, and route ALL our randomness through it.
function seeded(seed) {
const rng = makeRng(seed);
M.rand = (lo = 0, hi = 1) => lo + rng() * (hi - lo); // same signature, seeded source
M.randInt = (lo, hi) => Math.floor(lo + rng() * (hi - lo + 1));
return rng;
}
seeded(12345); // now the "random" galaxy you make is the SAME galaxy every reload. shareable.
See where this is going? A framework isn't just about saving keystrokes. It's a place to bake your best ideas in permanently, so every future project is born already knowing them. Reproducible randomness used to be a thing you remembered to add. Now it's a thing your tools just have.
Here's a pain we've all felt. Halfway through tuning a sketch, your code is full of magic numbers - 30 here, 200 there, 1.3 somewhere else - and you can't remember which knob does what. The grown-up move is to pull every tweakable value out into one params object at the top, so all your knobs live in one honest little control panel.
// brick #4: ONE params object. every magic number lives here, named, in one place.
const params = {
count: 200, // how many particles
speed: 1.3, // how fast they drift
radius: 30, // how big each one is
hue: 330, // base colour
seed: 12345, // reproducibility (episode 24)
};
That looks trivial, but it changes how a piece grows. When every setting is named and gathered in one spot, you can see your whole design at a glance, you can tweak one number and know exactly what it touches, and - this is the good bit - you're one tiny step away from making those knobs interactive. Because a params object is just data, you can walk over it and generate a slider for each number automatically. Here's a rough little auto-panel that does exactly that.
// because params is just data, we can BUILD a slider for each number automatically.
function buildPanel(params, onChange) {
const panel = document.querySelector("#panel");
for (const key in params) {
if (typeof params[key] !== "number") continue;
const slider = document.createElement("input");
slider.type = "range";
slider.min = 0;
slider.max = params[key] * 3 || 1; // a rough range around the starting value
slider.value = params[key];
slider.addEventListener("input", () => {
params[key] = parseFloat(slider.value); // live-edit the param...
onChange && onChange(); // ...and tell the sketch to react
});
const label = document.createElement("label");
label.textContent = key;
panel.append(label, slider);
}
}
Drag a slider and your art changes live. We are only just scratching the surface of that idea here - proper control panels get a whole episode of their own later - but even this scrappy version turns a static sketch into a little instrument you can play with your mouse. That's the moment tuning stops feeling like editing code and starts feeling like playing.
A creative framework that can't save its output is only half a tool. Way back in episode 20 we learned to export a canvas as a PNG and record it to video, and in episode 29 we cared about clean output for the blockchain. Both of those are pure framework material - you want them to be one function call away, always. So let me add a save button to our toolbox.
// brick #5: save the current frame as a PNG (episode 20). one call, from anywhere.
function savePNG(canvas, name = "sketch") {
const link = document.createElement("a");
link.download = `${name}-${Date.now()}.png`; // timestamp so you never overwrite a good one
link.href = canvas.toDataURL("image/png"); // the canvas as an image, right now
link.click();
}
// bind it to a key so hitting "s" any time grabs the frame you're looking at:
window.addEventListener("keydown", (e) => {
if (e.key === "s") savePNG(document.querySelector("#c"));
});
And because this is your framework, you get to make it fit your habits. I make a lot of pieces for print, so my real save helper also renders a second time at 4x the resolution before grabbing the PNG, so the file's big enough to print without going fuzzy. Here's the shape of that idea - render huge, save, shrink back.
// export at HIGHER resolution than the screen - for print, or a crisp NFT (episode 29).
function saveHiRes(draw, size, scale = 4, name = "sketch") {
const big = document.createElement("canvas"); // an off-screen canvas, much bigger
big.width = size * scale;
big.height = size * scale;
const ctx = big.getContext("2d");
ctx.scale(scale, scale); // draw at normal coords, but denser pixels
draw(ctx, { frame: 0, t: 0, size }); // render ONE frame into the big canvas
savePNG(big, name + "-hires");
}
That's the whole philosophy of framework-building in one function, really. Nobody else's toolkit renders at exactly 4x by default, because that's my need, from my work. Yours will have different bricks - maybe you export SVG for a pen plotter (episode 105), maybe you always want a transparent background. The framework is a portrait of the kind of art you make.
Right now our bricks are scattered - sketch, M, seeded, savePNG. That works, but as it grows it'll start colliding with variable names in your actual sketches, and it feels messy. The tidy move is to gather everything under one single name - I call mine CC - so the whole framework is one object you reach into. This is the same "one thing in the middle" idea we kept hitting in the audio arc, just applied to our tools.
// brick #6: gather it all under ONE namespace so it never collides with your sketch code.
const CC = {
sketch,
savePNG,
saveHiRes,
seeded,
buildPanel,
...M, // spread the maths helpers in too: CC.lerp, CC.map, CC.TAU, ...
};
// now a whole project only ever touches ONE global. clean.
CC.seeded(999);
CC.sketch((ctx, { t, size }) => {
ctx.fillStyle = "#111";
ctx.fillRect(0, 0, size, size);
for (let i = 0; i < 5; i++) {
const x = CC.map(i, 0, 4, 100, size - 100); // evenly spaced (episode 5's grids)
const y = size / 2 + Math.sin(t + i) * 100;
ctx.fillStyle = `hsl(${i * 60}, 70%, 60%)`;
ctx.beginPath();
ctx.arc(x, y, 20, 0, CC.TAU);
ctx.fill();
}
});
One import, one name, everything in it. Ship CC as a single file, drop it into any new project, and you're drawing art in three lines. That's a finished tool. When you get here, genuinly stop and use it for a while before adding anything else - go make five pieces with it. The framework earns its next feature by you missing that feature during real work, remember?
I told you at the top that framework-building is a trap, and now that you've seen how nice it is to build one, I have to say it again louder, because the nicer it feels the more dangerous it gets. Every feature you add to a framework is a feature you now have to maintain, remember, and work around when it doesn't fit. A toolkit with fifty helpers you half-remember is worse than five you know cold. Complexity has a cost even when each piece is small.
There's a name for the thing you should watch for: premature abstraction - solving a problem you don't have yet. The cure is a stupid-simple rule I actually follow. Don't abstract a pattern until you've written it three times. Once is fine, just write it. Twice, copy-paste it and feel a little itch. The third time you write the same thing, that's the universe telling you it's a real pattern, and now you lift it into CC. Not before.
// the rule that keeps a framework from eating your life: the "rule of three".
// 1st time you write a pattern: just write it, inline.
// 2nd time: copy-paste it. yes, really. resist the itch to abstract.
// 3rd time: NOW lift it into your framework. three real uses = a real pattern.
//
// abstracting on the FIRST use is guessing. you'll guess the shape wrong and
// build a flexible thing nobody needs. wait for the pattern to prove itself.
I know it feels backwards - copy-pasting on purpose, when every instinct screams "don't repeat yourself". But early abstractions are almost always wrong abstractions, built around a guess about the future. Waiting for the third use means you abstract around evidence instead of a hunch, and the helper you build actually fits. A little bit of duplication is far cheaper than the wrong abstraction. That's maybe the most useful engineering lesson in this whole series, and it applies way beyond art.
Here's the brief, and it's a lovely one because the deliverable is a tool you'll actually keep. Start your own CC framework - but grow it, don't design it. Begin with just sketch() and the M maths helpers from today, nothing more. Get them into one file. Then go back through a few of your favourite sketches from this whole series - your particle system, your flow field, a shader piece, whatever you loved - and rebuild them on top of your CC.
Here's the crucial part: as you rebuild, keep a little "wishlist" comment at the bottom of the file. Every time you reach for something the framework doesn't have and you have to write it inline - a colour-palette helper, a grid iterator, a save-as-SVG - jot it on the wishlist. Do not add it yet. Rebuild three or four sketches first. Then look at your wishlist and see which things you wrote three or more times. Those - and only those - earn a place in CC. Everything you wrote once stays inline. That's the rule of three, applied to your own hands.
// your starting point + your discipline. begin tiny, let the sketches tell you what to add.
const CC = { sketch, ...M }; // that's IT. resist adding more until you've earned it.
// WISHLIST (things I reached for and didn't have - tally the marks):
// palette() || <- reached for it twice. not yet.
// grid() ||| <- three times. THIS one graduates into CC now.
// saveSVG() | <- once. stays inline.
Build the framework out of your art, not instead of it. If at the end of an afternoon you have a scrappy little CC file and four rebuilt sketches, you did it exactly right. If you have a gorgeous 500-line framework and no art, you fell in the trap - and now you know its name :-).
Building your own tools opens a door that stays open for the rest of your creative-coding life, and it changes what kinds of questions you can even ask. Once you own the loop and the setup and the export, you start noticing new things about your sketches - like why is this one running at eight frames a second when that one flies? Your framework is now the natural place to answer that, because it's the one bit of code every sketch shares. And once you've got tidy tools and they run fast, the next thought is nearly always "could I make this into something a non-coder could play with?" - a proper interface, knobs and buttons, a thing you hand to a friend. Both of those threads pull straight out of the toolkit we started today, and both are exactly where we're headed next. Build your little CC first though - genuinly rebuild a few sketches on it - because everything coming up assumes you've got a home of your own to build in.
sketch() is brick one: it handles the canvas, the high-DPI (retina) blur fix, the animation loop, a frame counter and elapsed time - so every new piece starts from the interesting line, not twenty lines of boilerplatelerp, map, clamp, rand, TAU) collected once means every project reads the same and behaves the same. Consistency is quiet power - you stop thinking about the tools and think about the artM.rand through a seeded sourceparams object - it's an honest control panel, and because it's just data you can auto-generate a slider for each knob and tune your art livesavePNG, hi-res render - episodes 20 and 29) belong in the framework too. Yours will fit your habits: print-res, transparent, SVG - the toolkit is a portrait of the art you makeCC) so it never collides with sketch code - ship it as one file, draw art in three linesSo that's the leap the sound-art episode was quietly setting up: from someone who uses creative-coding tools to someone who builds them. And the wild part is how little code it took - a sketch bootstrap, a handful of maths helpers, a save button, one namespace to hold them - and suddenly every future project of yours is born already knowing your best tricks. Just remember the trap. Make the art, let the toolkit settle out of it, and abstract only what you reached for three times. Go build yourself a kitchen - then actually cook in it :-).
Sallukes! Thanks for reading.
X