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

趙康發表於2022-01-26

書接前文: 從零開始, 開發一個 Web Office 套件 (1): 富文字編輯器
這是一個系列部落格, 最終目的是要做一個基於HTML Canvas 的, 類似於微軟 Office 的 Web Office 套件, 包括: 文件, 表格, 幻燈片... 等等.
對應的Github repo 地址: https://github.com/zhaokang555/canvas-text-editor

2.5 觀察一下幻燈片中的文字框

image

我們發現:

  • 一個文字框中有若干行文字
  • 一行文字中每個字元的大小, 樣式都可能不一樣 (廢話)
    • 但是, 上一篇文章中, 我們設定樣式, 是以行為物件的
  • 任意一個單獨的字元, 都可以用滑鼠選中 (廢話)
    • 但是, 上一篇文章中, 我們計算文字包圍盒, 是以行為整體計算的
  • 一行文字如果過長, 可以自動折行(預設行為, 可以修改)
  • 當我調整文字框的寬度, 折行位置隨之改變
  • 文字框的位置和寬高都是可以調整的.

為了將來我們的web slides中能夠用上CanvasTextEditor, 我們需要重構一下我們的程式碼.

2.6 重構Editor

  1. 修改src/core/CanvasTextEditor.ts, 為編輯器加上位置屬性, 寬高屬性, 樣式屬性:

image

  1. 修改src/demo/App.tsx, 給編輯器傳入位置資訊:

image

  1. 修改src/core/CanvasTextEditor.ts:

實現render函式, 渲染一個臨時邊框:

image

新增renderBorderCirclor函式, 渲染邊框上的8個圓形控制點:

image

  1. 最終效果:

image

2.7 設計Editor的整體架構

如下圖所示:

一張幻燈片(Slide)中可能有多個編輯器(Editor), 一個編輯器中可能有多個段落(Paragraph), 一個段落中可能有多行(SoftLine), 一行中可能有多個字元(Char).

2.8 自頂向下實現

接下來, 我們按照自頂向下的方式, 來一步步實現這個架構:

2.8.1 Paragraph

  1. 修改src/core/CanvasTextEditor.ts, 新增一個欄位 paragraphs:

image

其中, 每個Paragraph要接收這幾個引數:

  • chars: Char[]
  • canvas context
  • left
  • top
  • maxWidth

為什麼Paragraph的建構函式裡, 要直接接收Char列表, 而不是SoftLine列表呢?
因為一個SoftLine並不是真正的一行, 而是根據每個Char的寬度和Paragraph的maxWidth, 實時計算出來的.
我們以後將會實現這樣的feature: 如果使用者調整了Editor的大小, Paragraph的maxWidth隨之改變, 所有的SoftLine都會重新計算. 類似下圖一樣:

image

  1. 新建檔案src/core/CanvasTextEditorParagraph.ts

如上圖, 在構造Paragraph時, 我們需要實現2個邏輯:

  • calcLayoutForSoftLines:
    • 根據maxWidth, 將所有的Char進行分組, 得到softLines. (類似我們上一篇中的splitContentIntoLines方法)
    • 根據每個softLine中的Chars的大小, 計算每個softLine的大小
  • calcLayout:
    • 根據每個softLine的大小, 計算Paragraph的大小

然後, 我們來實現這兩個邏輯:

image

image

2.8.2 SoftLine

新建src/core/CanvasTextEditorSoftLine.ts檔案, 並在其建構函式中, 計算傳入的所有chars的位置:

2.8.3 Char

  1. 新建src/core/CanvasTextEditorChar.ts檔案:

  1. 目前暫時先支援定製colorfontSize兩個樣式:

  1. 另外, 要對外暴露setPosition方法, 方便在SoftLine中為每個Char設定位置:

2.8.4 刪除CanvasTextEditorText

由於之前src/core/CanvasTextEditorText.ts中的邏輯現在已經分散到了Paragraph, SoftLine, Char中, 所以現在可以刪除這個檔案.

2.8.5 最終效果

2.9 行內文字底部對齊

截止到目前為止, 出現了一個小問題: 一行內不同大小的文字, 他們的縱向對齊方式, 是以頂部為基線的.

為了看得更清楚, 我們給每個字元加上輔助邊框和背景色, 修改src/core/CanvasTextEditorChar.ts:

image

修改src/core/CanvasTextEditor.ts, 再加上幾個漢字:

image

這樣, 可以更清晰地看出, 不同大小的文字是頂部對齊的:

image

為什麼會出現行內文字縱向頂部對齊呢? 因為我們之前為了方便, 將textBaseline設定為了top:

這樣設定之後, 包圍盒頂部座標 和 fillText(text, x, y)中的y座標就相等了. 我們之前把它們統一記作top.
現在, 我們不得不放棄之前的偷懶方式, 將兩者分別記錄:

  • fillText(text, x, y)中的y記作top
  • 將包圍盒頂部座標記作boundingBoxTop

修改src/core/CanvasTextEditorChar.ts:

看一下效果:

2.10 再議textBaseLine

這次行內文字縱向對齊的問題解決了, 可是新的問題來了: 為什麼所有的文字整體上移了?
因為我們已經把textBaseLine恢復成了預設值alphabetic. 繪製文字的基線下移了, 且文字的座標(left, top)沒變, 所以相當於文字上移了.

image

為了解決這個問題, 我暫時想到了一種方法:

  1. 我們需要將每一行文字統一向下偏移一個長度offsetY
  2. 每一行的offsetY, 取決於行內所有字元fontBoundingBoxAscent的最大值
  3. 在渲染行內的每個字元時, 統一加上這個偏移值

接下來我們來實現, 修改src/core/CanvasTextEditorSoftLine.ts:

image

效果:

image

文字上移的問題解決了, 棒!

(未完待續)

相關文章