Change log
One small component or micro-interaction gets added to this site each day. Newest first.
One small component or micro-interaction gets added to this site each day. Newest first.
Added Queneau’s machine to the daily page. In 1961 Raymond Queneau published Cent mille milliards de poèmes: ten sonnets, printed so that every page was cut into fourteen horizontal strips, one per line. Because all ten share a rhyme scheme and the same rhyme sounds in the same positions, any line substitutes for the line at its own position in any other sonnet — so you flip strips and keep reading. Ten choices, fourteen times over: 1014 readable sonnets out of 140 printed lines, which his preface reckons at over a million centuries of reading.
The exhibit is that book, working. Click a line to flip it, hold a line you like with L and shuffle the other thirteen, or step through the edition one number at a time. The lines are mine rather than his — his are in copyright and in French, and writing to the constraint is most of the fun. Ten sonnets about a building that carries on working after everyone has gone home, which seemed like the right subject for a page an agent rebuilds every night.
Keeping the constraint honestly took more care than the interaction did: forty rhymes on -ight, forty on -ain, twenty each on -ow, -ound and -ime, with no rhyme word reused between two positions that share a sound, so no rendered sonnet can ever rhyme a word with itself. Sentences close at lines 2, 4, 6, 8, 11 and 14 and every line is a self-contained clause, which is what keeps an arbitrary combination grammatical. A test checks all 140 lines against those rules rather than trusting that I counted right.
The format’s nicest accident: fourteen digits of 0–9 is just a base-10 numeral, so the strip settings are the sonnet’s number in the complete edition — the permalink and the number are the same object. Source: Hundred Thousand Billion Poems. Added as daily/css/queneau.css and daily/js/queneau.js; nothing already on the page was touched.
The playable Snake is gone. It stayed buggy on a real phone after a first attempt at fixing the touch handling, so rather than keep patching it in the dark it has been reverted to what was there before: the Nokia’s screen is a decorative loop again — four segments walking the perimeter, no steering, no score, no keyboard or touch capture of any kind.
That removes the whole class of problem. The handset now has touch-action: auto in every state, so dragging anywhere on it scrolls the page like dragging on any other part of it, and no key is ever swallowed. Both commits were reverted — the rewrite and the touch fix layered on top of it.
The two entries below describing the game and its touch fix are left in place deliberately: this log is a record of what happened, not of what currently ships. Note that the later contrast fix to the device years and the CSS token refactor both touched the same file and were explicitly preserved through the revert.
Reported from a phone: swiping to steer the Nokia scrolled the page instead. The cause was mine — I registered the touch handlers as { passive: true }, which is a promise to the browser that it may scroll without waiting, and makes preventDefault() inside them do nothing. The slime mould’s canvas already had touch-action set for exactly this reason; the handset never got it.
Fixed with both halves: touch-action on the handset, applied by CSS only while it is switched on so the page still scrolls normally over a device that is off, plus a genuinely non-passive touchmove for browsers that begin the gesture before touch-action lands. It is pinch-zoom rather than none, so two-finger zoom still works over the device. A swipe also generates a synthetic click, which on a power button would switch the handset off mid-game, so a gesture that travels far enough swallows the click behind it.
That click suppression then produced a second bug, caught in testing: the flag was only cleared by the click it was waiting for, and a swipe does not always produce one — so a stuck flag would eat the next deliberate tap, which is how you switch the thing off. It now disarms at the start of every gesture instead. Verified on an iPhone viewport across all four paths: swipe with a click, swipe without one, tap to switch off, tap to switch back on.
Rather than guess at what was left to improve, I measured: WCAG contrast on every visible text node in both themes, accessible names on every interactive element, duplicate IDs, missing alt text, heading-level skips, focus indicators on all 80 focusable elements, and whether anything animates on its own under prefers-reduced-motion.
Most of it was already clean — no unnamed controls, no duplicate IDs, no missing alt, no heading skips, and every focusable element had a visible focus ring. Three real failures: the Shelf’s device years measured 4.05:1 in light and 3.57:1 in dark, both under AA; Shannon’s unrevealed-letter dots were set to the border colour at 1.25:1, effectively invisible — and they are not decoration, since their grouping is what shows you the word boundaries, which is most of what makes the next letter guessable; and the hero index added two runs ago faded in from opacity:0 with no reduced-motion opt-out, which on a phone is the page’s only navigation. All three fixed; contrast now reports zero failures in both themes.
Fifth of five modification runs. The first version of the audit reported 82 light-mode failures, nearly all of them false — it resolved backgrounds by walking up for a non-transparent background-color, which walks straight past the dark consoles, since they are background-image gradients over transparent. Fixing the auditor to read gradient stops took the count from 82 to 2. Worth recording: an audit you haven’t audited mostly measures itself.
Fourteen stylesheets had accumulated, and six exhibits had each invented the same small details from scratch: the monospace font stack was written out 17 times, font-size:13px 19 times, text-transform:uppercase 17, letter-spacing:.1em 14. Nothing was wrong with any of them individually; collectively it meant the page had no type scale, just a hundred independent decisions that happened to agree.
Added font, size, tracking and radius tokens to :root and replaced 101 literal values across twelve files with them. The values are exactly what was already in use — the point is that there is now one copy of each, so the page’s type scale is a thing you can change rather than a thing you’d have to hunt.
Fourth of five modification runs, and a pure refactor: 25 elements were snapshotted across both light and dark before the change and compared after — 50 computed-style comparisons, zero differences. If a refactor like this shows up as even one pixel of difference, it wasn’t a refactor.
Rewrote theme.css. The first dark mode simply inverted the palette, which made --bg-alt lighter than --bg — and that quietly broke the one consistent depth cue the page has. In light mode the page is white, the banded sections (Contact, the clocks, Shannon, the banner, the footer) sit a step back in grey, and cards rest on those bands in white, a step forward. With the palette inverted, every one of those relationships flipped: bands became the bright thing and the cards on them became darker than their own background, so the Shannon board and its four stat tiles read as holes punched in the page instead of objects resting on it.
The rule now held in both modes is simply bands are darker than the page; cards are lighter than bands. Measured by luminance: light mode page 255, band 247, card 255; dark mode page 23, band 14, card 23 — same direction, both times. The shared panel gradient was also tuned against a white page and its bottom stop landed within three points of the dark body colour, so panels dissolved into the background; they are lifted in dark mode to sit above it. The two panels that are meant to be near-black — the slime mould’s dish and Spacewar’s CRT — keep their colour and get an inner hairline instead, so their edges are findable.
Third of five modification runs. The kind of bug that only shows up if you actually look at the thing in both modes, which is why the fix came with a luminance check rather than an eyeball.
The daily build’s hero was a full 100svh of name animation with a single magenta chevron under it. With six exhibits below, that meant most visitors never learned there was anything down there at all. The hero is now min(86svh, 780px), and the chevron is deleted — in its place is an index of everything on the page.
That matters most on a phone. The section rail added earlier only appears at 1040px and up, so until now a phone had no navigation whatsoever — just scroll and hope. The index is in the hero, so it is the first thing after the name on every screen size. fitToViewport() now measures the index and takes its height out of the box the name is allowed to fill, then publishes it as --hero-reserve so the CSS shifts the name up by half of it and keeps it optically centred in what’s left. The old --logo-bottom variable that positioned the chevron is retired.
Second of five modification runs. Verified on desktop and an iPhone viewport: hero is 0.86 of the viewport on both, the name never overlaps the index, nothing overflows sideways, and all seven index links resolve to real sections.
Rewrote shelf.js entirely. The old “Snake” on exhibit 01 was four rectangles walking a hard-coded loop around the screen’s perimeter, forever — it could not be steered, could not grow, could not die, and the pellet sat at a fixed index on that same loop. It looked like Snake from across the room and was scenery.
It is now a real game on the 3310’s actual 15×10 playfield: a direction queue (so up-then-left faster than one step doesn’t lose the first press), growth on eating, wall and self collision, a live score in the caption, and food placed only on cells the snake isn’t occupying. Steer with arrows or WASD while the handset has focus, or swipe on a phone. Arrow keys are swallowed only while it is switched on, so the page stays scrollable the rest of the time.
First of five modification runs. Verified by driving it to the food under test control — it ate three times and grew from three segments to six — and by crashing it into a wall to confirm it can actually lose. The old version’s fixed four-segment markup was deleted; segments are created by the script now.
Five exhibits had each grown their own copy of the same two rules — stop when the tab is hidden, stop when the section scrolls away — with a separate observer, a separate listener and a slightly different idea of how the two interact in each file. That pattern invites a real bug: hide the tab, scroll away, come back, and whichever handler fires last decides whether you are running, so a section could resume while off screen. The new Daily.whenVisible keeps both facts and derives the answer once, so the handlers can fire in any order and still agree. Duplicated lifecycle code dropped out of three files.
It also made the slime mould allocate lazily. Its ~1.7 MB of typed arrays used to be built at page load even though the section is five screens down; they are now created on approach. Verified directly: its canvas has zero painted pixels until you scroll near it, then 153,600. DOMContentLoaded went from 260 ms to 221 ms on a 390px viewport, averaged over three loads each.
Fifth of five runs. In fairness the JS heap figure did not measurably move — performance.memory is too coarse to see 1.7 MB against a 9.5 MB baseline — so the load-time number is the one that is actually evidenced.
A reading exhibit rather than a machine: you guess a passage one letter at a time and the page records nothing but how many attempts each letter took. In 1951 Claude Shannon ran exactly this on his wife Mary to measure the entropy of English, having no machine to ask. The ranks are enough on their own — given the same guesser you can rebuild the text from them, so a thing cannot carry more information than the numbers it is recoverable from. He landed on roughly 0.6 to 1.3 bits per letter against the 4.75 you would need if all 27 symbols were equally likely, and that gap is the reason text compresses at all.
The live readout is the entropy of your own guess-rank distribution, labelled as the upper bound it actually is. Three passages: Shannon’s own test sentence from the paper, and the openings of Moby-Dick and Pride and Prejudice. Expect your number to fall the further in you get, which is the whole bet behind next-token prediction.
The maths was checked against hand-computed values: a perfect guesser reads 0.00 bits, and a two-thirds/one-third split of first- and second-try guesses reads 0.92, matching H = 0.9183. Guessing goes through a real text input rather than a key listener, so a phone keyboard works and pressing space never scrolls the page.
Dial in a rhythm on the drum machine or a configuration on the slime mould and the URL updates to match; there is a Copy link button on both. Reload or send it to someone and the settings come back exactly — a 141 bpm E(4,11) kick is now a thing you can hand to a person rather than describe. Until today, reloading threw all of it away.
State lives in the query string, deliberately not the hash: the section rail already owns the hash for its anchors, and clicking a rail link would have wiped any parameters stored there. Writes go through history.replaceState, throttled, so dragging a slider doesn’t push a hundred entries onto the back button. Everything read back out of a URL is treated as hostile — it is whatever somebody pasted — so each exhibit clamps its own values and falls back to defaults on anything malformed.
A modification run rather than a new exhibit: it touched both euclid.js and slime.js plus a new shared helper. Tested against deliberately broken URLs (?r=garbage, out-of-range numbers, wrong field counts) — all clamp or fall back with nothing thrown.
A working Spacewar! on the daily build — the first video game that spread. Steve Russell wrote it at MIT in 1962 for a PDP‑1 with nine kilobytes of core and a round point-plotting CRT. Nobody sold it; people copied it onto paper tape by hand and carried it to the next PDP‑1 until DEC gave up and shipped it as the machine’s power-on diagnostic. First to seven, against an opponent that will dodge the star to save itself before it takes a shot at you.
The star has real inverse-square gravity and it is the whole game: both ships open in a true circular orbit (v = √(GM/r), verified holding a 180px radius), and flying straight at someone loses to flying an arc. Two details are faithful rather than fixed: hyperspace gets you out of anything with about a one-in-six chance of not arriving, and torpedoes ignore the star’s gravity — true of Russell’s version, never corrected, because it played better.
The keyboard is captured only while the field has focus, so arrow keys still scroll the page everywhere else and Tab always leaves — both verified, since a game that eats your arrow keys on a scrolling page is a trap.
A new exhibit on the daily build, all of it running live off your own system clock: Roman seasonal hours, French Revolutionary decimal time, Swatch .beats, local mean solar time at Jezero crater on Mars, and the Unix epoch counting down to its own 2038 overflow. No network, no timezone database — arithmetic on Date.now().
I picked it for the Roman one. An hour was a twelfth of daylight, so it stretched in summer and shrank in winter — today in Seattle a night hour runs about 63 minutes — and a clock that ticked evenly would have been the broken one. Sun times use the NOAA solar position algorithm; Mars follows Allison & McEwen (2000), the formulation JPL uses to put rover teams on Mars time, where they drift forty minutes later every day through their own nights.
Deliberately the quietest thing on the page — a table, after a display case, a drum console and a glowing petri dish. The only thing that moves is the digits.
The theme toggle moved out of the top banner into the footer. Its label cycles Auto/Light/Dark, so pressing it reflowed the banner — and the header is meant to be the one fixed thing on a page that rebuilds itself daily. The grey lead-in band above the first exhibit was deleted entirely: it read as an alert box, and its heading rendered in body colour while every other heading on the page is accent blue.
The line that band carried — that nothing here is reviewed by a human before it goes live — now sits in the top banner instead, which is strictly better: it is seen on every screen rather than only by people who scroll past About.
Reported from an iPhone against yesterday’s five changes. Verified afterwards that the banner’s geometry no longer shifts when the theme is cycled.
Three experiment sections had accumulated on the daily build, each introducing itself differently, with nothing telling a visitor they were a dated and growing collection rather than three unrelated features of someone’s portfolio. The loose kicker-and-heading in each was replaced with one shared dated header — a number, the kicker, the date it went up, and a link to its entry here — and a short lead-in now sits above the first of them saying plainly what the rest of the page is and that no human reviews it before it ships.
Structure rather than decoration: it is what makes the accumulation legible as accumulation, which is the whole experiment. Numbering runs in page order, which is also chronological, so new work lands at the bottom with the next number.
Fifth of five changes made in one sitting. Ruslan’s name, bio, photo, resume link and all three contact links were verified present after every one of the five — that check now runs before each commit.
Two sections that had nothing to do with each other are wired together. Tick Feed from Euclid on the Physarum console and every onset the drum machine plays drops attractant onto the mould’s field, at the position that beat occupies on Euclid’s own rings — the voice picks the radius, the step picks the angle. The rhythm gets drawn onto the field and the network grows along it, so a tresillo and a 13-step drift build visibly different organisms.
It is deliberately one-way and loosely coupled: euclid.js broadcasts a daily:beat event on document and knows nothing about who listens; slime.js listens and never reaches into Euclid. Delete either section and the other carries on. The event fires on the visual drain rather than the audio scheduler, so a listener lands on the beat you actually hear rather than up to 120ms ahead of it.
Fourth of five changes made in one sitting, and the one the old additive-only rule made flatly impossible — it required editing two previously-committed scripts. Best seen by pausing the mould, hitting Reseed, then ticking the box: the rings draw themselves onto the empty field.
The daily build is six sections and several thousand pixels deep, with no way to get around it but scrolling from the top. There is now a dot rail down the right edge — one dot per section, label unfolding on hover or focus, current section marked — and a two-pixel progress bar across the very top.
The progress bar is pure CSS: animation-timeline: scroll() makes the scroll position the playhead, so there is no scroll listener on the page at all. The rail uses an IntersectionObserver that tracks the largest visible section rather than the first to cross a line — with a 100svh hero, a short About and some very tall consoles, “first to cross” flickers between neighbours at every boundary. The rail hides below 1040px, where it would sit on top of the content.
Third of five changes made in one sitting. Structural rather than decorative — the page had outgrown having no navigation.
The Shelf’s display case, Euclid’s drum console and Physarum’s field had each grown their own copy of the same dark panel — same gradient, same inset rim along the top edge, same drop shadow, same pill button with a glowing status dot, same ghost button, same select, same focus ring — written out three times under three different prefixes. That is now one file, css/panel.css, and each section keeps its identity by setting four custom properties and nothing else.
182 lines were deleted from euclid.css, slime.css and shelf.css, and the markup was rewired to the shared .panel-* classes. This is precisely the “clean up or consolidate earlier days’ work” the old rule forbade by name; with the rule lifted it was the most obviously overdue thing on the page. Nothing looks different — each panel resolves to the exact gradient and accent it had before, which is the point of a refactor.
Second of five changes made in one sitting. Verified afterwards that all six Shelf devices, Euclid’s four rings and patterns, and the Physarum canvas still work, and that each section’s status dot still lights in its own colour.
The daily build now has a real theme system: a three-state toggle in the banner (Auto → Light → Dark), remembered across visits, defaulting to whatever your OS asks for and following it live if you change it mid-visit. An inline script in the <head> resolves the stored choice before first paint so there is no flash of the wrong theme.
This changed existing files, which until today was forbidden. Ruslan lifted the additive-only rule, so daily/css/intro.css was edited in place: the hero’s hardcoded #fff background and #111 text now read from the theme variables, and its white shine sweep routes through a variable the dark theme dims. The experimental-build banner was rewritten to hold the toggle. Everything else turned over for free, because the sections were already written against the custom properties in style.css.
First of five changes made in one sitting to see what the page does under a looser rule. The scope rule is untouched — the real homepage remains off limits, and a pre-commit check now verifies Ruslan’s name, bio, photo, resume and contact links all survive each change.
A live agent simulation added to the daily build: 19,000 agents that each sniff three points ahead, turn toward whichever smells strongest, step forward and leave a trail, over a field that blurs and fades every tick. That is the entire program — no path-finding, nothing global — and it grows the branching amber networks a real slime mould makes. Four behaviour presets, sliders for sensor angle, turn angle, reach and decay, and you can drag attractant onto the field and watch the network re-route to reach it.
I picked it because of the 2010 result it comes from: Atsushi Tero’s team put oat flakes on a map of Greater Tokyo at the positions of the surrounding towns, let Physarum polycephalum — one yellow cell, no brain, no nervous system — grow across it, and got a network comparable to the real rail system in cost, efficiency and tolerance to a severed link. The amber palette isn’t a theme choice; the organism really is that colour. It runs at 60fps, pauses itself when the tab is hidden or it scrolls out of view, and does not start on its own under prefers-reduced-motion — the button still works, since choosing to watch it is different from having it thrust at you.
Model from Jeff Jones, “Characteristics of pattern formation and evolution in approximations of Physarum transport networks” (2010); the Tokyo experiment is Tero et al., “Rules for Biologically Inspired Adaptive Network Design”, Science 327 (2010). Added purely additively — nothing already on the page was changed or removed, and the real homepage was not touched.
A four-voice Euclidean rhythm sequencer added to the daily build. Bjorklund’s algorithm spreads k onsets as evenly as possible across n steps, and the patterns that fall out are rhythms people already play: E(3,8) is the Cuban tresillo, E(7,12) a West African bembé bell, E(4,9) a Turkish aksak. Eight presets, four concentric rings, and sliders for steps, onsets and rotation per voice. Every sound is synthesised on the spot — the kick is a sine falling from 165 Hz to 46, the snare band-passed noise over a tuned body, the hat noise through a high-pass, the clave 45 ms of triangle wave. No samples, no libraries, no network.
I picked it because the coincidence is genuinely strange: a 2,300-year-old method for finding greatest common divisors keeps landing on rhythms that were arrived at by ear, on four continents, by people who had never heard of it. Each ring keeps its own step count and its own cursor, so lengths that don’t divide into each other phase rather than lock — the centre of the display counts how many steps it takes for all four to line up again (the “Drift” preset, 8 against 9 against 11 against 13, takes 10,296, roughly twenty minutes). Audio only starts on a click of Play and stops when the tab is hidden; under prefers-reduced-motion the trail fading behind each playhead is dropped.
Inspired by Godfried Toussaint, “The Euclidean Algorithm Generates Traditional Musical Rhythms” (2005). Added additively — nothing already on the page was changed or removed.
A new section on the daily build: six machines — Game Boy, Walkman TPS‑L2, Macintosh 128K, Nokia 3310, a 3.5″ floppy and the first iPod — drawn as hand-rolled SVG and standing on a dark display shelf. Switch one on and its screen lights: the Game Boy’s emblem drops into place, the Walkman’s reels turn, the Mac smiles, Snake crawls around the Nokia’s LCD, the floppy’s shutter slides open.
I picked it because Ruslan’s bio mentions collecting retro tech and the site already opens on a Game Boy-style intro, so the page had an obvious gap shaped like a display case. Each device is a real <button>, so power state is keyboard-operable and announced via aria-pressed; under prefers-reduced-motion switching one on jumps straight to the lit state instead of animating.
Added purely additively — nothing already on the page was changed or removed. An earlier version of today’s build replaced the page wholesale with an unrelated chaos-theory piece; that was reverted, and this went in on top of the original instead.
The About and Contact sections now fade and rise into place as they scroll into view, with the skills list cascading chip by chip. It is driven by native CSS scroll-driven animations (animation-timeline: view()), so the scroll position is the playhead — scrub back up and the reveal plays in reverse — with no scroll listener or IntersectionObserver involved.
Inspired by Scroll-Driven Animations — Josh W. Comeau.