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:
parent
52c700b843
commit
d93b92f4a5
2 changed files with 41 additions and 0 deletions
|
|
@ -320,6 +320,10 @@ body { overflow: hidden; background: var(--desktop); background-size: 2px 2px;
|
||||||
html.dark .timeline-head {
|
html.dark .timeline-head {
|
||||||
background: rgba(0, 0, 0, .58);
|
background: rgba(0, 0, 0, .58);
|
||||||
}
|
}
|
||||||
|
.time-ruler {
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
.timeline-body-scroll {
|
.timeline-body-scroll {
|
||||||
flex: 1; min-height: 0; display: flex; align-items: flex-start;
|
flex: 1; min-height: 0; display: flex; align-items: flex-start;
|
||||||
overflow-y: auto; overflow-x: hidden;
|
overflow-y: auto; overflow-x: hidden;
|
||||||
|
|
|
||||||
|
|
@ -303,6 +303,34 @@
|
||||||
(rf/dispatch [::events/draft-click-seg (:mark seg) cf])))}
|
(rf/dispatch [::events/draft-click-seg (:mark seg) cf])))}
|
||||||
(str (clip-label scene segs (:mark seg)) " " cf "f")])]))
|
(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 -------------------------------------------------------------
|
;; --- timeline -------------------------------------------------------------
|
||||||
|
|
||||||
(defn- scrub! [content fps zoom ev]
|
(defn- scrub! [content fps zoom ev]
|
||||||
|
|
@ -373,6 +401,15 @@
|
||||||
:on-click #(swap! tracks-open? not)}
|
:on-click #(swap! tracks-open? not)}
|
||||||
(if @tracks-open? "<- HIDE" "->")]]
|
(if @tracks-open? "<- HIDE" "->")]]
|
||||||
[:div.lanes-hscroll {:ref (fn [n] (reset! lanes-scroll n))}
|
[: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}
|
[:div.ann-lanes {:style {:height lane-h :width width}
|
||||||
:on-mouse-down #(scrub! @content fps zoom %)}
|
:on-mouse-down #(scrub! @content fps zoom %)}
|
||||||
;; annotation bars — solid for saved, dashed for the in-progress draft
|
;; annotation bars — solid for saved, dashed for the in-progress draft
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue