無限畫布用 CSS transform 縮放整個圖層
物件只存世界座標,由外層容器套 transform: translate() scale() 一次縮放整個圖層,讓瀏覽器用 GPU 合成。
做法
<div
className="ic-world"
style={{ transform: `translate(${offsetX}px, ${offsetY}px) scale(${scale})` }}
>
<rect x={100} y={100} width={200} height={200} />
{/* 所有物件用世界座標畫 */}
</div>矩形寫在 SVG 裡的就是世界座標數字,從頭到尾沒動過。要縮放時改外層 transform,瀏覽器把 .ic-world 當一張圖整張等比縮放,裡面所有 children 一起被縮。
視覺比喻
把 .ic-world 想成一張透明貼紙,物件貼在貼紙上的世界座標位置不動,是整張貼紙縮小,所以物件看起來也跟著變小。
為什麼選這套架構
- 資料乾淨:物件只存世界座標,從不為了顯示重算
- GPU 加速:改
transform不會觸發 re-layout,只重新合成圖層,60fps 沒問題 - 可擴充:新增物件類型只要會畫世界座標,不必懂縮放邏輯
幾乎所有無限畫布(Figma、Miro、tldraw、Excalidraw)都用這套。
另一條(不推薦的)路
讓每個物件自己算:
<rect x={100 * scale + offsetX} y={...} width={200 * scale} />缺點:每次平移/縮放所有物件 props 都變,React 全部 re-render,失去 GPU 合成優勢,到處要記得算座標。
副作用
CSS scale 連邊框、文字、控制點都會一起縮,要保持「螢幕上看起來固定大小」的元素必須反向除以 scale → 見 CSS transform 縮放下 strokeWidth 要除以 scale。