{"id":51477395,"url":"https://github.com/mlekhi/termcade","last_synced_at":"2026-07-06T22:30:35.976Z","repository":{"id":368143960,"uuid":"1281762147","full_name":"mlekhi/termcade","owner":"mlekhi","description":"Software 3D renderer for the terminal","archived":false,"fork":false,"pushed_at":"2026-06-29T07:13:52.000Z","size":244,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-29T09:12:05.657Z","etag":null,"topics":["3d","ascii","rasterizer","renderer","terminal","truecolor","tui","typescript"],"latest_commit_sha":null,"homepage":"https://www.npmjs.com/package/ascii-3d","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mlekhi.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE.md","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-26T22:36:38.000Z","updated_at":"2026-06-29T07:13:53.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/mlekhi/termcade","commit_stats":null,"previous_names":["mlekhi/termcade"],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/mlekhi/termcade","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mlekhi%2Ftermcade","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mlekhi%2Ftermcade/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mlekhi%2Ftermcade/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mlekhi%2Ftermcade/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mlekhi","download_url":"https://codeload.github.com/mlekhi/termcade/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mlekhi%2Ftermcade/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35208141,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-07-06T02:00:07.184Z","response_time":106,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["3d","ascii","rasterizer","renderer","terminal","truecolor","tui","typescript"],"created_at":"2026-07-06T22:30:34.890Z","updated_at":"2026-07-06T22:30:35.943Z","avatar_url":"https://github.com/mlekhi.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# termcade\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"https://raw.githubusercontent.com/mlekhi/termcade/main/assets/coin.gif\" width=\"240\" alt=\"A spinning Mario-style gold coin rendered in the terminal with termcade\" /\u003e\n  \u003cbr /\u003e\n  \u003cem\u003eA spinning coin, rendered entirely in the terminal. See \u003ca href=\"examples/coin/scene.ts\"\u003eexamples/coin\u003c/a\u003e.\u003c/em\u003e\n\u003c/p\u003e\n\ntermcade renders real 3D in your terminal. It rasterizes triangles in plain\nTypeScript and paints them with truecolor half-blocks or shape-matched glyphs.\nThere's no GPU, no WebGL, and no native dependencies, just math and characters.\n\nThe renderer is stateless. It doesn't run a loop or own the screen; you call it\nwith a scene and it hands back a buffer of pixels (or a string of characters)\nthat you can do whatever you like with.\n\n```\n┌───────────┐   ┌──────────────┐   ┌─────────────────────┐\n│ rasterize │ → │ RenderTarget │ → │ toHalfBlock / glyph │ → terminal\n└───────────┘   └──────────────┘   └─────────────────────┘\n```\n\n## Install\n\n```bash\nnpm install termcade\n```\n\nIt's ESM only and runs on Node 18+ or Bun, with zero runtime dependencies.\n\n## Getting started\n\nHere's a spinning, lit cube. It's really only three steps: make a buffer,\nrasterize a mesh into it, and turn the pixels into characters.\n\n```ts\nimport {\n  RenderTarget, rasterize, downsample, toHalfBlock,\n  cube, lambertMaterial, cameraMatrices,\n  mat4Multiply, mat4RotX, mat4RotY, normalize3, type Camera,\n} from 'termcade';\n\nconst SS = 2;\nconst cols = process.stdout.columns ?? 80;\nconst rows = process.stdout.rows ?? 24;\n\n// A buffer rendered at 2 pixels per terminal cell, which is the half-block trick.\nconst target = new RenderTarget(cols * SS, (rows - 1) * 2 * SS);\n\nconst mesh = cube(1);\nconst camera: Camera = {\n  eye: { x: 0, y: 0, z: 4.5 }, target: { x: 0, y: 0, z: 0 }, up: { x: 0, y: 1, z: 0 },\n  fovy: Math.PI / 3, near: 0.1, far: 100,\n};\nconst light = normalize3({ x: -0.4, y: 0.7, z: 0.6 });\n\nlet t = 0;\nsetInterval(() =\u003e {\n  t += 1 / 30;\n  target.clear(0, 0, 0);\n  const { viewProjection } = cameraMatrices(camera, target.width / target.height);\n  const model = mat4Multiply(mat4RotY(t * 0.6), mat4RotX(t * 0.35));\n  const mvp = mat4Multiply(viewProjection, model);\n\n  rasterize(target, mesh, lambertMaterial, { mvp, model, lightDir: light, ambient: 0.15 });\n\n  process.stdout.write('\\x1b[H' + toHalfBlock(downsample(target, SS)));\n}, 1000 / 30);\n```\n\nThe animation is nothing more than nudging `t` each tick and rebuilding the\nmodel matrix from it. There's no scene graph and no framework to learn. If you\nwant it to spin faster, change a number; if you want it to stop, stop calling\n`rasterize`.\n\n## What's in the box\n\n| Export | What it does |\n| --- | --- |\n| `RenderTarget` | RGB color buffer plus a depth buffer (render at 2x height for half-blocks) |\n| `rasterize` | perspective-correct software triangle rasterizer, depth-tested |\n| `Material` (`{ vertex, fragment }`) | the style hook, a shader pair. Bring your own, or use a built-in |\n| `lambertMaterial` / `glassMaterial` / `wispMaterial` / `pieceMaterial` | ready-made looks |\n| `cube` / `quad` / `tetrahedron` / `parseObj` | meshes, or load your own `.obj` |\n| `cameraMatrices` plus `mat4*` / `vec*` helpers | camera and linear algebra |\n| `toHalfBlock` | `▀` upper half-block: two stacked pixels per cell, with coalesced truecolor escapes |\n| `toShapeGlyph` | picks the character whose ink *shape* best matches each cell |\n| `toLuminance` | the classic brightness-ramp ASCII look |\n| `downsample` | box-averages a supersampled buffer down for antialiased edges |\n| `bloom` | an additive glow post-pass |\n\n## Writing a material\n\nA material is just two functions, a vertex shader and a fragment shader. The\nvertex stage projects each point into clip space, and the fragment stage decides\nthe color of each pixel.\n\n```ts\nimport { type Material, type Mat4, mat4MulVec4 } from 'termcade';\n\nconst flat: Material\u003c{ mvp: Mat4 }\u003e = {\n  vertex: (u, v) =\u003e ({\n    clip: mat4MulVec4(u.mvp, { ...v.position, w: 1 }),\n    world: v.position, normal: v.normal, uv: v.uv, color: v.color,\n    bary: { x: 0, y: 0, z: 0 },\n  }),\n  fragment: () =\u003e ({ r: 255, g: 80, b: 200, a: 1 }), // RGBA, or return null to discard\n};\n```\n\nBecause every visual style lives in the material, one renderer can drive all of\nthem. The built-ins are a good place to start reading if you want to write a\nfancier one.\n\n## Try it\n\n```bash\nnpm run example     # live spinning cube (press q to quit)\nnpm run snapshot    # render one frame to .snapshots/cube.ppm (headless, no TTY)\n```\n\n## Loading textures (Node only)\n\nPNG decoding is the one piece that needs a Node builtin (`node:zlib`), so it\nlives behind its own subpath and the core renderer never imports it. That keeps\nthe main entry safe to bundle for the browser.\n\n```ts\nimport { decodePng } from 'termcade/png';   // Node and Bun only\nimport { sampleTexture } from 'termcade';    // platform-neutral\n\nconst tex = decodePng(await readFile('logo.png'));\nconst rgba = sampleTexture(tex, 0.5, 0.5);\n```\n\n## Compatibility\n\nEach of these was checked by packing the tarball, installing it into a fresh\nproject, and running it for real.\n\n| Target | Status |\n| --- | --- |\n| Node 18+ (ESM) | ✅ |\n| Bun (ESM and native TS) | ✅ |\n| TypeScript via `tsx` | ✅ |\n| `tsc` types (`Bundler` and `NodeNext` resolution) | ✅ |\n| esbuild and other bundlers, Node target | ✅ |\n| esbuild and other bundlers, **browser** (core, no `termcade/png`) | ✅ ~13 kB, no Node builtins |\n| `require()` (CommonJS) | ❌ ESM only, use `import` or a dynamic `import()` |\n| `termcade/png` in the browser | ❌ needs `node:zlib` |\n\nIn short, the main entry is pure compute and bundles for the browser, while\n`termcade/png` is the only thing tied to Node or Bun. If you're on CommonJS,\nreach it through a dynamic `import()`.\n\n## Development\n\n```bash\nnpm install\nnpm run type-check   # tsc --noEmit\nnpm run build        # emit dist/ (js and d.ts)\nnpm run example      # run the cube example via tsx\n```\n\nThe source is plain ESM TypeScript with explicit `.ts` import specifiers, and\nthe build rewrites those to `.js` on emit. Everything lives in `src/`, the\npublic API is the `src/index.ts` barrel, and `src/png.ts` is the only Node-bound\nmodule.\n\nContributions are welcome. The main things to keep in mind are to leave the\nrenderer dependency-free and the public surface small, so please open an issue\nbefore any large changes.\n\n## License\n\nMIT, see [LICENSE](LICENSE). Attributions are in [NOTICE.md](NOTICE.md).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmlekhi%2Ftermcade","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmlekhi%2Ftermcade","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmlekhi%2Ftermcade/lists"}