Sources
Three ways in
A local file is the simple case: it is read with URL.createObjectURL and decoded in place. A direct video link works too, as long as the server sends an Access-Control-Allow-Origin header — without one the browser will play the video but refuse to let the canvas read it back.
A YouTube or TikTok link is the case that cannot work in a page. The player is a cross-origin iframe, and the media URL behind it is signed and CORS-locked, so there are no pixels to read. Capture a tab goes around the problem from the other side: share the tab that is playing the clip and the frames arrive as an ordinary MediaStream, which the same renderer treats like any other source. Running the page from serve.command adds the second route — a small local helper fetches the link with yt-dlp and serves the file from this origin, so pasting a link works directly.
What the dials do
Resolution is the whole picture
The video is never drawn to the screen. Each frame is scaled down to exactly one pixel per character cell, that pixel's luminance is corrected and quantised onto the glyph ramp, and the row is printed as text. Raising the resolution adds columns, which shrinks the type to match — the grid always fills the same frame.
Colour comes from the frame itself. The glyph layer is drawn white into an offscreen canvas and the downsampled frame is composited through it, so every cell keeps its own colour at full opacity — the character carries the brightness, the fill carries the hue. Saturation pivots around each cell's luminance, so pushing it never changes which glyph gets picked.
| Control | Range | Effect |
|---|---|---|
| Resolution | 24 – 320 cols | Columns across the frame. Rows follow from the video's aspect ratio and the cell aspect. High values are detailed but soft; low values are legible as type. |
| Cell aspect | 1.0 – 2.8 | How tall a character cell counts as, relative to its width. Around 2.0 keeps a monospace grid looking square; lower stretches the image vertically. |
| Brightness | −0.6 – +0.6 | Added to every channel before quantising, so it lifts the colour and the glyph density together. Push it up to pull detail out of dark footage. |
| Contrast | 0.4 – 2.8 | Scales every channel around mid grey. High contrast makes the ramp snap between empty and solid. |
| Saturation | 0 – 2.5 | Pushes each cell's colour away from its own grey. Zero renders in luminance only; above one puts the colour back that averaging a cell down to one pixel takes out. |
| Glyph set | 5 sets | Ramp is the classic ten-step; Fine is a 70-step ramp for detail; Blocks and Bars use Unicode shading; Binary is ones and zeros. |
| Colour | 3 modes | Full colour masks the frame's own colour through the glyphs at full opacity. Ink and Accent fall back to a single theme colour graded by ramp opacity. |