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

braillecanvas

v0.4.0

Published

A braille framebuffer for the terminal, with 24-bit colour and collision-avoiding labels. 2x4 dots per cell.

Downloads

1,359

Readme

braillecanvas

A braille framebuffer for the terminal, with 24-bit colour and labels that refuse to overlap.

Unicode braille addresses 2×4 dots per character cell, so an 80×24 terminal is really a 160×96 canvas. That's what separates a rendering from ASCII art.

⠄ sin ⠄⣀⡤⠤⠤⣀⠄ ⠄ ⠄ ⠄ ⠄ ⠄ ⠄ ⠄ ⠄ ⠄ ⠄ ⠄ ⠄ ⠄ ⠄ ⠄ ⠄⣀⠤⠤⠤⣀⠄ ⠄ ⠄ ⠄ ⠄ ⠄ ⠄ ⠄ ⠄ ⠄ ⠄
     ⡠⠊     ⠉⠢⡀                           ⢀⠔⠉     ⠑⢄
⠉⠑⢄⡠⠊         ⠈⠢   ⢀⠔⠉⠉⠑⢄                ⣐⠕⠉⠉⠒⢄     ⠑⢄         ⢀⠔⠉⠉⠒⢄
  ⠔⠡⡀           ⠑⡀⡐⠁     ⠡⡀            ⢀⡚⠁     ⠡⡀     ⠢⡀      ⡐⠁     ⠡⡀
⢀⠊  ⠐⠄           ⠜⢂       ⠐⠄          ⡠⠕        ⠐⠄     ⠐⠄    ⠔        ⠐⠄
⠉⠉⠉⠉⠉⠉⢍⠉⠉⠉⠉⠉⠉⠉⠉⢉⠍⠉⠉⠩⡉⠉⠉⠉⠉⠉⠉⠉⢍⠉⠉⠉⠉⠉⠉⠉⠉⢝⠍⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⢍⠉⠉⠉⠉⠉⠉⢋⠉⢉⠍⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉⠉
       ⠢      ⠠⠂    ⠐⠄       ⢂     ⠠⠪⠂             ⢂      ⠡⡂    cos

Install

npm install braillecanvas

Use

import {Canvas} from 'braillecanvas';

const c = new Canvas(72, 10);              // terminal columns, rows
c.line(0, 20, 143, 20);                    // dot coordinates: 2x the cols, 4x the rows

for (let x = 0; x < 144; x++)
    c.set(x, 20 - Math.round(18 * Math.sin(x / 12)), 0x3987e5, 2);

c.tryText(2, 0, 'sin', 0x3987e5);          // cell coordinates
console.log(c.toString());

A series is one call, and a frame around it is another:

const c = new Canvas(60, 8);
const pts = Array.from({length: 120}, (_, i) => [i, 16 + Math.round(13 * Math.sin(i / 9))]);
c.rect(0, 0, 119, 31);                     // plot frame
c.polyline(pts, 0x3987e5);                 // the series

polyline skips a non-finite point rather than throwing or truncating: NaN is what a projection returns for a sample behind the viewer, and one bad reading in a thousand should leave a gap, not delete the rest of the series.

Example: an image, dithered

examples/image.js renders a picture as braille. A 110×65 terminal is a 220×260 bitmap — enough for a recognisable face.

the Mona Lisa rendered as coloured braille

# any format -> PPM (the example has no dependencies, and Node can't decode JPEG)
python3 -c "from PIL import Image; Image.open('in.jpg').save('out.ppm')"

node examples/image.js out.ppm --cols 110 --btc        # colour: best quality
node examples/image.js out.ppm --cols 60 --mono --ordered   # monochrome

Use --btc for colour. It derives each cell's dot pattern from the cell's own eight pixels — cluster them on their widest colour axis, and the pattern records which pixel went which way — instead of taking the pattern from a fixed dither grid and fitting two colours to whatever split that produced. A threshold grid imposes structure on smooth regions that have none. Measured on the Mona Lisa at 93 columns, against ordered dither plus two colours:

| | dither + 2 colours | --btc | |---|---|---| | mean per-dot colour error | 44.1 | 24.8 | | cells with no texture | 35.1% | 1.6% |

Use --ordered for anything small in monochrome. The default is Floyd–Steinberg error diffusion, which is right for large photographic renders but works against you at low dot counts: it preserves local average tone by scattering error, so every region lands near its own mean and the whole picture reads as uniform texture. Ordered (Bayer) dithering uses a fixed threshold pattern, so equal tones always produce the same dot arrangement — the eye reads that regularity as a distinct shade rather than as noise.

Do not pre-boost contrast or gamma for a colour render. It is tempting — a 1-bit grid needs contrast — but with two colours per cell the tone is carried by colour, not dot density, and a boosted curve just crushes cells to all-dots-or-none. Measured on the Mona Lisa at 93 columns: contrast 1.8 plus gamma 2.2 left 32% of cells with 0 or 8 dots lit (flat, textureless), against 3.4% for a plain autocontrast.

Two limits worth knowing before you reach for this:

  • Below ~90 columns, photographs do not work. There are not enough dots for a face. Line art, charts and sparklines are fine much smaller, because they are already high-contrast.
  • Monochrome cannot render every image, however you tune it. The Mona Lisa measures 159 luminance on her face against 192 for the sky behind her — the background is brighter than the subject, so a 1-bit density render is a silhouette by construction. Check the histogram before blaming the settings.

Colour is applied per cell, and each cell carries two of them: a foreground for the lit dots and a background for the gaps, so a cell straddling an edge — skin against sky — keeps the edge instead of averaging it into mud. Measured on this image: mean per-dot colour error falls from 45.7 to 21.3, a 53% improvement.

Example: a live server monitor

examples/monitor.js plots real /proc data — CPU, memory and load average over a rolling window, with a per-core bar strip along the bottom. It is 78×20 cells, which is a 156×80 dot framebuffer.

a terminal system monitor drawn in braille

node examples/monitor.js                    # ~20s, then exits
node examples/monitor.js --frames 500 --interval 250
node examples/monitor.js --no-colour        # pipe-friendly

It exercises the three things below in one picture: the gridlines are drawn at weight 0 so a trace crossing them keeps its colour, the labels are placed with tryText so a crowded chart drops a label rather than clipping one, and the traces resolve to a quarter of a character cell.

What it does that a plain dot canvas doesn't

Most braille canvases set and clear dots. The two things that turn that into something you can actually put a chart in:

Colour, arbitrated. A cell holds eight dots but takes one foreground colour, so when several marks share a cell one has to win. set(x, y, rgb, weight) lets the heavier mark keep the colour — pass a magnitude, a z-depth, an importance, whatever ranks your marks. Blending would turn a dense cluster grey, and the eye does the same thing anyway.

Labels that decline rather than clip. tryText places a label only if the space is free and returns whether it went in, so callers place in priority order and whatever doesn't fit is simply not drawn — which is what a cartographer would do. It also flips to the other side of its anchor at the frame edge, so you never get Saturn rendered as Sat.

API

| | | |---|---| | new Canvas(cols, rows, {aspect}) | aspect is dot height/width; 1.0 is square | | set(x, y, rgb?, weight?) | light one dot (dot coords) | | setCellBackground(col, row, rgb) | paint a cell's background (cell coords) | | line(x0, y0, x1, y1, rgb?, weight?) | Bresenham | | dottedLine(..., step = 2) | every step-th dot | | polyline(points, rgb?, weight?) | connected run; [x,y] pairs or {x,y} objects | | rect(x0, y0, x1, y1, rgb?, weight?) | outline; corners in any order | | fillRect(x0, y0, x1, y1, rgb?, weight?) | solid | | circle(cx, cy, r, rgb?, weight?) | ring | | fillCircle(cx, cy, r, rgb?, weight?) | disc | | text(col, row, str, rgb?) | unconditional, cell coords | | tryText(col, row, str, rgb?, {pad}) | places only if free; returns boolean | | clear() | dots, colours and labels | | toString({colour, background}) | ANSI; background defaults to null (inherit) |

Coordinates: set/line take dot coordinates (canvas.width = cols*2, canvas.height = rows*4). text/tryText take cell coordinates, because glyphs can't live on the dot grid.

Two things worth knowing

The dots are very nearly square, which is easy to assume otherwise. A character cell is about 1:2; two columns and four rows gives spacing of w/2 and h/4 = w/2. Measured in Ubuntu Mono 11: cell 8.00 × 17px, so dots are 4.00 × 4.25px — an aspect of 1.062. aspect corrects that residual 6% by scaling y about the origin, so a circle drawn with aspect: 1.062 comes out round rather than 6% flat. Ignore it and shapes are slightly squat, not unrecognisable.

Out-of-range coordinates light nothing. NaN, undefined and negative fractions are all rejected rather than clamped — a projection returns NaN for a point behind the viewer, and silently plotting that in the corner is worse than dropping it.

Dotted guide lines are not cosmetic. Scaffolding competes with the data it points at, and on a dense canvas it wins, which is backwards. A worked example: on a 100×28 terminal, solid figures for 89 constellations put 545 lit dots of line against 186 of star — 2.9:1. A step of 3 brings it to about 1.1:1, which finally puts more ink into the subject than the guides.

Used by

  • starwheel — a live planisphere in your terminal
  • terrafirma — a real sky as your GNOME wallpaper

Extracted from those, so the awkward parts — colour arbitration, label collision, NaN coordinates from a projection — are ones it has already hit.

Changelog

  • 0.4.0 — shape primitives: polyline, rect, fillRect, circle, fillCircle. Every one clips to the canvas rather than bounding its iteration count, so a shape far larger than the view still draws the part you can see — circle(40, 500, r=480) on an 80×32 canvas has an arc straight through the middle, and an earlier draft of circle refused it outright.
  • 0.3.3dottedLine shares line's Liang–Barsky clip; a 1e8-length dotted line went from 629 ms to 0.2 ms.

Licence

MIT.