Flow Around Obstacle
Text flows dynamically around a draggable shape — line by line layout control.
This demo uses layoutNextLine() to lay out text one line at a time, with a different available width per line based on an obstacle's position. Drag the shape and watch text reflow in real time.
prepareWithSegments()layoutNextLine() What this demonstrates
The layoutNextLine() API lets you lay out text one line at a time with a different
maximum width for each line. This enables text flow around arbitrary shapes — something that
is extremely difficult with CSS alone and impossible without per-line width control.
Relevant Pretext API
prepareWithSegments(text, font)— prepare with segment datalayoutNextLine(prepared, cursor, maxWidth)— one line at a time, variable widthLayoutCursor— tracks position between line calls
Why this is hard with the DOM
CSS shape-outside exists but is limited to floated elements and simple shapes.
Pretext gives you full programmatic control: any shape, any position, computed dynamically.
This is the kind of layout control that print typesetting has had for decades but the web has not.
Quick start
import { prepareWithSegments, layoutNextLine, buildFont } from '@chenglou/pretext';
// Obstacle is an ellipse the user can drag or auto-orbit
const obsX = 350, obsY = 80, obsW = 160, obsH = 140;
const pad = 14; // breathing room around the obstacle
const containerWidth = 650;
const lineHeight = 26;
const font = buildFont(16);
const prepared = prepareWithSegments(text, font);
let cursor = { segmentIndex: 0, graphemeIndex: 0 };
let y = 0;
const lines = [];
while (y < 2000) {
// Ellipse collision: does this line's vertical center intersect?
const cx = obsX + obsW / 2, cy = obsY + obsH / 2;
const rx = obsW / 2 + pad, ry = obsH / 2 + pad;
const lineMid = y + lineHeight / 2;
const dy = (lineMid - cy) / ry;
let xStart = 0;
let maxWidth = containerWidth;
if (Math.abs(dy) < 1) {
// Line intersects the ellipse — compute horizontal extent
const dx = rx * Math.sqrt(1 - dy * dy);
const ellipseLeft = cx - dx;
const ellipseRight = cx + dx;
// Choose whichever side of the obstacle is wider
if (ellipseLeft <= 60) {
// Obstacle on the left — text goes right
xStart = Math.max(0, ellipseRight);
maxWidth = containerWidth - ellipseRight;
} else {
// Obstacle on the right — text stays left
maxWidth = Math.max(40, ellipseLeft);
}
}
if (maxWidth < 20) { y += lineHeight; continue; }
// layoutNextLine: lay out ONE line at this specific width
// The cursor carries forward so the next call picks up where we left off
const line = layoutNextLine(prepared, cursor, maxWidth);
if (!line) break;
lines.push({ text: line.text, x: xStart, y, width: line.width });
cursor = line.end; // carry cursor to next line
y += lineHeight;
}
// Render each line at its computed (x, y) position
lines.forEach(l => renderLineAt(l.text, l.x, l.y));