系列:一張投影片背後的工程:從零打造網頁簡報編輯器
對應程式版本:Git tag
day-08版本說明:本文記錄當日實作;目前版本新增的多選拖曳、框選與吸附仍使用同一套座標換算。
畫布縮小後,物件看起來變小,滑鼠事件回報的距離也仍是螢幕像素。如果直接把這個距離寫進文件,50% 顯示時物件只會移動游標距離的一半,游標和物件很快就會分開。
Day 05 決定用固定邏輯畫布搭配 CSS transform。這一篇把顯示比例、指標位移和文件座標串起來,並用兩種縮放比例確認公式不是只在 100% 時碰巧成立。
文件永遠保存 1600 × 900 的邏輯尺寸,文字與圖片的 x、y、width、height 都以這個空間為準。外層 canvas-frame 使用縮放後的實際寬高參與頁面排版,內層 SlideRenderer 則保留原尺寸並套用 CSS transform。
<div style={{
width: canvas.width * scale,
height: canvas.height * scale,
}}>
<SlideRenderer style={{ transform: `scale(${scale})` }} />
</div>
第三種是 Pointer Event 的 clientX 與 clientY,它們位於瀏覽器視窗座標。拖曳時不能把視窗座標直接當成投影片座標,只需要比較本次指標位置與按下位置的差,再依顯示比例換算。
useCanvasScale 觀察中央工作區,不假設尺寸只會在視窗 resize 時改變。ResizeObserver 發現容器改變後,用 getBoundingClientRect() 取得寬高,扣除 56 像素留白,再分別計算寬度與高度能容納的比例:
const fitScale = Math.min(
1,
availableWidth / canvasWidth,
availableHeight / canvasHeight,
)
取較小值能讓兩個方向都不超出容器;上限為 1,讓「符合視窗」不會自動放大超過 100%。工具列另提供 50%、75%、100% 與 125% 的明確比例,使用者也能隨時切回符合視窗。
縮放是介面狀態,不會跟著簡報保存。換一台螢幕重新開啟時,文件座標保持不變,再依新的可用空間計算顯示比例。
換算集中在一個純函式:
function screenDeltaToCanvas(
deltaX: number,
deltaY: number,
scale: number,
) {
if (!Number.isFinite(scale) || scale <= 0) {
throw new RangeError('Canvas scale must be greater than zero.')
}
return { x: deltaX / scale, y: deltaY / scale }
}
例如游標向右移動 100 像素、向上移動 25 像素:
| 顯示比例 | 邏輯 X 位移 | 邏輯 Y 位移 |
|---|---|---|
| 50% | 200 | -50 |
| 125% | 80 | -20 |
除以比例後,畫面上的移動距離會重新乘上同一個 CSS scale,因此物件仍跟著游標。函式也拒絕零、負數與非有限比例,避免無效設定產生 Infinity 或 NaN 並污染文件。
useElementDrag 在 pointerdown 記錄兩個起點:螢幕上的指標位置,以及文件中的物件位置。pointermove 計算螢幕差值、除以目前比例、加回物件起點,最後四捨五入成邏輯座標。
const delta = screenDeltaToCanvas(
event.clientX - screenStart.x,
event.clientY - screenStart.y,
scale,
)
const next = {
x: Math.round(elementStart.x + delta.x),
y: Math.round(elementStart.y + delta.y),
}
這個 next 只用於畫面預覽。pointerup 才送出一次 element/move,pointercancel 則清除預覽並保留原文件。一次拖曳不會在每個 pointermove 都觸發自動儲存,也替之後的復原紀錄保留一個清楚的操作單位。
next 在顯示前會先經過 clampElementPosition,以邏輯畫布和物件尺寸限制位置;預覽與放開時提交的都是限制後的座標。左上邊界至少為 0;右下邊界分別是畫布寬高減去物件寬高,所以縮放比例不會影響物件是否超出投影片。
座標純函式測試直接驗證 0.5 與 1.25,並確認比例為 0 時拋出錯誤。邊界測試把 { x: -20, y: 920 } 限制成 { x: 0, y: 800 },確保整個 300 × 100 物件留在 1600 × 900 畫布內。
介面測試則在預設 50% 測試環境中,將游標從 (100, 100) 移到 (150, 130)。螢幕位移 (50, 30) 轉成邏輯位移 (100, 60),物件因此從 (180, 170) 到達 (280, 230),放開後只提交一個最終位置。
Edge 手動驗收也切換不同縮放比例拖曳,游標與物件位移一致。day-08 當時的 6 個測試檔、24 個案例,以及 lint 和 production build 都通過。
下一篇會處理選取與取消選取的事件規則,避免點擊文字、物件或畫布空白時互相搶走狀態。