From d93b92f4a564bcdc31159272da11e1a4f86467ba Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 30 Jun 2026 14:14:48 -0400 Subject: [PATCH] feat: add time ruler with tick marks above timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- tl/resources/public/css/app.css | 4 ++++ tl/src/tl/views.cljs | 37 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/tl/resources/public/css/app.css b/tl/resources/public/css/app.css index 1817a2d..f4060dd 100644 --- a/tl/resources/public/css/app.css +++ b/tl/resources/public/css/app.css @@ -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; diff --git a/tl/src/tl/views.cljs b/tl/src/tl/views.cljs index 380a7f8..716c2d7 100644 --- a/tl/src/tl/views.cljs +++ b/tl/src/tl/views.cljs @@ -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