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

punktraster

v0.1.1

Published

Canvas-rendered dot raster preloader. Zero dependencies, one rAF for the whole page.

Readme

Configurable rows, columns, tile shape, and animation pattern. One requestAnimationFrame drives every instance on the page.

Punktraster is German for a dot raster, the screen that renders a continuous tone as a grid of dots. That is the whole component in one word.

  • One <canvas> per loader, not one node per tile.
  • Zero runtime dependencies. 3.0 kB min+gzip for the core, 0.3 kB for an adapter.
  • Vanilla core, with React and Solid adapters over the same engine.
  • 400 tiles cost about 0.12 ms per frame.

Install

npm install punktraster

ESM only. React and Solid are optional peers; install whichever you use.

Use it

Vanilla

import { createGridLoader } from "punktraster";

const loader = createGridLoader(document.querySelector("canvas"), {
  rows: 5,
  cols: 5,
  pattern: "helix",
});

loader.update({ speed: 2 });
loader.stop();
loader.destroy();

The canvas is yours. The library sizes it, paints it, and sets its ARIA attributes, and it never creates or inserts DOM.

React

import { GridLoader } from "punktraster/react";

<GridLoader size={10} pattern="ripple" className="text-blue-500" />;

Color defaults to the canvas's inherited currentColor, so a text color class is enough.

For a canvas you own, use the hook:

import { useGridLoader } from "punktraster/react";

function Spinner() {
  const { ref, handle } = useGridLoader({ rows: 8, cols: 8 });
  return <canvas ref={ref} />;
}

Prop changes flow through update(), so the animation phase survives every re-render. An inline delay={(r, c) => ...} is a new function each render, so wrap it in useCallback or hoist it.

Solid

import { GridLoader } from "punktraster/solid";

<GridLoader size={10} pattern="sweep" class="text-blue-500" />;

The ./solid entry ships preserved JSX under the solid export condition, so your Solid compiler emits the right output for the browser, the server, or hydration. This is why renderToString works.

Options

| Option | Default | Notes | | ------------------- | -------------- | ------------------------------------------------------------------------------- | | rows, cols | 5 | Any size. Above ~2500 tiles, see the limits below. | | shape | "circle" | "circle" or "square". | | radius | 0 | Corner radius in CSS px for squares, clamped to half the tile. | | size | 5 | A 5x5 grid measures size * 3 CSS px, matching the loader this is modelled on. | | tileSize | derived | Explicit tile edge in CSS px. Overrides size. | | gap | 0.75 * tile | Explicit gap in CSS px. | | speed | 1 | Multiplier. 1 is a 2600 ms cycle. | | color | currentColor | Any CSS color. Omit to track the canvas's computed color. | | pattern | "helix" | helix, ripple, sweep, random, download, or upload. | | delay | none | (row, col, rows, cols) => number in 0..1. Overrides pattern. | | label | "Loading" | Sets role="status". Pass null for aria-hidden. | | autoplay | true | | | reducedMotion | "auto" | "off" opts out of the reduced-motion collapse. | | respectVisibility | true | Pause while scrolled out of view. |

createGridLoader returns { canvas, width, height, start, stop, update, render, destroy }. stop() freezes on the current frame and start() continues from the same phase. destroy() is idempotent.

Patterns

Each preset is a one-line function of a tile's position returning a normalized phase delay.

  • helix phases by angle around the center via Math.atan2. The brightness curve has two peaks half a cycle apart, so the wave reads as a double arm sweeping around.
  • ripple phases by distance from the center.
  • sweep phases linearly across the diagonal.
  • random phases by a stateless hash of the position, so it is identical across renders, instances, and reloads.
  • download and upload phase along a chevron, so a V travels down or up the field and reads as a moving arrow. Give them more rows than columns for the clearest read.

Write your own with delay. Any finite return value is wrapped into 0..1, and non-finite values become 0.

How it stays cheap

  • One shared ticker. Every live instance shares a single requestAnimationFrame. 200 loaders schedule one frame, verified in demo/verify.html.
  • One baked sprite. The tile is drawn once to an OffscreenCanvas and blitted per tile with drawImage, so antialiasing is computed once instead of rows * cols times per frame. Instances with matching geometry and color share a single sprite.
  • A lookup table. The keyframe walk and its cubic-bezier easing are resolved once at import into a 512-entry Float32Array. Per tile per frame the brightness is one add, one multiply, one truncate, one mask, and one array read. No allocation, no branch, no Math call.
  • Absolute-time phase. Phase comes from the current timestamp, never from accumulated deltas, so jank cannot make it drift and a backgrounded tab resumes exactly where it should.
  • It stops when it cannot be seen. Instances pause on document.hidden and, by default, while scrolled out of view.

Measured in Chrome, unminified build, one instance:

| Grid | Tiles | Per frame | | ----- | ----- | --------- | | 5x5 | 25 | 0.008 ms | | 12x12 | 144 | 0.044 ms | | 20x20 | 400 | 0.117 ms | | 50x50 | 2500 | 0.81 ms |

Limits worth knowing

  • Every tile has its own alpha, so draw calls cannot be batched. One drawImage per tile per frame is the floor. Past roughly 2500 tiles you are into multi-millisecond frames, and the answer there is WebGL rather than a cleverer 2D loop.
  • Sizing goes through props, not CSS. The library sets inline width and height, and inline styles beat a stylesheet rule, so a .spinner { width: 40px } is ignored.
  • The footprint lands within one device pixel of the ideal. Tiles are placed on integer device pixels so each blit is 1:1 and nothing shimmers. A 5x5 at the default size is exactly 15 CSS px at integer device pixel ratios; at 1.5x the box has to round, because 22.5 device pixels does not exist.
  • currentColor is polled, not observed. An instance without an explicit color re-reads its computed color twice a second from inside its own tick, and immediately on a prefers-color-scheme change. A theme switch can therefore take up to 500 ms to reach the canvas. Pass color to skip this entirely.
  • Many separate canvases cost compositor memory regardless of how cheap the JavaScript is. Hundreds of loaders on one page would be better served by one canvas drawing many grids, which this does not do.

Reduced motion

Under prefers-reduced-motion: reduce the grid collapses to a single slow pulse on a fixed 3 second cycle with one alpha for all tiles. It reacts live when the setting changes. Pass reducedMotion: "off" to opt out.

Development

Built on Vite+.

npm run build      # vp pack: Rolldown build, .d.ts, publint, attw
npm test           # vp test: 111 tests
npm run check      # vp check: oxfmt + oxlint
npm run typecheck  # tsc, two projects
npm run demo       # vp dev on :4611, opens the landing page
npm run hero       # re-render assets/hero.gif from the built library

The site is index.html plus three demo pages, all dependency-free. Every page imports dist relative to itself, so the deployed layout mirrors the repo and no paths get rewritten:

  • index.html is the landing page, with live rasters rather than a recording.
  • demo/gallery.html is the one to open first. Twenty specimens side by side, each a live canvas with the exact options that produced it and a copy button, plus a color row and real placements: inside a button, inline with text, in list rows, as a typing indicator, and behind a blocking overlay.
  • demo/index.html is the playground: every pattern and shape, live speed and size sliders, a theme toggle to watch currentColor track, and a 500-instance panel with a frame rate readout.
  • demo/verify.html prints the footprint and per-frame measurements, and is where the numbers above come from.

The test suite is mostly pure functions, so the wave, the patterns, and the geometry are pinned without a DOM. Two tests are load-bearing beyond their size: one asserts the main canvas builds zero paths during ticks, which locks in the sprite architecture, and one compiles the published Solid entry for both the browser and the server, which is the only proof that shipping preserved JSX works.

Releasing

.github/workflows/ci.yml runs check, typecheck, test, and build on every push and pull request. The build step also runs publint and attw, so packaging regressions fail CI rather than reaching npm.

Publishing is gated on a version tag, so nothing reaches the registry until you push one:

npm version patch      # writes the version and the matching v* tag
git push --follow-tags

.github/workflows/publish.yml then re-runs the whole suite, refuses to continue when the tag disagrees with package.json, and publishes with npm provenance.

The first release needs credentials

Trusted publishing cannot perform a package's first publish, because npm requires the package to exist before a trusted publisher can be attached to it. So the order is:

npm install -g npm@latest   # npm 10 has no `trust` command
npm login
npm publish                 # claims the name and creates the package

Then attach the trusted publisher, either on npmjs.com under the package's Access settings, or from the CLI. npm trust needs npm 11.15 or later and account-level 2FA, and it reads the repository from package.json:

npm trust github punktraster --file publish.yml --allow-publish

From then on every tag publishes keyless, and the NPM_TOKEN secret and its env block in the workflow can be deleted.

Trusted publishing also needs npm 11.5.1 or later. Node 22 still bundles npm 10, so the workflow upgrades npm before publishing; without that step OIDC fails with ENEEDAUTH.

The repository field must stay accurate at mono424/punktraster, since npm resolves the README's relative image paths and the provenance link through it.

The hero image is committed rather than built in CI, because rendering it needs Chrome. Re-run npm run hero when the default look changes.

License

MIT