fillText()'s fourth argument, maxWidth, does not wrap: it squeezes the glyphs until the text fits. To wrap, measure word by word and break before the word that would overflow (kept in demos/ch02/wrap.js).
<canvas id="c" style="width:600px; height:100px"></canvas><script src="hidpi.js"></script>
<script>
function wrapLines(ctx, text, maxWidth) {
const lines = []; let line = '';
for (const word of text.split(/\s+/)) {
const test = line ? line + ' ' + word : word;
if (line && ctx.measureText(test).width > maxWidth) { lines.push(line); line = word; }
else line = test;
}
return lines.concat(line ? [line] : []);
}
const ctx = hidpi(c); ctx.font = 'bold 18px Georgia';
[['Small Steps to Big Summits', '#3f7d3a', 10], ["The Clockmaker's Paradox", '#b5452f', 160]]
.forEach(([title, color, x]) => {
ctx.fillStyle = color; ctx.fillRect(x, 4, 130, 92); ctx.fillStyle = '#fff3e0';
wrapLines(ctx, title, 110).forEach((line, k) => ctx.fillText(line, x + 10, 30 + k * 22));
});
ctx.fillStyle = '#5b3f99'; ctx.fillRect(320, 4, 130, 92); ctx.fillStyle = '#fff3e0';
ctx.fillText('Patterns of the Deep Web', 330, 30, 110); // maxWidth squeezes
</script>
A word wider than the box still overflows, as Clockmaker's does; the cover designer (Cover Designer) shrinks the font until the longest word fits. Splitting on spaces fails for Chinese, Japanese and Thai, which do not separate words; Intl.Segmenter with granularity: 'word' finds their break points.
<!doctype html>
<style>
body { margin: 0; padding: 8px; font: 13px system-ui, sans-serif; background: #fafaf7; }
canvas { width: 100%; max-width: 600px; height: 160px; display: block; background: #fff; border: 1px solid #ddd; }
</style>
<canvas id="c"></canvas>
<label>Cover width <input type="range" id="w" min="90" max="180" value="130"></label>
<script>
function hidpi(c, r = devicePixelRatio) {
c.width = c.clientWidth * r; c.height = c.clientHeight * r;
const ctx = c.getContext('2d'); ctx.scale(r, r); return ctx;
}
// Break before the word that would overflow maxWidth
function wrapLines(ctx, text, maxWidth) {
const lines = []; let line = '';
for (const word of text.split(/\s+/)) {
const test = line ? line + ' ' + word : word;
if (line && ctx.measureText(test).width > maxWidth) { lines.push(line); line = word; }
else line = test;
}
return lines.concat(line ? [line] : []);
}
const ctx = hidpi(document.getElementById('c'));
function draw(w) {
ctx.clearRect(0, 0, 600, 160);
ctx.font = 'bold 18px Georgia';
[['Small Steps to Big Summits', '#3f7d3a', 10], ["The Clockmaker's Paradox", '#b5452f', 20 + w]]
.forEach(([title, color, x]) => {
ctx.fillStyle = color; ctx.fillRect(x, 4, w, 130);
ctx.fillStyle = '#fff3e0';
wrapLines(ctx, title, w - 20).forEach((line, k) => ctx.fillText(line, x + 10, 30 + k * 22));
});
// maxWidth (4th argument) squeezes the glyphs instead of wrapping
const x = 30 + 2 * w;
ctx.fillStyle = '#5b3f99'; ctx.fillRect(x, 4, w, 130); ctx.fillStyle = '#fff3e0';
ctx.fillText('Patterns of the Deep Web', x + 10, 30, w - 20);
ctx.font = '11px sans-serif'; ctx.fillStyle = '#263238';
ctx.fillText('wrapLines()', 10, 150); ctx.fillText('long word overflows', 20 + w, 150);
ctx.fillText('maxWidth squeezes', x, 150);
}
const slider = document.getElementById('w');
slider.addEventListener('input', () => draw(+slider.value));
draw(130);
</script>