CANVAS增量渲染:裁剪和拼接,小数与残影

减少用户滚动页面时的性能消耗以换取优异的交互体验,是终端应用性能优化的重中之重。本文以笔者最近解决的几个 canvas 增量渲染问题为视角,管窥其背后 canvas 的运作机制。

在给应用做性能优化时,用户界面的交互流畅性是重中之重,而在解决交互卡顿问题时,终端开发遇到的一个普遍场景就是用户设备的屏幕比应用的页面要小得多。不论是移动端、PC 端还是嵌入式设备,开发者都需要减少在用户滚动页面时的性能消耗,以换取优异的交互体验。腾讯文档表格去年就已经在用 canvas 增量渲染来优化滚动性能,本文以笔者最近解决的几个 canvas 增量渲染问题为视角,管窥其背后 canvas 的运作机制。

(阅读本文大概需要 9 分钟)

一.代码中的像素如何绘制在屏幕上

1.绘制过程:从代码到图像,从坐标到像素。

我们知道,开发中使用的像素——可以称为逻辑像素,与用户设备上的像素——可以称为物理像素,是不同的概念,简单理解可以认为物理像素=逻辑像素 x 设备像素比。我们调用 canvas,绘制路径、形状和纹理,执行各种计算,使用的像素都是逻辑像素,生成的图像也都是以矢量表示。

浏览器在样式计算、排版等操作中可以使用这些矢量表示的图像,但是为了能够绘制到屏幕中的一个一个像素点上,最终还是要将这些矢量表示转换为位图。这个转换的过程就是栅格化(rastering),在现代浏览器中通常有专门的线程负责。

作为终端开发,我们无需太过关注浏览器内部的处理过程。真正需要了解的是 canvas 在这套机制下如何运作。接下来我们就绘制一条 1px 的线,看看 canvas 如何处理一个逻辑像素。

// 部分代码

Image

可以看到,在 y 坐标 100 处绘制出来的线边缘有模糊,明显不是 1 像素。这是因为 canvas 会以传入的坐标的中点来计算宽度。也就是说,“在 100 像素处绘制宽为 1 像素的线”,会被处理为“从第 100 像素的中点开始绘制宽为 1 像素的线”。

那么问题来了,第 100 像素的中点开始到结尾只剩下半个像素,于是 canvas 就把剩下的半个像素画到了第 101 个像素的起点到中点。需要注意的是,本段文字中提到的像素都是前文所指的逻辑像素,所以才能进行取中点来计算这样的操作。接下来是更大的问题,半个像素交给浏览器,绘制出来是什么样子?我们用精准的取色器看一看:

Image

可以很清楚地看到这一条 1 像素的线实际上横跨了两个像素,但是颜色并非纯黑,而是变淡了不少。此时不难猜测,浏览器进行栅格化时对于小数像素的处理是按一整个像素计,但会根据这个小数像素占据的空间比例改变其 alpha 值(例如 0.1 就是 10%透明度),使得最终呈现在位图上的像素点是一个带透明度的原有色值。

猜测完了,可以求证一下。这次我们用 fill 方法画 11 条线,逻辑像素长度从 3.0 到 4.0。为了看得更清楚,把画好的线用 drwaImage 放大 50 倍,关掉 smooth 效果。让我们运行代码,来看看发生什么事了:

// 部分代码

Image

不难看出,随着小数像素递增,第 4 个像素的黑色透明度也递增了,直到 4.0 像素变为纯黑色。由此我们可以得知 canvas 使用透明度对小数像素进行处理。

知道原理后画出屏幕上的 1 像素就好办了,只要从第 100.5 像素开始画宽度 1 像素的线,就能成功画出一条在屏幕上横跨 2 像素,但宽度只有 1 像素的线:

Image

当然,在像素比不同的屏幕上或用户缩放屏幕时这条线可能又不是 1 像素了,这时候就需要把宽度除以像素比 devicepixelRatio 才能得到真正代表 1 个物理像素的逻辑像素数值。

2.处理非整数:画布虽大,半个像素都不能少

既然知道 canvas 会将小数像素处理为占用整数个像素并按照透明度计算色值,在日常开发中,为了精确画出我们想要的颜色,就应该适当避免小数像素。为什么是“适当避免”?个人认为,虽然 JavaScript 对浮点数的处理性能开销确实更大,但无法忽略那些需要精确计算坐标的场景。我们将坐标传给 canvas 进行绘制之前,并不一定要在每一步运算都取整,运算时大可保留小数部分,传入 canvas 之前取整就好。

那么,对付小数像素只要取整就够了吗?

二.裁剪、清除、填充

1.裁剪再填充:光天化日之下,视口之内,不容差错

canvas 提供了便利的裁剪和填充 API。通过 drawImage、clip、clearRect、fill 等几个 API 的结合,就能实现滚动时增量渲染用户可见区域,避免渲染整个画布带来过大的性能开销。如图所示,当用户滚动页面时,先将当前画布上的图片在滚动方向上移动,再增量渲染因为滚动空出来的视口内区域:

Image

由于移动画布、清除滚动区域的内容、重新绘制滚动的区域这些操作都是在用户眼皮底下进行,坐标的精确度就显得十分重要了,一旦坐标对不上,就会出现上图动画那样文字错位甚至残影等问题。在进入下一环节前,我们先来看看 clip、clearRect、fill 这几个 API 的实现效果。当然,为了将前面的知识用上,我们采用小数坐标进行绘制。

为了看得更清楚,这次我们将线的长度提升到 10 逻辑像素,并在放大后的图像上画标尺,每一格代表放大前的 1 个像素。

先来看看 clip 的行为。将这条 10 像素的线中间 clip 4 像素,clip 的起点从第 3 像素开始按 0.1 像素递减,然后再用 clearRect 清除整个填充区域:

// fill 和 clear 的范围都设置为整个图形

ImageImage

可以看到 clip 确实如我们熟知的,限制了 clearRect 的区域,即使传入小数,小数像素对 clip 区域的限制也有效并且以透明度的形式体现:clip 了 0.1 个像素,那么即使 clear 整个像素,最终也只有 0.1 个像素被清除,表现就是绘制出来的图像是 90%的黑色。

接下来看看 clearRect 的行为。对代码做一个微小的改动,把 clearRect 的区域设置为与 clip 一样,clip 区域保持不变:

// fill 的范围设置为整个图形

Image

观察绘制的 11 条线,可以看到当 clearRect 传入的像素小数小于 0.5 时,对应像素区域的颜色并不会被清除,而传入大像素小数大于 0.5 时,整个像素的颜色都被清除了。至于为什么会是这样的表现,HTML 规范中并未说明 clearRect 在面对小数参数时如何处理,但我们仍然能从该方法的规范定义中找到一些蛛丝马迹:

Clears all pixels on the bitmap in the given rectangle to transparent black.

在位图上清除范围内的所有像素,并且将区域设为透明。

最后来看看 fill 的行为。为了观察 fill 在 clip 区域内的影响,fill 的范围同样设置为整个图形。只需要对上述代码做微小的修改,增加一行,把 clear 的区域再 fill 回去。

// fill 的范围设置为整个图形

Image

fill 的行为明显也受 clip 的影响,如图从上往下数第六条线,clip 坐标为 2.5,clearRect 时清除了整个像素,fill 时由于 clip 的限制,只能填充回来 50%的颜色。

那么这样的表现差异会带来什么问题?

2.消除残影:白璧无瑕胜微瑕

由于 clip、clearRect、fill 在小数像素处理上的不一致,当我们执行前文所述的增量渲染逻辑时,即使裁剪和填充的坐标能够完全对得上,填充完成后还是有可能出现与原图不一致的表现,因为被 clear 掉的小数像素并没有办法重新填充。这样就造成用户界面上出现纯色块的颜色突然变浅、边框线和文字残影等显示异常。

对于小数像素不好处理的问题,我们可能很快就会想到取整。那么,回忆一下第二节提出的问题,对付小数像素只要取整就够了吗?

还是用代码来说明答案比较简单直接。画一个纯色色块,用 clip+clearRect 挖掉其中的一块,再原样 fill 回去,执行这些操作时坐标保持一致。为了验证取整是否能解决问题,我们给这个色块加上缩放效果。运行代码看结果:

Image

缩放的时候边缘有白色线,而且还挺明显。读到这里,原因各位应该已经很明白了——缩放导致传入的整数像素又变成了小数像素,于是 clear 掉的颜色又没有被 fill 回来。

既然知道了原理,解决方案也就变得很简单,只要保证 clear 的区域小于 clip 的区域,那不管如何缩放,都不会出现被 clear 的区域无法重现 fill 的情况。当 clip 坐标是整数时,我们可以加个 0.5,不是整数,就向上取整:

const clipRect = [startX, startY, repaintWidth, attrHeight];

总结

本文在多个代码示例的辅助下讲解了 HTML5 中 canvas 对小数像素坐标的处理机制,介绍了一些 canvas 使用过程中经典问题的解决方案。笔者水平有限,如有错漏欢迎指正。

参考文献:

文末贴个小广告,欢迎加入腾讯文档技术团队,一起探索前端开发的边界!

关于AlloyTeam

AlloyTeam 是国内影响力最大的前端团队之一,核心成员来自前 WebQQ 前端团队。

AlloyTeam负责过WebQQ、QQ群、兴趣部落、腾讯文档等大型Web项目,积累了许多丰富宝贵的Web开发经验。

这里技术氛围好,领导nice、钱景好,无论你是身经百战的资深工程师,还是即将从学校步入社会的新人,只要你热爱挑战,希望前端技术和我们飞速提高,这里将是最适合你的地方。

加入我们,请将简历发送至 alloyteam@qq.com,或直接在公众号留言~

期待您的回复?

最近文章:

首页 - Wiki
Copyright © 2011-2025 iteam. Current version is 2.146.0. UTC+08:00, 2025-10-12 05:32
浙ICP备14020137号-1 $访客地图$