万里长征的第一步: 我们先开发一个基于canvas的富文本编辑器. 之后, 这个编辑器可以用在我们所有类型的文档中(文档, 表格, 幻灯片...). 对应的Github repo 地址: https://github.com/zhaokang555/canvas-text-editor
工欲善其事, 必先利其器. 首先我们来配置项目环境
我们的富文本编辑器项目包含两大部分:
我们使用Vite (https://cn.vitejs.dev/)作为我们的打包工具. 为什么使用Vite, 而不是Webpack呢? 可以看这里: https://cn.vitejs.dev/guide/why.html
我们新建2个文件夹:
src/demo
: src
目录下的所有文件挪到这里src/core
: 用于存放编辑器本体src/core
目录下, 新建一个文件: CanvasTextEditor.ts
. 写上最简单的代码, 在canvas上渲染出一行Hello, world!
:src/demo/App.tsx
, 初始化CanvasTextEditor
:
添加文件 src/demo/main.scss
修改文件 src/demo/main.tsx
, 引入main.scss
效果:
首先, 我们要找到一种方法, 来确定任意一段文字的包围盒. 为什么要确定包围盒呢? 因为:
CanvasRenderingContext2D 提供了 measureText API, 可以帮我们度量文字尺寸:
接下来, 我们来看一下这个API都返回了哪些有用的信息.
修改src/core/CanvasTextEditor.ts
, 将measureText接口返回结果打印出来:
问题来了, fontBoundingBox和actualBoundingBox的区别是什么呢? MDN是这样描述的:
看完文档, 还是不确定哪一个使我们想要的. 所以, 我们来给canvas上添加一些辅助线, 来帮助我们更形象地对比下两者的区别. 我们用红色画出actualBoundingBox, 用绿色画出fontBoundingBox:
注意, 为了方便计算, 我们将textBaseLine设置为top. 如果有小伙伴不熟悉textBaseLine, 可以看MDN提供的这张图:
回到正题, 渲染结果如下:
问题来了, fontBoundingBoxDescent 多出来的那一部分究竟是什么呢? 让我们修改文字内容再试一次:
这次两个矩形基本重合了. 所以, actualBoundingBoxDescent中的actual的意思就很明显了: 实际渲染出的字符距离baseLine的最大距离. 而fontBoundingBoxDescent是不关心实际渲染字符的, 它只关心所有可用的字符.
所以, 为了一致性, 我们使用后者.
既然找到了计算文字包围盒的方法, 接下来, 我们需要在每次绘制文字的时候, 将其缓存起来, 方便我们后续使用. 新建文件src/core/CanvasTextEditorText.ts
:
修改src/core/CanvasTextEditor.ts
, 使用一个数组将我们想要渲染的文字都储存起来:
接下来, 我们要实现的是这个功能:
当我们的鼠标hover到文字上的时候, 需要修改鼠标的样式, 类似CSS中的cursor: text
;
我暂时想到了一种简单的方案: 就是当鼠标移动到某些区域的时候, 修改canvas的style, 加上cursor: text
. 当鼠标移出这些区域的时候, 去掉cursor: text
;
问题来了, 如何获取到鼠标在canvas中的坐标呢? 我们可以先用一种简单的方案: 监听mousemove, 并且和canvas的位置作差.
修改src/core/CanvasTextEditor.ts
:
重构src/core/CanvasTextEditorText.ts
:
最终效果:
截止到目前, 一切似乎都很正常. 但是, 当我们的文本很长的时候, 它并不会折行. 这就导致过长的文字会显示不全. 因此, 我们需要实现一个功能: 当文字触碰到canvas边缘的时候, 可以自动折行.
实现这个功能之前, 我们先对现有代码进行一下重构, 让我们可以清晰地看到canvas的边缘:
修改src/demo/main.scss
, 给body一个背景色:
修改src/core/CanvasTextEditor.ts
, 给canvas一个白色背景色:
重构src/core/CanvasTextEditorText.ts
, 给文字设置一个黑色默认颜色:
这样, 我们可以清晰地看到, 文字后半段没有显示:
接下来, 我们来解决文字显示不全的问题. 我暂时想到了一种算法:
当渲染一段文字之前, 我们先测量一下这段文字的长度a, 再计算一下文字起点距离canvas边缘的距离b
1. 如果a <= b, 那么直接渲染即可.
2. 如果a > b, 那么就需要将文字分成多行. 先找到一个符合要求的最长第一行. 以此类推, 直到第n行.
3. 如果后期遇到了性能问题, 我们就使用二分法, 来确定每一行的字符数, 优化算法性能.
然后, 我们来实现这个算法:
然后, 我们在CanvasTextEditorText的构造函数中调用这个算法, 用来: 1. 获取到分割后的lines 2. 计算出多行文字的真实高度 3. 在render中渲染出每一行
然后看一下最终效果:
文字折了两次, 变成了三行, 很棒!
(未完待续)