iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
Software Development

從零打造網頁簡報編輯器系列 第 8

Day 08|畫布縮放後座標如何保持正確

  • 分享至 

  • xImage
  •  

系列:一張投影片背後的工程:從零打造網頁簡報編輯器

對應程式版本:Git tag day-08

版本說明:本文記錄當日實作;目前版本新增的多選拖曳、框選與吸附仍使用同一套座標換算。

畫布縮小後,物件看起來變小,滑鼠事件回報的距離也仍是螢幕像素。如果直接把這個距離寫進文件,50% 顯示時物件只會移動游標距離的一半,游標和物件很快就會分開。

Day 05 決定用固定邏輯畫布搭配 CSS transform。這一篇把顯示比例、指標位移和文件座標串起來,並用兩種縮放比例確認公式不是只在 100% 時碰巧成立。

先分清楚三種尺寸

文件永遠保存 1600 × 900 的邏輯尺寸,文字與圖片的 xywidthheight 都以這個空間為準。外層 canvas-frame 使用縮放後的實際寬高參與頁面排版,內層 SlideRenderer 則保留原尺寸並套用 CSS transform。

<div style={{
  width: canvas.width * scale,
  height: canvas.height * scale,
}}>
  <SlideRenderer style={{ transform: `scale(${scale})` }} />
</div>

第三種是 Pointer Event 的 clientXclientY,它們位於瀏覽器視窗座標。拖曳時不能把視窗座標直接當成投影片座標,只需要比較本次指標位置與按下位置的差,再依顯示比例換算。

讓畫布符合可用空間

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,因此物件仍跟著游標。函式也拒絕零、負數與非有限比例,避免無效設定產生 InfinityNaN 並污染文件。

拖曳期間先預覽,放開才寫入文件

useElementDragpointerdown 記錄兩個起點:螢幕上的指標位置,以及文件中的物件位置。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/movepointercancel 則清除預覽並保留原文件。一次拖曳不會在每個 pointermove 都觸發自動儲存,也替之後的復原紀錄保留一個清楚的操作單位。

next 在顯示前會先經過 clampElementPosition,以邏輯畫布和物件尺寸限制位置;預覽與放開時提交的都是限制後的座標。左上邊界至少為 0;右下邊界分別是畫布寬高減去物件寬高,所以縮放比例不會影響物件是否超出投影片。

讓測試刻意離開 100%

座標純函式測試直接驗證 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 都通過。

下一篇會處理選取與取消選取的事件規則,避免點擊文字、物件或畫布空白時互相搶走狀態。

參考資料


上一篇
Day 07|把文字與圖片變成可操作的物件
下一篇
Day 09|選取、文字焦點與中文輸入如何共存
系列文
從零打造網頁簡報編輯器13
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言