Canvas 文字渲染只能靠 measureText 瞎蒙,Chrome 156 今天用四个 API 把这件事彻底写进了标准

你在 canvas 上画了一行文字,想知道用户点了哪个字符——这件事以前只能靠 hack。

ctx.measureText() 只返回一个总宽度,没有任何 API 能告诉你「第五个字符从哪开始、到哪结束」。代码编辑器要做语法高亮选区、富文本要做逐字符着色、实现一个光标需要自己算坐标。你要么用隐藏的 DOM 节点做命中测试,要么写一堆字体度量估算——这些 hack 遇到 emoji 合字根本不准。

Chrome 156 今天把这件事用四个 API 彻底原生化了。

getTextClusters() — 把文字拆成字形簇

TextMetrics.getTextClusters() 把一段文字拆成最小的可渲染单元(grapheme cluster),每个簇包含字符内容、像素坐标、左右边界:

const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
ctx.font = '60px serif';

const tm = ctx.measureText('Colors 🎨 are 🏎️ fine!');
const clusters = tm.getTextClusters();

clusters.forEach((cluster, i) => {
  console.log(`字符"${cluster.text}" 起始x=${cluster.x.toFixed(1)} 宽度=${(cluster.x + cluster.width - cluster.x).toFixed(1)}`);
});

每个 cluster 还保留了原始字符串和起止索引,重新渲染时不用重新分词。

配合 ctx.fillTextCluster(cluster, x, y) 可以把任意簇重新渲染到任意位置——逐字符着色、逐字符动画(比如文字炸裂效果)终于有了标准 API,不再需要截取 substr 重新测量:

const colors = ['orange', 'navy', 'teal', 'crimson'];
clusters.forEach((cluster) => {
  ctx.fillStyle = colors[cluster.begin % colors.length];
  ctx.fillTextCluster(cluster, 0, 0);
});

getSelectionRects() — 选区矩形

做代码编辑器或富文本渲染时,你需要知道某段文字在屏幕上的哪块区域。getSelectionRects(begin, end) 接受字符偏移,返回选中范围对应的矩形数组:

const tm = ctx.measureText("let's do this");
const selection = tm.getSelectionRects(9, 13); // "this" 的矩形

ctx.fillStyle = 'rgba(59, 130, 246, 0.3)';
selection.forEach(rect => {
  ctx.fillRect(rect.x, rect.y, rect.width, rect.height);
});

矩形坐标相对于文本原点(受 textAlign / textBaseline 影响),可以精确叠在文字上方做选区高亮。

getActualBoundingBox(begin, end) — 单字符紧包围盒

getActualBoundingBox(begin, end) 返回指定范围的实际包围盒(考虑斜体 overhang),比 width 属性精确得多:

const tm = ctx.measureText('hello world');
const box = tm.getActualBoundingBox(0, 5); // "hello" 的紧包围盒

console.log(`左上(${box.x.toFixed(1)}, ${box.y.toFixed(1)}) 宽${box.width.toFixed(1)} 高${box.height.toFixed(1)}`);

配合 strokeStyle 可以给特定字符画下划线、高亮边框——做文本校对工具或代码高亮时最管用。

getIndexFromOffset(x) — 坐标转字符索引

上面三个 API 都是「字符→坐标」,这个 API 是反向的:给定 x 坐标,返回最近的字符索引。实现光标点击定位、IME 输入法候选词窗口定位,全靠它:

const tm = ctx.measureText('hello world');
const index = tm.getIndexFromOffset(150);
console.log(`点击落在第 ${index} 个字符`);

这四个 API 构成了 canvas 文字的完整坐标体系:从测量到选区到命中测试,终于不用再靠 DOM hack。

浏览器支持与降级

Chrome 156(Desktop + Android + WebView)正式上线。Origin Trial 从 Chrome 144 到 152 已验证完毕,API 稳定无 breaking change。Safari 和 Firefox 暂无信号,渐进增强写法:

if (tm.getTextClusters) {
  // 使用 Enhanced TextMetrics
} else {
  // 回退到 measureText 估算
}

下一步

如果你在实现富文本编辑器、代码编辑器的 canvas 渲染,或任何需要逐字符操作 canvas 文字的场景——把这四个 API 加入你的工具箱。下一步可以研究 fillTextCluster 的 transform 参数(支持任意旋转和缩放),以及如何配合 RequestAnimationFrame 做逐帧字符动画。

评论区

0 条评论

登录后可评论。

铁锈·Rust工具链 16 阅读