很多时候我们需要做一个响应式的页面,如果页面中存在一个Canvas,调整尺寸的时候一般就需要重绘它。

在大部分情况下,操作 Canvas 并重绘并非不可接受,但是在一些场景中,我们既需要保持高分辨率的 Canvas,又需要它能适应屏幕的宽度,比如我们创建了一个 Canvas 用于捕捉屏幕上的视频流或者图片流,并且可能启动了一个 MediaRecorder 录制该 Canvas,在这个时候,强行修改Canvas的尺寸会影响正在录制的 MediaRecorder ,即便不在录制过程中,也会降低画面的分辨率,那么有什么其他方法吗?

做法一

使用一个新的 Canvas 来进行渲染,与原本的录制 Canvas 隔离。

我们可以将录制或者相关的业务逻辑放在另一个 Canvas中,可能是 offscreenCanvas。
然后我们在 requestAnimationFrame 或类似的时机,将 offscreenCanvas的内容绘制到实际显示的 Canvas 中。

这个方法的优势在于整体的数据流向不会变,缺点是额外绘制一个 Canvas 总是一种性能损耗,并且对上屏的 Canvas,我们依然要去监听 resize 并且重设 Canvas 的宽高。

做法二

仔细回顾一下我们的需求,如果要做到最完美的版本,即:我们需要有一个方案,他能够让 Canvas 全尺寸绘制并且不影响所有Canvas 相关的API,与此同时,它还要支持 CSS 的响应式逻辑。

没错,SVG。

SVG能够在 DOM 中创造一块独立的渲染逻辑,其中由 SVG 标签的 viewBOX 定义了整个渲染区域的坐标和尺寸,而在 DOM 中,它就像一个图片一样,可以使用 object-fit 或者 width 等 CSS 属性来实际控制它的尺寸和渲染方式。

于是在一个 Vue Template 中,我们可以这样写一个 Canvas:

<svg :viewBox="`0 0 ${loadedArea?.width} ${loadedArea?.height}`" xmlns="http://www.w3.org/2000/svg" class="w-full">
  <foreignObject x="0" y="0" :width="loadedArea?.width" :height="loadedArea?.height">
    <canvas ref="ImageCanvas" :height="loadedArea?.height" :width="loadedArea?.width" class="canvas"></canvas>
  </foreignObject>
</svg>
/src/tools/radar-chart/index.vue#L323-L328

使用 foreignObject ,并且把 SVG 的 viewbox 设成和 Canvas 相同的尺寸,就可以使 Canvas 完全铺满 SVG 的渲染空间。

最后,原有的逻辑完全不需要修改,依然可以通过 JS 去操作 Canvas的 API ,可以说是一种比较完美的解决方法。