Technology · 20 September 2026 · 4 min read
Every Card Has a Sketch
Move your mouse over a card on the blog and the cover starts doing something. How the sketches work.
Photograph © Ken Reid
Every Card Has a Sketch
Move your mouse over any card on the blog listing and the cover starts doing something. The tabu search one has queens on a chessboard getting banned from squares they just left. The LaTeX one types out \section{Why} on the left while a little page sets itself on the right. I made one for every post (67 at the time of writing), then got a bit carried away and did the homepage stats, the publications and the book wall as well. This is how they work. Part of the series on how this site is built.
This post has one too, of course. Hover here (or tab to it) to watch it.
Quick jargon guide
- Canvas: the browser element you draw on with code instead of markup. Every sketch here is a canvas laid over a card's cover photo.
- requestAnimationFrame: the browser's "call me back before the next repaint". Sixty times a second on most screens, a hundred and twenty on some laptops. This bit me, see below.
- MutationObserver: a way to get told when something new lands on the page, so code can deal with cards that show up after it loaded.
What a sketch is
Each one is a function, in essence. It gets a width, a height and a random number generator, and hands back two more functions: step, which moves things along, and draw, which paints. Here's the offline-reading one with some fat trimmed:
def('reading-this-site-offline', function (w, h, rnd) {
var t = 0; // frame counter; the engine advances this, not the browser's clock
return {
// one cycle is 300 frames: 90 online, then 210 offline, then round again
step: function () { t = (t + 1) % 300; },
draw: function (ctx) {
var off = t > 90;
// the wifi symbol: three arcs, faded out once "off"
for (var i = 0; i < 3; i++) {
ctx.strokeStyle = off ? alpha('#ffffff', 0.15) : C.ink;
ctx.beginPath(); ctx.arc(w * 0.2, h * 0.5, 10 + i * 10, 3.6, 5.8); ctx.stroke();
}
// a little page, sliding left to right, looping every 100 frames
var k = phase(t * 1.5, 100);
rect(ctx, w * 0.62 + w * 0.22 * k, h * 0.42, 14, 18, C.b);
// the caption underneath, swapping with the wifi state
text(ctx, off ? 'offline, still reading' : 'online', w * 0.5, h * 0.9, 9, off ? C.b : C.dim, 700, 'center');
}
};
});
The engine
First, the engine finds the cards. Cards on this site get rendered by five or six different scripts at different moments (the blog listing, the series pages, the related posts under each article, the homepage), and my first version made each of those scripts call the engine once it had finished rendering. Now the engine watches the page for anything being added and hooks up any card it has a sketch for.
Second, it steps sixty times a second whatever the screen is doing. The sketches count frames, and my first version stepped once per requestAnimationFrame, which is fine on a 60 Hz monitor but silly on a 120 Hz one: every ant ran at double speed, every tide went out in half the time. In a throttled background tab they crawled instead. So the engine keeps its own clock now and steps once per sixtieth of a second of real time, with a cap so a tab that's been hidden for a minute doesn't try to catch up all in one go.
Third, it doesn't load until it's needed. The three files come to 39 KB compressed, which is a lot to send a phone for a hover effect it can't trigger (no mouse, no hover, nothing to do). So no page includes them any more. The first time a mouse moves, and only if the browser says hovering is a thing it can do and you haven't asked for reduced motion, the shared script fetches all three. On a desktop they turn up about a second before you could want them, which is close enough for me, and phones don't store them at all.
Keeping them from rotting
There are two checks. A commit gets refused if any post in the listing has no sketch, so I can't publish without drawing one, to make sure I don't forget (something I do frequently without lots of checkpoints for myself!). And the test suite builds every sketch, runs it for 120 frames, and fails if it throws or the canvas is still blank at the end.
If you're reading this on a phone you've seen none of it, and that's fine. Nothing to hover with, nothing to download, and the cards are just cards.