触发条件 (Occurrence Conditions)

  1. 架构:双层 Canvas 手写板。activeCanvas 承载「正在写的那一笔」并对其调用 getContext('2d', { desynchronized: true }) 以压低触控笔延迟;mainCanvas 承载已提交的历史笔迹。
  2. 提交方式:提笔时用 位图搬运 收尾——mainCtx.drawImage(activeCanvas, 0, 0) 把活跃层 1:1 拷到主层,再 clearRect 清空活跃层。这是为了让「抬笔瞬间」的笔画与书写预览逐像素一致,避免矢量重放带来的抬笔跳变/变细。
  3. 运行环境:真实浏览器 + GPU 合成(Chrome / Edge 桌面端,deviceScaleFactor 任意)。至少桌面 Chrome 实测恒定复现。

故障现象 (Symptoms)

  • 整笔凭空消失:书写过程墨迹正常显示,抬笔瞬间笔画连同活跃层一起消失,主层没有任何墨迹落地。现象上就是「写完了但字没上去 / 笔记不会下来」。
  • 与输入设备强相关,看起来像设备问题:快速划动的鼠标、低延迟/高采样率笔、事件与 pointerup 挤在同一帧内的写法更容易中招,慢速、事件跨度大的书写更容易侥幸成功。于是很容易被误判为「某某笔不兼容」「人家人品问题」。
  • drawImage 与 getImageData 不报错:读回得到的是全透明图像而非异常。活跃层在 clearRect 前肉眼可见有墨,拷完却什么都没有,因此从日志与返回值上都看不出问题。
  • 迷惑性极强的负结果:无头 Chromium(Playwright / Puppeteer 默认)会忽略 desynchronized 选项,所有自动化用例全绿;只有真实 Chrome 才复现。这会让「自动化测试通过」和「用户说丢笔」长期并存。

尝试方案与局限 (Attempted Solutions & Trade-offs)

方案具体操作结果与局限
归因到笔/系统差异换 Apple Pencil 型号、换设备、换浏览器无法解释,因为根因与笔无关
归因到坐标换算排查 PDF 空间 ↔ 显示空间转换换算正确,但整笔丢失发生在换算之前
先 getImageData 再 putImageData期望强制回读读的仍是同一个 desynchronized 表面,同样不可靠
等一帧再 drawImagerequestAnimationFrame 后再搬运仍不可靠,实测不足以恢复
无头浏览器复现Playwright 无头跑通假阳性:无头忽略该选项,掩盖问题

根因 (Root Cause)

desynchronized: true 让 Canvas2D 绕过常规的合成/同步路径,直接把绘制结果交给合成器(乃至 GPU 图层),使绘制与显示解耦、延迟更低。代价是这张画布不再保证能被同步读回为一致内容。

drawImage() / getImageData() 要求源表面在调用时刻有确定的、CPU 可读的内容,而 desynchronized 表面恰恰不提供这个保证。于是提交阶段读到一个空(或旧)表面:drawImage 拷过去的是空白,紧接着的 clearRect 又把唯一可见的活跃层清掉——笔画就此彻底蒸发。

关键在于「低延迟绘制」与「可被读取的提交源」是互斥的两种用法,同一个画布不能同时承担。

解决步骤 (Resolution)

立即生效(改功能层,不动墨迹数学):把 activeCanvas 的 desynchronized: true 去掉即可,其余逻辑完全不用改。

// 提交源必须可读,不能开 desynchronized
this.activeCtx = this.activeCanvas.getContext('2d')!;

代价是活跃层多约一帧的显示延迟。用 A/B 实测:同一份代码、同一组事件,只切换这一个选项,主层墨迹从 0 变为 586 / 599(鼠标 / 触控笔)。

若必须保留低延迟:让提交走矢量重放而非位图搬运——提笔时把这一笔的曲线点重画到主层。注意重放必须复用增量渲染的同一套分段与线宽取值规则(每段取相邻两点线宽均值、midpoint → 控制点 → midpoint 的二次贝塞尔),否则会退回「抬笔时笔画跳变/变细」的老问题。改动落在墨迹引擎的提交分支,属于需要先与手感负责人确认的领域。

下次如何识别 (Recognition)

  • 症状是「整笔消失」而非笔迹变细/错位/分叉时,优先怀疑提交链路,而不是手感算法或坐标换算。
  • 代码库里看到 getContext('2d', { desynchronized: true }),同时该画布又被 drawImage / getImageData / toDataURL 当源用——立刻判定为缺陷,不需要先复现。
  • 做 Canvas 相关的自动化验证时,无头浏览器不能作为该选项的证据来源;涉及延迟优化、GPU 合成、desynchronized、toDataURL 的用例必须在真实有头浏览器里跑。

相关概念


本文归档于个人知识库 · 领域:CS & AI · 模块:04 Project_CS_AI/Frontend