從零開始, 開發(fā)一個 Web Office 套件 (1): 富文本編輯器
時間:2023-07-23 22:12:02 | 來源:網(wǎng)站運營
時間:2023-07-23 22:12:02 來源:網(wǎng)站運營
從零開始, 開發(fā)一個 Web Office 套件 (1): 富文本編輯器:原文首發(fā)自我的博客: 從零開始, 開發(fā)一個 Web Office 套件 (1): 富文本編輯器
《從零開始, 開發(fā)一個 Web Office 套件》系列博客目錄這是一個系列博客, 最終目的是要做一個基于HTML Canvas 的, 類似于微軟 Office 的 Web Office 套件, 包括: 文檔, 表格, 幻燈片... 等等.
富文本編輯器
萬里長征的第一步: 我們先開發(fā)一個基于canvas的富文本編輯器. 之后, 這個編輯器可以用在我們所有類型的文檔中(文檔, 表格, 幻燈片...).
對應(yīng)的Github repo 地址:
https://github.com/zhaokang555/canvas-text-editor富文本編輯器 在線 Demo:
https://zhaokang555.github.io/canvas-text-editor/1. Environment setup
工欲善其事, 必先利其器. 首先我們來配置項目環(huán)境
1.1 初步構(gòu)想
我們的富文本編輯器項目包含兩大部分:
- 編輯器本體
- 可以單獨打包發(fā)布到npm上
- 暫定使用TypeScript開發(fā)
- demo
- 若干純靜態(tài)網(wǎng)頁, 用于展示編輯器的功能
- 暫定使用React + TypeScript開發(fā)
1.2 Vite
我們使用Vite (
https://cn.vitejs.dev/)作為我們的打包工具.
為什么使用Vite, 而不是Webpack呢? 可以看這里:
https://cn.vitejs.dev/guide/why.html1.3 使用Vite初始化項目
1.4 調(diào)整項目目錄結(jié)構(gòu)我們新建2個文件夾:
src/demo
:- 用于存放所有的demo頁
- 將原先
src
目錄下的所有文件挪到這里
1.5 Hello, world!
- 在
src/core
目錄下, 新建一個文件: CanvasTextEditor.ts
. 寫上最簡單的代碼, 在canvas上渲染出一行Hello, world!
:
2. 修改
src/demo/App.tsx
, 初始化
CanvasTextEditor
:
3. 添加SASS依賴, 并重置瀏覽器重置樣式
添加文件
src/demo/main.scss
修改文件
src/demo/main.tsx
, 引入
main.scss
效果:
2. 富文本編輯器(MVP)
2.1 計算文字包圍盒
首先, 我們要找到一種方法, 來確定任意一段文字的包圍盒. 為什么要確定包圍盒呢? 因為:
- 當(dāng)我們的鼠標(biāo)hover在文字上方的時候, 需要產(chǎn)生相應(yīng)的樣式變化. 在DOM中, 這個功能是瀏覽器幫我們實現(xiàn)的. 但是現(xiàn)在在canvas中, 因為整個canvas對于瀏覽器來說, 就是一個柵格圖像, 所以我們需要自己計算, 實現(xiàn)這個功能.
- 當(dāng)我們在文字上方點擊的時候, 需要在對應(yīng)位置插入閃爍的光標(biāo).
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接口返回結(jié)果打印出來:
問題來了, fontBoundingBox和actualBoundingBox的區(qū)別是什么呢? MDN是這樣描述的:
- actualBoundingBox: 渲染文本的矩形邊界
- fontBoundingBox: 渲染文本的所有字體的矩形邊界
看完文檔, 還是不確定哪一個使我們想要的. 所以, 我們來給canvas上添加一些輔助線, 來幫助我們更形象地對比下兩者的區(qū)別. 我們用紅色畫出actualBoundingBox, 用綠色畫出fontBoundingBox:
注意, 為了方便計算, 我們將textBaseLine設(shè)置為top. 如果有小伙伴不熟悉textBaseLine, 可以看MDN提供的這張圖:
回到正題, 渲染結(jié)果如下:
問題來了, fontBoundingBoxDescent 多出來的那一部分究竟是什么呢? 讓我們修改文字內(nèi)容再試一次:
這次兩個矩形基本重合了. 所以, actualBoundingBoxDescent中的actual的意思就很明顯了: 實際渲染出的字符距離baseLine的最大距離. 而fontBoundingBoxDescent是不關(guān)心實際渲染字符的, 它只關(guān)心所有可用的字符.
所以, 為了一致性, 我們使用后者.
2.2 緩存(記錄)文字包圍盒
既然找到了計算文字包圍盒的方法, 接下來, 我們需要在每次繪制文字的時候, 將其緩存起來, 方便我們后續(xù)使用. 新建文件
src/core/CanvasTextEditorText.ts
:
修改
src/core/CanvasTextEditor.ts
, 使用一個數(shù)組將我們想要渲染的文字都儲存起來:
2.3 根據(jù)鼠標(biāo)位置, 修改鼠標(biāo)樣式
接下來, 我們要實現(xiàn)的是這個功能:
當(dāng)我們的鼠標(biāo)hover到文字上的時候, 需要修改鼠標(biāo)的樣式, 類似CSS中的
cursor: text
;
我暫時想到了一種簡單的方案: 就是當(dāng)鼠標(biāo)移動到某些區(qū)域的時候, 修改canvas的style, 加上
cursor: text
. 當(dāng)鼠標(biāo)移出這些區(qū)域的時候, 去掉
cursor: text
;
問題來了, 如何獲取到鼠標(biāo)在canvas中的坐標(biāo)呢? 我們可以先用一種簡單的方案: 監(jiān)聽mousemove, 并且和canvas的位置作差.
修改
src/core/CanvasTextEditor.ts
:
重構(gòu)
src/core/CanvasTextEditorText.ts
:
最終效果:
2.4 文本自動折行
截止到目前, 一切似乎都很正常. 但是, 當(dāng)我們的文本很長的時候, 它并不會折行. 這就導(dǎo)致過長的文字會顯示不全. 因此, 我們需要實現(xiàn)一個功能: 當(dāng)文字觸碰到canvas邊緣的時候, 可以自動折行.
實現(xiàn)這個功能之前, 我們先對現(xiàn)有代碼進行一下重構(gòu), 讓我們可以清晰地看到canvas的邊緣:
修改
src/demo/main.scss
, 給body一個背景色:
修改
src/core/CanvasTextEditor.ts
, 給canvas一個白色背景色:
重構(gòu)
src/core/CanvasTextEditorText.ts
, 給文字設(shè)置一個黑色默認(rèn)顏色:
這樣, 我們可以清晰地看到, 文字后半段沒有顯示:
接下來, 我們來解決文字顯示不全的問題. 我暫時想到了一種算法:
當(dāng)渲染一段文字之前, 我們先測量一下這段文字的長度a, 再計算一下文字起點距離canvas邊緣的距離b
1. 如果a <= b, 那么直接渲染即可.2. 如果a > b, 那么就需要將文字分成多行. 先找到一個符合要求的最長第一行. 以此類推, 直到第n行. 3. 如果后期遇到了性能問題, 我們就使用二分法, 來確定每一行的字符數(shù), 優(yōu)化算法性能.
然后, 我們來實現(xiàn)這個算法:
然后, 我們在CanvasTextEditorText的構(gòu)造函數(shù)中調(diào)用這個算法, 用來:
1. 獲取到分割后的lines
2. 計算出多行文字的真實高度
3. 在render中渲染出每一行
然后看一下最終效果:
文字折了兩次, 變成了三行, 很棒!
(未完待續(xù))