In 2015, years before AI coding tools, Chris Cooley drew three micro animations frame by frame and played them with CSS sprites: headphones that plug in and play, a coffee cup that fills, a laptop that opens and types. The Wayback Machine kept them. Here they are live, beside the same three on Fizlet: once as they were, once as an arcade.
2015, gzip: the CSS and the 4-line toggle. Bricks: none
4,912 B
the remake, gzip: JS (the kernel and the strip that plays the frames) and CSS
12,402 B
the arcade, gzip: JS (kernel, @fzlt/core/fx, @fzlt/core/sound, @fzlt/core/hud) and CSS
0
animation frames a second the arcade asks for once a round is over
“There were a lot of animations before Fizlet, and in between. We kept writing the same thing over and over, a little different each time for a slightly different use. So we built Fizlet to make it easier for us. Then we figured other people could get something out of it too, so we open-sourced it.”
Chris Cooley · why Fizlet exists
01 THREE WAYS · LIVE
Same sprite sheets. ×100 the life.
Press the switches in the first two columns. In the arcade, hit everything: the headphones, the cup, the keys.
2015
01 THE ORIGINAL
2015 · CSS sprites
Oh, that's cool.
no bricks
CSS + 4 lines1,870 B
The 2015 CSS, byte for byte, and the 4-line class toggle that stood in for Angular's ng-click. The headphone switch is a div, as it was: mouse only.
REMAKE
02 THE REMAKE
Fizlet · remake
Same thing. Rebuilt in minutes.
@fzlt/core3,372 B
Same frames, timings and clicks, rebuilt in about 6 minutes of agent time (container clock). The kernel owns the clock instead of @keyframes, and sleeps when nothing moves.
×100
03 THE ARCADE
Fizlet · arcade
Now hit it. ×100.
@fzlt/core3,372 B
@fzlt/core/fx2,792 B
@fzlt/core/sound1,062 B
@fzlt/core/hud1,306 B
Score 0Combo ×0Best ×0
The same 2015 frames. Hit the cups: bass, bounce, notes, an equalizer on the phone. Pour and sip: splash, steam, slurp. Open the lid, then hammer the keys: clicks, code spray, a lit screen. Hit fast to chain the combo.
Reduced motion: the 2015 column and the remake hold their first frame (a wrapper rule on this page; 2015 had none), and the arcade lands each press on its still frame with no particles, squash or shake; it makes a sound only when you press, and the Sound key mutes it. At rest the page asks for no animation frames, even after a round in the arcade; the site's tests count them on every build. On the page both Fizlet columns ship as one script, 14,218 B gzip (they share the kernel), plus 1,425 B of CSS. The sprite sheets are the same files in all three.
Give the page back: every engine stops and every style, class and attribute Fizlet wrote goes back as it was. Press either Fizlet column to start it again.
02 ONE COMPARISON · MEASURED
Side by side. One comparison.
Measured on this build · gzip unless marked
012015 · CSS sprites
02Fizlet · remake
03Fizlet · arcade
Bricks
none
@fzlt/core3,372 B
@fzlt/core3,372 B
@fzlt/core/fx2,792 B
@fzlt/core/sound1,062 B
@fzlt/core/hud1,306 B
JavaScript, gzipthe 4-line toggle · the column alone · the column alone
157 BSMALLEST
4,031 B
11,518 B
CSS, gzipsprites.css, byte for byte · its own · its own
1,713 B
881 BSMALLEST
884 B
Code in all, gzip
1,870 BSMALLEST
4,912 B
12,402 B
Icon fontthe 2015 play switch · the same file · SVG icons instead
11,948 B
11,948 B
noneSMALLEST
3 sprite sheetsthe 2015 files, untouched
706,140 B in all three
Sound
none
none
a synth, no files
Animation frames a second at restafter a round, laptop open
CSS loops while open
loops while open, as 2015 did
0SMALLEST
03 HOW EACH ONE MOVES
Taken apart. Every byte counted.
2015
01 THE ORIGINAL
2015 · CSS sprites
Bricks: none. CSS and 4 lines.
The process: draw every frame by hand, lay the frames side by side on one long sheet, then show the sheet through a box one frame wide. @keyframes slide background-position from the first frame to the last, and steps(n) makes it jump frame to frame instead of gliding. A click toggles one class, which swaps the animation.
The click: the toggle flips .close, which swaps the animation and restarts it.
The 2015 toggle, all 4 lines
document.addEventListener('click', function (e) {
var sw = e.target.closest('[data-toggle]');
if (sw) sw.parentNode.classList.toggle(sw.getAttribute('data-toggle'));
});
REMAKE
02 THE REMAKE
Fizlet · remake
Bricks: the kernel. Nothing else.
@fzlt/core3,372 B
The @keyframes become data: each segment is a first frame, a last frame, the jumps and the seconds, read by one engine per sprite. beatsPlug 1.4s steps(13) is { a: 0, b: 13, n: 13, d: 1.4 }. The kernel owns the clock, writes background-position through the ledger only when the frame changes, and sleeps when nothing plays.
// strip.ts: the frame at time t, exactly steps(n), jump-end
a + floor((t - w) / d * n) / n * (b - a)
Like 2015, the laptop's code scrolls forever once it is open, so the remake keeps asking for frames while it does. That is the fair copy.
thump, clack, keys, pour, slurp: a synth, no files
@fzlt/core/hud1,306 B
score, combo, best: change-only writes
The art is the 2015 frames, untouched. Squash, wobble and the stage shake are one style each, written through the ledger and given back when they stop. The particles draw on one canvas per sprite, only while one is alive. Each sprite's engine and the column's own (HUD, combo, shake) settle on their own, so after the last spark the page asks for nothing.
Measured on this build (site/demos/before-ai-2015/build.mjs): a brick's size is the whole brick, minified and gzipped with everything it pulls in, as npm run size measures it; a column's size is that column alone, bundled and gzipped the way this page's script is, so it counts only the parts of each brick it uses. Sheet and font sizes are the files on disk. The 0 at rest is counted by tests/site.mjs in Chromium on every build.