DOM ENGINEV0.21.3FIZLET

0.17.0 RELEASE NOTES

Fizlet 0.17.0

By Chris Cooley ↗ · · CHANGELOG.md ↗

  • Website: people and the daily contest. /people/ lists who makes the animations: Chris Cooley (the maker) and three house personas, Frame Zero, Hex Hazel and DJ Mempool, Fizlet characters that every card, page and JSON-LD node marks "House persona — a Fizlet character, not a real person" (no photos, no links). /people/<id>/ shows the avatar, role, what they make, prompt style, favorite games, hobbies, their gallery entries and two stats measured on this build (entries, total live gzip bytes). The maker's favorite games stay a KIT: placeholder ("Not picked yet") until he fills them. Data: site/contributors/<id>.json (adds kind, description, style, hobbies, favoriteGames, persona handle/promptStyle/samplePrompt, and avatar).
    • Avatars: original 32 × 32 pixel art as letter grids, drawn as inline SVG from Board UI tokens only (AVATAR_PALETTE, one path per color, crispEdges + image-rendering: pixelated); each 0.7–0.9 KB gzip, budget 2 KB in tests/people.test.mjs. The maker's is a stylized rider in a dirt-bike helmet, not a likeness.
    • /contest/: the rules (prompt ≤ 60 characters, one entry per person per day, judged by the team, at least 12 entries and one winner a day, winners featured on /gallery/ with a Winner badge), today's theme, how to enter (a GitHub issue or a PR; no backend), past winners from data, and open visitor prompts marked planned, not live. A day is site/contest/days/<date>.json (generated by the engine's fe content contest); its entries are the gallery entries stamped "contest": "<date>". The build fails on a contest prompt over 60 characters, two entries by one person on a day, an unknown author, or a winner that is not one of the day's entries or has fewer than 12 (scripts/people.mjs). Launch day 2026-10-31: Halloween.
    • Footer links to People and Daily contest; tests/site.mjs checks the measured stats, the persona line, that only the maker is a schema.org Person, and the contest page's keys.
  • Website: a themed gallery, a heroes row and contributor profiles (launch infrastructure).
    • Data, one file each: every gallery entry (site/gallery/entries/<id>.json) now has kind ("hero" or "micro"), theme and author (a contributor id; it replaces by). Themes are data (site/gallery/themes/<id>.json: title, blurb, order; Halloween, Thanksgiving, Christmas, Heroes and comics, Crypto memes, Battle of the Models, Everyday): a new theme is a file, not code. Contributors are site/contributors/<id>.json (the owner's, chris-cooley, to start). The build checks every entry against them and writes the index, gallery/index.json (each entry's page, author name and sizes measured on that build); nothing shared is hand-edited.
    • /gallery/: theme sections (an h2 and a #theme-<id> anchor each), newest first inside each, still lazy-mounted through fizlet/slot. A row of theme chips (bui-key links: each jumps to its theme without script, filters the page with it) and a search box over title, prompt, theme and author (site/gallery/browse.ts + search.ts, no dependencies; the index is fetched on the first keystroke). Pages of 24 (/gallery/page/2/ …, built only once there are more than 24). Every card says "by <name>", linked to the profile.
    • /heroes/ (new): the hero animations as large cards (at least 300 × 300 px, bigger on desktop) in a scroll-snap row; prev / next keys and the arrow keys move a card. The card in the centre plays (an IntersectionObserver band); the rest are posters. A Play key per card pauses or plays it (the only way to play under reduced motion). Full screen through the Fullscreen API, or an overlay where it is missing; Esc closes and focus returns to the key that opened it. A size badge on each card (its gallery bundle). Fizz and Layers are the first heroes.
    • /people/<id>/ (new): a contributor's profile and every animation of theirs.
    • Fixed: the gallery's "TAP TO PLAY" rule left its @media block open, so the size badge styles applied only without reduced motion.
    • tests/site.mjs: pages, sections and chips in order, index rows against the built files, attribution links, the filter and the search in a browser, the heroes row (centre plays alone, next / arrow keys, pause, full screen and the overlay with focus return, reduced motion posters and play on request) and the profiles; tests/gallery.test.mjs for the data model.
  • board-page runs in its root's own document and window. boardPage(e, { root }) now measures, observes and writes in root.ownerDocument (its viewport, getComputedStyle, observers, scroll and resize, highlights), so a board inside an iframe's page works. Before, it always used the global document and window. Swarm Protocol needs this: its API runs in the document it is given.
  • The ledger's Recall normalises style text in the element's own document, not the global one, so restoring an element in another document (or with no global document) works.
  • fizlet/perf: the card pins box-sizing: content-box. Its inline width (120px plus padding) assumed content-box, so on a page with a global * { box-sizing: border-box } (the DOM VIRUS hub and DOM Battlefield both) its right column clipped. Node test asserts it.
  • Website: a size badge on every live demo ("9.4 KB GZ"): the layers hero, the blast board, the fizz can, the quick start, each brick page's demo and every gallery entry. The number is what that demo ships, gzipped, with every chunk it imports statically (scripts/live-bytes.mjs), measured on the built file at build time and never typed; kit tokens only; screen readers get the whole description as one (role="img" + aria-label, no extra CSS needed) and the tooltip has the byte count. One component for all of them: sizeBadge(bytes, live, what) in site/size-badge.mjs (it replaces the gallery's { name, note } form; one .size-badge style). A module's own size names its scripts/sizes.mjs entry instead of a file ("size:fizlet (kernel)", the notes' kernel badge). tests/site.mjs fails if a live demo has no badge, if a badge's bytes differ from a fresh measurement of the file it names, or if it is hidden. The demos' own standalone pages (demos/fizz/, examples/fizz/) have none on their demo yet; they are listed in the test. The layers story page shows its badge in the caption, through the same component.
  • The fizz, dialled in (the brand animation, site/demos/fizz/). A tap crunches the can (it squashes and dents, and stays dented), then the tab pops. The fizz sprays out of the card: a geyser, a burst and a glug fly on a pixel layer the card's width, reaching from 70% of the card's height above it down to its bottom (no pointer), so the top of the spray is no longer cropped. The card shows the side view and a top-down view of the same moment, looking into the can as the tab opens and it fizzes (22 × 22 sprite pixels from the same tokens). The full-page zoom is gone: fizlet/camera moves only the inside of the card, in on the opening, across to the top view while it fizzes, then home. Headline "Crack the can. / Hear the fizz."; CAM 1 · SIDE / CAM 2 · TOP labels on the card (HTML, not in the chunk).
    • Bytes: the scene draws spriteURL frames (each rasterised once into a bitmap, so no SVG work per frame) instead of the whole sprites() cache. Scene and loader are two files with no shared chunk (a split cost ~260 B). The story state is plain locals. To fit, the screen shake, the third pop word, the slow bubbles, a second drip and the drip's fall off the base went (it runs down the side and splashes at the floor).
    • Measured (Chromium 153, 4× CPU throttle): live chunk 9,701 B gzip (5.3% under 10,240), loader 2,023 B, poster 3,457 B; script per frame 1.3 ms avg, 4.0 ms p95, 18.2 ms max; 0 frames settled, offscreen and with reduced motion; CLS from the slot 0. tests/fizz.mjs: the spray leaves the card, and the camera moves the card, never the page.
  • Layers story: controls (owner feedback). A play/pause key (the kit's arcade key) on one side and a 3D slider on the other that scrubs the explode/assemble timeline: vertical down the right edge when the slot is wide, along the bottom when it is tall. The slider is a small copy of the stack (eight tilted plates on a kit rail, collision in green) that fans out as the real one does, with the kit's arcade key as its knob. role="slider": arrows, Page Up/Down, Home/End, touch and mouse drag, the kit's focus ring. Scrubbing pauses; play resumes from that pose; paused, it settles (0 frames). Reduced motion: the play key is hidden and the scene never loads, but the slider still scrubs the still view (the poster is posed by one CSS variable, --e). One writer at a time: the inline controller before the scene loads, the scene (through its ledger) once it is mounted, so Recall restores the controls exactly. The figure is now labelled and described (aria-describedby) instead of role="img", so its controls stay reachable. The HUD's clock, the tank hit flash, the scene's hex fallbacks for kit tokens (the figure is always a .bui root with board-ui.css) and its data-settle option went to keep the live chunk ≥ 5% under budget on 0.15.0's bricks: 9,713 B gzip (≤ 9,728); poster, controls and inline script 5,258 B (budget 6 KB). The demo page shows the live chunk on the size badge (site/size-badge.mjs). Tests: the controls with the scene (pause settles, keys, pointer, play, Recall) and under reduced motion (keys, pointer); the budget test now fails above 9,728 B.
  • Layers story: clean labels at every size (owner feedback: "at the end the layer labels get a little jumbled"). Four causes, each fixed:
    • while the fix phase dimmed the other layers, opacity on a preserve-3d layer flattened it, so its label lost its counter-rotation and smeared into the layer's plane; the scene now dims a layer's children, never the layer;
    • the scene's state classes (o, f, b, k) could match a host page's CSS: the demo page's .k kicker stretched every label for the patched phase. Now lz--o, lz--f, lz--b, lz--k;
    • labels faded with the open amount, so they slid together and ran off the edge as the stack folded; they now show only while the stack is fully open (in after it opens, out before the fold starts);
    • the exploded gap was fixed, so on narrow slots labels sat closer than their height. The gap now widens to keep a 28px label pitch on screen, and the scene measures its widest label at mount (one layout read), so a wide fallback monospace no longer clips. The reduced-motion poster gets the same pitch rule in CSS. New browser test: through one full cycle at 390 and 1440, every visible label faces the screen, stays inside the figure and overlaps no other (fails on 0.10.0). Measured (Chromium 153, 4× CPU throttle): live chunk 9,609 → 9,717 B gzip (still ≥ 5% under 10 KB), poster + loader 2,962 → 3,153 B; script per frame 0.8 ms median, 3.1 ms p95; CLS 0; 0 frames settled.
  • Website: notes (/notes/), posts written for fizlet.com from site/notes/<slug>.json (generated, never hand-edited), on the site's own parts: section head, panels, stat cards, kit keys, and the kernel's size badge measured on the build. BlogPosting + FAQPage JSON-LD; listed in the sitemap and under Notes in llms.txt. First note: "Moving the games onto a 4 KB kernel" (every number from the size ledger and this changelog).
  • fizlet/steer (new brick, 950 B gzip, budget 1,280): seek, flee, arrive, wander and patrol for movers on typed arrays (position, heading, wander goal, patrol leg), DOM-free. The heading turns at most turn rad/s and walls are checked per axis, like DOM Battlefield Blitz's tank drive; its defaults are Blitz's numbers. A test runs wander, seek and arrive frame by frame against Blitz's practice movers, bots and wingmen (same seed, same walls: identical positions and headings). flee and patrol are the same drive pointed away from a point, or along a route. No allocation per frame across 1M frames with 16 movers: it ships its own hypot (the same bits as Math.hypot, tested), because V8 boxes Math.hypot's arguments, and blocked(i, at) gets the spot in a typed array for the same reason.
  • fizlet/coach (new brick, 1,192 B gzip, budget 1,536): the hints and tutorial layer of DOM Battlefield and Blitz. flag(key, value) is a once-only localStorage switch whose reads and writes are each wrapped in try/catch (blocked storage reads "not set"). tour(engine) runs the tutorial steps, then cycles tips, says coach:step on the ring, and allocates nothing per frame. card(host) is the Board UI coach card (DOM Battlefield's dashed card, on the kit tokens): it writes only what changed and rings the control it is about. Tests: tour frame by frame against Blitz's practice coach (steps, time on the step, the card shown, the power-up events), flag against Battlefield's tips switch (each reads what the other wrote; blocked and missing storage), and the card in Chromium.

Every release