A typographic ASCII art playground built on top of Pretext — a fast, accurate, and comprehensive text measurement and layout library by Cheng Lou. pretex explores what becomes possible when you have precise, DOM-independent text metrics: generative ASCII rendering with proportional fonts, live camera-to-ASCII conversion, and editorially composed text that flows dynamically around draggable objects on a canvas.
pretex operates in three distinct modes, toggled from the sidebar:
Generative — A canvas samples brightness from a procedural shape (circle, wave, grid, spiral, radial, noise) and maps it to characters from a chosen set. The renderer supports both monospace and proportional fonts (Georgia in bold, normal, and italic weight bands), meaning glyphs vary not just in shade but in visual weight and width. Six cursor interaction modes — shape warp, void, spotlight, ink, lens, and repel — let you distort or reveal the field in real time.
Camera — Captures a live webcam feed, downsamples it to a grid, and renders the result as a scrolling ASCII <pre>. Optional colorization modes map luminance or chroma values directly to the text. Frame rate is governed by an RAF-based animation loop with adjustable speed and pause controls.
Editorial — Uses @chenglou/pretext to lay out long-form article text line by line across a canvas. Draggable glowing orbs act as obstacles: text reflows around them on every frame, wrapping around their bounding boxes. This mode demonstrates Pretext's variable-width layout API (layoutNextLine) working inside a live animation loop — something impossible to do with CSS alone.
- Next.js 16 (App Router, Turbopack)
- React 19
- Tailwind CSS 4
- @chenglou/pretext — the underlying text layout engine
- Node.js 20 or later
- npm
git clone <repo-url>
cd pretext-visual
npm installnpm run devOpen http://localhost:3000 in your browser.
npm run build
npm startpretext-visual/
├── app/
│ ├── layout.tsx # Root layout, metadata, global styles
│ ├── page.tsx # Client root: AppState, mode routing, ResizeObserver
│ └── globals.css # Tailwind v4 import, base resets, scrollbar styles
├── components/
│ ├── AsciiCanvas.tsx # Generative canvas renderer
│ ├── CameraAscii.tsx # Webcam capture and ASCII rendering
│ ├── EditorialCanvas.tsx # Pretext-powered text-around-orbs canvas
│ ├── Sidebar.tsx # All control panels per mode
│ └── Topbar.tsx # Navigation bar
├── hooks/
│ ├── useAnimationLoop.ts # RAF loop with speed and pause support
│ └── useCamera.ts # getUserMedia, frame capture to ImageData
├── lib/
│ ├── brightness.ts # Perlin-style noise, per-shape brightness sampling
│ ├── charPicker.ts # Map brightness to glyph by density or width
│ ├── glyphMeasure.ts # Build glyph tables via canvas measureText
│ └── imageProcessing.ts # Luma, contrast, gamma, sharpness, heatmap color
├── types/
│ └── index.ts # AppState and all shared types
├── next.config.ts
├── tsconfig.json
└── package.json
The editorial mode is the primary demonstration of Pretext's layout engine. Rather than measuring text height with getBoundingClientRect or estimating line counts, the canvas calls prepareWithSegments once on the article text, then drives layoutNextLine inside the animation loop:
import { prepareWithSegments, layoutNextLine } from '@chenglou/pretext'
const prepared = prepareWithSegments(article, font)
let cursor: LayoutCursor = { segmentIndex: 0, graphemeIndex: 0 }
let y = topPadding
while (true) {
const orb = orbAtRow(y)
const maxWidth = orb ? canvasWidth - orb.radius * 2 : canvasWidth
const line = layoutNextLine(prepared, cursor, maxWidth)
if (line === null) break
ctx.fillText(line.text, orb ? orb.radius * 2 : 0, y)
cursor = line.end
y += lineHeight
}Because layoutNextLine accepts a different maxWidth per line and performs no DOM access, this runs entirely on the main thread inside a requestAnimationFrame callback without any layout reflow. As orbs move, the text reflows on every frame.
For more on the Pretext API — including prepare, layout, walkLineRanges, measureLineStats, and the rich-inline helper — see the Pretext repository.
Next.js 16 detects a lockfile at the monorepo root (/Users/ishaanchoubey/package-lock.json) and emits a workspace-root warning. To silence it, set turbopack.root in next.config.ts:
const nextConfig: NextConfig = {
turbopack: {
root: __dirname,
},
}See the Turbopack root directory docs for details.
MIT. Copyright 2026 Ishaan Choubey.