canvas 选区同步靠了七八年的土办法,今天 Chrome 157 彻底原生化了

每次在 canvas 上画完文本想高亮选区,都要偷偷在页面里塞一个隐藏的 <div> 做测量——这不是传闻,是 Flutter Web、Canvas 编辑器、创意编程框架共同踩了七八年的坑。Chrome 157 今天把这个 DOM 同步的土办法彻底原生化了。

旧世界:canvas 选区靠 DOM 同步

canvas 上的 measureText() 只返回一个 width。如果想知道用户点击了哪段文字、选区覆盖了哪些像素,标准做法是:

// 在 canvas 旁边放一个透明 DOM 节点
const ghost = document.createElement('div');
ghost.style.cssText = 'position:absolute;visibility:hidden;white-space:nowrap;';
document.body.appendChild(ghost);

// 每次 canvas 内容变化,同步字体信息
function syncToGhost(text, font) {
  ghost.style.font = font;
  ghost.textContent = text;
  return ghost.getBoundingClientRect();
}

// 靠 selectionStart + getBoundingClientRect() 估算选区
const sel = window.getSelection().getRangeAt(0);
const rect = ghost.getBoundingClientRect(); // 每次都要触发 layout

这个模式有三个根本问题:

  1. DOM 和 canvas 状态必须严格同步 — 字体、大小、换行任何一处不一致,测量就偏差
  2. 性能代价高 — 每个字符的选区都要触发一次 layout 计算
  3. RTL / 混合方向文本基本无解 — DOM 的 getSelection() 对 canvas 内容是盲的

Chrome 157 的 4 个新 API

Chrome 157 为 TextMetrics 增加了四个新方法,全部在 canvas 内部完成,不再需要 DOM 同步:

const canvas = document.getElementById('editor');
const ctx = canvas.getContext('2d');
ctx.font = '18px "Noto Sans SC", sans-serif';
const metrics = ctx.measureText('Hello 你好 World');

// ① 根据像素偏移量找到字符索引(光标定位用)
const idx = metrics.getIndexFromOffset(canvas, 85); // x=85px 处是哪个字符

// ② 拿到一段文字在屏幕上对应的所有矩形(选区高亮用)
const rects = metrics.getSelectionRects(0, 12); // 0 到 12 字符的选区矩形

// ③ 拿到子字符串的精确边界框(碰撞检测用)
const box = metrics.getActualBoundingBox(0, 5); // "Hello" 的边界盒

// ④ 按书写方向分组建簇(逐字动画用)
const clusters = metrics.getTextClusters();
clusters.forEach(cluster => {
  drawCharacter(ctx, cluster.glyphs, cluster.advance);
});

这四个 API 的能力:

API 解决了什么问题 之前只能靠什么
getIndexFromOffset 知道用户点击了第几个字符 selectionStart + DOM
getSelectionRects 绘制正确的选区高亮矩形 window.getSelection() + 隐藏 div
getActualBoundingBox 精确知道某段文字的像素边界 DOM getBoundingClientRect()
getTextClusters 逐字符/逐词独立渲染动画 手动切分字符串 + 手动算位

Flutter Web 已经靠它跑了近一年

这个功能不是实验室玩具。Flutter Web 在 2025 年初就接入了 Origin Trial(Chrome 144–152,历时约 9 个月),用 getTextClusters() 和 getSelectionRects() 重写了 WebParagraph 的文本布局引擎,在正式发布前已经有大量生产流量验证。

Canvas 创意编程也有成熟案例——文字沿路径编辑器(text-on-a-path)和逐字爆炸动画,之前必须用 DOM 节点同步才能实现的效果,现在一行 canvas API 搞定。

兼容性现状(截至 2026 年 10 月):

  • ✅ Chrome/Chromium:157+(2026 年 10 月 7 日稳定版)
  • ⚠️ Firefox:暂无表态(Mozilla standards-positions Issue #1144 未给出信号)
  • ⚠️ Safari/WebKit:持保留意见(偏好 TextLine 这样的行级抽象,而非扩展 TextMetrics)

检测与降级

function canvasTextEditingSupported() {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  const metrics = ctx.measureText('test');
  return (
    typeof metrics.getIndexFromOffset === 'function' &&
    typeof metrics.getSelectionRects === 'function'
  );
}

if (canvasTextEditingSupported()) {
  // 使用新 API
} else {
  // 降级方案:保留 ghost div 逻辑
  initGhostDivFallback();
}

接下来可以做的事

  1. 打开 Chrome 157,用 Igalia 的在线 Demo 实际感受 canvas 内直接选中文本
  2. 检查你的 canvas 编辑器是否还在用 ghost div 模式——是的话,现在有原生替代了
  3. 关注 Firefox 的 standards-positions 进展;若 Safari 也跟进,canvas 文本编辑的跨浏览器兼容成本会大幅下降

评论区

0 条评论

登录后可评论。

阿跨·跨端开发 10 阅读