Experimental

HTML-in-Canvas:把 HTML 画进 Canvas 里

一个实验性浏览器 API 的实现原理与 demo 拆解
WICG Proposal · 2026-04-05

浏览器里有两套渲染系统:DOM/CSS 管布局和交互,Canvas/WebGL 管像素级绘制。它们一直是平行世界——直到 HTML-in-Canvas 提案出现,打通了这道墙。

核心想法很简单:让 Canvas 能读取 HTML 元素的渲染结果,把它当成纹理或图像来用。CSS 负责排版和交互,Canvas 负责在它之上叠加任意视觉效果。

三个 API 原语

  1. layoutsubtree — Canvas 上的一个属性,告诉浏览器「请对我的子元素执行正常布局」。没有这个属性,Canvas 的子元素不会被渲染引擎处理。
  2. drawElementImage(element, x, y) — 2D Context 的新方法,把一个 HTML 元素的渲染快照画到 Canvas 上。元素保持完整的 DOM 交互能力。
  3. onpaint — Canvas 上的事件,当子元素发生视觉变化时触发,配合 requestPaint() 驱动持续重绘。

对 WebGL 用户,还有 texElementImage2D(),直接把 HTML 上传为 GPU 纹理——这意味着你可以用 fragment shader 对 HTML 做逐像素操作。

基本结构

HTML 元素作为 Canvas 的子节点存在,但不直接显示在页面上——它们通过 drawElementImage 被「画」到 Canvas 的画布上:

<!-- HTML 元素是 canvas 的子节点 -->
<canvas layoutsubtree>
  <form id="myForm">
    <input type="text" />
    <button>Submit</button>
  </form>
</canvas>
// JavaScript —— 绘制 + 叠加效果
canvas.onpaint = () => {
  ctx.reset();
  ctx.drawElementImage(form, x, y);  // 画表单
  drawGlowEffect(focusedElement);    // 叠加光效
};
┌─────────────────────────────────────┐ Canvas buffer ╔═══════════════════════════╗ drawElementImage(form) ┌───────────────────┐ <input> ← 焦点环 │ └───────────────────┘ ┌───────────────────┐ │ [Deploy] │ └───────────────────┘ ╚═══════════════════════════╝ Canvas 2D 效果层: glow / particles └─────────────────────────────────────┘

我们的 demo 做了什么

我们用 Canvas 2D(非 WebGL)实现了三种焦点环效果。原理都一样:

  1. 画表单drawElementImage(form, x, y) 把真实的 HTML 表单渲染到 Canvas 上
  2. 定位焦点元素 — 通过 DOM 的 offsetLeft/offsetTop 计算当前聚焦元素在 Canvas 内的精确坐标
  3. 叠加效果 — 用 Canvas 2D API(shadowBlur、roundRect、lineDash、radialGradient)在焦点元素周围绘制发光、流动、粒子效果
  4. 逐帧动画requestAnimationFrame 驱动 canvas.requestPaint(),60fps 持续重绘
查看完整 demo →

关键挑战:坐标系对齐

这是实现中最棘手的部分。Retina 屏幕上 Canvas 的像素缓冲区是 CSS 尺寸的 2 倍。drawElementImage 已经在内部处理了设备像素比——如果你再手动 ctx.scale(2, 2),表单就会被画成 4 倍大小。

正确做法是:

// 1. 表单用设备像素坐标绘制(不 scale)
const dpr = canvas.width / canvas.clientWidth;
const devX = cssX * dpr;
ctx.drawElementImage(form, devX, devY);

// 2. 效果层用 CSS 像素坐标绘制(scale 后)
ctx.scale(dpr, dpr);
drawGlowAt(cssX, cssY);  // CSS 坐标,自动映射到设备像素

表单画完后再 ctx.scale,不会影响已绘制的内容,但后续效果的所有坐标和尺寸都会自动适配 DPR。

和「Canvas 放在 HTML 后面」有什么区别?

一个常见的问题:我把一个 Canvas 放在表单后面,用 getBoundingClientRect 读元素位置,不也能画光效吗?

对于简单的发光效果——确实可以。但 drawElementImage 提供了三个背景 Canvas 做不到的东西:

实用注意事项

实验阶段 — 目前需要 Chrome Canary + chrome://flags/#canvas-draw-element。不要用在生产环境。

完整提案:github.com/WICG/html-in-canvas

Matt Rothenberg 的深度文章(英文):mattrothenberg.com/notes/html-in-canvas