live scene
a real three.js scene — geometry, camera, lighting — re-drawn as text every frame. drag your pointer across it.
createAsciiArt({ canvas, source: { scene, camera, update } })
// a render engine, not a font trick
three.js scenes · images · video · your webcam · 2D canvases — re-drawn as phosphor glyphs every frame. scroll-driven stories, audio reactivity, zero postprocessing deps.
agents: your entry point is /start.md — quickstart, API, recipes. the button copies a ready prompt.
the scene is drawn into a buffer with ~one texel per character cell, MSAA-averaged — then a fullscreen pass stamps a glyph per cell by luminance, phosphor persistence smears the bright ones, and a CRT pass bends, glows and scans the result. that first trick makes the expensive pass ~two orders of magnitude smaller than naïve full-res rendering.
source ──► grid buffer ──► ASCII pass ──► persistence ──► CRT pass ──► screen scene, ~cols × rows glyph per phosphor curvature, glow, image, MSAA ×4 cell by afterglow scanlines, glitch, video… luminance power envelope
a real three.js scene — geometry, camera, lighting — re-drawn as text every frame. drag your pointer across it.
createAsciiArt({ canvas, source: { scene, camera, update } })
any image, in full colour — each glyph takes the source's own chroma. slide back to pure phosphor.
art.frame.colorize = 1
a plain 2D canvas being redrawn — plasma in this case — piped through the same pipeline. persistence turns motion into phosphor trails.
createAsciiArt({ canvas, source: my2dCanvas })
you, at sixty frames a second, in phosphor glyphs. the closest thing to looking into a 1978 monitor.
source: (await createWebcamSource()).video
the "font" is a runtime-built atlas from any string, dark → bright. swap it live; bring your own characters.
art.setRamp(RAMPS.katakana)
P1 green, P3 amber, ice, hot magenta — the tint is a per-frame uniform, so a palette is one line.
art.frame.tint.set('#ffb454')
datamosh, cell scramble, degauss jitter, phosphor smear — and a power envelope that collapses the tube to a bright line, then a dot, then black.
frame.glitch / frame.scramble / frame.power
a WebAudio analyser feeds bass / mid / treble / beat into the same per-frame contract. running on a synthetic beat — feed it your mic.
await audioReactor.connectMic()
the source lands in a grid-sized buffer — one MSAA-averaged texel per character cell. the ASCII pass only ever reads one sample per cell, so full-res shading would be ~50–200× wasted work.
a runtime canvas builds the glyph atlas from any ramp string — no font assets. each cell picks its glyph by luminance; scramble swaps a fraction for garbage characters.
persistence is a ping-pong max(current, previous × decay) — bright glyphs leave real phosphor trails, frame-rate corrected, free when off.
one last pass: barrel curvature, chromatic aberration, glow, scanlines, vignette — plus the drivable envelopes: datamosh, degauss jitter, and the power collapse.
// one call per canvas. three.js is the only peer dep.
import { createAsciiArt, RAMPS } from '@borkiss/tre/core'
const art = createAsciiArt({
canvas,
source: myImageOrVideoOrCanvasOrScene,
ramp: RAMPS.classic,
fit: 'cover',
})
// drive the look any time — no re-renders, no allocations
art.frame.colorize = 1 // glyphs take the source's colour
art.frame.persistence = 0.6 // phosphor trails
art.frame.glitch = 0.4 // datamosh
art.frame.power = 0 // CRT off. bright line → dot → black
art.snapshot() // → PNG data URL
React? <AsciiRenderer /> drops the same pipeline into any R3F canvas.
scroll stories? ScrollStage maps Lenis → one GSAP timeline → your params.
the README has both.
@borkiss/tre/corethe graphics engine alone — pipeline, standalone art, sources, audio, ramps. peer dep: three, nothing else.
@borkiss/treeverything — plus <AsciiRenderer /> for R3F, ScrollStage, act tables and the one master GSAP timeline.