npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@oxide/ascii-shader

v0.1.0

Published

ASCII animation engine: programs run per cell per frame, like a fragment shader over characters. Derived from play.core.

Readme

ascii-shader

ASCII animation engine: a program runs once per grid cell per frame, like a fragment shader over characters. Derived from play.core by Andreas Gysin, folding together other parts of two internal forks:

  • the engine and canvas renderer from mitos (Display P3, CSS-variable colors, deterministic frame export), with the playback loop rewritten, and
  • the DOM text renderer from the oxide.computer website fork (row-diffed, span-run-coalesced, real selectable text).

Zero runtime dependencies. React is an optional peer dependency for the @oxide/ascii-shader/react wrapper.

Quick start

import { createAnimation } from '@oxide/ascii-shader'

// A program is a plain object (usually an imported module).
const program = {
  settings: { cols: 60, rows: 16, fps: 24, frames: 120 },
  main({ x, y }, context) {
    const ramp = ' .:-=+*#%@'
    const t = context.frame / 120
    const v = Math.sin(x * 0.3 + t * Math.PI * 2) * 0.5 + 0.5
    return ramp[Math.floor(v * (ramp.length - 1))]
  },
}

const animation = createAnimation(program, {
  element: document.querySelector('pre')!, // <pre> → text renderer, <canvas> → canvas renderer
})

animation.pause()
animation.seek(42) // works while paused
animation.play()

Program model

A program exports any of:

| Function | Runs | Signature | | ------------------------------------------- | ------------------------------ | ------------------------------------------------------------ | | boot | once, after fonts load | (context, buffer, userData) | | pre | every frame, before main | (context, cursor, buffer, userData) | | main | every frame, once per cell | (pos, context, cursor, buffer, userData) => Cell \| string | | post | every frame, after main | (context, cursor, buffer, userData) | | pointerMove / pointerDown / pointerUp | on pointer events | (context, cursor, buffer, userData) | | cleanup | on dispose() | () |

main returns a string (the character) or a Cell ({ char, color?, backgroundColor?, fontWeight? }), merged into the existing cell. context is a frozen per-frame snapshot: { frame, time, cols, rows, metrics, width, height, settings, runtime: { fps } }.

Animate with context.frame, not context.time — the frame counter advances exactly once per rendered frame and never while paused, so playback is deterministic, scrubbable, and exportable. context.time is accumulated play time in ms (it pauses while paused), for convenience only.

Programs are module-level singletons: keep per-instance state in userData (the third argument to createAnimation, passed to every hook) rather than in module-scope lets if the same program can mount twice.

Settings precedence is defaults < runSettings < program.settings — a program's exported settings pins its intended grid/fps; the host can still override at runtime via updateSettings (e.g. a smaller mobile grid).

API

createAnimation(program, settings, userData?) returns:

| Member | Notes | | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | play() / pause() / toggle(force?) | Explicit playback control. Idempotent. | | seek(frame) / step(delta = 1) | Jump/scrub. Renders even while paused; wraps into [0, frames). | | renderFrame(frame) | Synchronous render, no rAF — read getBuffer() right after. For export/snapshotting. | | playing / frame | Current state (getters). | | ready | Promise<void>, resolves after fonts + boot. | | on(event, handler) | 'boot' \| 'frame' \| 'play' \| 'pause' \| 'ended'; returns unsubscribe. 'frame' fires only when a frame is actually rendered. | | updateSettings(patch) | Live-patch fps, frames, loop, cols, rows, colors… undefined values are ignored; takes effect immediately. | | getBuffer() / getMetrics() / getSettings() / text() | Introspection. | | dispose() | Cancels the loop, removes pointer listeners, releases the renderer. |

Key settings: element (required), cols/rows (omit to derive from element size), fps (default 30), frames (loop length; omit for endless), loop (default true; false pauses on the last frame and emits 'ended'), autoplay (default true), renderer ('text' | 'canvas' or a custom { render(context, buffer) }, default inferred from the element), color, backgroundColor, fontWeight, and canvas-only padding and supersample.

Renderers

Text (<pre>): one block <span> per row; rows unchanged since the last frame are skipped via a back-buffer diff, same-styled runs coalesce into one span, and default-styled runs emit bare spans. Output is real selectable text that inherits font and sizing from CSS (size with container queries). Colors are emitted as-is — use oklch() or color(display-p3 …) directly for wide-gamut colors; CSS variables ('--accent') pass through natively.

Canvas (<canvas>): renders on a Display P3 context when available, resolves CSS-variable colors to concrete values (canvas can't dereference them), supports per-cell color/backgroundColor/fontWeight, and only reallocates the backing store when the grid/scale actually changes. Set supersample above 1 if the canvas will be zoomed.

Export helpers: getContent(buffer, cols, rows) for plain text and getColoredRows(buffer, cols, rows, defaultColor) for per-row color runs (SVG export).

React

import { AsciiAnimation } from '@oxide/ascii-shader/react'
import * as program from './programs/wave'
;<AsciiAnimation
  program={program}
  alt="ASCII wave animation"
  className="my-animation"
  cols={isSmall ? 72 : 144}
  rows={24}
/>

The wrapper plays only while visible (IntersectionObserver; tune with playOnVisible / visibleThreshold), tracks prefers-reduced-motion into settings.reducedMotion (programs branch on it, e.g. jump to the final state), reserves the final layout with an invisible spacer <pre> so the font-loading boot delay causes no layout shift, and exposes the controller via onReady for seek/export. Grid/fps prop changes apply via updateSettings without re-booting the program; only a program change recreates the animation.

Development

npm install
npm run build   # tsc → dist/
npm test        # unit tests for the pure modules (vitest)
npm run lint    # oxlint (type-aware)
npm run fmt     # oxfmt

examples/index.html exercises both renderers and the playback API — build, then serve the repo root.

License

MPL-2.0, except files derived from play.core (animation.ts, metrics.ts, fps.ts, renderers/text.ts), which remain under the Apache License 2.0 — Copyright ertdfgcvb (Andreas Gysin).