How the Interactive Posts Work
Someone on Cyberspace (new social media website that has such a cool vibe to it) recently asked me how I set up the simulators on my website, if I used a specific library. I responded saying how it's raw javascript, but that got me thinking: I intend to keep making these kind of educational and fun blogs, so why not make a little framework to make it easier on myself?
The algorithms themselves need to be separate, I wasn't planning on making a new JMetal, DEAP or similar. Part of the series on how this site is built.
Quick jargon guide
- Canvas: an HTML element that gives JavaScript a rectangle of pixels to draw on, every frame, with no memory of what was there before.
- devicePixelRatio: how many physical screen pixels sit behind one CSS pixel. Ignoring it can cause canvas drawings to come out blurry on phones and retina displays.
- CSS custom properties: variables that live in stylesheets (
--viz-ink: #2b2b2b). Scripts can read them at runtime, which is the hinge this whole system turns on. - MutationObserver: a browser API that fires a callback when part of the page changes, like an attribute being flipped by a theme toggle.
- Seeded RNG: a random number generator that produces the same "random" sequence every time you start it from the same seed.
- requestAnimationFrame: the browser's "call me before the next repaint" scheduler, the correct heartbeat for animation loops.
The CSS owns the colours
I always enjoyed color theory in computer science. The RGB values, pixels (did you know it stands for "picture element"?), bitmaps vs JPEGs vs other formats, it's super interesting to me. On this website, the theme toggle cannot restyle it the way it restyles text, and the first version of the ant post hardcoded its colours in JavaScript and looked wrong in one theme or the other. Every widget's colours live on its wrapper element as CSS custom properties, with a dark-theme override block, and the drawing code reads them fresh at draw time:
function vizColors(el) {
var cs = getComputedStyle(el);
function v(name) { return cs.getPropertyValue(name).trim(); }
return {
series: [v('--viz-s1'), v('--viz-s2'), v('--viz-s3'), v('--viz-s4')],
ink: v('--viz-ink'), muted: v('--viz-muted'),
grid: v('--viz-grid'), surface: v('--viz-surface')
};
}
new MutationObserver(function () { draw(); })
.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
The observer redraws the moment data-theme gets toggled, so a running simulation changes palette mid-run. This has no practical value whatsoever.
Randomness must be reproducible
Math.random() is banned from the demos. Every widget gets its randomness from mulberry32, a seeded generator:
function mulberry32(seed) {
var a = seed >>> 0;
return function () {
a |= 0; a = (a + 0x6D2B79F5) | 0;
var t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
When a reader reports that the race chart does something odd around 90,000 evaluations, I can watch the same 90,000 evaluations they did. "New cities" buttons just bump the seed.
Phones are the real test
No canvas ever gets a fixed rendered height now: height is computed from measured width, so narrow screens get a taller aspect instead of a squashed ribbon. Side-by-side panes stack vertically below ~480px, and sliders become full-width rows on small screens.
A canvas only gets touch-action: none if it has drag interaction, so chart canvases never trap a reader mid-scroll.
And because rules I merely remember are rules I eventually break, the test suite enforces them. The smoke test auto-discovers every blog page containing a <canvas> tag (no registration list to forget to update) and loads each at 360px wide in a headless browser. It fails the build on horizontal overflow, a canvas wider than the viewport, a canvas squashed under 100px tall, or any console error.
The chassis checklist
What every new interactive post starts from:
- A wrapper div carrying the
--viz-*palette, light values plus a dark override block. vizColors()read at draw time, plus a MutationObserver for theme flips.fitCanvas()handling devicePixelRatio and re-running on resize.- mulberry32 for every random draw, seeds visible near the top of the file.
- A requestAnimationFrame loop with a speed slider controlling work per frame, not frame rate.
role="img"and a realaria-labelon every canvas.- A standalone single-file version of the demos in
blog/downloads/.
Common questions
Why raw canvas instead of a chart library?
The demos ARE the content, and libraries make the common case easy at the price of making the odd case a bit more awkward. A four-way race chart with collision-nudged end labels, step-function lines, and a temperature strip sharing its x-axis is the odd case.
Don't the simulations drain phone batteries?
They only run while you press Run, and they do their work inside a requestAnimationFrame budget, so the browser throttles them in background tabs and off-screen.
Why not SVG, which the theme could style directly?
SVG restyles beautifully but costs per element, and these simulations push thousands of points and segments per frame.