Canvas Text Layout
Compute line breaks with Pretext and render to <canvas> — beyond the DOM.
Canvas does not have built-in multiline text rendering. Pretext provides the line-breaking logic, so you can render text to any target — Canvas 2D, WebGL, or any custom renderer.
prepareWithSegments()layoutWithLines() What this demonstrates
Pretext computes line breaks and positions. The canvas simply draws each line at its computed position. This decoupling means the same layout engine works for DOM, Canvas, SVG, WebGL, or even server-side rendering.
Relevant Pretext API
prepareWithSegments(text, font)— get segments for full line datalayoutWithLines(prepared, width, lineHeight)— returns lines with text and widths
Why this matters
Canvas-based text editors, game UIs, data visualization labels, and custom rendering engines all need line breaking. Without Pretext, you have to implement your own or hack it through hidden DOM elements. Pretext gives you correct, Unicode-aware line breaking as a pure function.
Quick start
import { prepareWithSegments, layoutWithLines } from '@chenglou/pretext';
// Canvas has NO built-in multiline text. Pretext provides
// the line-breaking logic; canvas just draws each line.
const fontSize = 18;
const lineHeight = 30;
const maxWidth = 520;
const font = `${fontSize}px Inter, sans-serif`;
// prepareWithSegments gives line-level data (text + width per line)
const prepared = prepareWithSegments(text, font);
const result = layoutWithLines(prepared, maxWidth, lineHeight);
// DPR-aware canvas setup — critical for crisp text on Retina
const canvas = document.querySelector('canvas')!;
const ctx = canvas.getContext('2d')!;
const dpr = window.devicePixelRatio || 1;
const pad = 50;
canvas.width = (maxWidth + pad + 20) * dpr;
canvas.height = (result.height + 60) * dpr;
canvas.style.width = (maxWidth + pad + 20) + 'px';
canvas.style.height = (result.height + 60) + 'px';
ctx.scale(dpr, dpr);
// Color helpers for per-line visualization
const rainbow = ['#7c6cf0', '#3ecf8e', '#f5a623', '#06b6d4', '#ec4899'];
function heatColor(ratio: number): string {
const r = Math.round(255 * ratio);
const b = Math.round(255 * (1 - ratio));
return `rgb(${r}, 80, ${b})`;
}
// Render each line — Pretext computed the breaks, we just draw
const originX = pad;
const originY = 30;
for (let i = 0; i < result.lines.length; i++) {
const line = result.lines[i];
const y = originY + i * lineHeight;
const fillRatio = line.width / maxWidth;
// Line background box — opacity reflects how full the line is
ctx.fillStyle = rainbow[i % rainbow.length] + '18';
ctx.fillRect(originX, y, line.width, lineHeight - 1);
// Width fill bar — visual indicator of line utilization
ctx.fillStyle = heatColor(fillRatio) + '55';
ctx.fillRect(originX + maxWidth + 4, y + 4,
fillRatio * 30, lineHeight - 9);
// Draw the actual text
ctx.font = font;
ctx.fillStyle = rainbow[i % rainbow.length];
ctx.textBaseline = 'top';
ctx.fillText(line.text, originX, y + (lineHeight - fontSize) / 2);
// Line number gutter
ctx.font = '10px JetBrains Mono, monospace';
ctx.fillStyle = '#5c5c6e';
ctx.textAlign = 'right';
ctx.fillText(`${i + 1}`, originX - 8, y + (lineHeight - 10) / 2);
ctx.textAlign = 'left';
}