Post
2025年12月2日 下午09:31
歡迎所有互動

Lightbox Refactor(圖片/影片瀏覽器重構)

這次重構前列下的目標有:

  • 解決部分使用者不習慣滑鼠點擊黑邊不能關閉 Lightbox 的不方便
  • 改善狀態切換動畫流暢度
  • 現有三層架構(Carousel、Interaction Detection、Video Control Overlay)跟泛型(Carousel<T>, T ∈ Default, Editor)過度複雜問題
  • 修好 pinch 手勢放大圖片時錯誤使用畫面中心點作為參考點的問題

實際跳下去之後才發現問題比想像中麻煩:

  • Lightbox 狀態管理太過複雜,常有兩到三層以上的 props 傳遞
  • 既有 Flex/Grid box model 容易觸發 browser reflow
  • 原本的 pan 完全依靠 top/left 移動圖片,過度依賴瀏覽器的 flow engine,無法善用 Hardware Acceleration
  • double tap zoom-in 有跟 pinch 手勢相同的問題
  • pan 的 boundary 錯誤設定在 w-screen/h-screen 的圖片 wrapper 上,移動圖片計算邊界會包含黑邊
  • letterbox/pillarbox(水平/垂直貼齊邊緣)計算完全依賴 window.clientHeight/Width,顯示範圍不可能為了提供滑鼠點擊的黑邊而修改
  • 將 Lightbox 關閉行為 defer 後,瀏覽器的上一頁行為無法預測,pop history 時經常會在非預期的時間點觸發 state update 打回 lightbox,觸發雙重關閉截斷切換動畫
  • 縮放手勢鎖定之後會阻擋觸控裝置的橫向捲動
  • 既有 scroll event 偵測圖片 pan-to-close(觸控裝置常見的拖曳圖片以關閉)的方式在某些裝置上無法確定使用者手勢何時結束,常會有手指尚未放開前圖片就自己關閉的問題
  • 黑邊出現位置無法預測難以綁定點擊事件

在經過三天的痛苦之後,決定使用整套全新的架構處理上述問題:

  • 轉用 Context 跟 Compound Component 的組合
  • 與狀態切換相關的行為使用 React 19.2 ViewTransition 搭配 startTransition
  • 搭配 use-gesture library 設定 event threshold
  • zooming 時針對事件位置進行 offset 補償,同時使用 transform: translate/scale 調整圖片顯示位置
  • 使用 motion 的 MotionValue 及 useTransform 隨需計算與動畫相關的狀態
  • 自行維護 useResizeObeserver,觀測 wrapper 大小變化
  • CSS media selector 針對模糊/精準游標裝置分別提供不同的 Lightbox pointer event 穿透

Context 跟 Compound Component

Compound Component 的介紹應該可以不用提太細,詳細概念可以參考: https://www.patterns.dev/react/compound-pattern/

簡單來說是將複雜的相依關聯用同個 Context 串起來同步狀態,方便外部 caller 不需要手動傳入每個 props,只需要使用需要的部分就可以。

比較值得注意的是為了避免過度 rerender,這裡使用的 Context 只提供三個 state:

  • mode: 'lightbox' | 'thumbnail':代表現在在縮圖還是放大的狀態,Item、Over...
...顯示更多 (371 字)

狀態切換行為使用 React 19.2 ViewTransition 搭配 startTransition

前陣子把 nextjs 升級到 16 之後,最大的好處就是現在可以直接使用 React 19.2 的 ViewTransition Component 了。

過去的 Lightbox 雖然也有使用瀏覽器的 SPA view-transition API,但使用的方式有點獵奇。

在使用 ViewTransition 之後可以搭配 useTransition hook 的 startTransition 進行狀態及動畫的觸發管理,省掉不少時間。

但在搭配上一頁關閉圖片的功能時還是有點小問題沒辦法...

...顯示更多 (306 字)

use-gesture 及 event threshold

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

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

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

  • onClick:單擊關閉控制元件層、雙擊縮放
  • onDrag:拖曳(pan)圖片,根據縮放狀態不同有不同行為
  • onDragEnd:計算是否需要關閉 Lightbox,或是將圖片拉回 boundary 內。
  • onWheel/onWheelEnd 將 macOS 的觸控板事件綁到 onDrag/onDragEnd 上。實際上 onWheelEnd 是模擬事件,並沒有辦法精確知道手勢放開的時...
...顯示更多 (683 字)
Mosir 開發筆記14 篇文章
歡迎所有互動

講到圖片色差就剛好想到另一件事

Mosir 並沒有使用 imagemagick、libvips、ffmpeg/libav 或 handbrake。

雖然對處理過影像/影片的人來說可能會很詭異,但這幾個解決方案真的是肥到我沒有很想用,而且 dependency 也多到一個爆炸。

不選擇常用方案的後果是前前後後加起來大概有一整個人月的時候都消耗在多媒體處理上。雖然過程中也學了不少東西,但我不確定這是不是好做法。

不過相對而言的好處是,Mosir 的 media worker 是 stream-based 的,從 network source、buffer、processor、network target 可以一口氣跑完,沒有不必要的暫存檔。未來(我不確定有沒有機會)也可以支援直播或是 voice chat。

歡迎所有互動

關於多媒體檔案處理 pipeline 的一些事情

圖片/影片上傳是大多數服務都會需要的功能,但處理多媒體檔案的架構其實是蠻麻煩的事情。

架構一:收到檔案之後直接拿來用

如果伺服器在收到檔案之後直接拿來使用當然是最快也最輕鬆的。如果經營的是雲端硬碟那好像也沒什麼問題。但如果是要分享給他人的東西⋯⋯:

  1. 使用者上傳的檔案多半都很大:一般手機拍攝的照片大約在 2~4MB 不等,影片可以到 512MB~1GB。網路流量費用跟儲存費用都會相當可觀。
  2. 不同系統對格式的支援不同:分享圖片或影片的最大原因還是希望讓其他人看。可惜現在已經不是以前那個只有 jpeg 的時代了,A 拍攝的照片用 B 的手機不一定能看,...
...顯示更多 (526 字)
歡迎所有互動

UI 狀態變更的動畫

Mosir 某些狀態變更的動畫是有被刻意放慢的。

例如正在儲存草稿...。儲存動作本身大約需要 0.1ms(千分之 0.1 秒),動畫被放慢到 1.5 秒。

這只是為了讓功能看起來有在動作,實際上對功能本身沒有什麼影響。

歡迎所有互動

照片裁切的數學

前幾天上線的照片裁切功能其實隱藏了蠻多幾何學。

不花時間寫下來我怕會忘