DOM ENGINEV0.21.3FIZLET

2015 BEFORE AI · THE INTERACTIVE COMPANION

Drawn by hand in 2015.
Rebuilt in Fizlet.

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.

By Chris Cooley ↗ · Read the note: Before AI: 2015

  • 1,870 B

    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.

  1. 01 THE ORIGINAL

    2015 · CSS sprites

    Oh, that's cool.

    • no bricks
    • CSS + 4 lines 1,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.

  2. 02 THE REMAKE

    Fizlet · remake

    Same thing. Rebuilt in minutes.

    • @fzlt/core 3,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.

  3. 03 THE ARCADE

    Fizlet · arcade

    Now hit it. ×100.

    • @fzlt/core 3,372 B
    • @fzlt/core/fx 2,792 B
    • @fzlt/core/sound 1,062 B
    • @fzlt/core/hud 1,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 sprites02Fizlet · remake03Fizlet · arcade
Bricksnone
  • @fzlt/core 3,372 B
  • @fzlt/core 3,372 B
  • @fzlt/core/fx 2,792 B
  • @fzlt/core/sound 1,062 B
  • @fzlt/core/hud 1,306 B
JavaScript, gzipthe 4-line toggle · the column alone · the column alone157 B SMALLEST4,031 B11,518 B
CSS, gzipsprites.css, byte for byte · its own · its own1,713 B881 B SMALLEST884 B
Code in all, gzip1,870 B SMALLEST4,912 B12,402 B
Icon fontthe 2015 play switch · the same file · SVG icons instead11,948 B11,948 Bnone SMALLEST
3 sprite sheetsthe 2015 files, untouched706,140 B in all three
Soundnonenonea synth, no files
Animation frames a second at restafter a round, laptop openCSS loops while openloops while open, as 2015 did0 SMALLEST

03 HOW EACH ONE MOVES

Taken apart.
Every byte counted.

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.

Headphones · beats.png · 29 frames × 300 px = 8,700 px wide · 46,595 B
.beats{animation:beatsPlug 1.4s steps(13),beatsOn 1s steps(14) infinite 1.4s}
.beats.paused{animation-play-state:paused}
@keyframes beatsPlug{0%{background-position:0}100%{background-position:-3900px}}
@keyframes beatsOn{0%,100%{background-position:-4200px}33%{background-position:-8400px}}

The click: the toggle flips .paused, which swaps the animation and restarts it.

Coffee · coffee.png · 12 frames × 84 px = 1,008 px wide · 6,961 B
.coffee{animation:coffeeFill .6s steps(11) forwards .2s}
.coffee.drinkMe{animation:coffeeDrink .6s steps(11) backwards .2s}
@keyframes coffeeFill{0%{background-position:0 0}100%{background-position:-924px 0}}
@keyframes coffeeDrink{from{background-position:-924px 0}to{background-position:0 0}}

The click: the toggle flips .drinkMe, which swaps the animation and restarts it.

Laptop · computer.svg · 30 frames × 210 px = 6,300 px wide · 652,584 B
.comp{animation:comp 1.2s steps(13) .2s,compType .8s steps(8) 1.4s,compScroll 1.2s steps(10) infinite 2.2s}
.comp.close{animation:compOut .8s steps(15) backwards .1s}
@keyframes comp{0%{background-position:0 0}100%{background-position:-2730px 0}}
@keyframes compType{0%{background-position:-2520px 0}100%{background-position:-4200px 0}}
@keyframes compScroll{0%{background-position:-4200px 0}100%{background-position:-6300px 0}}
@keyframes compOut{0%{background-position:-3150px 0}100%{background-position:0 0}}

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'));
});

02 THE REMAKE

Fizlet · remake

Bricks: the kernel. Nothing else.

  • @fzlt/core 3,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.

03 THE ARCADE

Fizlet · arcade

Bricks: kernel, @fzlt/core/fx, @fzlt/core/sound, @fzlt/core/hud.

    • @fzlt/core 3,372 B
    the clock, settle, the ledger
    • @fzlt/core/fx 2,792 B
    notes, code, splash, steam, combo words
    • @fzlt/core/sound 1,062 B
    thump, clack, keys, pour, slurp: a synth, no files
    • @fzlt/core/hud 1,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.

04 THE ARCHIVE

The 2015 page.
Still on the archive.

Open the 2015 capture on the Wayback Machine ↗

Captured 23 April 2015 at chriscooley.me/projects/css-animations. Opens in a new tab.