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),以提升視覺穩定感。