What happens when you can layout text in ~0.09ms instead of 30ms? Among other things: you can build games where every single game element is made of characters — and it runs at 60fps without breaking a sweat.
The pretext.cool community showcase includes two fully playable text-based games built with Pretext. Here's how they work, and what you can learn from the approach.
Traditional text-based games (think roguelikes, terminal games) sidestep the layout problem by using monospaced fonts and grid coordinates. Every character is the same width, so positioning is just multiplication.
Pretext games can use any font with any character widths — proportional fonts, variable fonts, emoji, CJK characters. That means you need real text measurement, running on every frame, for every game entity.
At DOM-based measurement speeds (15–30ms for a scene with hundreds of text elements), you'd be spending your entire frame budget on measurement before the physics loop even runs. Pretext's ~0.09ms layout time per 500-text batch changes the equation entirely.
Pretext Breaker is a complete Breakout/Arkanoid clone where every brick is a text character, the ball is a period or bullet glyph, and the paddle is a horizontal string of dashes.
What makes it technically interesting:
The classic Breakout challenge is collision detection — specifically, detecting which edge of a brick the ball hit. In a traditional sprite-based Breakout, bricks are rectangles with known pixel dimensions. In Pretext Breaker, brick dimensions are determined by font metrics, not hardcoded values.
The game uses prepare() once per level to measure all brick characters, then calls layout() on every physics frame to get the current bounding boxes of all text elements. This lets the game do pixel-perfect collision detection against actual text rendering — the ball bounces off the real edge of a glyph, not an approximation.
The architecture:
// At level load: measure all brick glyphs
const brickData = bricks.map(b => ({
prepared: prepare(b.char, "32px monospace"),
x: b.x,
y: b.y,
}));
// Every frame: get exact hit boxes
function gameLoop() {
const hitBoxes = brickData.map(b => {
const lineInfo = layoutWithLines(b.prepared, Infinity, 32);
return {
...lineInfo.lines[0], // single-line layout = exact width
x: b.x,
y: b.y,
};
});
updatePhysics(ball, paddle, hitBoxes);
render(hitBoxes, ball, paddle);
requestAnimationFrame(gameLoop);
}
Play it: pretext.cool/demo/pretext-breaker — built by rinesh.
Tetris × Pretext is a faithful Tetris implementation where tetrominoes are composed of text characters instead of colored blocks. The I-piece might be four I characters in a row; the S-piece two offset rows of Ss.
What makes it technically interesting:
Tetris has a rotation system — pieces rotate in place, and the rotated shape must fit within the well. With pixel-art sprites, rotation is just a lookup table of pre-drawn shapes. With text characters, rotation changes the visual weight and apparent size of the piece depending on glyph metrics.
The game uses Pretext to measure character dimensions in all four rotation states at load time, caching the results. The rotation logic then operates on the cached measurements, keeping the game loop pure arithmetic even when the piece shape changes.
Key insight — separation of concerns:
Game state (pure JS arrays)
↓
Pretext layout (pure arithmetic, cached measurements)
↓
Canvas rendering (draw characters at computed positions)
The game state never touches the DOM. Pretext is the bridge between "where should this piece be" (game logic) and "how wide is this character" (font reality). Rendering is a final, one-way pass to canvas.
Play it: pretext.cool/demo/tetris-pretext — built by shinichimochizuki.
Both games share a common architecture worth extracting:
prepare() is your initialization cost — pay it at load time or when content changes, never inside the game loop.
// ✅ Good — prepare at startup
const assets = {
ball: prepare("●", "32px monospace"),
paddle: prepare("━━━━━━━", "32px monospace"),
bricks: BRICK_CHARS.map(c => prepare(c, "32px monospace")),
};
// ❌ Bad — prepare inside the loop
function gameLoop() {
const ballSize = prepare("●", "32px monospace"); // re-runs measurement every frame
}
layout() for Collision, Canvas for RenderingPretext tells you where characters should be; canvas renders them. Keep these concerns separate.
A character in a proportional font has a natural width determined by the font. Work with this rather than against it — let glyph widths define your collision geometry instead of trying to force characters into fixed pixel grids.
Games are just one application. The same pattern — prepare once, layout every frame — works for:
Browse the full showcase to see all 17 community demos, or start building with the getting started guide.
Games built by rinesh and shinichimochizuki. Pretext library by chenglou.