Mio: ship the mascot as a standalone drop-in script - #505
Open
AllTerrainDeveloper wants to merge 1 commit into
Open
Mio: ship the mascot as a standalone drop-in script#505AllTerrainDeveloper wants to merge 1 commit into
AllTerrainDeveloper wants to merge 1 commit into
Conversation
Mio in one `<script>` tag, on any page, with no WordPress and no build
step on the page's side:
<script src="mio.min.js" data-mio-colliders="h1, h2"></script>
Everything under `extensions/mio-js/` — 420 kB of it is a trimmed
PixiJS, and a little over three hundred lines are ours.
**Not a copy.** The simulation, the renderer, the soft body, the
silhouettes and the palette are imported straight from `src/mio/`, so
there is one Mio and one place to change it; a retune of the springs
or a correction to the brand hues lands in the library on the next
build. Nothing is configurable, deliberately: this ships
`MIO_DEFAULTS`, the reference design, and no way to alter it. Someone
who wants a teal Mio wants the plugin.
Three things the shell provides that a blog does not:
- **PixiJS.** Bundled, published on `window.PIXI` for the duration of
the mount only and then restored, so a page with its own PixiJS
doesn't find its global swapped underneath it.
- **A layer.** Fixed and full-viewport, appended to `<body>`, with the
two rules that matter inlined. `pointer-events: none` on it is
load-bearing: only the small round handle riding the body takes
clicks, so a click one pixel off Mio reaches the link underneath.
- **A desk.** `src/colliders.ts` answers
`wp.os.getWallpaperSurfaces()` from a CSS selector, so any element
can be something Mio bumps into and settles on. Rects are content
boxes — margin and padding both off, so the boundary is the text and
not the whitespace a stylesheet parked around it — and are re-read
as the page scrolls. Only ever installed when `wp.os` is genuinely
free; on a page already running the shell, Mio uses the real desk.
Two shell dependencies are replaced at build time by aliases, each
pointing at a stand-in whose header says what it stands in for:
`../hooks` (no `wp.hooks` on a blog, and the real module throws
without it — actions come out as DOM CustomEvents instead) and
`./style-panel` (the "Make it yours" panel, which would drag the
`<os-*>` kit, the overlay loader and i18n into a bundle whose whole
point is being one file). A third shell dependency appearing in the
graph fails the build rather than shipping broken.
**On trimming PixiJS**, because the numbers are not intuitive — 880 kB
minified to start with, 420 kB now, 124 kB gzipped:
- `import * as PIXI` was worth ~400 kB on its own. A namespace import
must produce a complete object, so every export of the barrel is
retained and tree-shaking never starts. The entry names the four
symbols Mio uses and builds the global itself.
- Tree-shaking cannot touch the rest by design: Pixi registers each
feature by side effect, and every `init.mjs` is in its own
`sideEffects` allowlist. So the build redirects the unreachable ones
to an empty module — the event system (nothing has an `eventMode`),
the WebGPU renderer (`autoDetectRenderer` tries WebGL first, and no
browser has WebGPU without WebGL), `@xmldom/xmldom` (166 kB, and
reachable only from Pixi's Web Worker adapter). `PIXI_UNUSED` in
`vite.config.js` carries the argument for each. The canvas fallback
is deliberately kept: unlike WebGPU, that one is reachable.
- The failure mode of a trim is silence, so the list is pinned to a
PixiJS version and the build fails if an entry stops matching.
`BUNDLE_REPORT=1 npm run build` writes the treemap this came from.
A mount failure logs and cleans up rather than throwing — a mascot
must never take a stranger's page down with it.
`dist/` is committed on purpose: the deliverable is a file you can
hand someone, and a build output nobody can find is not one.
`demo/index.html` is a single static page with headings as collision
markers, served by `npm run demo`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Mio in one
<script>tag, on any page — no WordPress, no build step on the page's side:Everything lands under
extensions/mio-js/.Not a copy of Mio
The simulation, the renderer, the soft body, the silhouettes and the palette are imported straight from
src/mio/, so there is one Mio and one place to change it — a retune of the springs or a correction to the brand hues reaches the library on the next build, without anyone remembering to port it.Nothing is configurable, deliberately. This ships
MIO_DEFAULTS, the reference design, and no way to alter it. Someone who wants a teal Mio wants the plugin.Three things the shell provides that a blog does not
wp.os.loadModules( [ 'pixijs' ] )window.PIXIfor the duration of the mount only and then restored, so a page with its own PixiJS doesn't find its global swapped underneath it.#os-miostyled bydesktop.css<body>with the two rules that matter inlined.pointer-events: noneis load-bearing — only the small round handle riding the body takes clicks, so a click one pixel off Mio reaches the link underneath.wp.os.getWallpaperSurfaces()— the live desksrc/colliders.tsanswers the same interface from a CSS selector, so any element becomes something Mio bumps into and settles on. Content-box rects — margin and padding off, so the boundary is the text rather than the whitespace a stylesheet parked around it — re-read as the page scrolls. Only installed whenwp.osis genuinely free; on a page already running the shell, Mio uses the real desk.Two shell dependencies are replaced at build time by aliases, each pointing at a stand-in whose header says what it stands in for:
../hooks(nowp.hookson a blog, and the real module throws without it — actions come out as DOM CustomEvents:mio:mounted,mio:grabbed,mio:dropped, …) and./style-panel(the "Make it yours" panel, which would drag the<os-*>kit, the overlay loader and i18n into a bundle whose whole point is being one file). A third shell dependency appearing in the graph fails the build rather than shipping something broken.Trimming PixiJS: 880 kB → 420 kB (124 kB gzipped)
The numbers are not intuitive, so the reasoning is in
PIXI_UNUSEDinvite.config.jsand summarised here.import * as PIXIwas worth ~400 kB on its own. A namespace import must produce a complete object, so every export of Pixi's barrel is retained and tree-shaking never starts. The entry names the four symbols Mio uses —Application,Container,Graphics,BlurFilter— and builds the global itself.Tree-shaking cannot touch the rest, by design. Pixi registers each feature by side effect (two dozen
init.mjsfiles callingextensions.add( … )), and every one is in Pixi's ownsideEffectsallowlist — correct, and exactly why no bundler may remove them. The four-line init is never the cost; the pipes it registers drag their renderer subtree in behind them. So the build redirects the unreachable ones to an empty module:@xmldom/xmldom— 166 kBDOMParser). A document never selects it. Not the SVG parser — that one uses the platform's.autoDetectRenderertries webgl → webgpu → canvas. WebGPU is only reached on a browser with WebGPU but not WebGL, which does not exist.pointer-events: noneand nothing is given aneventMode— the drag is a DOMpointerdownon the handle div.preparealso cascaded away 127 kB of text renderer, the only thing that reached it.The canvas renderer is deliberately kept: unlike WebGPU, that fallback is genuinely reachable on a machine with WebGL disabled.
The failure mode of a trim is silence — a moved path stops trimming without complaining and the bundle quietly gets 400 kB heavier. So the list is pinned to a PixiJS version and the build fails if any entry matches nothing.
BUNDLE_REPORT=1 npm run buildwrites the treemap all of this came from.A mount failure logs and cleans up rather than throwing: a mascot must never take a stranger's page down with it.
Testing
viteandpixi.jsresolve from the repo root, so there is nothing to install.demo/index.htmlis a single static page with headings as collision markers, dashed so the exact collision rect is visible. Verified by hand: drag, throw, bounce off the viewport walls, land on a heading and ride it while scrolling, fall through the headings' margins, and the glow and interior sheen (the onlyBlurFilteruses, and the thing a trim could plausibly have broken).dist/is committed on purpose — the deliverable is a file you can hand someone, and a build output nobody can find is not one.🤖 Generated with Claude Code