{"_id":"intermission","_rev":"4-73f6c0ae20124358b2e6bda6b96fdf97","name":"intermission","dist-tags":{"latest":"0.1.2"},"versions":{"0.1.0":{"name":"intermission","version":"0.1.0","keywords":["loader","spinner","loading","react","canvas","ai","agent","thinking"],"author":{"name":"Ben Howdle","email":"benhowdle89@gmail.com"},"license":"MIT","_id":"intermission@0.1.0","maintainers":[{"name":"benhowdle","email":"benhowdle89@gmail.com"}],"homepage":"https://benhowdle89.github.io/intermission","bugs":{"url":"https://github.com/benhowdle89/intermission/issues"},"dist":{"shasum":"6d5471ac0601cf73c82ecf45da39bc46345cb276","tarball":"https://registry.npmjs.org/intermission/-/intermission-0.1.0.tgz","fileCount":37,"integrity":"sha512-Hdlw/3qIRhT7KcpOO3Nb2Xw5Laox/9dTHyv9juZgNWrQglLshRNlnoBI+2c3HfTeK7qws7zTJftIl4oN2F8gHQ==","signatures":[{"sig":"MEYCIQD9H6WuXvJjIura7IAylOKwhN4WD4Wy/6r21ObJ1qL0vgIhAP/EOateA7TTHNXNmrQEOBDmf+PwMvAczMBZv4FPwBQy","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":256853},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./grid":{"types":"./dist/grid.d.ts","import":"./dist/grid.js","require":"./dist/grid.cjs"},"./tape":{"types":"./dist/tape.d.ts","import":"./dist/tape.js","require":"./dist/tape.cjs"},"./react":{"types":"./dist/react.d.ts","import":"./dist/react.js","require":"./dist/react.cjs"},"./academy":{"types":"./dist/academy.d.ts","import":"./dist/academy.js","require":"./dist/academy.cjs"},"./package.json":"./package.json"},"gitHead":"6055e6df8b030d7ef76989fb7cc580bbd5454e54","scripts":{"dev":"tsup --watch","demo":"node demo/build.mjs","build":"rm -rf dist && tsup","typecheck":"tsc --noEmit","prepublishOnly":"npm run typecheck && npm run build"},"_npmUser":{"name":"benhowdle","email":"benhowdle89@gmail.com"},"deprecated":"Superseded by 0.1.1","repository":{"url":"git+https://github.com/benhowdle89/intermission.git","type":"git"},"_npmVersion":"10.8.2","description":"Loaders, in three prints. Six states, three visual worlds, one API.","directories":{},"sideEffects":false,"_nodeVersion":"20.20.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","react":"^18.3.1","esbuild":"^0.28.1","react-dom":"^18.3.1","typescript":"^5.6.3","@types/react":"^18.3.12"},"peerDependencies":{"react":">=17","react-dom":">=17"},"peerDependenciesMeta":{"react":{"optional":true},"react-dom":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/intermission_0.1.0_1784711990057_0.27452511554465464","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"intermission","version":"0.1.1","keywords":["loader","spinner","loading","react","canvas","ai","agent","thinking"],"author":{"name":"Ben Howdle","email":"benhowdle89@gmail.com"},"license":"MIT","_id":"intermission@0.1.1","maintainers":[{"name":"benhowdle","email":"benhowdle89@gmail.com"}],"homepage":"https://benhowdle89.github.io/intermission","bugs":{"url":"https://github.com/benhowdle89/intermission/issues"},"dist":{"shasum":"cfe1f6cb3f564c14fa1e29906c3fc7fb71205257","tarball":"https://registry.npmjs.org/intermission/-/intermission-0.1.1.tgz","fileCount":37,"integrity":"sha512-zjqKtG4upMw4+ZdPl5QfWKpFiB8Gczzlz0UvndXVNXeS/aM8VaaFmkD7ZvcIWDWEFg4+zAZBDbfKF/J1yYJVSQ==","signatures":[{"sig":"MEYCIQD3fCFB1M/a1dBGhLJsTv2/m+7TDW5SmgkBXRU2UELCbQIhAJQheK/6K1ubY/njf1cg//gmzFTdYJ3UTG1IjKoY0AlY","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/intermission@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":264383},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./grid":{"types":"./dist/grid.d.ts","import":"./dist/grid.js","require":"./dist/grid.cjs"},"./tape":{"types":"./dist/tape.d.ts","import":"./dist/tape.js","require":"./dist/tape.cjs"},"./react":{"types":"./dist/react.d.ts","import":"./dist/react.js","require":"./dist/react.cjs"},"./academy":{"types":"./dist/academy.d.ts","import":"./dist/academy.js","require":"./dist/academy.cjs"},"./package.json":"./package.json"},"gitHead":"c781e88af6ab74f3040e3a3a73f2b5adfcef40c1","scripts":{"dev":"tsup --watch","demo":"node demo/build.mjs","gifs":"node scripts/render-gifs.mjs","build":"rm -rf dist && tsup","verify":"node scripts/verify-renderers.mjs","typecheck":"tsc --noEmit","prepublishOnly":"npm run typecheck && npm run build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:23e20864-ed86-44be-8a4e-eda85188eaa3"}},"repository":{"url":"git+https://github.com/benhowdle89/intermission.git","type":"git"},"_npmVersion":"11.18.0","description":"Loaders, in three prints. Six states, three visual worlds, one API.","directories":{},"sideEffects":false,"_nodeVersion":"20.20.2","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.3.5","react":"^18.3.1","gifenc":"^1.0.3","esbuild":"^0.28.1","react-dom":"^18.3.1","typescript":"^5.6.3","@types/react":"^18.3.12","@napi-rs/canvas":"^1.0.2"},"peerDependencies":{"react":">=17","react-dom":">=17"},"peerDependenciesMeta":{"react":{"optional":true},"react-dom":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/intermission_0.1.1_1784713583506_0.5511685332910312","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"intermission","version":"0.1.2","description":"Loaders, in three prints. Six states, three visual worlds, one API.","keywords":["loader","spinner","loading","react","canvas","ai","agent","thinking"],"license":"MIT","type":"module","sideEffects":false,"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./react":{"types":"./dist/react.d.ts","import":"./dist/react.js","require":"./dist/react.cjs"},"./academy":{"types":"./dist/academy.d.ts","import":"./dist/academy.js","require":"./dist/academy.cjs"},"./grid":{"types":"./dist/grid.d.ts","import":"./dist/grid.js","require":"./dist/grid.cjs"},"./tape":{"types":"./dist/tape.d.ts","import":"./dist/tape.js","require":"./dist/tape.cjs"},"./package.json":"./package.json"},"scripts":{"build":"rm -rf dist && tsup","dev":"tsup --watch","typecheck":"tsc --noEmit","prepublishOnly":"npm run typecheck && npm run build","demo":"node demo/build.mjs","verify":"node scripts/verify-renderers.mjs","gifs":"node scripts/render-gifs.mjs","promo":"node scripts/render-promo.mjs"},"peerDependencies":{"react":">=17","react-dom":">=17"},"peerDependenciesMeta":{"react":{"optional":true},"react-dom":{"optional":true}},"devDependencies":{"@napi-rs/canvas":"^1.0.2","@types/react":"^18.3.12","esbuild":"^0.28.1","gifenc":"^1.0.3","react":"^18.3.1","react-dom":"^18.3.1","tsup":"^8.3.5","typescript":"^5.6.3"},"engines":{"node":">=18"},"author":{"name":"Ben Howdle","email":"benhowdle89@gmail.com"},"homepage":"https://benhowdle89.github.io/intermission","repository":{"type":"git","url":"git+https://github.com/benhowdle89/intermission.git"},"bugs":{"url":"https://github.com/benhowdle89/intermission/issues"},"publishConfig":{"access":"public"},"gitHead":"05f9d7ed399ef3986d89972cd2c5ec1da2823d33","_id":"intermission@0.1.2","_nodeVersion":"20.20.2","_npmVersion":"11.18.0","dist":{"integrity":"sha512-RTaCubO7vy5DAnt9m3RPEWga5aa6D+dnlK9p2xK82U9I6lc7wuGJmWEXMfiH9rfwqjk6OJgAZzoVpv9S+QtRrw==","shasum":"32811eb3fc2d36ab1e6250f12bda687313f5266e","tarball":"https://registry.npmjs.org/intermission/-/intermission-0.1.2.tgz","fileCount":37,"unpackedSize":267218,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/intermission@0.1.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIFCMh+vvT58UkXJU53VULVwkmFtQouX8vCpXoSuI+IamAiEAkIKJ4sm6T412ns3/qOyF4tLiBMY87TXggIjc328KQSI="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:23e20864-ed86-44be-8a4e-eda85188eaa3"}},"directories":{},"maintainers":[{"name":"benhowdle","email":"benhowdle89@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/intermission_0.1.2_1784714494663_0.3220667101881627"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-22T09:19:49.941Z","modified":"2026-07-22T10:01:35.125Z","0.1.0":"2026-07-22T09:19:50.186Z","0.1.1":"2026-07-22T09:46:23.620Z","0.1.2":"2026-07-22T10:01:34.797Z"},"bugs":{"url":"https://github.com/benhowdle89/intermission/issues"},"author":{"name":"Ben Howdle","email":"benhowdle89@gmail.com"},"license":"MIT","homepage":"https://benhowdle89.github.io/intermission","keywords":["loader","spinner","loading","react","canvas","ai","agent","thinking"],"repository":{"type":"git","url":"git+https://github.com/benhowdle89/intermission.git"},"description":"Loaders, in three prints. Six states, three visual worlds, one API.","maintainers":[{"name":"benhowdle","email":"benhowdle89@gmail.com"}],"readme":"# Intermission\n\n**Loaders, in three prints.**\n\nSix states. Three visual worlds. One API.\n\nMost loader libraries give you one look and hope it suits your product. Intermission\nfixes the *states* and lets you swap the *world*. The state vocabulary is identical\nacross every print, so changing worlds is a one-line change that never moves a pixel\nof layout.\n\n### Academy\n\n![Academy - the six states](https://raw.githubusercontent.com/benhowdle89/intermission/main/media/academy.gif)\n\n### Grid\n\n![Grid - the six states](https://raw.githubusercontent.com/benhowdle89/intermission/main/media/grid.gif)\n\n### Tape\n\n![Tape - the six states](https://raw.githubusercontent.com/benhowdle89/intermission/main/media/tape.gif)\n\n**[Try them live →](https://benhowdle89.github.io/intermission)** - switch prints,\nchange speed, scrub through states.\n\n```bash\nnpm install intermission\n```\n\nReact is an optional peer dependency - the core works without it.\n\n---\n\n## Quick start\n\n```tsx\nimport { Loader, IntermissionProvider } from \"intermission/react\";\nimport { tape } from \"intermission/tape\";\n\nexport default function App() {\n  return (\n    <IntermissionProvider print={tape}>\n      <Loader state=\"thinking\" />\n      <Loader state=\"done\" size={20} />\n    </IntermissionProvider>\n  );\n}\n```\n\nSet the print once at the root and every loader below follows it. Or pass one\nper instance, with no provider at all:\n\n```tsx\nimport { Loader } from \"intermission/react\";\nimport { grid } from \"intermission/grid\";\n\n<Loader state=\"searching\" print={grid} size={48} />;\n```\n\n### Next.js App Router\n\nA `Print` is an object of drawing functions, and **functions cannot be passed\nfrom a Server Component to a Client Component**. So this fails:\n\n```tsx\n// app/page.tsx - a Server Component by default\nimport { IntermissionProvider } from \"intermission/react\";\nimport { tape } from \"intermission/tape\";\n\n<IntermissionProvider print={tape}>  {/* ✗ Functions cannot be passed… */}\n```\n\nImport the print *inside* a client boundary instead, so it never crosses:\n\n```tsx\n// app/providers.tsx\n\"use client\";\n\nimport { IntermissionProvider } from \"intermission/react\";\nimport { tape } from \"intermission/tape\";\nimport type { ReactNode } from \"react\";\n\nexport function Providers({ children }: { children: ReactNode }) {\n  return <IntermissionProvider print={tape}>{children}</IntermissionProvider>;\n}\n```\n\n```tsx\n// app/page.tsx - still a Server Component\nimport { Loader } from \"intermission/react\";\nimport { Providers } from \"./providers\";\n\nexport default function Home() {\n  return (\n    <Providers>\n      <Loader state=\"thinking\" />\n    </Providers>\n  );\n}\n```\n\n`<Loader>` itself is fine in a Server Component - `state`, `size` and `speed` are\nall serializable. It is only `print` that has to stay client-side.\n\nThe same applies to `print={grid}` as a direct prop: pass it from a client\ncomponent, or use the provider.\n\n---\n\n## Recipes\n\n### Drive it from your own state\n\nThe state names are deliberately generic, so they can come straight off an\nexisting machine without a mapping layer:\n\n```tsx\nconst [phase, setPhase] = useState<LoaderState>(\"listening\");\n\n// …later\nsetPhase(response.streaming ? \"composing\" : \"done\");\n\n<Loader state={phase} />;\n```\n\n### Inline with text\n\nAt small sizes, let the text do the announcing and hide the loader from screen\nreaders with `label={null}`:\n\n```tsx\n<span style={{ display: \"inline-flex\", alignItems: \"center\", gap: 8 }}>\n  <Loader state=\"searching\" size={16} label={null} />\n  Searching your documents…\n</span>\n```\n\n### Two prints on one page\n\n`print` overrides the provider per instance:\n\n```tsx\n<IntermissionProvider print={academy}>\n  <Loader state=\"working\" />              {/* academy */}\n  <Loader state=\"working\" print={grid} /> {/* grid    */}\n</IntermissionProvider>\n```\n\n### Match the surface to the print\n\nEach print declares the ground it was drawn for:\n\n```tsx\n<div style={{ background: tape.ground }}>\n  <Loader state=\"thinking\" print={tape} />\n</div>\n```\n\n### Slow it down, or hold it still\n\n```tsx\n<Loader state=\"thinking\" speed={0.5} />  {/* half speed        */}\n<Loader state=\"done\" paused />           {/* freeze on a still */}\n```\n\n`paused` defaults to the user's `prefers-reduced-motion` setting, so passing it\nexplicitly is an override in both directions.\n\n---\n\n## The six states\n\nEvery print implements all six. Five loop; `done` plays once and holds.\n\n| State | For |\n| --- | --- |\n| `thinking` | Working something out, no visible progress |\n| `searching` | Looking through a corpus, scanning, retrieving |\n| `listening` | Waiting on input - a mic, a stream, a webhook |\n| `working` | A task in flight with a definite end |\n| `composing` | Assembling output - writing, drafting, building |\n| `done` | Finished. Settles once, then holds. |\n\nBecause the vocabulary is fixed, `state` can be driven straight from your own\nmachine without a mapping layer.\n\n---\n\n## The three prints\n\n| Print | Reference | The rules it never breaks |\n| --- | --- | --- |\n| **Academy** | Wes Anderson | Bilateral symmetry every frame · stepped timing, nothing eases · flat fill, no gradients · built for a cream ground |\n| **Grid** | TRON | No curves - every ring is a polygon · additive glow only · trails decay behind the head · orange stays rare enough to mean something |\n| **Tape** | VHS | Colour channels drift apart · scanlines over everything · rows occasionally slip · shapes stay plain, the medium carries them |\n\nThe reference lives in the *motion grammar*, not in costume. That's what keeps\nthese usable next to a paragraph of text instead of reading as a novelty.\n\nEach print declares the ground it was designed against:\n\n```ts\nimport { academy } from \"intermission/academy\";\n\nacademy.ground; // \"#F2E8D5\"\nacademy.palette; // [\"#2B2724\", \"#8C3B3B\", …]\n```\n\nNothing enforces it - a print will draw on any background - but Academy on black\nor Grid on white will not look like the demo.\n\n---\n\n## `<Loader>`\n\n| Prop | Type | Default | |\n| --- | --- | --- | --- |\n| `state` | `LoaderState` | - | **Required.** One of the six. |\n| `print` | `Print` | from context | Falls back to the nearest provider. |\n| `size` | `number` | `64` | Edge length in CSS pixels. Always square. |\n| `speed` | `number` | `1` | Time multiplier. |\n| `paused` | `boolean` | reduced-motion | Rest on a representative still. |\n| `label` | `string \\| null` | the state name | Announced to assistive tech. `null` hides it. |\n| `className` | `string` | - | |\n| `style` | `CSSProperties` | - | |\n\nChanging `state`, `print`, `size` or `speed` updates in place - the canvas is\nnever torn down mid-animation.\n\n### Accessibility\n\nBy default a loader is `role=\"img\"` with `aria-label` set to the state name. When\nadjacent text already says it, hide the loader instead of announcing it twice:\n\n```tsx\n<span>\n  <Loader state=\"thinking\" size={20} label={null} />\n  Thinking…\n</span>\n```\n\n---\n\n## Without React\n\nThe core has no framework dependency.\n\n```ts\nimport { mount } from \"intermission\";\nimport { academy } from \"intermission/academy\";\n\nconst handle = mount(canvas, {\n  print: academy,\n  state: \"working\",\n  size: 64,\n  speed: 1,\n});\n```\n\n`mount` returns a handle. Every method updates in place:\n\n| Method | |\n| --- | --- |\n| `setState(state)` | Also restarts the clock, so `done` replays. |\n| `setPrint(print)` | Swap worlds without remounting. |\n| `setSize(size)` | Re-scales for device pixel ratio. |\n| `setSpeed(speed)` | |\n| `setPaused(paused)` | |\n| `destroy()` | Removes it from the shared animation loop. |\n\n`mount` also takes `paused` and `maxDpr` (default `2` - beyond that costs more\nthan it shows).\n\n---\n\n## Writing your own print\n\nA print is six draw functions plus some metadata. The primitives the built-in\nthree are made from are exported for exactly this:\n\n```ts\nimport { TAU, stepq, polygon, polyPoint, hash, type Print } from \"intermission\";\n\nconst pulse = (c: CanvasRenderingContext2D, t: number, s: number) => {\n  const r = s * 0.3 * (0.75 + 0.25 * Math.sin(t * 3));\n  c.fillStyle = \"#E0A526\";\n  c.beginPath();\n  c.arc(s / 2, s / 2, r, 0, TAU);\n  c.fill();\n};\n\nexport const mine: Print = {\n  name: \"mine\",\n  title: \"Mine\",\n  ground: \"#111111\",\n  palette: [\"#111111\", \"#E0A526\"],\n  settleDuration: 0.8,\n  states: {\n    thinking: pulse,\n    searching: pulse,\n    working: pulse,\n    listening: pulse,\n    composing: pulse,\n    done: pulse,\n  },\n};\n```\n\nA draw function receives a context already scaled for device pixel ratio and\nalready cleared. Draw in CSS pixels against a `size` × `size` box with the origin\nat its top left.\n\nTwo things worth knowing:\n\n- **`t` restarts when the state changes**, which is what lets `done` play once\n  and hold rather than looping.\n- **Anything drawn outside the box is clipped by the canvas.** If you use\n  `shadowBlur`, inset the artwork enough that the glow lands inside - otherwise\n  it gets a hard square edge cut through it. `npm run verify` checks for this.\n\n`stepq` is what gives Academy its snap; `polyPoint` is what runs Grid's light\ntrail along a path without curves.\n\n---\n\n## Size\n\nPer entry point, minified and gzipped:\n\n| Entry | Minified | Gzipped |\n| --- | --- | --- |\n| `intermission` (core) | 2.3 kB | 1.2 kB |\n| `intermission/react` | 2.5 kB | 1.2 kB |\n| `intermission/academy` | 2.6 kB | 1.2 kB |\n| `intermission/tape` | 3.4 kB | 1.6 kB |\n| `intermission/grid` | 4.1 kB | 1.7 kB |\n\nPrints are separate entry points and the package is marked `sideEffects: false`,\nso importing one never bundles the others. A React app using a single print ships\nroughly **3.6 kB gzipped**.\n\n---\n\n## Notes\n\n- **One animation frame, shared.** Twenty loaders on a page cost one\n  `requestAnimationFrame` callback, not twenty.\n- **Server-safe.** The React entry ships `\"use client\"`, so `<Loader>` can be\n  used directly inside a Server Component. It renders a plain `<canvas>` on the\n  server and starts animating on hydration. The one exception is the `print`\n  prop, which has to be passed from a client boundary. See\n  [Next.js App Router](#nextjs-app-router).\n- **Respects `prefers-reduced-motion`** by resting on a representative still\n  frame. Pass `paused` explicitly to override in either direction.\n- **ESM and CommonJS**, with types for both, verified against a real install on\n  every CI run.\n- Requires the Canvas 2D API - every browser since roughly 2015.\n\n---\n\n## Scripts\n\n```bash\nnpm run build      # bundle to dist/ (ESM + CJS + types)\nnpm run typecheck  # tsc --noEmit\nnpm run verify     # render all 18 states headlessly; fail if any is blank,\n                   # frozen, or drawing outside its box\nnpm run demo       # build the self-contained landing page to demo/dist\n```\n\n## Releasing\n\n```bash\nnpm version minor\ngit push --follow-tags\n```\n\nCI verifies the tag matches `package.json`, then publishes to npm with\nprovenance and cuts the GitHub release.\n\n## Licence\n\nMIT © Ben Howdle\n","readmeFilename":"README.md"}