背景
在表格细分领域中,主要的技术实现分为 DOM 和 Canvas, 满足不同的使用场景,各有优劣,DOM 更灵活更方便,很多事情浏览器渲染引擎已经帮你处理好了,缺点是性能较差,交互实现不灵活,Canvas 解决了 DOM 的缺点,但是也带来更高的上手成本和开发难度,本文就着重讲解使用 Canvas 实现 S2 表格在文本渲染上遇到的那些事。
让文本渲染更清晰
DPR
是 “Device Pixel Ratio”(设备像素比)的缩写,指的是物理像素和设备独立像素的比率,不同的屏幕 DPR
也不同,像我们平时用的 2k 外接屏 DPR 是 1
, Mac 视网膜屏幕 DPR 是 2
.
在 Canvas 中需要额外适配,不然文本,图形等场景会模糊,也就是将 canvas
的宽高放大 n 倍 (n = DPR)
1 | const canvas = document.getElementById('your-canvas-id'); |
1 | ctx.fillText('哈哈', 0, 0); |
:::warning
需要特别注意的是 canvas.style.width
和 canvas.width
是不同的,一个是 Canvas DOM 节点的 width
属性,一个是 CSS 样式,有本质上的区别。高清适配改变的是 canvas.width
, 所以 DOM 的尺寸是不变的,不会影响页面布局。
:::
1 | canvas.style.width = xxx |
除此之外,还有 DPR 切换的场景,比如用户将浏览器从 2K 显示器移动到的另外一个 4K 的显示器 🖥, 由于 Canvas 是按照移动前的屏幕 DPR 进行的适配,所以也会有模糊的问题,解决的方法是通过 matchMedia
对 DPR 进行监听,重新更新 canvas.width/height
即可。
1 | // devicePixelRatio: 1 |
文本排版与换行
排版
在 Canvas 中要进行文字的排版和换行并不是一件容易的事,如果是 DOM, 我们通过编写简单的 CSS
就可以实现布局,浏览器已经帮我们计算好了一切,但在 Canvas 画板中,所有元素都是需要自己计算的,文本 (Text), 边框 (Line), 单元格 (Rect) 所有的一切。可以理解所有元素都是绝对定位, 要手动控制所有元素的 top/right/bottom/left
如图所示,我们想绘制一个 数量
单元格,文本的右边,需要绘制一个 ▽
排序 icon, 简化的步骤如下:
- 计算出单元格的盒模型 (BBox), 类似与浏览器那样
- 计算出单元格的宽高和坐标 (x: 200, y: 100, width: 100, height: 30)
- 计算出
数量
文本的宽度,考虑文字的对齐方式 (靠左/居中/靠右) 后,计算文本坐标 - 计算出
▽
icon 的宽高,考虑文本的 margin 和 padding 后,计算 icon 坐标
由于还有滚动的场景,所以还需要额外考虑坐标的同步
S2/packages/s2-core/src/cell/header-cell.ts at a09d1fe0507a566f154a407c92f13ff48beafc99 · antvis/S2
对于 S2 这种单元格布局的场景,如果能像 CSS 那样使用 flex
或者 grid
布局就非常的高效了,事实上,Facebook (Meta) 提供了跨平台布局引擎一个 Yoga, 可以实现类似的效果,如果写过 <font style="color:rgba(0, 0, 0, 0.85);">React Native</font>
的同学肯定会很熟悉
AntV/G 5.0
也封装了相应的插件 [g-plugin-yoga](https://g.antv.antgroup.com/plugins/yoga)
不过由于历史原因和升级成本的问题,S2 没有迁移到该方案。
文本省略
解决了排版的问题,我们还需要考虑文本省略的场景,在 CSS 中,如果我们设置了 text-overflow: ellipsis
和 overflow: hidden
那么浏览器就会自动省略文本,并已 ...
展示
对于 S2 的表格场景,单元格的宽度是固定的,并默认就是溢出异常,所以需要计算是否需要展示省略号
具体的实现感兴趣可以查看:
S2/packages/s2-core/src/utils/text.ts at ab0d0e768cf627249b38205b8c77d95a20b3a901 · antvis/S2
:::info
一个有趣的点是:按照浏览器的规范,当给定的宽度连省略号都不足以展示的话,会直接对文字进行截断,而不是显示 ...
:::
多行文本
在 CSS 中,我们可以通过 line-clamp
实现多行文本
1 | display: -webkit-box; |
对于 S2 的表格场景,单元格的宽度是固定的,以换两行为例:
- 首先计算当前文本是否存在省略号
- 如果存在省略号,说明一行文字显示不下,那么进行分词
- 分词后,对第二行的文字进行相同的操作,第 n 行依次类推
1 | protected drawTextShape() { |
S2/packages/s2-core/src/cell/corner-cell.ts at ab0d0e768cf627249b38205b8c77d95a20b3a901 · antvis/S2
在 S2 2.0 中,得益与 G 5.0 渲染引擎的升级,内置了多行文本的能力,具体实现如下:
G/packages/g-lite/src/services/TextService.ts at ee938ca12ac343d0506b7fa41bbd27d92fdd4d28 · antvis/G
文本抗锯齿
在游戏中,我们经常看到 垂直同步、动态模糊、抗锯齿 这些关键词
其中抗锯齿中的 “锯齿” 就像下图一样
如果用过 Windows “不知名软件” - “画图” 的话,画一条线就会看到下面这样
在浏览器中,也会出现锯齿效果,专业名词叫<font style="color:rgb(27, 27, 27);">anti-aliasing effect</font>
, 不同的浏览器渲染引擎有不同的表现。
浏览器允许我们写子像素,也就是 width: 12px
width: 12.3px
都能正常渲染,每一个像素点可以看做一个正方形的格子,一旦文本边缘不再处于像素格网的整数位置,就会出现锯齿。要解决这个问题也很简单。对像素进行取整即可。
对于 CSS 可以开启 -webkit-font-smoothing
来优化
1 | .smoothed { |
在 Canvas 中可以开启 webkitImageSmoothingEnabled
来优化
1 | const ctx = canvas.getContext('2d'); |
如果不开启抗锯齿的话,在 Canvas 中还会出现抖动的问题,可以点击下文了解更多
文本宽高测量与字体的影响
在表格渲染中,需要渲染大量的文本,需要计算文本的宽高用于布局坐标的计算,从而实现排版,宽度受浏览器的字体和符号影响
常见的文本有汉字,数字,英文,甚至 Emoji 😸
, Emoji 通常是由多个 Unicode 字符组成的复合字符,宽度各不相同,那对于这种场景,如果我们想保证测量的宽度一致,那么可以使用 [等宽字体](https://zh.wikipedia.org/zh-hans/%E7%AD%89%E5%AE%BD%E5%AD%97%E4%BD%93)
1 | JetBrains Mono, Fira Code, Source Code Pro,Menlo,FiraCode-Medium,FiraCode-Light,FiraCode-Light,'Courier New', monospace |
1 | const canvas = document.getElementById('your-canvas-id'); |
使用 Canvas Context 的 measureText
, 获取到 TextMetrics 后,我们可以拿到相应的宽高,对于相同的文本,我们可以缓存起来,提高计算性能
1 | public measureText = memoize( |
离屏 Canvas 与字体的影响
通常我们说的 离屏 Canvas
是指没有 append 到 body 上 (用户不可见) 的 Canvas
, 它存在于内存中,用于做一些复杂计算和绘制,在处理完成后,再绘制回原本的 Canvas, 以提升性能
1 | const offscreenCanvas = document.createElement('canvas'); |
现代浏览器也提供一个实现性的标准实现 OffscreenCanvas, 由于不是本文重点,这里略过
1 | myEntity.offscreenCanvas = document.createElement("canvas"); |
由于离屏 Canvas 不存在文档流中,那么也会出现文本宽度测量不一致的问题,解决方法也很简单,将其添加到 body 中 (默认隐藏), 即可得到一致的字体表现。
文本无障碍体验
> 无障碍(Accessibility)是指为人们提供平等访问信息、服务和环境的设计理念和实践。在 Web 开发中,无障碍性是确保所有人,包括身体残障、认知障碍和感知障碍的人,都能够获得和使用网站和应用程序的能力。 >无障碍体验目前苹果做的最好,对于前端来说,很多组件库也做了一些无障碍的适配,比如 Ant Design
对于表格这类大量文字的场景,对于使用 Canvas 的表格的 S2 来说是一大痛点,比如文字无法被选中,无法被键盘访问,无法添加 aria
标签,无法被搜索等问题。同时对 SEO 也不友好。
要解决这个问题,我们可以在 Canvas 的上方添加 DOM 蒙层,并解析 Canvas 内的文本元素,绝对定位一个相同的 DOM 版本的节点,提升可访问性,G 5.0 提供了 g-plugin-a11y 插件,可以很方便的实现该能力。
参考文章
html5-canvas-sprite-optimisation
Improving HTML5 Canvas Performance