{"_id":"@digitalmeadow/ascii-renderer","name":"@digitalmeadow/ascii-renderer","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@digitalmeadow/ascii-renderer","version":"0.1.0","type":"module","description":"DOM-based ASCII character renderer inspired by play.core","author":{"name":"Digital Meadow","email":"inbox@digitalmeadow.studio","url":"https://digitalmeadow.studio"},"keywords":["ascii","renderer","text","play.core"],"license":"MIT","sideEffects":false,"main":"./dist/index.js","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"devDependencies":{"@types/node":"26.2.0","typescript":"7.0.2","vite":"8.2.1"},"publishConfig":{"access":"public"},"scripts":{"dev":"vite","build":"vite build && tsc","preview":"vite preview"},"_id":"@digitalmeadow/ascii-renderer@0.1.0","_integrity":"sha512-RNfdzrIuPjW+pwWdVsw6s4FdKGX/Wm9juH28zOVYkonfCp07Z7blbptDdaWoZ79WOcZwqSEmt+nIMv1xjMkcaA==","_resolved":"/private/var/folders/6l/8mnj4s0x7974_pxh9sys_3jh0000gn/T/8dc00faaf612439111338f60e107d3df/digitalmeadow-ascii-renderer-0.1.0.tgz","_from":"file:digitalmeadow-ascii-renderer-0.1.0.tgz","_nodeVersion":"25.2.1","_npmVersion":"11.6.2","dist":{"integrity":"sha512-RNfdzrIuPjW+pwWdVsw6s4FdKGX/Wm9juH28zOVYkonfCp07Z7blbptDdaWoZ79WOcZwqSEmt+nIMv1xjMkcaA==","shasum":"03da390215678902015e6ae0f057bd282a288bc3","tarball":"https://registry.npmjs.org/@digitalmeadow/ascii-renderer/-/ascii-renderer-0.1.0.tgz","fileCount":19,"unpackedSize":31658,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIGh/0uareZD7yzb4tVe5ECZLFM8MZZ/AnGcf2uRy3qCkAiEAqLI8vJZs+41Rd5zoK4MazJ500jk6TOdl5lJEtX4+BGA="}]},"_npmUser":{"name":"lairdkruger","email":"accounts@lairdkruger.com"},"directories":{},"maintainers":[{"name":"lairdkruger","email":"accounts@lairdkruger.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/ascii-renderer_0.1.0_1787176298886_0.027396637982092464"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-19T21:51:38.618Z","0.1.0":"2026-08-19T21:51:39.025Z","modified":"2026-08-19T21:51:39.334Z"},"maintainers":[{"name":"lairdkruger","email":"accounts@lairdkruger.com"}],"description":"DOM-based ASCII character renderer inspired by play.core","keywords":["ascii","renderer","text","play.core"],"author":{"name":"Digital Meadow","email":"inbox@digitalmeadow.studio","url":"https://digitalmeadow.studio"},"license":"MIT","readme":"# @digitalmeadow/ascii-renderer\n\nDOM-based ASCII character renderer inspired by [play.core](https://github.com/ertdfgcvb/play.core).\n\nRenders a grid of styled characters into a `<pre>` overlay on top of any DOM element. Time-based (delta-driven) loop on `requestAnimationFrame`, with row-level diffing to minimize DOM updates.\n\n## Install\n\n```bash\npnpm add @digitalmeadow/ascii-renderer\n```\n\n## Usage\n\n```ts\nimport { AsciiRenderer } from \"@digitalmeadow/ascii-renderer\";\nimport type { Program, Surface, RenderContext } from \"@digitalmeadow/ascii-renderer\";\n\nlet x = 0;\n\nconst program: Program = {\n  pre(ctx: RenderContext) {\n    x = (x + 20 * ctx.delta) % ctx.cols; // 20 cells per second, at any frame rate\n  },\n  draw(surface: Surface, ctx: RenderContext) {\n    surface.set(Math.floor(x), 0, \"X\", \"var(--color-green)\");\n  },\n};\n\nconst renderer = new AsciiRenderer(document.querySelector(\".target\")!, program);\nrenderer.start();\n\n// Later:\n// renderer.stop();\n// renderer.destroy();\n```\n\n## Timing\n\nThe loop runs on `requestAnimationFrame` and hands each frame a `delta` in seconds. Express every rate in units per second and the program is frame-rate independent by construction.\n\n- `fps` **throttles rendering only**. The skipped time is banked and still reaches the program, so a capped renderer animates at the same wall-clock speed as an uncapped one — it just does it in coarser steps. `fps: 0` (the default) renders every animation frame.\n- `delta` is clamped to `maxDelta` (default `0.1s`, never below the `fps` interval) so resuming from a stalled tab or a paused debugger does not fast-forward the simulation.\n- `ctx.time` accumulates clamped deltas rather than tracking wall clock, so it stops while the tab is hidden.\n- `fps` is settable at runtime: `renderer.fps = 30`.\n\n## Drawing: `draw` vs `main`\n\nTwo ways to fill the grid, and they compose — `main` runs first, then `draw` over the top.\n\n- **`draw(surface, ctx)` — scatter.** Write only the cells the scene occupies. Cost scales with scene content, not grid size. Correct choice for sparse scenes (particles, text, sprites). Last write wins, so draw back-to-front.\n- **`main(coord, ctx)` — per-cell shader.** Called for every cell, so cost is O(cols × rows) per frame regardless of what the scene contains. Ergonomic for full-screen effects; a bad fit for anything sparse.\n\n## API\n\n### `new AsciiRenderer(target, program, options?)`\n\n- `target: HTMLElement` — the container element. Must have `position: relative` (set automatically if `static`).\n- `program: Program` — lifecycle hooks (see below).\n- `options: AsciiRendererOptions` — `{ fps?, maxDelta?, fontSize?, fontFamily?, color?, backgroundColor? }`\n\n### `renderer.start()` / `.stop()` / `.destroy()`\n\nStart/stop the render loop. `destroy()` also removes the overlay and all listeners.\n\n### `renderer.onFrame(cb)` → `this`\n\nRegister a callback called after each frame render. Returns `this` for chaining.\n\n### `renderer.resize()`\n\nRecalculate columns/rows from target dimensions. Called automatically via `ResizeObserver`.\n\n### Accessors\n\n`fps` (read/write), `cols`, `rows`, `cellWidth`, `lineHeight`.\n\n## Program interface\n\n```ts\ninterface Program {\n  pre?: (ctx: RenderContext) => void;\n  main?: (coord: Coord, ctx: RenderContext) => Cell | string | null;\n  draw?: (surface: Surface, ctx: RenderContext) => void;\n  post?: (ctx: RenderContext) => void;\n}\n\ninterface Coord {\n  x: number;\n  y: number;\n  index: number;\n}\n\ninterface RenderContext {\n  frame: number; // frames rendered since start()\n  time: number; // seconds of simulated time\n  delta: number; // seconds since the previous frame, clamped\n  cols: number;\n  rows: number;\n  metrics: { cellWidth: number; lineHeight: number; aspect: number };\n  cursor: { x: number; y: number; active: boolean };\n}\n\ninterface Cell {\n  char: string;\n  color?: string; // CSS color value\n  backgroundColor?: string; // CSS color value\n}\n```\n\n- `pre(ctx)` — advance simulation state.\n- `main(coord, ctx)` — per-cell shader. Return a `Cell`, a `string` (char only), or `null`/`undefined` for transparent.\n- `draw(surface, ctx)` — scatter pass. `surface.set(x, y, char, color?, backgroundColor?)` clips out-of-bounds writes.\n- `post(ctx)` — after the grid is final, before it reaches the DOM.\n\n## How it works\n\n- Creates a `<pre class=\"ascii-overlay\">` as a child of the target element\n- `<pre>` is absolutely positioned, `pointer-events: none`, transparent background\n- Font metrics are measured from the target's computed styles\n- Each frame: `pre()` → clear → `main()` per cell → `draw()` → `post()` → render\n- `Surface` mutates cells in place and records, per row, the column span actually written and whether it changed — clearing and reading cost what the scene occupies, not the size of the grid\n- Rows are block-level with `contain: layout style`. As inline boxes they shared one formatting context, so rewriting a single row re-laid-out every glyph in the grid\n- Rows are reconciled, not rewritten: a changed row updates `textContent`/`className` on the spans already there instead of reparsing `innerHTML`, and unchanged runs are skipped\n- Colour pairs become generated stylesheet classes rather than inline `style` attributes, so each is parsed once and the computed style is shared. Authored `var()` references are preserved, so themes still apply\n- `renderer.timings` reports smoothed per-phase cost (`sim` / `draw` / `dom`) for profiling\n- Pauses cleanly across `visibilitychange`; `ResizeObserver` handles layout changes\n- Cursor tracking via `pointermove` on the target element\n\n## Credits\n\nInspired by [play.core](https://github.com/ertdfgcvb/play.core) by Andreas Gysin.\n","readmeFilename":"README.md","_rev":"1-4f3a559f50e3b39d7958722f6ec3c45f"}