ctx.letterSpacing and ctx.wordSpacing take CSS lengths such as '4px' and became Baseline (newly available) in March 2025. ctx.fontKerning ('normal' or 'none'), fontStretch and textRendering mirror their CSS namesakes but are missing in Safari 10 , so they are not Baseline.
<canvas id="c" style="width:600px; height:106px"></canvas><script src="hidpi.js"></script>
<script>
const ctx = hidpi(c); ctx.fillStyle = '#1f5f8b';
const draw = (text, y, set) => {
ctx.save(); Object.assign(ctx, { font: '28px "Times New Roman"' }, set);
const w = ctx.measureText(text).width; ctx.fillText(text, 10, y);
ctx.font = '12px sans-serif'; ctx.letterSpacing = '0px'; ctx.fillStyle = '#263238';
ctx.fillText(`${JSON.stringify(set)}: ${w.toFixed(1)}px`, 330, y - 8); ctx.restore();
};
draw('AVATAR WAVE', 30, { fontKerning: 'normal' });
draw('AVATAR WAVE', 62, { fontKerning: 'none' });
draw('AVATAR WAVE', 96, { letterSpacing: '4px' });
</script>
Kerning tucks each A under its V or T, saving 20 pixels; Windows' Georgia measured the same either way, so check your font. Letter spacing follows every character, the last included, and measureText() counts it: subtract one spacing when centering. A unitless '4' is invalid and ignored.
<!doctype html>
<style>
body { margin: 0; padding: 8px; font: 13px system-ui, sans-serif; background: #fafaf7; }
canvas { width: 100%; max-width: 600px; height: 170px; display: block; background: #fff; border: 1px solid #ddd; }
</style>
<canvas id="c"></canvas>
<label>letterSpacing <input type="range" id="ls" min="0" max="12" value="4"> <span id="lsv">4px</span></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;
}
const ctx = hidpi(document.getElementById('c'));
// Draw a line of text with some settings and report its measured width
function draw(text, y, settings) {
ctx.save();
Object.assign(ctx, { font: '28px "Times New Roman", serif', fillStyle: '#1f5f8b' }, settings);
const w = ctx.measureText(text).width; // measureText counts the spacing too
ctx.fillText(text, 10, y);
ctx.fillStyle = 'rgb(224 154 16 / 0.35)'; ctx.fillRect(10, y + 4, w, 4);
ctx.font = '12px sans-serif'; ctx.letterSpacing = '0px'; ctx.wordSpacing = '0px';
ctx.fillStyle = '#263238';
ctx.fillText(`${JSON.stringify(settings)} → ${w.toFixed(1)}px`, 330, y - 8);
ctx.restore();
}
function render(spacing) {
ctx.clearRect(0, 0, 600, 170);
draw('AVATAR WAVE', 36, { fontKerning: 'normal' });
draw('AVATAR WAVE', 76, { fontKerning: 'none' });
draw('AVATAR WAVE', 116, { letterSpacing: spacing + 'px' }); // '4' without a unit is ignored
draw('AVATAR WAVE', 156, { wordSpacing: spacing * 3 + 'px' });
}
const slider = document.getElementById('ls');
slider.addEventListener('input', () => {
document.getElementById('lsv').textContent = slider.value + 'px';
render(+slider.value);
});
render(4);
</script>