Post
2025年12月2日 下午09:31

useTransform 與 MotionValue

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

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

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

因此,這邊我們將 x, y 保留在 MotionValue 中,由 motion 管理動畫,並且藉由一個 useTransform 計算現在的 gesture-mode,必要時觸發 rerender。Handler 也可以直接 .get() 這個計算結果來用,不需要因為 state update 就重造 handler。

const gestureModeMotionValue = useTransform(() => {

if (zoomScaleMotionValue.get() > 1.0) {

return 'zoom';

}

const movement = Math.hypot(offsetXMotionValue.get(), offsetYMotionValue.get());

if (movement > DRAG_ACTIVATE_THRESHOLD) {

return 'pan-to-close';

}

return 'none';

});

// update gesture mode based on motion values

useMotionValueEvent(gestureModeMotionValue, 'change', (latest) => {

setGestureMode(latest as 'none' | 'zoom' | 'pan-to-close');

});

(註:在不需要 motion 的狀況下用 Zustand 應該也可行,單純用 React ref 則做不到根據狀態觸發更新的效果)

useResizeObserver

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

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

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

export const useResizeObserve...

...顯示更多 (547 字)

Lightbox pointer event 穿透

黑邊關閉圖片是這次重構的重點之一,但也是最麻煩的。

以附圖為例:

  • 橘色斜線區域是應該要觸發關閉的黑邊區域。
  • 綠色網狀區是圖片本身,應該要觸發拖曳或縮放效果。而且拖曳時橘色區域應該關閉。
  • 紅色是切換圖片的手勢區域。

理想的做法是在橘色區域綁 onClick⋯⋯但 HTML 沒有 event mask 這種東西。而且黑邊的大小跟位置也不固定,用 event position 判斷並不實際。

CSS 有 pointer-event: none 可以使用,理論上在 Lightbox wrapper 設定事件穿透,讓後面的 backdrop 收得到事件就沒有問...

...顯示更多 (367 字)

綜合之後,就是這次的更新😊

一個盡量模仿原生介面的 Web Lightbox。

做完之後又可以繼續拖稿 App 了