Voronoi Text Cells
Drag seed points and watch text reflow inside shifting Voronoi cells.
This demo divides the canvas into Voronoi cells based on draggable seed points. Each cell contains text that reflows to fit the cell's irregular boundaries. As you drag seeds, cell shapes change and text adapts in real time. Seeds also slowly orbit their positions for a mesmerizing ambient effect.
prepareWithSegments()layoutNextLine() What this demonstrates
Text flowing inside arbitrary non-rectangular regions. Each Voronoi cell has different
boundaries at each y-coordinate, and layoutNextLine() handles the variable
width per line. Dragging seeds reshapes all cells simultaneously.
Relevant Pretext API
prepareWithSegments(text, font)— prepare each cell's textlayoutNextLine(prepared, cursor, maxWidth)— variable width per line based on cell geometry
Why this is impressive
Traditional layout engines cannot flow text inside arbitrary polygons. With Pretext's line-by-line layout, any shape boundary can determine text width. This opens doors to magazine-style layouts, infographics, and creative typographic compositions.
Quick start
import { prepareWithSegments, layoutNextLine, buildFont } from '@chenglou/pretext';
// 6 seed points with positions, colors, and text to fill each cell
const seeds = [
{ x: 200, y: 150, baseX: 200, baseY: 150, color: '#7c6cf0',
text: 'Typography is the art and technique of arranging type...' },
{ x: 500, y: 300, baseX: 500, baseY: 300, color: '#3ecf8e',
text: 'Good design is as little design as possible...' },
// ... more seeds
];
// For each scan-line y, find which cell owns each x pixel (nearest seed)
function getCellBoundsForSeed(seedIdx, y) {
let left = Infinity, right = 0;
for (let x = 0; x < canvasWidth; x += 6) {
let closest = 0, minDist = Infinity;
for (let s = 0; s < seeds.length; s++) {
const dist = (x - seeds[s].x) ** 2 + (y - seeds[s].y) ** 2;
if (dist < minDist) { minDist = dist; closest = s; }
}
if (closest === seedIdx) {
if (x < left) left = x;
if (x + 6 > right) right = x + 6;
}
}
return left < right ? { left, right } : null;
}
// Layout text inside each Voronoi cell — width varies per line
const font = buildFont(12);
const lineHeight = Math.round(12 * 1.5);
for (const seed of seeds) {
const prepared = prepareWithSegments(seed.text, font);
let cursor = { segmentIndex: 0, graphemeIndex: 0 };
// Scan from top to bottom of canvas, one line at a time
for (let y = 10; y < canvasHeight - 10; y += lineHeight) {
const bounds = getCellBoundsForSeed(seedIdx, y);
if (!bounds || bounds.right - bounds.left < 24) continue;
// layoutNextLine adapts to whatever width the cell has at this y
const line = layoutNextLine(prepared, cursor, bounds.right - bounds.left - 8);
if (!line) break; // all text placed
ctx.fillText(line.text, bounds.left + 4, y);
cursor = line.end; // advance cursor for next line
}
}
// Animation: orbit seeds around base positions for ambient motion
function animate(time) {
for (let i = 0; i < seeds.length; i++) {
if (!seeds[i].dragging) {
seeds[i].x = seeds[i].baseX + Math.cos(time * 0.3 + i * 1.2) * 30;
seeds[i].y = seeds[i].baseY + Math.sin(time * 0.21 + i * 1.5) * 18;
}
}
// Recompute all cell layouts — layoutNextLine is fast enough for 60fps
render();
}