Wave Distortion
A sine wave continuously distorts the text column width — every line has a different width, updated every frame.
This demo creates a mesmerizing flowing effect by applying a sine wave to the available width of each line. The wave animates continuously, and the text reflows smoothly to fill the undulating shape. Adjust amplitude, frequency, and speed to create different wave patterns.
prepareWithSegments()layoutNextLine() What this demonstrates
Continuous per-line variable-width layout driven by a mathematical function. Each line's available width is computed from a sine wave, creating a flowing shape that the text fills. The wave phase animates every frame, causing the entire text to reflow continuously.
Relevant Pretext API
prepareWithSegments(text, font)— prepare oncelayoutNextLine(prepared, cursor, maxWidth)— different width per line, driven by sine wave
Why this is hard with the DOM
There is no CSS property that varies container width per line of text. CSS shapes and floats only offer limited control. With Pretext, any mathematical function can determine line widths — sine waves, Perlin noise, physics simulations, or user-driven interactions.
Quick start
import { prepareWithSegments, layoutNextLine, buildFont } from '@chenglou/pretext';
const font = buildFont(15);
const prepared = prepareWithSegments(longText, font);
const lineHeight = 24, margin = 16;
const containerWidth = 750;
let waveAmplitude = 120; // how far the wave pushes (px)
let waveFrequency = 3; // oscillations across the height
let waveSpeed = 1.5; // phase advance per frame
let wavePhase = 0;
function tick() {
wavePhase += 0.02 * waveSpeed; // animate the wave continuously
let cursor = { segmentIndex: 0, graphemeIndex: 0 };
let y = 0;
const lines = [];
while (y < 3000) {
const lineMid = y + lineHeight / 2;
// Sine wave: positive offset → push from left, negative → push from right
const waveVal = Math.sin((lineMid / 100) * waveFrequency + wavePhase);
const offset = waveVal * waveAmplitude;
let xStart = margin;
let availWidth = containerWidth - margin * 2;
if (offset > 0) {
// Positive: indent from the left side
xStart = margin + offset;
availWidth = containerWidth - margin - xStart;
} else {
// Negative: shrink from the right side
availWidth = containerWidth - margin * 2 + offset;
}
// Clamp: never go below 40px — avoids degenerate 1-word lines
availWidth = Math.max(40, availWidth);
const line = layoutNextLine(prepared, cursor, availWidth);
if (!line) break;
lines.push({ text: line.text, x: xStart, y, maxW: availWidth, offset });
cursor = line.end;
y += lineHeight;
}
// --- SVG edge paths: trace the wave shape along both margins ---
const leftEdge = lines.map((l, i) =>
(i === 0 ? 'M' : 'L') + ' ' + l.x + ' ' + (l.y + lineHeight / 2)
).join(' ');
const rightEdge = lines.map((l, i) =>
(i === 0 ? 'M' : 'L') + ' ' + (l.x + l.maxW) + ' ' + (l.y + lineHeight / 2)
).join(' ');
// Render as <path d={leftEdge} /> and <path d={rightEdge} />
renderLines(lines);
requestAnimationFrame(tick); // every frame = full reflow
}