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

@glowbox/led-grid

v1.1.0

Published

Framework-agnostic 3D LED-grid display — a tiny WebGL voxel canvas you draw on.

Readme

@glowbox/led-grid

Framework-agnostic 3D LED-grid display — an nx×ny×nz lattice of glowing "LEDs" you draw on like a tiny 3D canvas, rendered in WebGL and orbitable (auto-spin + drag + zoom). Zero dependencies.

yarn add @glowbox/led-grid
import { createLedDisplay } from '@glowbox/led-grid';

const d = createLedDisplay(canvas, {
	size: [8, 8, 8],
	led: { glow: 3, offColor: '#0a0a12' },
	color: { background: '#000', gain: 1.1 },
	camera: { autoOrbit: true },
	interaction: { zoom: true }
});
d?.onFrame((d, dt) => {
	d.clear();
	d.sphere([4, 4, 4], 3, '#00aaff'); // filled=false → a hollow shell
	d.plot(2, 5, 1, [1.4, 0.8, 0]); // array form; >1 blooms
});

The display owns the WebGL render, orbit, zoom, resize and the animation loop — you just write voxels each frame. It holds no content of its own.

Colours

Everywhere a colour is accepted (background, offColor, tint, and every draw call) the type is Color:

  • [r, g, b] numbers in 0..1 — values >1 are extra-bright (more bloom / HDR headroom).
  • any CSS string — '#0a0a12', '#f80', 'rgb(0 128 255)', 'tomato', 'hsl(…)'. (Strings are 0..2550..1, so only the array form can exceed 1.)

Unlit LEDs draw nothing by default (offColor is black) — only lit LEDs show, so the display reads as floating light with no dark lattice. To hint the physical grid, set offColor to a faint colour: it renders a tiny speck (led.offSize, default 0.35 of the glow) at each off node, the size of a real LED package.

Rendering (led.style)

Two looks, both reading on any background (dark or light):

  • hologram (default) — LEDs as real emitters: each lit LED is a bright point with a soft glow halo, tone-mapped and composited in front of the background. Internally an HDR (half-float) bloom pipeline (emissive → blur → ACES tone-map → over-composite), falling back to an LDR over-composite when the half-float WebGL extensions are unavailable.
  • comic — flat cel-shaded pop-art: each lit LED is a solid disc (or square) with an optional bold ink outline — a comic-book / Ben-Day look. Brightness is posterized into a few bands (keeping each LED's hue and relative brightness), so tonal content — images, fading trails — reads faithfully. Set led.vivid for the punchy flat variant instead: every lit LED at the full value of its hue (dim voxels read vivid) — great for bold graphic content. Opaque and depth-tested; pairs well with a light "paper" background.

glow/offColor/offSize apply to the hologram style; outline/outlineColor to comic. shape: 'square' makes LEDs tile gap-free (no bg between them); 'round' is the dotty LED-grid look.

Performance. Only lit voxels are packed and drawn each frame, so cost scales with how many LEDs are on, not with the grid volume — a thin shape in a huge grid stays cheap. (Setting offColor to a non-black lattice necessarily draws every node, so leave it black for big sparse grids.) The display.stats object exposes live fps / frameMs / drawMs / renderMs for profiling. Measured with scripts/bench-led-grid.mjs (640×480, hologram, auto-orbit; Apple M1, Chromium 149, ANGLE Metal) — everything below holds a vsync-locked 60 fps:

| grid · program | lit voxels | draw ms | render ms | | ----------------- | ---------- | ------- | --------- | | 32³ · torus shell | 1,560 | 1.5 | 0.1 | | 32³ · full fill | 32,768 | 0.2 | 1.2 | | 64³ · torus shell | 6,280 | 4.8 | 0.2 | | 64³ · full fill | 262,144 | 0.8 | 4.5 |

(drawMs is your callback — here the torus SDF scan dominates at 64³; renderMs is the GL submit, tracking lit count.) Re-run the script on your own hardware for real numbers; software rasterizers (SwiftShader) are far slower than any real GPU.

Voxel API

Canvas-like drawing on the grid. plot / add / get take scalar x, y, z; line / box / sphere take Vec3 tuples [x, y, z]. Every color is a Color (see above).

| method | does | | ------------------------------------------------------- | ----------------------------------------------------------------------------- | | plot(x, y, z, color) | set one voxel (overwrite) | | add(x, y, z, color) | additive blend into a voxel (accumulate — for overlapping glows) | | get(x, y, z): RGB | read a voxel's [r, g, b] | | clear(color?) | reset the whole grid (default black) | | fill(color) | set every voxel (alias of clear(color)) | | line(a, b, color) | voxel line between points a and b | | box(min, max, color, filled?) | axis-aligned box between two corners min/max — filled, else wireframe | | sphere(center, radius, color, filled?) | filled ball, else a ~1-voxel shell | | torus(center, major, minor, color, filled?, axis?) | ring around the axis (default 'y') — solid, else a tube shell | | cylinder(base, radius, height, color, filled?, axis?) | upright can from its base disc — solid, else wall + end caps |

Plus index(x, y, z) / inBounds(x, y, z) and the raw leds Float32Array (write it directly, then call markAll()). Draw headlessly with createVoxelGrid(nx, ny, nz) — same API, no canvas.

Coordinates. Integer coords 0..n-1 per axis; out-of-range is silently ignored. +x → right, +y → up, +z → toward the viewer (right-handed); voxel [0, 0, 0] is the min (left-bottom-back) corner, and the grid is centered in the view. If you write leds directly, the flat layout is x + nx * (y + ny * z) (× 3 RGB).

Options (all optional except size)

| group | fields (defaults) | | ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | led | style 'hologram'\|'comic' · shape 'round'\|'square' · stagger (false; brick lattice — offset every other row half a cell, also cuts moiré) · rgb (false; render each LED as R/G/B sub-emitters) · rgbLayout (auto|triad|quad|stripe; sub-die packing) · vivid (false; comic: flat full-value pop-art vs cel-shade) · outline (0.25; comic ink border, 0 = off) · outlineColor (dark) · size (0.6) · glow (2.2) · offColor (black = no lattice; set faint to hint the grid) · offSize (0.35) | | color | background ([.01,.01,.02]) · gain (1) · tint ([1,1,1]) | | camera | yaw/pitch (.6/.4) · distance (3.6) · fov (.9) · projection 'perspective'\|'orthographic' · autoOrbit (true — but defaults off under prefers-reduced-motion; set it explicitly to override) · orbitSpeed (.45) · pitchLimits (±1.4) | | interaction | drag (true) · dragSpeed (.01) · zoom (false) · zoomLimits ([1.5,10]) | | quality | pixelRatio (2) · antialias (true) · paused (false) · fps (uncapped; cap the loop — see below) |

Plus a top-level label — the canvas's accessible name (the display sets role="img" + aria-label; default 'LED grid'). While drag or zoom is enabled the canvas also gets touch-action: none, so touch-orbit doesn't fight page scroll.

Display methods

onFrame(cb) → stop() (cb receives (display, dt)dt is seconds since the last frame, clamped to ≤ 0.05 so a stall / tab-switch won't jerk your animation; callbacks stack in subscription order, so layer programs by subscribing several — each stop() removes only its own), render(), setGain(g), setPaused(b) (freeze → render on demand), setOptions(patch) (live-update anything but size), setCamera({yaw, pitch,distance}), resize(size?), snapshot(): string (PNG data URL — for previews / LED-cube frame export), dispose().

Frame-rate cap. quality.fps limits the render loop (e.g. { quality: { fps: 30 } }); omit it for the display's native refresh rate. It's a power / cadence knob — lower it for an always-on ambient display to cut GPU/battery, or match a hardware LED-cube's refresh rate. It is not a speed-up: each frame still costs the same, so it won't rescue a GPU-bound scene (watch display.stats.fps for the actual rate).

Pausing. Freeze the loop with quality.paused (or setPaused(true)); it already auto-pauses when the browser tab is hidden. For a display hidden within the page (a route/tab swap in your app), toggle quality.paused rather than unmounting — cheaper, and it keeps the WebGL context warm.

resize() recomputes the drawing buffer from the canvas box; resize([nx, ny, nz]) also changes the grid size in place — reallocating on the same canvas (no context loss), preserving camera + options. The display also recovers from WebGL context loss on its own (tab backgrounded, driver reset): it rebuilds the renderer on restore and repaints your content.

Framework wrappers build on this core — see @glowbox/svelte, @glowbox/react and @glowbox/vue; content helpers (GIF / image / text) live in @glowbox/extras. Live demos: https://eetu.github.io/glowbox/.