跳轉到

Planner Studio 座標系與縮換算邏輯 (Coordinate Transformation)

本文件說明 Planner Studio 縮放引擎(planner_zoom.js)如何處理螢幕座標(Client Y)、視圖內座標(Layout Y)與時間點(Minute)之間的轉換,這套邏輯確保了縮放時「滑鼠指向的時間點」能維持在螢幕上的相同位置。

1. 座標系定義

  • Client Y (clientY): 相對於瀏覽器視窗頂部的像素座標。
  • Layout Y (pointerLayoutY): 在滾動容器(Scroller)內部的視覺相對座標,已校正 CSS Transform Scale。
  • Content Y (contentY / targetContentY): 整個可滾動畫板的絕對高度座標(包含被捲上去的部分)。
  • Minute: 自當天 00:00 開始計算的分鐘數(0 到 1440)。

2. 核心換算公式

A. 關鍵指標獲取 (getSlotMetrics)

縮放引擎會先抓取第一個時間插槽(Slot)的 DOM 狀態: - safeScaleY: 容器的累積縮放比例。 - ppm (Pixels Per Minute): 每分鐘佔用的像素高度。 - firstMinute: 第一個插槽代表的分鐘數。 - firstYLayout: 第一個插槽相對於容器頂部的 Layout Y 座標。

B. 螢幕座標轉分鐘 (getMinuteAtClientY)

當滑鼠滾動時,我們需要知道滑鼠目前指在「哪一分鐘」: 1. 轉換為佈局座標: pointerLayoutY = (clientY - scrollerRect.top) / safeScaleY 2. 加上捲軸偏移: contentY = scroller.scrollTop + pointerLayoutY 3. 根據 PPM 換算出分鐘: minute = firstMinute + ((contentY - firstYLayout) / ppm)

C. 分鐘轉捲軸位置 (scrollMinuteToClientY)

縮放比例改變後,我們必須重新調整 scrollTop,讓目標 minute 仍出現在該 clientY: 1. 計算目標絕對高度: targetContentY = firstYLayout + ((minute - firstMinute) * ppm) 2. 推導目標捲軸位置: targetScrollTop = targetContentY - pointerLayoutY 3. 套用捲軸更新: scroller.scrollTop = targetScrollTop (或進行平滑插值)

3. 縮放動畫 (stepZoomFrame)

縮放過程使用 requestAnimationFrame 進行線性插值(Lerp)與緩動(Easing): - 每一幀更新 currentPxPerMinute。 - 調用 FullCalendar.updateSize() 同步 DOM 佈局。 - 立即調用 scrollMinuteToClientY 補償因高度變化導致的視覺偏移。

4. FullCalendar 整合與渲染指令

由於 FullCalendar 的佈局系統較為複雜,縮放引擎需精準並時地調用以下指令以確保視覺同步:

A. 動態屬性更新

  • setOption('contentHeight', height): 直接修改日曆畫板的總高度。在縮放時,我們計算 24 * 60 * currentPxPerMinute 並即時寫入,這會觸發 FullCalendar 重新計算所有事件的 Top/Height。
  • updateSize(): 強制 FullCalendar 重新讀取容器尺寸並重新佈局。這是縮放補償(Scroll Jump Fix)前的關鍵步驟,否則 scroller.scrollHeight 可能尚未更新。
  • batchRendering(fn): 用於大量更新事件(如搜尋或篩選)時,將多次 DOM 變更合併為一次渲染,避免畫面閃爍。

B. CSS 變數控制 (Theming)

為了讓 FullCalendar 內部元件(如 Slot、Event、Axis)與縮放級別連動,我們在 .fc 根節點注入了多個自定義變數:

變數名 作用對象 換算邏輯舉例
--slot-height .fc-timegrid-slot ppm * 15 (每 15 分鐘高度)
--event-font-size 事件標題文字大小 根據縮放比例在 9px ~ 18px 之間線性插值
--axis-font-size 時間軸文字大小 根據縮放比例在 10px ~ 15px 之間線性插值
--event-pad-y/x 事件內部填充 (Padding) 隨縮放級別動態調整,確保小比例時不會留白過多

這些變數在 planner.css 中被引用(例如 height: var(--slot-height) !important;),達成「JS 計算、CSS 渲染」的解耦設計。

5. 特殊處理:吸附 (Snapping)

在低縮放倍率(currentPxPerMinute < 3.5)時,錨點分鐘數會自動吸附至最近的 15 分鐘(Math.round(rawMin / 15) * 15),以提升視覺穩定感。