feat: add time ruler with tick marks above timeline

Display adaptive tick marks and time labels above the timeline to indicate
intervals. Tick spacing automatically adjusts based on zoom level:
- < 10 px/sec: 30s intervals
- < 20 px/sec: 10s intervals
- < 40 px/sec: 5s intervals
- < 80 px/sec: 1s intervals
- >= 80 px/sec: 0.5s intervals

Labels show minutes (m), seconds (s), or deciseconds (·) depending on interval.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-06-30 14:14:48 -04:00
parent 52c700b843
commit d93b92f4a5
2 changed files with 41 additions and 0 deletions

View file

@ -320,6 +320,10 @@ body { overflow: hidden; background: var(--desktop); background-size: 2px 2px;
html.dark .timeline-head {
background: rgba(0, 0, 0, .58);
}
.time-ruler {
user-select: none;
pointer-events: none;
}
.timeline-body-scroll {
flex: 1; min-height: 0; display: flex; align-items: flex-start;
overflow-y: auto; overflow-x: hidden;

View file

@ -303,6 +303,34 @@
(rf/dispatch [::events/draft-click-seg (:mark seg) cf])))}
(str (clip-label scene segs (:mark seg)) " " cf "f")])]))
;; --- timeline ruler -------------------------------------------------------
(defn- time-ruler [len fps zoom]
"Generate tick marks and labels for the timeline ruler.
Returns a vector of {:frame :px :label} with major/minor ticks."
(let [px-per-sec (* fps zoom)
;; choose tick interval in frames based on zoom level
tick-frames (cond
(< px-per-sec 10) (* 30 fps) ; 30 sec intervals
(< px-per-sec 20) (* 10 fps) ; 10 sec intervals
(< px-per-sec 40) (* 5 fps) ; 5 sec intervals
(< px-per-sec 80) fps ; 1 sec intervals
:else (/ fps 2)) ; 0.5 sec intervals
sec-per-tick (/ tick-frames fps)]
(loop [frame 0 tick-num 0 ticks []]
(if (>= frame len)
ticks
(let [px (* (/ frame fps) zoom)
total-sec (* tick-num sec-per-tick)
label (cond
(>= sec-per-tick 10) (let [m (int (/ total-sec 60))
s (int (mod total-sec 60))]
(if (> s 0) (str m ":" (if (< s 10) "0" "") s) (str m "m")))
(>= sec-per-tick 1) (str (int total-sec) "s")
:else (str (js/Math.round (* total-sec 10)) "·"))]
(recur (+ frame tick-frames) (inc tick-num)
(conj ticks {:frame frame :px px :label label})))))))
;; --- timeline -------------------------------------------------------------
(defn- scrub! [content fps zoom ev]
@ -373,6 +401,15 @@
:on-click #(swap! tracks-open? not)}
(if @tracks-open? "<- HIDE" "->")]]
[:div.lanes-hscroll {:ref (fn [n] (reset! lanes-scroll n))}
[:div.time-ruler {:style {:height 24 :width width :position "relative" :border-bottom "1px solid var(--ink)"}}
(let [ticks (time-ruler len fps zoom)]
(for [{:keys [frame px label]} ticks]
^{:key frame}
[:div {:style {:position "absolute" :left (str px "px") :top 0 :height "100%"
:display "flex" :flex-direction "column" :align-items "center"}}
[:div {:style {:height 8 :width 1 :background "var(--ink)" :margin-top 6}}]
[:div {:style {:font-size 10 :margin-top 2 :white-space "nowrap" :color "var(--mute)"}}
label]]))]
[:div.ann-lanes {:style {:height lane-h :width width}
:on-mouse-down #(scrub! @content fps zoom %)}
;; annotation bars — solid for saved, dashed for the in-progress draft