Greedy line breaking versus Knuth-Plass, with rivers and the breakpoint graph.
The same paragraph is justified to the same measure twice. Greedy first fit packs each line as full as it can and never looks back, so an unlucky long word leaves a line full of holes. Knuth-Plass, the algorithm inside TeX, gives every line an adjustment ratio r, a badness of 100|r|^3 and demerits of (10 + badness)^2, then finds the set of breaks with the fewest total demerits as a shortest path through a graph of feasible breakpoints, relaxing its tolerance when no path exists. Stretched spaces are tinted red, rivers of aligned gaps are traced in blue, each line's badness sits in the margin, and the graph underneath draws every feasible line as an arc with the optimal path in gold and greedy's choices in red.
Try it. Drag the blue handles (or click inside a column) to set the column width and watch both paragraphs reflow. Hover a line to see its ratio, badness and demerits and its arc in the graph. Enter or Space switches the text, R toggles rivers, and the arrow keys nudge the width. Left alone, the measure slowly breathes.
Paste this into Claude Code, Codex or any coding agent to get a simple version running, then take it wherever you like.
Build a side-by-side comparison of greedy line breaking and the Knuth-Plass algorithm with JavaScript and the HTML canvas element. Put everything in a single index.html file with no libraries or build step, so I can open it directly in a browser.
Start simple:
- Make a canvas that fills the window, stays sharp on high-DPI screens (scale by devicePixelRatio), and resizes with the window. Paint it an off-white paper color.
- Put a public domain paragraph in a string (for example the last paragraph of Darwin's Origin of Species) and measure every word once with ctx.measureText in a serif font, caching the widths.
- Model each space as glue: natural width w, able to stretch by w/2 or shrink by w/3. For a line, the adjustment ratio r is the missing width divided by the total stretch (or the excess divided by the shrink), badness is 100|r|^3, and demerits are (10 + badness)^2. The last line is set ragged with badness 0.
- Greedy: put as many words on each line as fit at natural spacing.
- Knuth-Plass: let best[j] be the fewest total demerits for setting the first j words. For each j, try every earlier break i whose line i..j can be set with r >= -1 and badness under a tolerance, keep the cheapest, and walk back from the end to recover the breaks.
- Draw both paragraphs fully justified in two columns of the same width, with each line's badness written in the left margin.
Once that works, make it beautiful:
- Add a draggable handle that sets the column width so both paragraphs reflow live, easing words to their new positions.
- Tint stretched spaces red, and trace rivers: gaps that overlap the gap below them for three or more lines.
- Under the columns, draw the breakpoint graph: each possible break as a dot on a line, each feasible line as an arc, the optimal path highlighted and greedy's breaks drawn below.
Explain the key ideas in short code comments. When you're done, tell me how to open it and suggest three directions I could take it next, such as adding hyphenation points with a penalty, Knuth's fitness classes for adjacent lines, or an emergency stretch pass for very narrow columns.