Post
December 2, 2025 at 09:31 PM

use-gesture 及 event threshold

為了簡化 event binding,我們選擇使用簡單的 use-gesture。

事後才發現其實沒有簡化多少,可以完全自己來

這裡我們只簡單綁定幾個事件:

  • onClick:單擊關閉控制元件層、雙擊縮放
  • onDrag:拖曳(pan)圖片,根據縮放狀態不同有不同行為
  • onDragEnd:計算是否需要關閉 Lightbox,或是將圖片拉回 boundary 內。
  • onWheel/onWheelEnd 將 macOS 的觸控板事件綁到 onDrag/onDragEnd 上。實際上 onWheelEnd 是模擬事件,並沒有辦法精確知道手勢放開的時間點。但 use-gesture 在模擬計算上處理得還可以,80% 狀況下不會意識到差異。
  • onMove:在滑鼠移動時重新顯示控制元件。
  • onPinch:處理多點觸控的縮放事件。
  • onPinchEnd:多點觸控的縮放結束事件,用來將圖片拉回 boundary 內。
  • onDragStart:阻止電腦觸發圖片 drag-n-drop 行為(例如把圖片拉下來存到桌面)。

Pinch 的處理算是我覺得用 use-gesture 用對的地方。API 提供的事件位置很方便。

但除此之外還是做了不少額外處理。

像是圖片的 boundary 處理(在圖片拉超過畫面時彈回來)主流做法有兩種:rubber band 彈力係數,在拖曳還沒結束前就提供阻力,超過邊緣越多圖片會動得越少(iOS 常用)。或是在超過邊緣後就不再提供拖曳(Android 常用,一般會搭配一點點邊緣顏色變化讓使用者知道這個邊緣過不去)。

考慮到邊緣其實跟縮放有關,為了簡化計算我們沒有使用 use-gesture 內建的 boundary 跟 rubber band,而是在使用者放開手勢後再將圖片彈回邊緣內。這麼做有幾個好處:

  1. 拖曳計算簡單而且迅速精準不易疼痛,event movement 跟 image movement 基本上是 1:1。
  2. 不會有回彈過頭的問題(rubber band 沒有處理好很容易發生在回彈之後圖片跑出另一側邊緣的狀況)。
  3. 在因為其他手勢(例如 double tap zooming)撞擊到邊緣時不需要中斷動畫,等動畫結束後再拉回來就好。這可以避免兩個動畫之間彼此對抗,讓圖片彈來彈去。

offset 補償與 transform: translate/scale

為了使用瀏覽器本身的 HW accel,我們改用 transform: translate/scale 控制圖片位置。同時也把圖片的 CSS position 固定在 absolute,讓圖片在平常的垂直水平置中過渡到 freeform 之間的動畫不會因為 reflow 而卡頓。

不過,同時帶來一個小問題:scale 過後的圖片是由本身的 origin 為中心平移,圖片本身座標空間不能直接對應到畫面上的顯示座標空間。也就是說,double tap 跟 pinch 事件取得的座標不能夠直接用來更新 transform。

為了解...

...Show more (327 chars)

useTransform 與 MotionValue

use-gesture 的 handler 相對來說有效率,但不可避免的必須依賴 gesture-mode 對事件進行不同處理。

我們可以將目前圖片的 x, y 儲存在 useState 內交由 React 管理,不過我們實際上不需要因為圖片動了一個 pixel 就觸發 rerender。

我們也可以在各個 handler 內計算目前應該有的 gesture-mode,但這時除了邏輯重複外,也可能會有 race condition。

因此,這邊我們將 x, y 保留在 MotionValue 中,由 motion 管理動畫,並且藉由一個 useTransform...

...Show more (648 chars)

useResizeObserver

圖片的顯示範圍並不是固定的,例如當進入放大模式後左右兩邊的黑邊會需要消失。這時使用 ResizeObserver 解耦就變得很方便。(而且效能也不錯)

方便使用,我們參考 useHooks 的 useIntersectionObserver 另外寫了一個 useResizeObserver 觸發 rerender。

// useResizeObserver hook to observe size changes of a DOM element, return a ResizeObserver entry

export const useResizeObserve...

...Show more (547 chars)