从零开始, 开发一个 Web Office 套件 (1): 富文本编辑器
这是一个系列博客, 最终目的是要做一个基于HTML Canvas 的, 类似于微软 Office 的 Web Office 套件, 包括: 文档, 表格, 幻灯片... 等等.
富文本编辑器
万里长征的第一步: 我们先开发一个基于canvas的富文本编辑器. 之后, 这个编辑器可以用在我们所有类型的文档中(文档, 表格, 幻灯片...).
对应的Github repo 地址: https://github.com/zhaokang555/canvas-text-editor
1. Environment setup
工欲善其事, 必先利其器. 首先我们来配置项目环境
1.1 初步构想
我们的富文本编辑器项目包含两大部分:
- 编辑器本体
- 可以单独打包发布到npm上
- 暂定使用TypeScript开发
- demo
- 若干纯静态网页, 用于展示编辑器的功能
- 暂定使用React + TypeScript开发
1.2 Vite
我们使用Vite (https://cn.vitejs.dev/)作为我们的打包工具.
为什么使用Vite, 而不是Webpack呢? 可以看这里: https://cn.vitejs.dev/guide/why.html
1.3 使用Vite初始化项目

1.4 调整项目目录结构
我们新建2个文件夹:
src/demo:- 用于存放所有的demo页
- 将原先
src目录下的所有文件挪到这里
src/core: 用于存放编辑器本体
1.5 Hello, world!
- 在
src/core目录下, 新建一个文件:CanvasTextEditor.ts. 写上最简单的代码, 在canvas上渲染出一行Hello, world!:

- 修改
src/demo/App.tsx, 初始化CanvasTextEditor:

添加SASS依赖, 并重置浏览器重置样式

添加文件
src/demo/main.scss

修改文件
src/demo/main.tsx, 引入main.scss

效果:

2. 富文本编辑器(MVP)
2.1 计算文字包围盒
首先, 我们要找到一种方法, 来确定任意一段文字的包围盒. 为什么要确定包围盒呢? 因为:
- 当我们的鼠标hover在文字上方的时候, 需要产生相应的样式变化. 在DOM中, 这个功能是浏览器帮我们实现的. 但是现在在canvas中, 因为整个canvas对于浏览器来说, 就是一个栅格图像, 所以我们需要自己计算, 实现这个功能.
- 当我们在文字上方点击的时候, 需要在对应位置插入闪烁的光标.
CanvasRenderingContext2D 提供了 measureText API, 可以帮我们度量文字尺寸:
- https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/measureText
- https://developer.mozilla.org/zh-CN/docs/Web/API/TextMetrics
接下来, 我们来看一下这个API都返回了哪些有用的信息.
修改src/core/CanvasTextEditor.ts, 将measureText接口返回结果打印出来:


问题来了, fontBoundingBox和actualBoundingBox的区别是什么呢? MDN是这样描述的:
- actualBoundingBox: 渲染文本的矩形边界
- fontBoundingBox: 渲染文本的所有字体的矩形边界
看完文档, 还是不确定哪一个使我们想要的. 所以, 我们来给canvas上添加一些辅助线, 来帮助我们更形象地对比下两者的区别. 我们用红色画出actualBoundingBox, 用绿色画出fontBoundingBox:

注意, 为了方便计算, 我们将textBaseLine设置为top. 如果有小伙伴不熟悉textBaseLine, 可以看MDN提供的这张图:

回到正题, 渲染结果如下:

问题来了, fontBoundingBoxDescent 多出来的那一部分究竟是什么呢? 让我们修改文字内容再试一次:

这次两个矩形基本重合了. 所以, actualBoundingBoxDescent中的actual的意思就很明显了: 实际渲染出的字符距离baseLine的最大距离. 而fontBoundingBoxDescent是不关心实际渲染字符的, 它只关心所有可用的字符.
所以, 为了一致性, 我们使用后者.
2.2 缓存(记录)文字包围盒
既然找到了计算文字包围盒的方法, 接下来, 我们需要在每次绘制文字的时候, 将其缓存起来, 方便我们后续使用. 新建文件src/core/CanvasTextEditorText.ts:

修改src/core/CanvasTextEditor.ts, 使用一个数组将我们想要渲染的文字都储存起来:

2.3 根据鼠标位置, 修改鼠标样式
接下来, 我们要实现的是这个功能:
当我们的鼠标hover到文字上的时候, 需要修改鼠标的样式, 类似CSS中的cursor: text;

我暂时想到了一种简单的方案: 就是当鼠标移动到某些区域的时候, 修改canvas的style, 加上cursor: text. 当鼠标移出这些区域的时候, 去掉cursor: text;
问题来了, 如何获取到鼠标在canvas中的坐标呢? 我们可以先用一种简单的方案: 监听mousemove, 并且和canvas的位置作差.
修改src/core/CanvasTextEditor.ts:

重构src/core/CanvasTextEditorText.ts:

最终效果:

2.4 文本自动折行
截止到目前, 一切似乎都很正常. 但是, 当我们的文本很长的时候, 它并不会折行. 这就导致过长的文字会显示不全. 因此, 我们需要实现一个功能: 当文字触碰到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中渲染出每一行

然后看一下最终效果:

文字折了两次, 变成了三行, 很棒!
(未完待续)
从零开始, 开发一个 Web Office 套件 (1): 富文本编辑器的更多相关文章
- 从零开始, 开发一个 Web Office 套件 (2): 富文本编辑器
书接前文: 从零开始, 开发一个 Web Office 套件 (1): 富文本编辑器 这是一个系列博客, 最终目的是要做一个基于HTML Canvas 的, 类似于微软 Office 的 Web Of ...
- 从零开始, 开发一个 Web Office 套件 (3): 鼠标事件
这是一个系列博客, 最终目的是要做一个基于HTML Canvas 的, 类似于微软 Office 的 Web Office 套件, 包括: 文档, 表格, 幻灯片... 等等. 对应的Github r ...
- 《从零开始, 开发一个 Web Office 套件》系列博客目录
这是一个系列博客, 最终目的是要做一个基于HTML Canvas 的, 类似于微软 Office 的 Web Office 套件, 包括: 文档, 表格, 幻灯片... 等等. 对应的Github r ...
- 从零开始, 开发一个 Web Office 套件(4):新的问题—— z-index
<从零开始, 开发一个 Web Office 套件>系列博客目录 这是一个系列博客, 最终目的是要做一个基于HTML Canvas 的, 类似于微软 Office 的 Web Office ...
- 从零开始,开发一个 Web Office 套件(5):Mouse hover over text
<从零开始, 开发一个 Web Office 套件>系列博客目录 这是一个系列博客, 最终目的是要做一个基于HTML Canvas 的, 类似于微软 Office 的 Web Office ...
- 从零开始,开发一个 Web Office 套件(6):光标 & Click 事件
<从零开始, 开发一个 Web Office 套件>系列博客目录 这是一个系列博客,最终目的是要做一个基于 HTML Canvas 的.类似于微软 Office 的 Web Office ...
- 从零开始,开发一个 Web Office 套件(7):新的问题—— Click 事件的 z-index
这是一个系列博客,最终目的是要做一个基于 HTML Canvas 的.类似于微软 Office 的 Web Office 套件(包括:文档.表格.幻灯片--等等). 博客园:<从零开始, 开发一 ...
- 从零开始,开发一个 Web Office 套件(9):拖动鼠标选中文字 Edge Case
这是一个系列博客,最终目的是要做一个基于 HTML Canvas 的.类似于微软 Office 的 Web Office 套件(包括:文档.表格.幻灯片--等等). 博客园:<从零开始, 开发一 ...
- 从零开始,开发一个 Web Office 套件(10):捕获键盘事件,输入文字
这是一个系列博客,最终目的是要做一个基于 HTML Canvas 的.类似于微软 Office 的 Web Office 套件(包括:文档.表格.幻灯片--等等). 博客园:<从零开始, 开发一 ...
随机推荐
- vnc mirror driver
2012年4月,大二下学期,平静的课堂上老师通知了一个比赛...第一届大学生软件设计大赛!然后我选了第六题:windows屏幕录像.就这样我就开始了我的vc开发生涯. 之前学了c/c++作为基础,自认 ...
- IDEA 无法显示 Run Dashboard 的解决方法
前言 最近开始接触微服务的项目,项目很多有时候本地调测需要启动多个项目,看到同事都是使用dashboard管理项目,服务和端口排列的整整齐齐.但是我dashboard里面啥都没有,一顿百度最后解决问题 ...
- Mysq索引优化(什么情况创建索引,什么情况不创建索引)
一.以下情况需要创建索引 1.主键自动建立唯一索引 2.频繁作为查询条件的字段应该创建索引 3.查询中与其他表关联的字段,外键关系建立索引 4.单键/组合索引的选择问题,组合索引性价比更高 5.查询中 ...
- windows10下vscode+cmake编译Qt5代码(2)
概述 本文依赖 前文 本文将介绍使用cmake语法屏蔽运行Qt exe的cmd窗口 解决办法 配置VS项目属性, 缺点: cmake重新 configure后,项目属性会重置,再次运行还将出现cmd弹 ...
- 【LeetCode】839. 相似字符串组 Similar String Groups (Python)
作者: 负雪明烛 id: fuxuemingzhu 公众号:每日算法题 本文关键词:LeetCode,力扣,算法,算法题,字符串,并查集,刷题群 目录 题目描述 解题思路 并查集 代码 刷题心得 欢迎 ...
- 【LeetCode】968. Binary Tree Cameras 解题报告(C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 日期 题目地址:https://leetcode.c ...
- 【LeetCode】565. Array Nesting 解题报告(Python & C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 日期 题目地址:https://leetcode.c ...
- 【LeetCode】592. Fraction Addition and Subtraction 解题报告(Python)
[LeetCode]592. Fraction Addition and Subtraction 解题报告(Python) 标签(空格分隔): LeetCode 作者: 负雪明烛 id: fuxuem ...
- 1030 - Discovering Gold
1030 - Discovering Gold PDF (English) Statistics Forum Time Limit: 2 second(s) Memory Limit: 32 M ...
- Go语言核心36讲(新年彩蛋)--学习笔记
新年彩蛋 | 完整版思考题答案 基础概念篇 Go 语言在多个工作区中查找依赖包的时候是以怎样的顺序进行的? 答:你设置的环境变量GOPATH的值决定了这个顺序.如果你在GOPATH中设置了多个工作区, ...