Skip to content

Mio: ship the mascot as a standalone drop-in script - #505

Open
AllTerrainDeveloper wants to merge 1 commit into
trunkfrom
feature/mio-standalone
Open

Mio: ship the mascot as a standalone drop-in script#505
AllTerrainDeveloper wants to merge 1 commit into
trunkfrom
feature/mio-standalone

Conversation

@AllTerrainDeveloper

@AllTerrainDeveloper AllTerrainDeveloper commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Mio in one <script> tag, on any page — no WordPress, no build step on the page's side:

<script src="mio.min.js" data-mio-colliders="h1, h2"></script>

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

Shell Here
PixiJS via wp.os.loadModules( [ '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.
#os-mio styled by desktop.css A fixed full-viewport layer on <body> with the two rules that matter inlined. pointer-events: none is 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 desk src/colliders.ts answers 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 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: 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_UNUSED in vite.config.js and summarised here.

import * as PIXI was 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.mjs files calling extensions.add( … )), and every one is in Pixi's own sideEffects allowlist — 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:

Trimmed Why it can't be reached
@xmldom/xmldom — 166 kB An XML parser, imported only by Pixi's Web Worker environment adapter (a worker has no DOMParser). A document never selects it. Not the SVG parser — that one uses the platform's.
WebGPU renderer — 84 kB autoDetectRenderer tries webgl → webgpu → canvas. WebGPU is only reached on a browser with WebGPU but not WebGL, which does not exist.
Event system — 83 kB The canvas is pointer-events: none and nothing is given an eventMode — the drag is a DOM pointerdown on the handle div.
Accessibility, DOMPipe, spritesheet — 42 kB No interactive display objects, no DOM containers, no textures. Trimming prepare also 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 build writes 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

cd extensions/mio-js
npm run build       # → dist/mio.js + dist/mio.min.js
npm run typecheck   # this library + the plugin src it imports
npm run demo        # → http://localhost:4321/

vite and pixi.js resolve from the repo root, so there is nothing to install. demo/index.html is 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 only BlurFilter uses, 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

Open WordPress Playground Preview

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant