系列:一張投影片背後的工程:從零打造網頁簡報編輯器
對應程式版本:Git tag
day-04版本說明:本文記錄當日實作;後續版本增加的物件與欄位不改變這裡的共用渲染方式。
上一篇定義了簡報、投影片和物件的資料結構。資料模型能保存內容,卻還不會自己變成畫面。這一篇先完成最短的渲染路徑:讀取目前投影片,依序把文字與圖片放到固定尺寸的畫布上。
第一版的渲染器 SlideRenderer 只接收兩項必要資料:一張 Slide 和文件的 canvas 尺寸。畫布背景來自投影片,每個物件則依照自己的型別選擇文字或圖片元件。以下省略選取、編輯與拖曳所需的 props,只保留渲染分流:
<SlideRenderer
slide={activeSlide}
canvas={presentation.canvas}
>
{activeSlide.elements.map((element) =>
element.type === 'text'
? <TextElementView key={element.id} element={element} />
: <ImageElementView key={element.id} element={element} />
)}
</SlideRenderer>
React 的 key 使用物件本身的 id,而不是陣列索引。新增、刪除或重新排序後,同一個物件仍能保有穩定身分,React 不會因為位置改變而誤用另一個元件的狀態。
渲染器本身是 position: relative,文字和圖片物件使用 position: absolute。因此模型中的 x、y、width、height 可以直接成為 CSS:
const frameStyle = {
left: element.x,
top: element.y,
width: element.width,
height: element.height,
}
絕對定位元素會以最近的定位祖先作為包含區塊,所以 left: 120px 代表距離投影片左側 120 個邏輯單位,而不是距離整個瀏覽器視窗。所有資料都留在固定的 1600 × 900 座標系統中;畫面縮放只影響外層呈現,不回寫物件資料。
這個做法也讓順序很清楚:陣列後面的物件會畫在前面的物件之上。未來加入圖層調整時,只需要改變 elements 的順序,不必另外建立一套座標規則。
文字物件由 TextElementView 顯示。平常 textarea 是唯讀,雙擊後才進入編輯狀態;輸入值改變時,再送出明確的文件更新 action。這讓選取物件與編輯文字維持兩種不同狀態,也能直接使用瀏覽器既有的游標、選字和中文輸入法行為。
文字大小、顏色和對齊方式仍來自文件:
style={{
fontSize: element.style.fontSize,
color: element.style.color,
textAlign: element.style.textAlign,
}}
圖片物件則保存 assetId,由 AssetImage 到 IndexedDB 取得 Blob 並建立可顯示的 URL。渲染器不用知道圖片實際存放方式,只負責把取得的內容放進指定矩形。
同一張投影片不只出現在中央畫布,也會出現在左側縮圖與列印頁面。如果三處各寫一套 JSX,背景、字型或圖片尺寸很容易逐漸不一致。
SlideRenderer 因此提供兩種用法:編輯區傳入可互動的 children;縮圖與列印不傳 children,改由渲染器產生靜態元素。兩者仍共用畫布尺寸、背景、物件框和資產讀取。空白投影片的提示只在互動情境顯示,不會印進 PDF。
day-04 的測試涵蓋中文文字輸入、圖片匯入、IndexedDB 還原,以及每張投影片各自產生列印頁面。當時 6 個測試檔、24 個案例全部通過;Edge 手動驗收也確認投影片內容能正確出現在編輯區、縮圖和列印預覽。
到這裡,資料模型已能透過同一個渲染器出現在編輯畫布、縮圖與列印頁面。下一篇會比較 DOM/CSS 與 Canvas,並說明為什麼第一版選擇現在這套畫布做法。