Pretext Lab
16Variable-width wrap
Arc 6 · Streaming & Variable-Width

Variable-width wrap

The web has had “text around an image” since it had images, but the CSS float is a crude instrument: the obstacle has to live in the document, reflow is the browser's business, and the text cannot react to anything the author can compute. Pretext re-opens this old problem by handing you the line-break loop directly.

Drag the warm rectangle. The paragraph re-routes around it, line by line, at the frame rate of the drag. The text is real text — selectable, measurable, font-metric-correct — but every line's width is a value you chose in JavaScript a millisecond ago.

drag the image · text re-routes
80

Mechanism

The fixed-width primitive layout() assumes every line has the same maximum width. The variable-width primitive does not. You call layoutNextLine(prepared, cursor, maxWidth) once per line, and you choose the maxWidth you pass in — a different number every line if you want.

The loop in this demo is four lines long. Start the cursor at { segmentIndex: 0, graphemeIndex: 0 }. For each baseline y, decide: does this line's vertical band overlap the image? If yes, subtract the image's horizontal band from the full column width. If no, use the full column. Call layoutNextLine with that width, paint the line it returns, advance the cursor to the line's end, step y forward by the line height. Repeat until the call returns null.

The returned LayoutLine is a full object — { text, width, start, end } — so the rendered line is the exact string the browser would have placed at that width, with the same word-break decisions. No probe insertions, no hidden measurements. The canvas measurements from prepareWithSegments() are doing all of the work in arithmetic.

Application

The design space that opens is the editorial one the web has historically punted on:

All four of these are one layoutNextLine loop with a different “what width is available at this y?” function.

“I went to the woods because I wished to live deliberately, to front only the essential facts of life, and see if I could not learn what it had to teach, and not, when I came to die, discover that I had not lived. I did not wish to live what was not life, living is so dear; nor did I wish to practise resignation, unless it was quite necessary.”

Henry David Thoreau, Walden, Ch. 2 (1854)

Direct Claude

“text that hugs the image” variable-width loop via layoutNextLine, one call per line “re-route around the obstacle” per-line maxWidth as a function of the baseline y “live drag, text follows” run the whole loop inside pointermove; no DOM reads “wrap around anything” replace “is this y inside the image?” with any shape predicate
Next: walking the lines to collect stats — line count, widest line — without ever materializing a string.
one prepare, many layoutNextLine calls — different width each time
import { prepareWithSegments, layoutNextLine } from '@chenglou/pretext';

const FONT = "400 18px 'EB Garamond', serif";
const LINE_HEIGHT = 18 * 1.55;
const prepared = prepareWithSegments(TEXT, FONT);

function relayout(imageY, imageBottom, columnWidth, indent) {
  let cursor = { segmentIndex: 0, graphemeIndex: 0 };
  let y = 0;
  const lines = [];

  while (true) {
    // Decide the available width for this line, based on where the image is.
    const bandTop = y;
    const bandBottom = y + LINE_HEIGHT;
    const overlapsImage = bandBottom > imageY && bandTop < imageBottom;
    const width = overlapsImage ? columnWidth - indent : columnWidth;

    const line = layoutNextLine(prepared, cursor, width);
    if (line === null) break;

    lines.push({ text: line.text, y, x: 0, width: line.width });
    cursor = line.end;           // advance cursor to the line's end
    y += LINE_HEIGHT;
  }
  return lines;
}