A Tetris board where the gaps left by the pieces spell your message.
Every character is precomputed as a self-contained unit — a fixed list of drops that stands on a whole divider row and leaves a whole divider row behind — so a message is assembled by stacking them. Nothing is searched at runtime: typing HI WORLD renders in under 10 ms.
Each letter is built one row at a time inside a four-row band. Three of the four rows are throwaway fillers; the one being kept is the row of the picture. A vertical I piece closes the band by filling an open column that runs through all four rows, completing the three fillers at once. They vanish (that's the line clear), and the kept row drops into place — with its gaps intact, hanging over empty space, which no gravity-only build could ever produce.
The recipe for every character was solved once offline (scripts/gen-recipes.ts, about 40 seconds) and stored in a 21 kB table. At runtime the table is read, the moves are offset to the right height, and the physics verifies each drop against the real board.
Pure gravity, no lines ever cleared. Enclosed counters (the inside of an O, the triangle in an A) are tied to the outside of the letter with a one-cell bridge — like stencil lettering on crates.
| Feature | What it does |
|---|---|
| 5 themes | ARCADE (seven colours), WHITE, NEON, AMBER, PAPER. Uniform-colour themes carry a cell outline so you can still see individual pieces. |
| Spinning pieces | Each piece spawns a few rotations short of its landing one and turns on the way down. Only the landing rotation touches the board. |
| Next-piece queue | Five upcoming pieces in the side panel. |
| Simulated score | Points per line (100/300/500/800 × level), combo bonus, level from lines cleared. |
| Vertical / Horizontal | One tall board, or one board per character side by side. |
| Top down / Bottom up | Vertical only. Reading upward is the same picture with the characters in the other order. |
| One by one / All at once | Horizontal only. Playing all boards together takes as long as the slowest letter instead of the sum. |
| Loop | When the message finishes it flashes, wipes (downward, upward, or flash-and-gone, alternating), and starts over. |
| Replay | Restart from an empty board without changing anything else. |
| Wipe transitions | Changing the message, mode or layout clears the old picture before the new one begins. |
| PNG export | The board as it looks right now. |
| GIF export | The whole build as an animated GIF: pieces falling, lines flashing, your colours, your layout. Rebuilt offscreen, not screen-recorded. Typically 400–700 kB, under a second to produce. |
| Demo on load | The page cycles through a showcase of ornamental messages until you type something. The speed slider works on the demo too. |
| Ornamental glyphs | Arrows (↑↓←→), a 2×2 checkerboard (▚) and a heart (♥). The checkerboard is the showpiece: every block it leaves is fenced in by gaps on all four sides. |
| Responsive layout | Controls on the left, board on the right from lg up. The controls stick to the viewport. On mobile they stack with the input first. |
| Title + footer | TYPOTRIS at the top, Marabunta Labs 2026 with links at the bottom. |
npm install
npm run dev| Script | What it does |
|---|---|
npm run dev |
Development server (Next.js + Turbopack). |
npm run build |
Production build. |
npm run recipes |
Regenerate the per-character recipe table (~40 s). |
npm run recipes:check |
Replay every character through the physics and compare cell by cell. |
npm run gif:check |
Encode a set of test GIFs and decode them back, pixel for pixel. |
npm run player:check |
Verify the player, the loop, the parallel stage and the GIF export. |
npm run check |
Lint + all three check scripts. |
src/
app/
page.tsx UI: controls, layout, showcase, footer
globals.css Themed animated background, grid, scanlines
components/
TetrisGrid.tsx Canvas, animation loop, imperative handle, solve logic
SidePanel.tsx Next-piece queue and score
lib/
pixel-font.ts 9×8 font data (letters, digits, punctuation, ornaments)
unit-layout.ts Geometry of a stackable unit: rows, parity, notches
builder.ts Depth-first tiling search (shared by both modes)
builder-unit.ts Offline: one unit's recipe, row by row with line clears
tetris-board.ts Core physics: drop, place, clear, profile
tetris-pieces.ts The seven tetrominoes
tetris-solver.ts Entry point: solve(text, mode) → moves + target
recipes.ts Runtime: stack precomputed units, verify each drop
recipes.generated.ts The table (generated, do not edit)
recipe-codec.ts 4-char-per-drop encoding
player.ts Player, Stage, layout, piece geometry
stage-draw.ts Paints a Stage to a canvas
gif.ts GIF89a + LZW encoder (hand-written, no dependencies)
gif-export.ts Offscreen replay → GIF frames
themes.ts Five palettes
scripts/
gen-recipes.ts Generate the recipe table
check-recipes.ts Verify recipes against the physics
check-gif.ts GIF round-trip (encode → decode → compare)
check-player.ts Player, stage, loop, parallel, upward-reading, GIF
gif-decode.ts GIF reader (for the check scripts only)
A unit has to use a whole number of pieces (4 cells each) and cleared rows (10 cells each). The cell count of a unit is glyph + 9·separators − notches, and 10 ≡ 2 (mod 4), so the clears always contribute an even number. That forces the unit's own count to be even. Odd glyphs (30 of the 48) get an extra separator row to flip the parity. The remaining freedom — how many rows to build by gravity vs. by a clearing band — is a single bit, pinned by the arithmetic, and directCandidates in unit-layout.ts offers both options.
Made by Marabunta Labs · 2026

