系列:一張投影片背後的工程:從零打造網頁簡報編輯器
對應程式版本:Git tag
day-05版本說明:本文記錄當日選型;目前版本仍沿用固定邏輯畫布與 DOM/CSS 渲染。
上一篇把文件裡的文字與圖片變成可見的投影片。接下來要決定更底層的問題:中央畫布應該使用一般 DOM 元素,還是把內容畫進 canvas?
兩種方式都能做出簡報編輯器,選擇取決於第一版最重要的操作。這個專案需要直接編輯中文、拖曳少量圖文物件、產生縮圖,最後還要讓瀏覽器列印成 PDF。因此我先比較實際需求,而不是只看繪圖效能。
| 需求 | DOM/CSS | Canvas |
|---|---|---|
| 文字輸入與選取 | 可直接使用 textarea 等原生元件 |
需要另外管理輸入框、游標和選字 |
| 中文輸入法 | 可接收 composition 事件 | 需要自行銜接隱藏或浮動輸入元件 |
| 物件位置與樣式 | 用 CSS 直接表達 | 每次變更都要重新繪製 |
| 點擊與拖曳 | 元素可直接接收指標事件 | 需要用座標自行判斷命中物件 |
| 語意與輔助技術 | 可使用 HTML 語意與 ARIA | 需另做可存取的替代內容 |
| 大量圖形 | DOM 數量多時可能增加版面與繪製成本 | 適合集中繪製大量像素或圖形 |
Canvas API 提供 JavaScript 繪圖表面,很適合遊戲、資料視覺化或大量圖形。不過它不會自動把畫面上的每個物件變成可互動的 HTML 元素。對目前以文字編輯為主、每頁物件數量有限的版本,DOM/CSS 可以少做一套輸入、命中測試與列印邏輯。
因此第一版選擇 DOM/CSS。這不是永遠不能改的架構承諾;如果未來加入數千個圖形、自由筆跡或複雜特效,可以再評估 Canvas,甚至只把特定圖層交給 Canvas。
簡報文件使用固定的 1600 × 900 邏輯尺寸。視窗變小時,不縮小文件裡的座標,而是計算一個只供顯示的比例:
const scale = Math.min(
1,
availableWidth / canvasWidth,
availableHeight / canvasHeight,
)
useCanvasScale 透過 ResizeObserver 取得可用空間,寬度與高度各自算出比例,再採用較小值,避免任何一邊超出容器。Math.min(1, ...) 讓「符合視窗」模式最多顯示為 100%;使用者仍可以用明確的縮放選項查看其他比例。
實際顯示時,內層仍維持 1600 × 900,只在渲染層套用 transform:
<div
className="canvas-frame"
style={{
width: presentation.canvas.width * scale,
height: presentation.canvas.height * scale,
}}
>
<SlideRenderer
className="slide-canvas"
style={{ transform: `scale(${scale})` }}
/>
</div>
CSS transform 改變的是元素的視覺呈現,不會替它在一般版面配置中重新保留縮放後的空間。因此外層 canvas-frame 明確使用縮放後的寬高,內層畫布則以左上角為縮放原點:
.slide-canvas {
position: absolute;
inset: 0;
transform-origin: top left;
}
這樣畫布在 50% 時,外層實際占用 800 × 450;文件和物件仍保存原本的 1600 × 900 座標。
縮放只套在顯示上,也代表滑鼠移動距離不能直接寫回文件。換算公式是:
canvasDelta = screenDelta / scale
例如游標在螢幕上向右移動 100 像素:
screenDeltaToCanvas 集中處理這個公式,並拒絕零、負數或非有限的比例。拖曳時,每次換算出的新位置都會先經過 clampElementPosition 限制在畫布內,放開時提交的就是限制後的位置。座標測試同時覆蓋 0.5 與 1.25,避免只在 100% 時看似正確。
Edge 手動驗收也在不同縮放比例下實際拖曳物件,游標與物件位移一致,結果通過。day-05 當時的 6 個測試檔、24 個案例,以及 lint 與正式建置都已通過。
到這裡,第一版的畫布做法已經確定:文件只保存 1600 × 900 邏輯座標,縮放只影響畫面顯示與指標位移換算。下一篇會處理多張投影片的新增、切換、複製、排序與刪除。