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

termcade

v0.1.1

Published

Software 3D renderer that draws to the terminal with truecolor half-blocks and shape-matched glyphs. Pure TypeScript, no GPU, no native deps.

Readme

termcade

termcade renders real 3D in your terminal. It rasterizes triangles in plain TypeScript and paints them with truecolor half-blocks or shape-matched glyphs. There's no GPU, no WebGL, and no native dependencies, just math and characters.

The renderer is stateless. It doesn't run a loop or own the screen; you call it with a scene and it hands back a buffer of pixels (or a string of characters) that you can do whatever you like with.

┌───────────┐   ┌──────────────┐   ┌─────────────────────┐
│ rasterize │ → │ RenderTarget │ → │ toHalfBlock / glyph │ → terminal
└───────────┘   └──────────────┘   └─────────────────────┘

Install

npm install termcade

It's ESM only and runs on Node 18+ or Bun, with zero runtime dependencies.

Getting started

Here's a spinning, lit cube. It's really only three steps: make a buffer, rasterize a mesh into it, and turn the pixels into characters.

import {
  RenderTarget, rasterize, downsample, toHalfBlock,
  cube, lambertMaterial, cameraMatrices,
  mat4Multiply, mat4RotX, mat4RotY, normalize3, type Camera,
} from 'termcade';

const SS = 2;
const cols = process.stdout.columns ?? 80;
const rows = process.stdout.rows ?? 24;

// A buffer rendered at 2 pixels per terminal cell, which is the half-block trick.
const target = new RenderTarget(cols * SS, (rows - 1) * 2 * SS);

const mesh = cube(1);
const camera: Camera = {
  eye: { x: 0, y: 0, z: 4.5 }, target: { x: 0, y: 0, z: 0 }, up: { x: 0, y: 1, z: 0 },
  fovy: Math.PI / 3, near: 0.1, far: 100,
};
const light = normalize3({ x: -0.4, y: 0.7, z: 0.6 });

let t = 0;
setInterval(() => {
  t += 1 / 30;
  target.clear(0, 0, 0);
  const { viewProjection } = cameraMatrices(camera, target.width / target.height);
  const model = mat4Multiply(mat4RotY(t * 0.6), mat4RotX(t * 0.35));
  const mvp = mat4Multiply(viewProjection, model);

  rasterize(target, mesh, lambertMaterial, { mvp, model, lightDir: light, ambient: 0.15 });

  process.stdout.write('\x1b[H' + toHalfBlock(downsample(target, SS)));
}, 1000 / 30);

The animation is nothing more than nudging t each tick and rebuilding the model matrix from it. There's no scene graph and no framework to learn. If you want it to spin faster, change a number; if you want it to stop, stop calling rasterize.

What's in the box

| Export | What it does | | --- | --- | | RenderTarget | RGB color buffer plus a depth buffer (render at 2x height for half-blocks) | | rasterize | perspective-correct software triangle rasterizer, depth-tested | | Material ({ vertex, fragment }) | the style hook, a shader pair. Bring your own, or use a built-in | | lambertMaterial / glassMaterial / wispMaterial / pieceMaterial | ready-made looks | | cube / quad / tetrahedron / parseObj | meshes, or load your own .obj | | cameraMatrices plus mat4* / vec* helpers | camera and linear algebra | | toHalfBlock | upper half-block: two stacked pixels per cell, with coalesced truecolor escapes | | toShapeGlyph | picks the character whose ink shape best matches each cell | | toLuminance | the classic brightness-ramp ASCII look | | downsample | box-averages a supersampled buffer down for antialiased edges | | bloom | an additive glow post-pass |

Writing a material

A material is just two functions, a vertex shader and a fragment shader. The vertex stage projects each point into clip space, and the fragment stage decides the color of each pixel.

import { type Material, type Mat4, mat4MulVec4 } from 'termcade';

const flat: Material<{ mvp: Mat4 }> = {
  vertex: (u, v) => ({
    clip: mat4MulVec4(u.mvp, { ...v.position, w: 1 }),
    world: v.position, normal: v.normal, uv: v.uv, color: v.color,
    bary: { x: 0, y: 0, z: 0 },
  }),
  fragment: () => ({ r: 255, g: 80, b: 200, a: 1 }), // RGBA, or return null to discard
};

Because every visual style lives in the material, one renderer can drive all of them. The built-ins are a good place to start reading if you want to write a fancier one.

Try it

npm run example     # live spinning cube (press q to quit)
npm run snapshot    # render one frame to .snapshots/cube.ppm (headless, no TTY)

Loading textures (Node only)

PNG decoding is the one piece that needs a Node builtin (node:zlib), so it lives behind its own subpath and the core renderer never imports it. That keeps the main entry safe to bundle for the browser.

import { decodePng } from 'termcade/png';   // Node and Bun only
import { sampleTexture } from 'termcade';    // platform-neutral

const tex = decodePng(await readFile('logo.png'));
const rgba = sampleTexture(tex, 0.5, 0.5);

Compatibility

Each of these was checked by packing the tarball, installing it into a fresh project, and running it for real.

| Target | Status | | --- | --- | | Node 18+ (ESM) | ✅ | | Bun (ESM and native TS) | ✅ | | TypeScript via tsx | ✅ | | tsc types (Bundler and NodeNext resolution) | ✅ | | esbuild and other bundlers, Node target | ✅ | | esbuild and other bundlers, browser (core, no termcade/png) | ✅ ~13 kB, no Node builtins | | require() (CommonJS) | ❌ ESM only, use import or a dynamic import() | | termcade/png in the browser | ❌ needs node:zlib |

In short, the main entry is pure compute and bundles for the browser, while termcade/png is the only thing tied to Node or Bun. If you're on CommonJS, reach it through a dynamic import().

Development

npm install
npm run type-check   # tsc --noEmit
npm run build        # emit dist/ (js and d.ts)
npm run example      # run the cube example via tsx

The source is plain ESM TypeScript with explicit .ts import specifiers, and the build rewrites those to .js on emit. Everything lives in src/, the public API is the src/index.ts barrel, and src/png.ts is the only Node-bound module.

Contributions are welcome. The main things to keep in mind are to leave the renderer dependency-free and the public surface small, so please open an issue before any large changes.

License

MIT, see LICENSE. Attributions are in NOTICE.md.