# Canvas

# Canvas 面试总览:位图画布、状态机与工程边界

⚡ 30 秒速记

  • Canvas 是即时模式的位图画布:浏览器只保留像素,不保留圆、线、文本这些可查询的图形对象
  • 一次绘制 =「设置状态 → 建路径 → fill() / stroke() 落笔」;save() / restore() 管的是画笔状态栈,不是像素快照
  • 高清屏模糊是位图像素不够:位图尺寸乘 devicePixelRatio,CSS 尺寸不变,坐标系 scale 回来
  • 动画每帧「更新状态 → 清屏 → 重绘」,用 requestAnimationFrame 驱动,离开页面要停
  • 大量动态图元、像素处理选 Canvas;要节点语义、无障碍、逐元素事件选 SVG / DOM

Canvas 就是一块像素画布,你用 JS 往上画,画完浏览器只记得像素,不记得你画的是圆还是线。 所以它的工作方式是「状态 + 路径 + 落笔」:先设 fillStyle、变换这些画笔状态,用 beginPath()、arc() 建路径,最后 fill() 或 stroke() 才真正画上去。想改某个图形没法「选中它」,只能改 JS 里的数据再整块重画。高清屏发糊要把位图尺寸乘上 devicePixelRatio。图元多、变化快就选 Canvas,要逐个点击、读屏、键盘访问就选 SVG 或 DOM。

CanvasRenderingContext2D 可以看成一台带状态的画笔。fillStyle、strokeStyle、变换矩阵、裁剪区等属性会影响之后的命令;beginPath() 只会清空当前路径,不会擦除画布像素,真正落笔要调用 fill() 或 stroke()。这也是常见 bug 的来源:忘记 beginPath() 会把旧子路径再次描边,忘记 restore() 会让缩放、透明度继续污染后续绘制。

工程里通常把「数据状态」作为事实来源,而不是试图从像素里还原对象。以拖拽图形为例,需要在 JavaScript 中保存每个图形的位置和命中区域,指针移动时自己做 hit test,然后清屏并按新状态完整重绘。复杂场景可以把静态背景画到离屏画布,只重绘变化层;像素计算可交给 Web Worker 或 OffscreenCanvas,但主线程上的 DOM 事件和最终合成仍要纳入性能预算。

高清适配必须区分两种尺寸:canvas.width/height 决定位图缓冲区,style.width/height 决定页面显示大小。只写 CSS 尺寸会把较少的物理像素拉伸;只放大位图又不缩放坐标系,会让逻辑尺寸改变。上面的实验会真实读取当前设备的 devicePixelRatio、重建 backing store,并从绘制结果采样像素,能直观看出两条路径的差别。

💬 面试官追问

  • 连画两条线,第二次 stroke() 把第一条又描了一遍,为什么?

    stroke() 不会清空路径,第二条线是接在旧路径后面的,描边时一起画了。每画一个独立图形前先调 ctx.beginPath(),它只清路径,不会擦掉已经画上去的像素。

  • 拖拽流程图节点时,怎么知道鼠标点中了哪个?

    画布上没有节点对象,得自己做命中检测:节点数据存在数组里,pointerdown 时把鼠标坐标换算成画布坐标,倒序遍历判断落在哪个节点里,复杂形状可以用 ctx.isPointInPath(path, x, y)。命中后改数据,再整块重画。

  • 画完一个旋转的元素后,后面所有组件都歪了,怎么回事?

    变换没还原。ctx 上的 rotate、globalAlpha、clip 会一直生效到你改回去为止。局部绘制用 ctx.save() 和 ctx.restore() 包起来,中途 return 的分支也别漏了 restore。

  • 大屏底图很复杂,只有几个数字每秒在变,整屏重画太卡,怎么优化?

    分层。静态底图提前画到离屏 canvas 上,每帧 drawImage 贴过来再画动态部分;或者叠两个 canvas,底下那层只画一次。像素计算重的话可以挪进 Worker 用 OffscreenCanvas。

  • 签名板在 2x 屏要清晰,导出的图又要固定 300×150,怎么做?

    显示时位图按 devicePixelRatio 放大,再用 ctx.scale(dpr, dpr) 把坐标系缩回逻辑尺寸。导出时新建一个 300×150 的 canvas,把高清画布 drawImage 缩进去再 toDataURL(),直接导出原画布会得到 600×300 的图。

webapp
公众号
开发者导航
切换夜间模式
点击侧边栏上一篇
折叠侧边栏
收起全部