Canvas 绘字与坐标变换:TextMetrics、基线和状态栈
本文目录
Canvas 2D 是立即绘制的位图表面。画出的像素不会自动变成 DOM 文本或可访问控件;需要图表、流程或交互图形时,仍要提供文字说明、数据表或其他可访问替代。普通标题、段落和可选中文本应直接使用 HTML,不要为了视觉效果把整页文字画进 canvas。
fillText 的 y 是基线
fillText(text, x, y) 和 strokeText(text, x, y) 在坐标处绘制文字,其中 y 默认表示文本基线,不是字形顶端。文字外观受 font、textAlign、textBaseline 和 direction 等上下文属性影响。Canvas 不会自动换行;可先用 measureText() 计算宽度并自行分行。传入 maxWidth 会让浏览器压缩字距、选窄字体或缩小字号,不适合用作稳定的排版策略。
function drawLabel(context, text, x, baselineY, maxWidth) {
context.save();
context.font = '600 20px system-ui, sans-serif';
context.fillStyle = '#17324d';
context.textAlign = 'start';
context.textBaseline = 'alphabetic';
const metrics = context.measureText(text);
context.fillText(text, x, baselineY, maxWidth);
context.restore();
return metrics.width;
}
const canvas = document.querySelector('#chart');
const context = canvas && canvas.getContext('2d');
if (context) {
const width = drawLabel(context, '销售趋势', 16, 36, 240);
console.log('measured width:', width);
}
measureText() 返回 TextMetrics,其中 width 是布局宽度;部分指标还提供实际字形边界。若使用 Web Font,在字体加载完成前测量可能使用回退字体,导致文本居中、标签避让或图表布局随后跳动。需要准确排版时,在 document.fonts.ready 后重绘。
Canvas 绘字不会修改当前路径;后续 fill()/stroke() 画的是已有路径,不会描边刚画的文字。文字描边要使用 strokeText()。多语言页面要明确方向与文本对齐语义;使用逻辑的 start/end 通常比硬编码 left/right 更合适。
局部坐标变换与 save/restore
translate()、rotate()、scale() 和 transform() 改变后续绘图所用的坐标系。用 save()/restore() 把临时样式、裁剪区和变换限制在一个绘制单元,避免污染后续图形:
function drawRotatedMarker(context, x, y) {
context.save();
context.translate(x, y);
context.rotate(Math.PI / 6);
context.fillStyle = '#0f766e';
context.fillRect(-12, -12, 24, 24);
context.restore();
context.beginPath();
context.moveTo(x - 30, y + 24);
context.lineTo(x + 30, y + 24);
context.stroke();
}
save() 保存绘图状态栈(包括变换、样式和裁剪状态),restore() 弹出最近一次状态;它们不会清空当前路径。因此新建独立形状时仍应调用 beginPath()。每个 save 都要有匹配的 restore,异常分支也要避免状态泄漏。
Canvas 尺寸与高像素密度
HTML 属性 width/height 决定绘图缓冲区尺寸;CSS 尺寸决定页面占用空间。只用 CSS 把小缓冲区拉大,通常会模糊;直接改缓冲区尺寸又会清空画面并重置上下文状态。高 DPR 画布应按 CSS 像素尺寸乘设备像素比设置缓冲区,再缩放坐标:
function sizeCanvas(canvas, context, cssWidth, cssHeight) {
const dpr = window.devicePixelRatio || 1;
canvas.style.width = cssWidth + 'px';
canvas.style.height = cssHeight + 'px';
canvas.width = Math.round(cssWidth * dpr);
canvas.height = Math.round(cssHeight * dpr);
context.setTransform(dpr, 0, 0, dpr, 0, 0);
}
响应式场景可通过 ResizeObserver 读取容器的 CSS 尺寸并重新设置缓冲区;改变 width/height 后必须重绘所有内容并重新设置变换和样式。不要反复以已有缩放矩阵为基础累乘,否则 DPR/尺寸变化会让坐标越变越大;需要时用 setTransform() 重置到明确矩阵。
验收包括 Retina/非 Retina 屏、浏览器缩放、字体尚未加载、长文本、RTL、容器 resize 和清空重绘。交互图形还要有键盘可用的 DOM 控件与文字说明,不能只依赖 canvas 像素。
参考:HTML canvas 元素、Canvas 2D fillText、measureText、Canvas transform、save、restore、WHATWG Canvas。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。