Tabs: open any symbol, and the clock follows it
Double-clicking a symbol in the pool, or an instance's row in the timeline, opens it as a tab above the stage; the stage, the rows, the transport and new shapes follow the open tab. Each tab gets a clock as long as it is: its own mixed tracks, the document's audio for the symbol it opens on, or silence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
6b41c6db94
commit
c81f91c442
8 changed files with 154 additions and 7 deletions
|
|
@ -165,3 +165,23 @@
|
|||
[document sid fallback-url store]
|
||||
(-> (buffer! document sid store)
|
||||
(.then (fn [buffer] (if buffer (wav-url buffer) fallback-url)))))
|
||||
|
||||
(defn clock!
|
||||
"Promise of the URL the transport should play while symbol `sid` is open.
|
||||
|
||||
The frame is derived from the audio element and from nothing else, so every
|
||||
open symbol needs a sound exactly as long as it is. In order: its own placed
|
||||
tracks, mixed; the document's audio file, for the symbol the document opens on
|
||||
and only that one; and otherwise SILENCE of the symbol's length — a ten-frame
|
||||
symbol played against the whole take's soundtrack would run ten frames and then
|
||||
keep the clock going for minutes."
|
||||
[document sid fallback-url store]
|
||||
(-> (buffer! document sid store)
|
||||
(.then (fn [buffer]
|
||||
(cond
|
||||
buffer (wav-url buffer)
|
||||
(and fallback-url (= sid (clip/opens-on document))) fallback-url
|
||||
:else (let [rate 44100
|
||||
n (max 1 (js/Math.ceil (* rate (/ (clip/frames document sid)
|
||||
(:fps document)))))]
|
||||
(wav-url (.createBuffer (js/OfflineAudioContext. 1 1 rate) 1 n rate))))))))
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@
|
|||
traversals a second, which is the one genuinely expensive thing you can do to
|
||||
a small app-db. If global interceptors are added later they are added to a
|
||||
chain these events are excluded from, not to `reg-global-interceptor`."
|
||||
(:require [arthur.clock :as clock]
|
||||
(:require [arthur.audio.mix :as mix]
|
||||
[arthur.clock :as clock]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.footage.store :as footage]
|
||||
[re-frame.core :as rf]))
|
||||
|
|
@ -133,3 +134,64 @@
|
|||
{:db db
|
||||
::pause! nil
|
||||
::seek! [(fps db) (frames db) 0]})))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; tabs
|
||||
;;
|
||||
;; A tab is an open symbol. `:open` is the one on screen and `:tabs` the ones
|
||||
;; beside it, in the order they were opened. Switching is `:open` and a clock:
|
||||
;; the stage, the timeline rows, the transport's length and a new shape's home
|
||||
;; all follow `:open` through their subscriptions without being told.
|
||||
|
||||
(defonce ^:private clock-url (atom nil))
|
||||
|
||||
(rf/reg-fx
|
||||
::clock!
|
||||
(fn [{:keys [id sid]}]
|
||||
(let [{:keys [clip audio store]} (footage/entry id)]
|
||||
(-> (mix/clock! clip sid audio store)
|
||||
(.then #(rf/dispatch [::clock-ready id sid %]))
|
||||
(.catch #(js/console.error %))))))
|
||||
|
||||
(rf/reg-event-fx
|
||||
::clock-ready
|
||||
(fn [{:keys [db]} [_ id sid url]]
|
||||
(if (and (= id (:clip/current db)) (= sid (get-in db [:ui :open])))
|
||||
(do
|
||||
;; A blob URL made for the last tab is released when the next one lands,
|
||||
;; and never the document's own file.
|
||||
(when-let [old @clock-url]
|
||||
(when (not= old url) (js/URL.revokeObjectURL old)))
|
||||
(reset! clock-url (when (and (not= url (:audio (footage/entry id)))
|
||||
(.startsWith url "blob:"))
|
||||
url))
|
||||
{:db (assoc-in db [:clip :audio] url)})
|
||||
{})))
|
||||
|
||||
(rf/reg-event-fx
|
||||
::open-symbol
|
||||
(fn [{:keys [db]} [_ sid]]
|
||||
(let [clip (:clip (footage/entry (:clip/current db)))]
|
||||
(if (or (nil? (clip/symbol clip sid)) (= sid (get-in db [:ui :open])))
|
||||
{}
|
||||
{:db (-> db
|
||||
(update-in [:ui :tabs] #(if (some #{sid} %) % (conj (vec %) sid)))
|
||||
(assoc-in [:ui :open] sid)
|
||||
(assoc-in [:playback :frame] 0)
|
||||
(assoc-in [:playback :playing?] false))
|
||||
::pause! nil
|
||||
::clock! {:id (:clip/current db) :sid sid}}))))
|
||||
|
||||
(rf/reg-event-fx
|
||||
::close-tab
|
||||
(fn [{:keys [db]} [_ sid]]
|
||||
;; The last tab cannot be closed: something is always on screen, because the
|
||||
;; transport and the stage have no meaning without a symbol.
|
||||
(let [tabs (get-in db [:ui :tabs])
|
||||
left (vec (remove #{sid} tabs))]
|
||||
(cond
|
||||
(empty? left) {}
|
||||
(not= sid (get-in db [:ui :open])) {:db (assoc-in db [:ui :tabs] left)}
|
||||
:else (let [i (.indexOf tabs sid)]
|
||||
{:db (assoc-in db [:ui :tabs] left)
|
||||
:dispatch [::open-symbol (get left (min i (dec (count left))))]})))))
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
(rf/reg-sub ::tone (fn [db _] (get-in db [:ui :tone])))
|
||||
(rf/reg-sub ::tool (fn [db _] (get-in db [:ui :tool])))
|
||||
(rf/reg-sub ::draft (fn [db _] (get-in db [:ui :draft])))
|
||||
(rf/reg-sub ::tabs (fn [db _] (get-in db [:ui :tabs])))
|
||||
(rf/reg-sub ::expanded (fn [db _] (get-in db [:ui :expanded])))
|
||||
(rf/reg-sub ::knobs (fn [db _] (get-in db [:ui :knobs])))
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,9 @@
|
|||
button. The uploaded bytes, the extraction job and the decoded footage stay
|
||||
separate records on the server, so dropping the same file twice does not decode
|
||||
it twice."
|
||||
(:require [arthur.events.footage :as footage]
|
||||
(:require [arthur.domain.clip :as clip]
|
||||
[arthur.events.footage :as footage]
|
||||
[arthur.events.playback :as pb]
|
||||
[arthur.events.ui :as ui]
|
||||
[arthur.subs.playback :as playback]
|
||||
[arthur.subs.render :as render]
|
||||
|
|
@ -62,7 +64,7 @@
|
|||
(for [sid library
|
||||
:let [sym (get-in clip [:symbols sid])]]
|
||||
^{:key sid}
|
||||
[item {:label (name sid)
|
||||
[item {:label (clip/symbol-name clip sid)
|
||||
:sub (str (:frames sym) "f · " (count (:nodes sym)) " nodes")
|
||||
:on? (= selection [:symbol sid])
|
||||
:draggable true
|
||||
|
|
@ -71,9 +73,10 @@
|
|||
(.setData (.-dataTransfer event) "text/plain"
|
||||
(str "symbol:" (subs (str sid) 1)))
|
||||
(set! (.. event -dataTransfer -effectAllowed) "copy"))
|
||||
:on-click #(rf/dispatch [::ui/select [:symbol sid]])}])))
|
||||
:on-click #(rf/dispatch [::ui/select [:symbol sid]])
|
||||
:on-double-click #(rf/dispatch [::pb/open-symbol sid])}])))
|
||||
(when (seq library)
|
||||
[:div.dim "drag onto the stage to place"]))))
|
||||
[:div.dim "double-click to open · drag to place"]))))
|
||||
|
||||
(defn- footage []
|
||||
(let [{:keys [loading? available chosen]} @(rf/subscribe [::playback/footage])]
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
[arthur.ui.params :as params]
|
||||
[arthur.ui.pool :as pool]
|
||||
[arthur.ui.stage :as stage]
|
||||
[arthur.ui.tabs :as tabs]
|
||||
[arthur.ui.timeline :as timeline]
|
||||
[arthur.ui.topbar :as topbar]
|
||||
[re-frame.core :as rf]))
|
||||
|
|
@ -33,6 +34,7 @@
|
|||
[topbar/view]
|
||||
[pool/view]
|
||||
[:section.view
|
||||
[tabs/view]
|
||||
[palette/bar]
|
||||
[stage/view]]
|
||||
[params/view]
|
||||
|
|
|
|||
30
frontend/src/arthur/ui/tabs.cljs
Normal file
30
frontend/src/arthur/ui/tabs.cljs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
(ns arthur.ui.tabs
|
||||
"The open symbols, one tab each, above the stage.
|
||||
|
||||
A tab is a symbol and nothing else — there is no document tab and no special
|
||||
first one, because no symbol is special. Double-clicking a symbol in the pool,
|
||||
or an instance's row in the timeline, opens it here."
|
||||
(:require [arthur.domain.clip :as clip]
|
||||
[arthur.events.playback :as pb]
|
||||
[arthur.subs.render :as render]
|
||||
[arthur.subs.ui :as sub]
|
||||
[re-frame.core :as rf]))
|
||||
|
||||
(defn view []
|
||||
(let [document @(rf/subscribe [::render/clip])
|
||||
tabs @(rf/subscribe [::sub/tabs])
|
||||
open @(rf/subscribe [::render/open])]
|
||||
[:div.tabs
|
||||
(doall
|
||||
(for [sid tabs :when (clip/symbol document sid)]
|
||||
^{:key (str sid)}
|
||||
[:div {:class (str "tab" (when (= sid open) " on"))
|
||||
:title (str sid)
|
||||
:on-click #(rf/dispatch [::pb/open-symbol sid])}
|
||||
[:span.name (clip/symbol-name document sid)]
|
||||
(when (< 1 (count tabs))
|
||||
[:button.close {:title "close this tab"
|
||||
:on-click (fn [^js e]
|
||||
(.stopPropagation e)
|
||||
(rf/dispatch [::pb/close-tab sid]))}
|
||||
"×"])]))]))
|
||||
|
|
@ -119,6 +119,7 @@
|
|||
:label (node-label id n)
|
||||
:kind :node
|
||||
:node-kind (:kind n)
|
||||
:of (:of n)
|
||||
:select [:node sid id rpath]
|
||||
:expandable? true
|
||||
:expanded? open?
|
||||
|
|
@ -194,12 +195,14 @@
|
|||
(str (.toFixed (or fps 0) 1) " paint/s"
|
||||
(when (and drop (pos? drop)) (str " · " (.toFixed drop 2) " f/paint")))]]))
|
||||
|
||||
(defn- label-cell [{:keys [path depth label kind node-kind select expandable? expanded?]}
|
||||
(defn- label-cell [{:keys [path depth label kind node-kind select expandable? expanded? of]}
|
||||
selection]
|
||||
[:div {:class (str "tl-label" (when (and select (= select selection)) " on"))
|
||||
:style {:padding-left (str (+ 4 (* 11 depth)) "px")}
|
||||
:title label
|
||||
:on-click #(when select (rf/dispatch [::ui/select select]))}
|
||||
:on-click #(when select (rf/dispatch [::ui/select select]))
|
||||
;; An instance's row opens the symbol it places, as a tab.
|
||||
:on-double-click #(when of (rf/dispatch [::pb/open-symbol of]))}
|
||||
[:button.tl-twist
|
||||
{:disabled (not expandable?)
|
||||
:on-click (fn [^js e]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue