Article
glyph art
glyph art splits an image or a video into a grid of cells, measures the tone of each cell, and draws one glyph in its place. The size of the glyph is what carries the tone: a dot where the cell is quietest, a heavy mark where it is loudest. It is ASCII art with size doing the work instead of the choice of character.
There is a second mode, halftone, which works on a different principle entirely — no cells, no marks, no ramp, just a rotated screen of dots. It has its own section near the end.
It runs entirely in your browser. Files you open stay on your machine — the page is not allowed to open a connection of its own. The only thing it ever loads is its own preset marks, as lazy vector data from the same address, and only once you pick a preset.
If something will not open, the browser check reports which capabilities you have.
Load a source
Drop a file anywhere on the canvas, or use the drop zone in the source panel. It reads MP4, MOV, WebM, PNG, JPEG and WebP. SVG works for marks but not as a source — browsers will not reliably decode one into a bitmap.
Decoding is the browser's job. If an MP4 or MOV will not open, convert it to H.264 or export an image sequence from your editor instead.
frame width sets the output from 256 to 4096 px; height follows the source. The grid still decides how many marks fit across it, while frame width decides how many pixels describe those marks. The canvas you are looking at is that exported frame. The − / percentage / + controls zoom it from 25% to 800% for inspection without changing the export.
Every numeric control has both a slider and a number field. Type a value and press Enter, or leave the field, to commit it.
The grid
cells is how many cells fit across the width, from 1 to 240. The height follows the source's own proportions. 72 is a good place to start: a face survives at 72, becomes an abstraction near 48, and turns into texture past 120. Below about eight pixels per cell the marks stop reading as marks and you are better off with printor; the panel tells you when you get there.
column gap and row gap put paper between the marks, as a share of a mark's own cell — 1 is a gap as wide as the mark's cell. A gap does not shrink the marks: each keeps the size cells gives it, and the grid simply holds fewer of them. A row gap on its own turns the picture into lines of print with white between them, the way leading does in a column of type.
hand loosens the grid. It is one knob covering three things at once — a small rotation, a small offset and a small size change, drawn separately for every cell. At 0 the result is a machine. Around 0.35 it reads hand-stamped.
The important part is that hand is drawn per cell, not per frame. It stays put while the sequence plays. If it were redrawn every frame the whole surface would boil, which looks like noise rather than like a hand.
Tone
Three controls decide which band a cell lands in.
levels is a black point and a white point. It is set automatically when you load a source, from the 1st and 99th percentile of the cell tones — a flat photo quantized into seven bands only uses four of them and looks dead, so this happens before you see anything. Drag the two thumbs to override it. If the source is genuinely flat, the automatic pass leaves it alone rather than amplifying noise.
bands is how many tones the image is cut into, from 2 to 24. Three is a poster, twelve is a halftone, seven is a ladder where you can count every rung. Changing the count stretches the ramp you already built rather than resetting it.
weight bends the whole ramp at once. Lower prints heavier, higher prints lighter and airier. It is the second most useful control after levels, and the tone ramp updates live so you can see what it is doing.
max mark is how far a mark may spill past its cell. Nothing is ever clipped — the marks are stamped into one full-frame mask, so a big one simply lands on top of its neighbours — but past about 1.2 they knit into a mass instead of staying countable. max ink is the other end of the same question: how much of its cell the darkest band asks to have inked.
Those two are what fit ramp reconciles. It raises max ink as far as it will go before the first mark would spill past max mark, checking every band and every mark rather than only the darkest one — a sparse mark in the middle of the ladder hits the ceiling long before the bottom does. Press it after loading a set of your own marks and the ramp climbs as far as those marks can carry it. A set of hairlines lands on a lighter ramp than a set of blocks, which is the truth about those marks rather than a failure to reach black.
Tones are averaged in linear light and banded in perceptual lightness. That is not a detail you have to care about, but it is the reason the midtones do not clog the way they do in most tools of this kind.
The tone ramp
The strip along the bottom is the tool. One column per band, lightest on the left, darkest on the right.
Each column shows its mark at the size it will actually print, relative to the cell. Read the wells left to right and you are looking at the tone curve itself.
The number under each well is that size as a percentage of the cell. Anything over 100% overflows into its neighbours, which is deliberate: the darkest band has to overflow or the shadows break into polka dots instead of holding together as mass. max mark sets how far that is allowed to go; the presets keep it to 1.15, so their marks stay countable.
- Band 0 is empty by default and reads
paper. Paper is a value, and keeping the lightest band blank is most of what makes a first drop look like a print. - Drag a column's slider to set that band's size by hand. A
·appears next to the number to say it is no longer on the curve.rebalanceputs every band back. - A
!in the corner of a well means the mark is too sparse to reach that band's tone even at full size.
Why the size is not simply the tone
Because ink coverage grows as the square of size. Map tone straight to size and a cell at half tone gets a quarter of the ink it should; midtones wash out and shadows slam shut.
So each band asks for a fraction of its cell to be inked, and the size is solved backwards from that target and the mark's own ink density, which is measured when the mark is loaded. This is what lets a dot, an X and a solid square sit on one ramp without any of them reading two bands off.
Presets
The presets are eras of Russian military history: 1700–1812 (the Northern and the Patriotic wars), the Crimean War, the Russo-Turkish War, the Russo-Japanese and First World wars, the Civil War, the local conflicts of 1936–1940 (Spain, Khalkhin Gol, Finland), the Great Patriotic War, and Korea, Vietnam, the Middle East and Afghanistan. Each is the type of its war — letters, numerals, seals and ornaments cut out of newspapers, decrees and posters of the time — sorted onto twelve levels. Every era but the Civil War comes twice: once in Russian print alone, and once with the foreign print of the same war mixed in.
Picking one changes the marks and the shape of their ramp, and nothing about the picture. The grid, the levels, the inversions and your source all stay where you put them, so you can flip between the sets to compare them.
Every Russian mark of an era prints. Nothing is chosen and nothing is left out: each mark is dealt onto the one level where it prints, the densest to the darkest, so a level holds hundreds of different impressions and no two neighbouring cells at the same tone are likely to share one.
The darkest levels get the most care, because their marks print largest and a repeat there is the one the eye catches. Only the solid sorts reach that much ink inside a cell, so on the darkest three levels a mark may grow past its cell — up to a cell and a half, where the shadows are meant to knit into mass — and those levels are topped up to the depth of the others with marks from lighter levels printed again, larger. Only crisp, full-sized impressions print there: a soft scan stretched past its cell is a smudge, and it goes to a lighter level where it prints small.
Every cell picks from its whole level, but not every mark equally. Newspaper text is mostly the same handful of letters, so a level cut from it is mostly impressions of those, and picked at random a picture would be a few dozen letterforms over and over. So the marks of a level are grouped by letterform, and a cell picks a letterform first — each as likely as any other — and then one of its impressions. Every mark still prints; the rare letters and faces simply come up as often as the common ones.
Where marks are chosen from a handful of hand-picked scans, the marks on a level are chosen to be as unlike each other as possible, not simply the ones that print it best. That matters more than it sounds: every mark in a level prints, cycling from cell to cell, so a level filled from the top of a ranked list ends up as ten impressions of the same letter — ten marks by the count, one by eye.
Which level a mark lands on is not a property of the mark. It is a property of the size the mark has to print at to hit that level's ink. A sparse letter reaches a light level at a comfortable size and a dark one only by overflowing its cell; a solid woodblock is the reverse. Every scan is measured for its ink, its proportion and its stroke width, and each level takes the marks that land at a printable size with a stroke thick enough to survive the raster.
The same arithmetic sorts wide marks onto the light end without anything looking at their proportion. A mark is fitted into its square cell by its long side, so one two and a half times as wide as it is tall reaches only two fifths of the cell the other way and inks two fifths of what its density suggests.
Each set carries its own max ink, because that is a fact about the material: airy letterpress cannot cover as much of a cell as a solid woodblock without spilling out of it. An era of airy print is lighter for exactly that reason and will not print a true black; the eras cut from dense newspaper pages are heavier, because a case of newspaper type contains genuinely solid sorts. That is the type, not the tool.
Foreign print
A war is printed on both sides of it. The second preset of an era keeps its Russian ramp exactly and mixes in the print of the other side: Le Moniteur universel for 1812, The London Gazette for the Crimea, an Ottoman gazette for the Russo-Turkish war, Asahi Shimbun and the Deutsche Kriegszeitung for 1904–1918, Spanish, Finnish, Mongolian and Japanese print for the conflicts of 1936–1940, the German press for 1941–1945, and Korean, Vietnamese, Arabic, Hebrew and Dari newspapers for the wars after it. Foreign marks are never more than 30% of a level and never the mark the level is sized from, so they read as an accent in the Russian texture rather than a second texture on top of it. They are an even sample of what each page prints, not the most striking of it — on a page the most striking marks are its ink blots.
The price of all this is weight. An era is a few megabytes of sheets, fetched only when you pick it, and it takes a moment on a slow connection.
Nothing recognised those letters. A page with its background removed is ink on transparency, so a letter is a connected island of ink, and finding those islands is ordinary image morphology — about a tenth of a second per page. The tool never needs to know which letter a mark is, because it sizes marks by the ink it measures, not by their names.
Spatial step gradient
Enable blend steps across the image in step gradient to move through every historical era across one picture. This is a spatial transition, not a change over time. The default order starts with the Great Patriotic War, then the Civil War, includes all the remaining eras and ends with Digital reactions.
Choose the direction, move historical steps with the arrow buttons, and choose each era's Russian or foreign-print variant. transition controls the width of the mixing area: 0 gives distinct areas, 1 mixes throughout the transition. Neighbouring steps exchange whole marks — no grey or translucent symbols. The source tone still controls their size. weight, max ink, max mark, levels, spacing, inversions and grid animation remain available. fit ramp checks every step. The per-band editor returns when you disable the gradient; your custom ramp has not been overwritten. Picking a single preset also disables the gradient without discarding its order.
The last step contains simplified monochrome fire and press F / salute marks, a separate views eye, and standalone counters. The eye is never joined to a counter. Every counter has exactly two digits: 17k and 1,7k, never 17,1k. There are 180 values on every level. The smallest three levels use counters only, avoiding tiny emoji silhouettes. Counters make up 65% of the darkest levels, with the remaining marks split between fire, the owner's press F trace and the separate eye. Counters use Roboto Medium contours; no installed font is needed. All these marks export as native SVG paths, without text, emoji, external fonts or embedded images. Historical and Church Slavonic scans are vectorized ahead of time too; preview, PNG, MP4 and SVG draw the same prepared outlines.
All selected steps load their local vector data the first time you enable the gradient, so this takes longer than loading a single era. Export waits until the marks are ready. save, share, undo and redo preserve gradient settings. SVG still exports the current playhead frame; PNG and MP4 retain their existing sequence behaviour.
Marks
Six abstract marks ship as the default set — a point, a cross, a saltire, a frame, a ring and a blot — spread across seven bands, with the lightest band left as paper.
load a set in the marks panel takes several PNG, JPEG, SVG or WebP files at once and spreads them across the bands in file order. That is the fast path from a folder of drawings to a working ramp.
To place marks one at a time, drop files straight onto a band's column in the ramp, or use the + button in it. Several files dropped on one column all go to that band, where they cycle. Click a mark's chip to take it off the band.
To use letters, type characters into the marks field, pick a font, and press add. Each character becomes one mark, and they go onto whichever band is selected. Click a column to select it.
An uploaded file is read as ink, not as a picture: white backgrounds contribute nothing and transparency is respected, so a black-on-white scan needs no preparation.
Making it move
A still with one mark per band produces one frame, because nothing about it changes. Motion comes from putting more than one mark on a band.
When a band holds several marks they cycle. hold is how many frames each mark is held for, 1 to 24, and the panel shows what that works out to in marks per second at the current frame rate.
hold infinite stops temporal symbol cycling while source video and grid keyframes keep moving. The seeded choice stays fixed for each cell and tone pool; crossing into another tone band or changing the grid can still choose another mark. Shuffle remains available. The setting does not shorten the sequence, and survives saved projects, links and undo.
In classic cycle, cells start on different marks, but their switches share the same frame boundaries. For less synchronized movement, choose scatter / matrix under timing. changes is the average percentage of eligible cells scheduled per change interval in milliseconds. For example, 10% over 250 ms spreads roughly that many updates across a quarter-second, in scattered locations, rather than switching the whole surface together. Every cell gets an independent clock, with a fresh seeded event time in each time block. One-mark bands cannot change; a weighted pool may pick the same mark again. The percentage is an average rate, not a strict per-frame quota.
This changes marks directly, without a fade, morph or raster retracing. Higher FPS makes the updates finer without changing their speed in seconds. Seeking backwards, saving a project, sharing a link and exporting reproduce the same schedule. Infinite hold overrides either mode. Ordinary scatter has no exact seamless loop; frames still sets clip length. Enabling scatter starts a four-second clip if the source had only one frame; an existing sequence length is left alone. The timing panel shows its duration.
For a still on a fixed grid, enable seamless loop under scatter. Its period is exactly frames / FPS. Every active cell follows a seeded ring of 2–4 distinct marks drawn from the full eligible pool, repeating an integer number of times per clip. A random phase offset spreads switches across the loop, including the last-to-first boundary. No closing batch or duplicate end frame is appended. At rare update rates, some cells remain static: an active cell needs at least two changes to leave its first mark and return. The percentage is an average update budget, subject to the output frame rate. The ring repeats identically on each playback loop; shuffle chooses fresh marks and schedules. This option is inactive for video sources or grid animation, which keep ordinary scatter rather than claiming to loop changing source content.
Marks sharing a band should be variations of the same mark — three drawings of the same X, not three unrelated shapes. Unrelated shapes on random phase is television static. Their tone stays even as they cycle: each one is resized to print the same coverage as the band's first mark.
For a still, frames sets the sequence length and seamless loop sets it to the shortest length where every band's cycle lines up again.
For a video, the length comes from the clip. frame rate posterizes time to between 1 and 60 fps: frame N is whatever the source shows at N ÷ frame rate seconds, held, not blended.
Halftone
halftone at the top of the left panel switches to a different principle, and it is worth saying plainly how different.
The glyph mode puts one mark in one cell of a square grid and carries tone in the mark's size. A halftone has no cells and no marks. It has a lattice of dot centres at a chosen ruling, rotated to an angle that has nothing to do with the frame, and tone is the area of each dot. Colour is not one ink tinted per cell either: the picture is separated into plates, each screened at its own angle, and the plates are multiplied together the way wet ink is. That is why a rosette looks like ink — rotating a grid of marks cannot produce one.
Because there is no ramp, the bands, the marks, the seed, hand and the cycling controls all disappear. What is left is a press.
The screen
lines is the ruling: how many lines fit across the frame's width, from 8 to 200. The panel converts it for you — 60 lines across a 2048 px frame is a 34 px dot.
angle rotates the whole screen set. 45° is the default because that is the angle at which a dot grid disappears; at 0° you see rows.
dot picks the shape — round, ellipse, square, diamond, line or cross. Every one of them is solved from the same ink area, so changing the shape changes the texture of the print and not its tone. That is the whole reason to solve from area rather than from a radius: a square dot and a round dot at the same "size" are a third of a tone apart.
Separation
mono is one black screen. duotone is two screens 30° apart in two inks of your choosing; the second ink is held out of the highlights, so the pair reads as two inks rather than as one ink printed twice. cmyk is four plates at 15°, 75°, 0° and 45° — the classic set, because two screens 30° apart make a rosette and two screens a few degrees apart make a moiré the size of the page.
Under cmyk, black is grey component replacement. At 0 the neutrals are built from cyan, magenta and yellow together, which is rich and registers badly — you can see the coloured fringing in the greys. At 1 the black plate carries the neutrals alone, the way newsprint does, and the greys come out clean.
Tone
levels works as it does in the glyph mode. gain bends tone into dot area: under 1 opens the shadows, over 1 holds them back. spread is the press — every dot is scaled before it prints, so over 1 is ink bleeding into the paper and under 1 is a starved screen.
invert colour takes the frame's negative: white dots on black, and the colour negative of a plate composite. The dots keep their sizes.
Frame and export
frame width is the same free 256–4096 px control as in glyph mode, and the height follows the source. A dot is drawn as a path at a fractional position, so there is nothing to align to. Both dimensions come out even, so MP4 never has to resize the frame.
In two or four inks, separations adds one folder per plate to the ZIP, each plate black on white. That is what a printer loads.
Colour and inverting
In the glyph mode, mono prints one ink on paper. source colour fills each mark with the average colour of its own cell, so the piece keeps the source's colour while tone is still carried entirely by size. (In halftone, colour comes from the separation instead — see above.)
There are two separate inversions and they do different things:
invert colourswaps the ink and the paper. White marks on black.invert rampreverses which band a tone lands on, so the heavy marks print on the bright cells and the light marks on the dark ones. The ramp you built stays exactly as it is; only the mapping into it flips.
Export
png writes a ZIP with one PNG per frame, and can carry three passes at once, each in its own folder:
flat— marks over paper, opaque.marks → alpha— the marks only, paper transparent.paper → alpha— the paper only, with the marks punched out of it.
Together with invert colour that covers all four variants. A README.txt in the ZIP records the frame rate and either the grid, the band count and the seed, or the ruling, the angle and the separation.
mp4 writes H.264 at the target frame rate. H.264 has no alpha channel, so MP4 is always the flat result. The frame is never resized on the way out.
svg writes the current frame as editable vector paths, and it means something different in each mode. In glyph mode it uses the same prepared contours as preview, PNG and MP4, without tracing again at export time. All 61,127 historical and curated scans are vectorized during library generation. Typed characters and uploaded marks are traced once when added; native reactions keep their original curves. Half-ink boundaries remove soft scan fringes, but cannot recover detail missing from an original. Raster outputs antialias the vectors at their actual output resolution instead of enlarging low-resolution sprites.
The file is plain filled paths. Nothing in it is a reference to be resolved — no shared symbols, no mask — because a browser resolves those and a drawing program may not, and a file that opens empty in Illustrator or Figma is not an export. The cost is that every impression carries its own outline, which is why those outlines are fitted with curves: a stamped dot stays round and a punched corner stays square, for a fraction of the points a polygon would need to do either. Even so, a fine grid on a wide frame makes a file of about a megabyte. All the impressions of one ink arrive as a single path, so an editor gets one object to work with; release it into pieces there if you want the marks apart.
In halftone mode nothing is traced. The dots were solved from ink area in the first place, so they are written as the curves and polygons they already are — the file is the screen itself and stays sharp at any size. Each plate is one path, named and filled with its own ink and set to multiply the way the inks do; a mono screen is a single black path on white. A fine ruling is a lot of dots, so a large cmyk frame makes a large file.
Export stops at 900 frames.
Seed, projects, keyboard
seed drives every per-cell draw — the hand jitter and the cycle phases. The same seed reproduces a piece exactly; reroll moves to the next one. Nothing in the tool uses unseeded randomness.
save writes a project file with your marks embedded in it. share puts the settings in the address bar — uploaded marks are too large for a URL, so a share link substitutes the shipped mark for them and says so. Use the project file if you want to keep your own marks. Preset marks survive a share link: they are a name and a path rather than a bitmap.
Keyboard: space plays and pauses, the arrow keys step a frame, holding \ shows the source underneath, and ⌘Z / ⇧⌘Z undo and redo.