從零開始, 開發一個 Web Office 套件 (1): 富文字編輯器

趙康發表於2022-01-20

這是一個系列部落格, 最終目的是要做一個基於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初始化專案

image

1.4 調整專案目錄結構

我們新建2個資料夾:

  • src/demo:
    • 用於存放所有的demo頁
    • 將原先src目錄下的所有檔案挪到這裡
  • src/core: 用於存放編輯器本體

1.5 Hello, world!

  1. src/core目錄下, 新建一個檔案: CanvasTextEditor.ts. 寫上最簡單的程式碼, 在canvas上渲染出一行Hello, world!:

image

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

image

  1. 新增SASS依賴, 並重置瀏覽器重置樣式
    image

    新增檔案 src/demo/main.scss
    image

    修改檔案 src/demo/main.tsx, 引入main.scss
    image

效果:

image

2. 富文字編輯器(MVP)

2.1 計算文字包圍盒

首先, 我們要找到一種方法, 來確定任意一段文字的包圍盒. 為什麼要確定包圍盒呢? 因為:

  • 當我們的滑鼠hover在文字上方的時候, 需要產生相應的樣式變化. 在DOM中, 這個功能是瀏覽器幫我們實現的. 但是現在在canvas中, 因為整個canvas對於瀏覽器來說, 就是一個柵格影像, 所以我們需要自己計算, 實現這個功能.
  • 當我們在文字上方點選的時候, 需要在對應位置插入閃爍的游標.

CanvasRenderingContext2D 提供了 measureText API, 可以幫我們度量文字尺寸:

接下來, 我們來看一下這個API都返回了哪些有用的資訊.

修改src/core/CanvasTextEditor.ts, 將measureText介面返回結果列印出來:
image

image

問題來了, fontBoundingBox和actualBoundingBox的區別是什麼呢? MDN是這樣描述的:

  • actualBoundingBox: 渲染文字的矩形邊界
  • fontBoundingBox: 渲染文字的所有字型的矩形邊界

看完文件, 還是不確定哪一個使我們想要的. 所以, 我們來給canvas上新增一些輔助線, 來幫助我們更形象地對比下兩者的區別. 我們用紅色畫出actualBoundingBox, 用綠色畫出fontBoundingBox:

注意, 為了方便計算, 我們將textBaseLine設定為top. 如果有小夥伴不熟悉textBaseLine, 可以看MDN提供的這張圖:

回到正題, 渲染結果如下:

image

問題來了, fontBoundingBoxDescent 多出來的那一部分究竟是什麼呢? 讓我們修改文字內容再試一次:

image

這次兩個矩形基本重合了. 所以, actualBoundingBoxDescent中的actual的意思就很明顯了: 實際渲染出的字元距離baseLine的最大距離. 而fontBoundingBoxDescent是不關心實際渲染字元的, 它只關心所有可用的字元.

所以, 為了一致性, 我們使用後者.

2.2 快取(記錄)文字包圍盒

既然找到了計算文字包圍盒的方法, 接下來, 我們需要在每次繪製文字的時候, 將其快取起來, 方便我們後續使用. 新建檔案src/core/CanvasTextEditorText.ts:

修改src/core/CanvasTextEditor.ts, 使用一個陣列將我們想要渲染的文字都儲存起來:

2.3 根據滑鼠位置, 修改滑鼠樣式

接下來, 我們要實現的是這個功能:

當我們的滑鼠hover到文字上的時候, 需要修改滑鼠的樣式, 類似CSS中的cursor: text;

image

我暫時想到了一種簡單的方案: 就是當滑鼠移動到某些區域的時候, 修改canvas的style, 加上cursor: text. 當滑鼠移出這些區域的時候, 去掉cursor: text;

問題來了, 如何獲取到滑鼠在canvas中的座標呢? 我們可以先用一種簡單的方案: 監聽mousemove, 並且和canvas的位置作差.
修改src/core/CanvasTextEditor.ts:
image
重構src/core/CanvasTextEditorText.ts:
image

最終效果:

2.4 文字自動折行

截止到目前, 一切似乎都很正常. 但是, 當我們的文字很長的時候, 它並不會折行. 這就導致過長的文字會顯示不全. 因此, 我們需要實現一個功能: 當文字觸碰到canvas邊緣的時候, 可以自動折行.

實現這個功能之前, 我們先對現有程式碼進行一下重構, 讓我們可以清晰地看到canvas的邊緣:

修改src/demo/main.scss, 給body一個背景色:

image

修改src/core/CanvasTextEditor.ts, 給canvas一個白色背景色:

image

重構src/core/CanvasTextEditorText.ts, 給文字設定一個黑色預設顏色:

image

這樣, 我們可以清晰地看到, 文字後半段沒有顯示:

image

接下來, 我們來解決文字顯示不全的問題. 我暫時想到了一種演算法:

當渲染一段文字之前, 我們先測量一下這段文字的長度a, 再計算一下文字起點距離canvas邊緣的距離b

1. 如果a <= b, 那麼直接渲染即可.
2. 如果a > b, 那麼就需要將文字分成多行. 先找到一個符合要求的最長第一行. 以此類推, 直到第n行.
    3. 如果後期遇到了效能問題, 我們就使用二分法, 來確定每一行的字元數, 優化演算法效能.

然後, 我們來實現這個演算法:

image

然後, 我們在CanvasTextEditorText的建構函式中呼叫這個演算法, 用來:
1. 獲取到分割後的lines
2. 計算出多行文字的真實高度
3. 在render中渲染出每一行

image

然後看一下最終效果:

文字折了兩次, 變成了三行, 很棒!

(未完待續)

相關文章