Compare commits
No commits in common. "78fb120edd711c7f13e965573b916544324c25ad" and "eca1a96b82caa2171faa26e40ef70290bf101ffc" have entirely different histories.
78fb120edd
...
eca1a96b82
17 changed files with 193 additions and 459 deletions
|
|
@ -1 +0,0 @@
|
|||
Subproject commit f4dd04764204506fc275180364d0366d693036f4
|
||||
|
|
@ -57,9 +57,8 @@
|
|||
THE EDITOR'S, NOT THE DOCUMENT'S. Showing a reference is a way of looking at
|
||||
the stage, like solo and zoom: not an undo step, not sent to collaborators,
|
||||
and it cannot reach an export. A layer is shown unless it is in `:hidden`, so
|
||||
footage brought in shows without being found and switched on first — once
|
||||
tracing itself is on, which it is not until asked for."
|
||||
{:on? false :opacity 0.5 :hidden #{}})
|
||||
footage brought in shows without being found and switched on first."
|
||||
{:on? true :opacity 0.5 :hidden #{}})
|
||||
|
||||
(def default
|
||||
{;; --- the document ---
|
||||
|
|
|
|||
|
|
@ -25,14 +25,13 @@
|
|||
another clip is `arthur.domain.bring`. This namespace is the document and the
|
||||
operations that only need the document.
|
||||
|
||||
NO SYMBOL IS SPECIAL. There is no reserved root id: which symbol is on screen
|
||||
is the editor's state, not the document's, and every function here that needs
|
||||
a symbol is told which. A new document has one symbol called `:main` because
|
||||
it has to be called something, and that is all the name means — it can be
|
||||
renamed or placed inside another symbol like any of them. What the document
|
||||
does say is `:root`, which symbol it opens on: a pointer, not a kind of
|
||||
symbol, the way a Flash file names its scene. See `opens-on` for why that
|
||||
cannot be worked out instead.
|
||||
NO SYMBOL IS SPECIAL. There is no reserved root and no pointer to one: which
|
||||
symbol is on screen is the editor's state, not the document's, and every
|
||||
function here that needs a symbol is told which. A new document has one symbol
|
||||
called `:main` because it has to be called something, and that is all the name
|
||||
means — it can be renamed, placed inside another symbol or deleted like any of
|
||||
them. `unplaced` answers the question a reserved root used to: which symbols
|
||||
nothing else places, and so which ones a person opening the document wants.
|
||||
|
||||
:fps is the output grid. A symbol's optional :fps names the native grid its
|
||||
frames were authored or measured on; absent means the document's grid."
|
||||
|
|
@ -53,7 +52,7 @@
|
|||
layer must not be able to have. Add the field here and to `leaf/leaves` and
|
||||
`leaf/clip` in the same commit."
|
||||
#{:name :fps :analyses :subjects :features :groups :width :height :symbols
|
||||
:palettes :default-palette :root})
|
||||
:palettes :default-palette})
|
||||
|
||||
(defn symbol
|
||||
"One of the clip's symbols, by id."
|
||||
|
|
@ -202,39 +201,18 @@
|
|||
(let [placed (into #{} (mapcat #(places clip %)) (keys (:symbols clip)))]
|
||||
(vec (sort-by str (remove placed (keys (:symbols clip)))))))
|
||||
|
||||
(defn- longest-unplaced
|
||||
"The longest symbol nothing else places, ties broken by id, and never a
|
||||
tracing symbol: that is footage nobody has placed yet, as long as its take."
|
||||
(defn opens-on
|
||||
"The symbol a document opens on: the longest one nothing else places, ties
|
||||
broken by id. The symbol that contains everything else is the longest of the
|
||||
unplaced ones in every document made so far, and a reserved name is what this
|
||||
replaces.
|
||||
|
||||
Never a tracing symbol: it is footage nobody has placed yet, and as long as
|
||||
the take it came from, so it would otherwise win."
|
||||
[clip]
|
||||
(first (sort-by (fn [sid] [(- (or (frames clip sid) 0)) (str sid)])
|
||||
(remove #(trace? (symbol clip %)) (unplaced clip)))))
|
||||
|
||||
(defn opens-on
|
||||
"The symbol a document opens on: its `:root`.
|
||||
|
||||
IT IS STORED, NOT WORKED OUT. It used to be the longest unplaced symbol, on
|
||||
the theory that the one containing everything else is always that. A take
|
||||
disproves it: imported footage is a symbol a thousand frames long, and the
|
||||
moment its instance is deleted, or the drop lands somewhere other than the
|
||||
root, nothing places it and it outranks a 120-frame `:main`. The document then
|
||||
opened on the take, and `set-root-fps` rewrote the take's rate to the
|
||||
project's on the way in.
|
||||
|
||||
A document with no `:root` — a demo, a fixture — still gets the old answer."
|
||||
[clip]
|
||||
(let [root (:root clip)]
|
||||
(if (contains? (:symbols clip) root) root (longest-unplaced clip))))
|
||||
|
||||
(defn pin-root
|
||||
"Give a document saved before `:root` existed the root it was made with.
|
||||
Every such document started as `blank`, whose root is `:main`, and ids never
|
||||
change, so `:main` is the answer wherever it survives; the old rule is only
|
||||
for documents that never had one."
|
||||
[clip]
|
||||
(cond-> clip
|
||||
(not (:root clip))
|
||||
(assoc :root (if (contains? (:symbols clip) :main) :main (longest-unplaced clip)))))
|
||||
|
||||
(defn set-root-fps
|
||||
"Set the document/output rate and the root symbol's editing rate together.
|
||||
|
||||
|
|
@ -272,7 +250,6 @@
|
|||
:width 320 :height 200
|
||||
:palettes {pal/default-id pal/default-palette}
|
||||
:default-palette pal/default-id
|
||||
:root :main
|
||||
;; No native fps yet: an untouched canvas follows the project grid. Imported
|
||||
;; and generated symbols carry their own rate explicitly.
|
||||
:symbols {:main {:id :main :frames blank-frames :nodes {}}}})
|
||||
|
|
@ -374,58 +351,6 @@
|
|||
(active-palette [this]
|
||||
"The palette selected by this resolver's most recently resolved frame."))
|
||||
|
||||
(defn- channel-value [store selection frame]
|
||||
(cond
|
||||
(nil? selection) nil
|
||||
(and (map? selection) (contains? selection :animated?))
|
||||
(ch/value-at selection frame store)
|
||||
:else selection))
|
||||
|
||||
(defn palette-at
|
||||
"The palette symbol `owner` draws in at `frame`, as the stage's root does: a
|
||||
palette id, or `{:from :to :t}` while its palette lane blends from one to the
|
||||
next. `inherited` is what an unset palette falls back to, then `default`."
|
||||
[clip store default owner frame inherited]
|
||||
;; A palette track has meaningful uncovered time. Ordinary held
|
||||
;; channels clamp to their first key before it, but doing that
|
||||
;; here would erase the gap before the first palette segment.
|
||||
(let [channel-value (partial channel-value store)
|
||||
fallback (or (channel-value (:palette owner) frame)
|
||||
inherited default)
|
||||
materialize (fn [choice]
|
||||
(cond
|
||||
(= pal/inherit choice) fallback
|
||||
(map? choice) (-> choice
|
||||
(update :from #(if (= pal/inherit %) fallback %))
|
||||
(update :to #(if (= pal/inherit %) fallback %)))
|
||||
:else choice))
|
||||
track (:palette-channel owner)
|
||||
track-value (if-let [ks (:keys track)]
|
||||
(some->> (keys ks)
|
||||
(filter #(<= % frame))
|
||||
sort last
|
||||
(get ks))
|
||||
(channel-value track frame))]
|
||||
(or (when-let [track-sid (and (keyword? (:palette-track owner))
|
||||
(:palette-track owner))]
|
||||
(let [track-symbol (symbol clip track-sid)
|
||||
palette-clip (first
|
||||
(filter (fn [n]
|
||||
(let [[a b] (node/placed-span n)]
|
||||
(and a (<= a frame) (< frame b))))
|
||||
(symbol/children (:nodes track-symbol))))
|
||||
palette-symbol (some-> palette-clip node/source
|
||||
(#(symbol clip %)))
|
||||
fallback (when (= :palette (:type palette-symbol))
|
||||
(:palette-ref palette-symbol))
|
||||
choice (get-in palette-clip [:channels [:palette]])
|
||||
start (some-> palette-clip node/placed-span first)]
|
||||
(or (when (and choice start)
|
||||
(materialize (channel-value choice (- frame start))))
|
||||
fallback)))
|
||||
track-value
|
||||
fallback)))
|
||||
|
||||
(defn resolver
|
||||
"Resolve an output frame, selecting native content at each symbol boundary.
|
||||
Every instance owns its cursors and buffers. The IResolver queries return
|
||||
|
|
@ -433,8 +358,51 @@
|
|||
[clip sid store palette opts]
|
||||
(let [context? (and (map? palette) (:palettes palette) (:offsets palette))
|
||||
active-palette-state (atom (:default palette))]
|
||||
(letfn [(root-selection-at [owner frame inherited]
|
||||
(palette-at clip store (:default palette) owner frame inherited))
|
||||
(letfn [(channel-value [selection frame]
|
||||
(cond
|
||||
(nil? selection) nil
|
||||
(and (map? selection) (contains? selection :animated?))
|
||||
(ch/value-at selection frame store)
|
||||
:else selection))
|
||||
(root-selection-at [owner frame inherited]
|
||||
;; A palette track has meaningful uncovered time. Ordinary held
|
||||
;; channels clamp to their first key before it, but doing that
|
||||
;; here would erase the gap before the first palette segment.
|
||||
(let [fallback (or (channel-value (:palette owner) frame)
|
||||
inherited (:default palette))
|
||||
materialize (fn [choice]
|
||||
(cond
|
||||
(= pal/inherit choice) fallback
|
||||
(map? choice) (-> choice
|
||||
(update :from #(if (= pal/inherit %) fallback %))
|
||||
(update :to #(if (= pal/inherit %) fallback %)))
|
||||
:else choice))
|
||||
track (:palette-channel owner)
|
||||
track-value (if-let [ks (:keys track)]
|
||||
(some->> (keys ks)
|
||||
(filter #(<= % frame))
|
||||
sort last
|
||||
(get ks))
|
||||
(channel-value track frame))]
|
||||
(or (when-let [track-sid (and (keyword? (:palette-track owner))
|
||||
(:palette-track owner))]
|
||||
(let [track-symbol (symbol clip track-sid)
|
||||
palette-clip (first
|
||||
(filter (fn [n]
|
||||
(let [[a b] (node/placed-span n)]
|
||||
(and a (<= a frame) (< frame b))))
|
||||
(symbol/children (:nodes track-symbol))))
|
||||
palette-symbol (some-> palette-clip node/source
|
||||
(#(symbol clip %)))
|
||||
fallback (when (= :palette (:type palette-symbol))
|
||||
(:palette-ref palette-symbol))
|
||||
choice (get-in palette-clip [:channels [:palette]])
|
||||
start (some-> palette-clip node/placed-span first)]
|
||||
(or (when (and choice start)
|
||||
(materialize (channel-value choice (- frame start))))
|
||||
fallback)))
|
||||
track-value
|
||||
fallback)))
|
||||
(selection-at [owner frame inherited]
|
||||
(let [selection (:palette owner)
|
||||
chosen (cond
|
||||
|
|
@ -746,9 +714,6 @@
|
|||
(when (and (contains? clip :default-palette) (map? (:palettes clip))
|
||||
(not (contains? (:palettes clip) (:default-palette clip))))
|
||||
[":default-palette must name a project palette"])
|
||||
(when (and (contains? clip :root) (map? (:symbols clip))
|
||||
(not (contains? (:symbols clip) (:root clip))))
|
||||
[(str ":root names missing symbol " (pr-str (:root clip)))])
|
||||
(for [[id p] (:palettes clip)
|
||||
:when (or (not= id (:id p)) (not (pal/valid-palette? p)))]
|
||||
(str "palette " (pr-str id) " is invalid or has a different :id"))
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@
|
|||
clip/<cid>/stage width, height
|
||||
clip/<cid>/palette/<pid> a named indexed palette asset
|
||||
clip/<cid>/palette-default the project fallback palette id
|
||||
clip/<cid>/root the symbol the document opens on
|
||||
clip/<cid>/source the analysis record this came out of
|
||||
clip/<cid>/subject/<subj> a tracked subject and its params
|
||||
clip/<cid>/feature/<fid> one feature: area, nodes, params
|
||||
|
|
@ -139,7 +138,6 @@
|
|||
(some-leaf (at "timing") (select-keys clip [:fps]))
|
||||
(some-leaf (at "stage") (select-keys clip [:width :height]))
|
||||
(some-leaf (at "palette-default") (select-keys clip [:default-palette]))
|
||||
(some-leaf (at "root") (select-keys clip [:root]))
|
||||
(some-leaf (at "analyses") (:analyses clip))
|
||||
(concat
|
||||
(for [[id v] (:subjects clip)] {(at "subject" (segment id)) v})
|
||||
|
|
@ -200,7 +198,6 @@
|
|||
"stage" (merge acc v)
|
||||
"analyses" (assoc acc :analyses v)
|
||||
"palette-default" (merge acc v)
|
||||
"root" (merge acc v)
|
||||
"palette" (assoc-in acc [:palettes (unsegment a)] v)
|
||||
"subject" (assoc-in acc [:subjects (unsegment a)] v)
|
||||
"feature" (assoc-in acc [:features (unsegment a)] v)
|
||||
|
|
@ -242,7 +239,7 @@
|
|||
false)
|
||||
(case (count p)
|
||||
;; The clip's own facts carry no id.
|
||||
3 (#{"name" "timing" "stage" "analyses" "palette-default" "root"} (nth p 2))
|
||||
3 (#{"name" "timing" "stage" "analyses" "palette-default"} (nth p 2))
|
||||
4 (#{"subject" "feature" "group" "palette"} (nth p 2))
|
||||
false))))]
|
||||
(vec
|
||||
|
|
|
|||
|
|
@ -113,9 +113,7 @@
|
|||
;; also normalizes documents saved by the earlier model,
|
||||
;; where changing project FPS left the root on its old
|
||||
;; editing grid.
|
||||
built (-> (:clip loaded)
|
||||
clip/pin-root
|
||||
(clip/set-root-fps (:fps (:clip loaded))))]
|
||||
built (clip/set-root-fps (:clip loaded) (:fps (:clip loaded)))]
|
||||
(let [entry (merge (select-keys built [:fps :width :height])
|
||||
{:label (str (or (.-name clip-json) cid) " (saved)")
|
||||
:cid cid
|
||||
|
|
|
|||
|
|
@ -841,7 +841,6 @@
|
|||
:width (first stage) :height (second stage)
|
||||
:palettes {pal/default-id pal/default-palette}
|
||||
:default-palette pal/default-id
|
||||
:root :main
|
||||
:subjects (into {} (map (fn [[id _]]
|
||||
[id {:id id :params {}
|
||||
:analysis (:id analysis)
|
||||
|
|
|
|||
|
|
@ -136,25 +136,6 @@
|
|||
:<- [::palette]
|
||||
(fn [compiled _] (:ramp compiled)))
|
||||
|
||||
(rf/reg-sub
|
||||
::palettes-shown
|
||||
:<- [::clip]
|
||||
:<- [::open]
|
||||
:<- [::store]
|
||||
:<- [::open-frame]
|
||||
:<- [::palette]
|
||||
;; The palettes the stage is drawing in at the playhead: the open symbol's own,
|
||||
;; BOTH ends while its palette lane blends from one to the next, and any an
|
||||
;; instance in it is set to. The palette strip asks it whether the palette it
|
||||
;; is editing is one of these.
|
||||
(fn [[document sid store frame compiled] _]
|
||||
(when-let [sym (and document (clip/symbol document sid))]
|
||||
(let [at #(clip/palette-at document store (:default compiled) % (or frame 0) nil)
|
||||
ends #(if (map? %) [(:from %) (:to %)] [%])]
|
||||
(into (set (ends (at sym)))
|
||||
(comp (filter :palette) (mapcat (comp ends at)))
|
||||
(vals (:nodes sym)))))))
|
||||
|
||||
(rf/reg-sub
|
||||
::store
|
||||
:<- [::clip-id]
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@
|
|||
half of this (see `@media` in app.css), and a drawer that is shut should not be
|
||||
holding video thumbnails live."
|
||||
(:require [arthur.domain.onion :as onion]
|
||||
[arthur.ui.menu :as menu]
|
||||
[re-frame.core :as rf]))
|
||||
|
||||
(def ^:private narrow?
|
||||
|
|
@ -147,9 +146,8 @@
|
|||
"− <readout> + for one zoomable surface. THE READOUT IS THE BUTTON BACK: it
|
||||
says what the zoom is, pressing it returns to normal, and it is accented while
|
||||
it has something to return from — so the quick way back is in the one place
|
||||
you already look to find out where you are. `more` joins the group: the
|
||||
stage's fit."
|
||||
[k label & more]
|
||||
you already look to find out where you are."
|
||||
[k label]
|
||||
(let [z @(rf/subscribe [::zoom k])
|
||||
[lo hi] (limits k)
|
||||
home (k normal)]
|
||||
|
|
@ -164,11 +162,10 @@
|
|||
(str (js/Math.round (* 100 z)) "%")]
|
||||
[:button {:disabled (>= z hi) :aria-label (str label " zoom in")
|
||||
:title (str "zoom " label " in")
|
||||
:on-click #(rf/dispatch [::set k (stepped k z false)])} "+"]
|
||||
(into [:<>] more)]))
|
||||
:on-click #(rf/dispatch [::set k (stepped k z false)])} "+"]]))
|
||||
|
||||
|
||||
(rf/reg-sub ::mat (fn [db _] (merge {:on? true :opacity 0.55} (get-in db [:ui :passepartout]))))
|
||||
(rf/reg-sub ::mat (fn [db _] (merge {:opacity 0.55} (get-in db [:ui :passepartout]))))
|
||||
(rf/reg-event-db ::mat (fn [db [_ k v]] (assoc-in db [:ui :passepartout k] v)))
|
||||
|
||||
(rf/reg-sub ::onion
|
||||
|
|
@ -178,42 +175,30 @@
|
|||
(fn [db [_ k v]]
|
||||
(assoc-in db [:ui :onion (:clip/current db) (get-in db [:ui :open]) k] v)))
|
||||
|
||||
(defn overlay
|
||||
"Something drawn over the stage that is not the picture: a switch, and its
|
||||
settings in a popover beside it. Tracing, onion and passepartout are all this
|
||||
one shape, so they read as three of a kind."
|
||||
[class label on? tip toggle! settings]
|
||||
[:div.group.overlay {:class class}
|
||||
[:button {:class (when on? "on") :aria-pressed (boolean on?) :title tip
|
||||
:on-click toggle!}
|
||||
label]
|
||||
[menu/popover {:label "▾" :title (str label " settings")} settings]])
|
||||
|
||||
(defn- slider [label lo hi v on-change]
|
||||
[:label label
|
||||
[:input {:type "range" :min lo :max hi :step 0.05 :value v
|
||||
:on-change #(on-change (js/parseFloat (.. % -target -value)))}]])
|
||||
|
||||
(defn onion-controls []
|
||||
(let [{:keys [on? before after opacity scope]} @(rf/subscribe [::onion])]
|
||||
[overlay "onion-controls" "onion" on?
|
||||
"Onion skin — neighboring cels of the selected lane"
|
||||
#(rf/dispatch [::onion :on? (not on?)])
|
||||
(fn [_]
|
||||
[:div.view-popout
|
||||
[:label "Scope"
|
||||
[:select {:value (name scope)
|
||||
:on-change #(rf/dispatch [::onion :scope (keyword (.. % -target -value))])}
|
||||
[:option {:value "selected"} "Selected lane"]
|
||||
[:option {:value "symbol"} "Whole symbol"]]]
|
||||
(for [[k label v] [[:before "Previous cels" before] [:after "Next cels" after]]]
|
||||
^{:key k} [:label label
|
||||
[:input {:type "number" :min 0 :max 5 :value v
|
||||
:on-change #(let [n (js/parseInt (.. % -target -value) 10)]
|
||||
(when-not (js/isNaN n)
|
||||
(rf/dispatch [::onion k (max 0 (min 5 n))])))}]])
|
||||
[slider "Opacity" 0.05 0.6 opacity #(rf/dispatch [::onion :opacity %])]
|
||||
[:small "Previous: red · Next: blue. Select a lane or one of its cels. Hidden during playback."]])]))
|
||||
[:div.group.onion-controls
|
||||
[:button {:class (when on? "on") :aria-pressed (boolean on?)
|
||||
:title "Onion skin — neighboring cels of the selected lane"
|
||||
:on-click #(rf/dispatch [::onion :on? (not on?)])} "onion"]
|
||||
[:details
|
||||
[:summary {:aria-label "Onion skin settings" :title "Onion skin settings"} "▾"]
|
||||
[:div.view-popout
|
||||
[:label "Scope"
|
||||
[:select {:value (name scope)
|
||||
:on-change #(rf/dispatch [::onion :scope (keyword (.. % -target -value))])}
|
||||
[:option {:value "selected"} "Selected lane"]
|
||||
[:option {:value "symbol"} "Whole symbol"]]]
|
||||
(for [[k label v] [[:before "Previous cels" before] [:after "Next cels" after]]]
|
||||
^{:key k} [:label label
|
||||
[:input {:type "number" :min 0 :max 5 :value v
|
||||
:on-change #(let [n (js/parseInt (.. % -target -value) 10)]
|
||||
(when-not (js/isNaN n)
|
||||
(rf/dispatch [::onion k (max 0 (min 5 n))])))}]])
|
||||
[:label "Opacity"
|
||||
[:input {:type "range" :min 0.05 :max 0.6 :step 0.05 :value opacity
|
||||
:on-change #(rf/dispatch [::onion :opacity (js/parseFloat (.. % -target -value))])}]]
|
||||
[:small "Previous: red · Next: blue. Select a lane or one of its cels. Hidden during playback."]]]]))
|
||||
|
||||
(defn fit-stage! []
|
||||
(when-let [area (.querySelector js/document ".stage-area")]
|
||||
|
|
@ -224,19 +209,14 @@
|
|||
(/ (- (.-clientHeight area) 32) h))])
|
||||
(js/requestAnimationFrame #(do (set! (.-scrollLeft area) 0) (set! (.-scrollTop area) 0)))))))
|
||||
|
||||
(defn stage-controls
|
||||
"The stage's own view, after the tracing switch: what else is drawn over it,
|
||||
then how big it is. Fit is in the zoom group because it IS a zoom."
|
||||
[]
|
||||
(let [{:keys [on? opacity]} @(rf/subscribe [::mat])]
|
||||
(defn stage-controls []
|
||||
(let [{:keys [opacity]} @(rf/subscribe [::mat])]
|
||||
[:<>
|
||||
[onion-controls]
|
||||
[overlay "passepartout-controls" "passepartout" on?
|
||||
"Darken the desk around the stage"
|
||||
#(rf/dispatch [::mat :on? (not on?)])
|
||||
(fn [_]
|
||||
[:div.view-popout
|
||||
[slider "Opacity" 0 1 opacity #(rf/dispatch [::mat :opacity %])]])]
|
||||
[:span.sep]
|
||||
[zoomer :stage "the stage"
|
||||
[:button {:on-click fit-stage! :title "Fit stage in view"} "fit"]]]))
|
||||
[:button {:on-click fit-stage! :title "Fit stage in view"} "fit"]
|
||||
[:details.passepartout-controls
|
||||
[:summary "passepartout"]
|
||||
[:div.view-popout
|
||||
[:label "Passepartout"
|
||||
[:input {:type "range" :min 0 :max 1 :step 0.05 :value opacity
|
||||
:on-change #(rf/dispatch [::mat :opacity (js/parseFloat (.. % -target -value))])}]]]]]))
|
||||
|
|
|
|||
|
|
@ -79,29 +79,3 @@
|
|||
:on-click (fn [] (close!) (on-click))}
|
||||
label
|
||||
(when sub [:span.sub sub])]))]])]))))
|
||||
|
||||
(defn popover
|
||||
"A button that opens a panel of settings rather than a list of commands: the
|
||||
onion's counts, a slider, the palette's name. Placed and dismissed exactly as
|
||||
`view` is — measured against its button, shut by a click anywhere else — so
|
||||
every popover in the window closes the same way. `body` is a function of the
|
||||
close function, for a panel with a control that should shut it."
|
||||
[_ _]
|
||||
(let [at (r/atom nil)]
|
||||
(fn [{:keys [label title class]} body]
|
||||
(let [close! #(reset! at nil)]
|
||||
[:div.menu-wrap {:class class}
|
||||
[:button {:class (str "pop" (when @at " on")) :title title :aria-label title
|
||||
:aria-haspopup "dialog" :aria-expanded (boolean @at)
|
||||
:on-click (fn [^js e]
|
||||
(if @at (close!) (reset! at (placement (.-currentTarget e)))))}
|
||||
label]
|
||||
(when-let [{:keys [left top bottom max-height]} @at]
|
||||
[:<>
|
||||
[:div.menu-scrim {:on-click close!}]
|
||||
[:div.menu.menu-drop.popout
|
||||
{:role "dialog" :aria-label title
|
||||
:style (cond-> {:left (str left "px") :max-height (str max-height "px")}
|
||||
top (assoc :top (str top "px"))
|
||||
bottom (assoc :bottom (str bottom "px")))}
|
||||
(body close!)]])]))))
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
The footage switch is NOT here. It is a viewing aid rather than something you
|
||||
set before you draw, so it is a section of the inspector. See `ui/params`."
|
||||
(:require [arthur.domain.channel :as channel]
|
||||
[arthur.domain.clip :as clip]
|
||||
[arthur.domain.node :as node]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.domain.symbol :as symbol]
|
||||
|
|
@ -23,8 +22,6 @@
|
|||
[arthur.events.ui :as ui]
|
||||
[arthur.subs.render :as render]
|
||||
[arthur.subs.ui :as sub]
|
||||
[arthur.ui.menu :as menu]
|
||||
[clojure.string :as str]
|
||||
[re-frame.core :as rf]
|
||||
[reagent.core :as r]))
|
||||
|
||||
|
|
@ -177,107 +174,30 @@
|
|||
:on-click #(pick! [:remap {}] placements)}
|
||||
"⇄"]]]]))
|
||||
|
||||
(defn- strip
|
||||
"Every slot of a palette, side by side: what it looks like at a glance."
|
||||
[{:keys [slots]}]
|
||||
[:span.strip (for [[i {:keys [hex]}] (map-indexed vector slots)]
|
||||
^{:key i} [:i {:style {:background hex}}])])
|
||||
|
||||
(defn- rename
|
||||
"The open palette's name, made editable. Committed on Enter or blur, so a
|
||||
rename is one undo step and not one per letter; Esc leaves it as it was."
|
||||
[pid palette done!]
|
||||
[:input.palette-name
|
||||
{:type "text" :default-value (:name palette) :auto-focus true
|
||||
:aria-label "palette name"
|
||||
:on-focus #(.select (.-target %))
|
||||
:on-blur (fn [^js e]
|
||||
(rf/dispatch [::project/palette-name pid (.. e -target -value)])
|
||||
(done!))
|
||||
:on-key-down (fn [^js e]
|
||||
(case (.-key e)
|
||||
"Enter" (.blur (.-target e))
|
||||
"Escape" (do (set! (.. e -target -value) (:name palette))
|
||||
(.blur (.-target e)))
|
||||
nil))}])
|
||||
|
||||
(defn- chooser
|
||||
"Every palette in the project as a row — its strip and its name — so what you
|
||||
would switch to is visible before you switch. The open one carries the edit
|
||||
button that renames it; the ones the stage is drawing in at the playhead carry
|
||||
a dot."
|
||||
[clip pid]
|
||||
(r/with-let [renaming? (r/atom false)]
|
||||
(let [on-stage @(rf/subscribe [::render/palettes-shown])]
|
||||
[:div.palette-list {:role "listbox" :aria-label "palettes"}
|
||||
(doall
|
||||
(for [[id p] (sort-by (comp str :name val) (pal/palettes clip))
|
||||
:let [open? (= id pid)]]
|
||||
^{:key (str id)}
|
||||
[:div.palette-row {:class (when open? "on")}
|
||||
(if (and open? @renaming?)
|
||||
[:div.choose [strip p] [rename id p #(reset! renaming? false)]]
|
||||
[:button.choose {:role "option" :aria-selected open?
|
||||
:title (if open? (str "editing " (:name p)) (str "edit " (:name p)))
|
||||
:on-click (fn []
|
||||
(when-not open?
|
||||
(reset! renaming? false)
|
||||
(rf/dispatch [::ui/set-tone 0])
|
||||
(rf/dispatch [::select id])))}
|
||||
[strip p]
|
||||
[:span.name (:name p)]
|
||||
(when (contains? on-stage id)
|
||||
[:span.on-stage {:title "on the stage at the playhead"}])])
|
||||
(when open?
|
||||
[:button.rename {:title (str "rename " (:name p)) :aria-label "rename palette"
|
||||
:on-click #(swap! renaming? not)}
|
||||
"✎"])]))])))
|
||||
|
||||
(defn- elsewhere
|
||||
"An info badge on the palette button when the palette being edited is not one
|
||||
the stage draws in at the playhead — not an error: editing another palette is
|
||||
how one is made ready for a palette lane. Hovering says where to change it.
|
||||
The card is placed in the viewport, as `ui/menu` places its panels, because
|
||||
the tool strip scrolls and clips its overflow."
|
||||
[pid palette]
|
||||
(r/with-let [at (r/atom nil)]
|
||||
(let [on-stage @(rf/subscribe [::render/palettes-shown])
|
||||
clip @(rf/subscribe [::render/clip])
|
||||
sid @(rf/subscribe [::render/open])]
|
||||
(when (and (seq on-stage) (not (contains? on-stage pid)))
|
||||
[:span.palette-info
|
||||
{:aria-label "palette not on the stage here"
|
||||
:on-mouse-enter (fn [^js e]
|
||||
(let [r (.getBoundingClientRect (.-currentTarget e))]
|
||||
(reset! at [(+ (.-right r) 8) (- (.-top r) 6)])))
|
||||
:on-mouse-leave #(reset! at nil)}
|
||||
"i"
|
||||
(when-let [[x y] @at]
|
||||
[:span.info-card {:role "tooltip" :style {:left (str x "px") :top (str y "px")}}
|
||||
[:strong (str (:name palette) " isn't on the stage here")]
|
||||
(str (or (clip/symbol-name clip sid) "This symbol")
|
||||
" draws in " (str/join " and " (map #(:name (get (pal/palettes clip) %)) on-stage))
|
||||
" at this frame. To draw in " (:name palette)
|
||||
", set the authoring palette in the symbol inspector, or blend to it with a palette lane.")])]))))
|
||||
|
||||
(defn assets
|
||||
"The palettes behind a button at the foot of the strip: switch, rename, add.
|
||||
The name is NOT printed in the strip: 44px holds no palette's name, and a name
|
||||
broken over three lines is worse than one shown when asked for."
|
||||
"Which palette asset the grid shows, and a new or duplicated one, for the
|
||||
tool strip, with a popout for the full asset controls."
|
||||
[]
|
||||
(let [[clip pid palette] (shown)]
|
||||
[:div.palette-assets-wrap
|
||||
[menu/popover {:class "palette-assets" :title (str "palette: " (:name palette))
|
||||
:label [:svg {:view-box "0 0 18 18" :width 18 :height 18}
|
||||
[:path {:d "M9 2C4.6 2 1.5 5.2 1.5 9c0 4 3.4 7 7.3 7 1.4 0 1.9-.9 1.4-1.9-.5-1 0-2.1 1.4-2.1h1.9c1.9 0 3-1.2 3-3.2C16.5 5 13.2 2 9 2Z"}]
|
||||
[:circle {:cx 5.5 :cy 8 :r 1.3}]
|
||||
[:circle {:cx 8.5 :cy 5 :r 1.3}]
|
||||
[:circle {:cx 12.5 :cy 6.5 :r 1.3}]]}
|
||||
(fn [_]
|
||||
[:div.palette-popout
|
||||
[chooser clip pid]
|
||||
[:div.row
|
||||
[:button {:title "a new 16-slot palette" :on-click #(rf/dispatch [::project/new-palette])} "new"]
|
||||
[:button {:title (str "a copy of " (:name palette) " you can retone")
|
||||
:on-click #(rf/dispatch [::project/duplicate-palette pid])} "duplicate"]]])]
|
||||
[elsewhere pid palette]]))
|
||||
[:details.palette-assets
|
||||
{:on-toggle (fn [e]
|
||||
(let [el (.-currentTarget e)
|
||||
box (.getBoundingClientRect el)
|
||||
pop (.querySelector el ".palette-popout")]
|
||||
(set! (.. pop -style -left) (str (+ 4 (.-right box)) "px"))
|
||||
(set! (.. pop -style -top) (str (max 8 (min (.-top box) (- (.-innerHeight js/window) 160))) "px"))))}
|
||||
[:summary {:title (:name palette)} [:span (:name palette)]]
|
||||
[:div.palette-popout
|
||||
[:strong (:name palette)]
|
||||
[:select {:value (str pid)
|
||||
:title "palette asset"
|
||||
:on-change (fn [e]
|
||||
(let [v (.. e -target -value)
|
||||
id (first (filter #(= v (str %)) (keys (pal/palettes clip))))]
|
||||
(rf/dispatch [::ui/set-tone 0])
|
||||
(rf/dispatch [::select id])))}
|
||||
(for [[id p] (sort-by (comp str :name val) (pal/palettes clip))]
|
||||
^{:key (str id)} [:option {:value (str id)} (:name p)])]
|
||||
[:button {:title "new 16-slot palette" :on-click #(rf/dispatch [::project/new-palette])} "+"]
|
||||
[:button {:title (str "duplicate " (:name palette) " — a copy you can retone")
|
||||
:on-click #(rf/dispatch [::project/duplicate-palette pid])} "clone"]]]))
|
||||
|
|
|
|||
|
|
@ -755,8 +755,7 @@
|
|||
destination-name (when-let [sid (:sid destination)]
|
||||
(clip/symbol-name clip sid))
|
||||
zoom @(rf/subscribe [::layout/zoom :stage])
|
||||
{:keys [on? opacity]} @(rf/subscribe [::layout/mat])
|
||||
opacity (if on? opacity 0)]
|
||||
{:keys [opacity]} @(rf/subscribe [::layout/mat])]
|
||||
[:div.stage-area
|
||||
(navigation)
|
||||
[:div.stage-wrap
|
||||
|
|
|
|||
|
|
@ -1241,9 +1241,10 @@
|
|||
(let [by-frame (group-by :at (concat key-items (mapcat :key-items cels)))]
|
||||
(for [[index f] (map-indexed vector (sort (distinct (concat keys (mapcat :keys cels)))))
|
||||
:when (and (<= 0 f) (< f frames))]
|
||||
^{:key index}
|
||||
(if-let [items (and (= :channel kind) (seq (get by-frame f)))]
|
||||
^{:key index} [key-dot (vec items) frames key-selection key-preview key-anchor key-state key-elements]
|
||||
^{:key index} [:div.tl-key {:aria-hidden true :style {:left (at% f frames)}}])))))]))
|
||||
[key-dot (vec items) frames key-selection key-preview key-anchor key-state key-elements]
|
||||
[:div.tl-key {:aria-hidden true :style {:left (at% f frames)}}])))))]))
|
||||
|
||||
(defn- cursor-hint
|
||||
"What the drag in flight would do, beside the pointer.
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@
|
|||
:title (str name " (" (.toUpperCase key) ") — " tip)
|
||||
:on-click #(rf/dispatch [::ui/set-tool t])}
|
||||
[:svg {:view-box "0 0 18 18" :width 18 :height 18} icon]]))]
|
||||
[palette/grid]
|
||||
[palette/assets]]))
|
||||
[palette/assets]
|
||||
[palette/grid]]))
|
||||
|
||||
(defn- size-control []
|
||||
(let [size @(rf/subscribe [::sub/brush])]
|
||||
|
|
@ -87,16 +87,17 @@
|
|||
[:span.dim.hint tip]))
|
||||
[:span.spacer]
|
||||
(let [{:keys [on? opacity]} @(rf/subscribe [::render/tracing])]
|
||||
[layout/overlay "tracing-controls" "tracing" on?
|
||||
"show tracing layers over the picture · never exported"
|
||||
#(rf/dispatch [::ui/tracing-on (not on?)])
|
||||
(fn [_]
|
||||
[:div.view-popout
|
||||
[:label "Opacity"
|
||||
[:input {:type "range" :min 0 :max 1 :step 0.05 :value opacity
|
||||
:on-change #(rf/dispatch [::ui/tracing-opacity (js/parseFloat (.. % -target -value))])}]]])])
|
||||
;; The stage's zoom and overlays: properties of the view and not of the
|
||||
;; document, so they sit in the view's own chrome rather than in the inspector.
|
||||
[:<>
|
||||
[:button {:class (when on? "on")
|
||||
:title "show tracing layers over the picture · never exported"
|
||||
:on-click #(rf/dispatch [::ui/tracing-on (not on?)])} "tracing"]
|
||||
[:input.trace-opacity
|
||||
{:type "range" :min 0 :max 1 :step 0.05 :value opacity :disabled (not on?)
|
||||
:title "how strongly tracing layers draw" :style {:flex "0 0 64px"}
|
||||
:on-change #(rf/dispatch [::ui/tracing-opacity (js/parseFloat (.. % -target -value))])}]])
|
||||
;; The stage's zoom: a property of the view and not of the document, so it
|
||||
;; sits in the view's own chrome rather than in the inspector.
|
||||
[layout/zoomer :stage "the stage"]
|
||||
[layout/stage-controls]]))
|
||||
|
||||
(defn adjust-last
|
||||
|
|
|
|||
|
|
@ -216,19 +216,10 @@
|
|||
|
||||
(deftest no-symbol-is-special
|
||||
(let [c (nested)]
|
||||
(testing "a document opens on its root, however long its unplaced symbols are"
|
||||
(testing "a document opens on the longest symbol nothing places"
|
||||
(is (= [:loose :main :outer] (clip/unplaced c)))
|
||||
(is (= :main (clip/opens-on c)))
|
||||
(is (= :main (clip/opens-on (clip/blank))))
|
||||
(is (= :loose (clip/opens-on (assoc c :root :loose))))
|
||||
(is (= c (leaf/clip "u" (leaf/leaves "u" c))))
|
||||
(is (seq (clip/problems (assoc c :root :gone)))))
|
||||
(testing "without a root, the longest symbol nothing places"
|
||||
(is (= :outer (clip/opens-on (dissoc c :root)))))
|
||||
(testing "a document saved before roots opens on :main, not a longer take"
|
||||
(is (= :main (:root (clip/pin-root (dissoc c :root)))))
|
||||
(is (= :outer (:root (clip/pin-root (-> c (dissoc :root) (update :symbols dissoc :main))))))
|
||||
(is (= :loose (:root (clip/pin-root (assoc c :root :loose))))))
|
||||
(is (= :outer (clip/opens-on c)))
|
||||
(is (= :main (clip/opens-on (clip/blank)))))
|
||||
(testing "an instance can go into any symbol, and spans that symbol's frames"
|
||||
(let [[n] (vals (get-in c [:symbols :outer :nodes]))]
|
||||
(is (= #{:inner} (node/sources n)))
|
||||
|
|
@ -450,9 +441,6 @@
|
|||
extended (nest/resize-out document :palettes [:change] 1 false)]
|
||||
(resolve 2)
|
||||
(is (= {:from :day :to :night :t 0.5} (clip/active-palette resolve)))
|
||||
(is (= (clip/active-palette resolve)
|
||||
(clip/palette-at document nil :day (clip/symbol document :main) 2 nil))
|
||||
"the palette strip asks the same question the resolver answers: both ends of a blend")
|
||||
(is (= [128 128 128]
|
||||
(nth (pal/effective-ramp context (clip/active-palette resolve))
|
||||
(pal/render-index context :day 1))))
|
||||
|
|
|
|||
|
|
@ -72,16 +72,14 @@ try {
|
|||
})()`);
|
||||
await sleep(250);
|
||||
|
||||
assert.equal(await evaluate(`document.querySelector('.onion-controls .menu-wrap > button').textContent`), '▾');
|
||||
assert.equal(await evaluate(`getComputedStyle(document.querySelector('.onion-controls summary')).listStyleType`), 'none');
|
||||
assert.equal(await evaluate(`document.querySelector('.onion-controls summary').textContent`), '▾');
|
||||
await evaluate(`document.querySelector('.onion-controls > button').click()`);
|
||||
await sleep(150);
|
||||
assert.equal(await evaluate(`document.querySelector('.onion-controls > button').getAttribute('aria-pressed')`), 'true');
|
||||
await evaluate(`document.querySelector('.onion-controls .menu-wrap > button').click()`);
|
||||
await evaluate(`document.querySelector('.onion-controls summary').click()`);
|
||||
await sleep(100);
|
||||
assert.equal(await evaluate(`!!document.querySelector('.menu.popout')`), true);
|
||||
await evaluate(`document.querySelector('.menu-scrim').click()`);
|
||||
await sleep(100);
|
||||
assert.equal(await evaluate(`!!document.querySelector('.menu.popout')`), false, 'a click outside shuts the onion settings');
|
||||
assert.equal(await evaluate(`document.querySelector('.onion-controls details').open`), true);
|
||||
await evaluate(`(() => {
|
||||
const c = cljs.core, k = c.keyword, m = (...xs) => c.hash_map(...xs), v = (...xs) => c.vector(...xs);
|
||||
const db = c.deref(re_frame.db.app_db);
|
||||
|
|
|
|||
|
|
@ -523,32 +523,32 @@ async function main() {
|
|||
// same synchronous block arrives while the tool is still unset and is dropped.
|
||||
// A person cannot click twice inside one microtask; a test should not either.
|
||||
const armed = await page.eval(`(() => {
|
||||
const button = document.querySelector('.toolbox button.tool[title^="Pen"]');
|
||||
const button = [...document.querySelectorAll('.palette-bar button')]
|
||||
.find(b => b.textContent === 'pen');
|
||||
if (!button) return false;
|
||||
button.click();
|
||||
return true;
|
||||
})()`);
|
||||
await sleep(100);
|
||||
// The overlay reaches past the stage on every side, so a stage pixel is
|
||||
// found through the SVG's own transform, as the stage finds it coming back.
|
||||
// Each point is its own eval: a click is a re-frame event, and the next one
|
||||
// reads the draft it added to.
|
||||
const PEN_AT = (x, y) => `(() => {
|
||||
const svg = document.querySelector('.paint-overlay.tool-pen');
|
||||
if (!svg) return false;
|
||||
const p = new DOMPoint(${x}, ${y}).matrixTransform(svg.getScreenCTM());
|
||||
svg.dispatchEvent(new PointerEvent('pointerdown', {
|
||||
bubbles: true, clientX: p.x, clientY: p.y,
|
||||
}));
|
||||
const painted = armed && await page.eval(`(() => {
|
||||
const svg = document.querySelector('.paint-overlay');
|
||||
const box = svg.getBoundingClientRect();
|
||||
for (const [x, y] of [[10, 10], [40, 10], [25, 40]]) {
|
||||
svg.dispatchEvent(new PointerEvent('pointerdown', {
|
||||
bubbles: true, clientX: box.left + x * box.width / 320,
|
||||
clientY: box.top + y * box.height / 200,
|
||||
}));
|
||||
}
|
||||
return true;
|
||||
})()`;
|
||||
let painted = armed;
|
||||
for (const [x, y] of [[10, 10], [40, 10], [25, 40]]) {
|
||||
painted = painted && await page.eval(PEN_AT(x, y));
|
||||
await sleep(60);
|
||||
}
|
||||
// Closed as a person closes it: on the first point.
|
||||
const finished = painted && await page.eval(PEN_AT(10, 10));
|
||||
})()`);
|
||||
await sleep(100);
|
||||
const finished = await page.eval(`(() => {
|
||||
const button = [...document.querySelectorAll('.palette-bar button')]
|
||||
.find(b => b.textContent === 'finish');
|
||||
if (!button || button.disabled) return false;
|
||||
button.click();
|
||||
return true;
|
||||
})()`);
|
||||
await sleep(250);
|
||||
const afterPaint = await page.eval(`(() => {
|
||||
const c = document.querySelector('canvas.stage');
|
||||
|
|
@ -556,8 +556,7 @@ async function main() {
|
|||
return [...d];
|
||||
})()`);
|
||||
check(painted && finished && beforePaint.join(',') !== afterPaint.join(','),
|
||||
'a polygon drawn with the paint controls appears on the canvas',
|
||||
`${beforePaint} -> ${afterPaint}`);
|
||||
'a polygon drawn with the paint controls appears on the canvas');
|
||||
for (const f of [8, 16]) {
|
||||
await page.eval(SEEK(f));
|
||||
await sleep(100);
|
||||
|
|
@ -598,18 +597,7 @@ async function main() {
|
|||
return [...c.getContext('2d').getImageData(25, 20, 1, 1).data];
|
||||
})()`);
|
||||
check(afterPaint.join(',') === reopenedPaint.join(','),
|
||||
'the painted polygon survives the project round trip', `${afterPaint} vs ${reopenedPaint}`);
|
||||
// A reopened document has nothing selected, so the shape's keys are not in
|
||||
// the inspector until it is selected again, as anybody would.
|
||||
await page.eval(`(() => {
|
||||
const c = cljs.core, k = c.keyword;
|
||||
const db = c.deref(re_frame.db.app_db);
|
||||
const nodes = c.get_in(c.get(arthur.footage.store.entry(c.get(db, k('clip/current'))), k('clip')),
|
||||
c.vector(k('symbols'), k('main'), k('nodes')));
|
||||
const id = c.first(c.filter((id) => c.name(id).startsWith('paint-'), c.keys(nodes)));
|
||||
re_frame.core.dispatch_sync(c.vector(k('arthur.events.ui/select'),
|
||||
c.vector(k('node'), k('main'), id, c.vector(id))));
|
||||
})()`);
|
||||
'the painted polygon survives the project round trip');
|
||||
await page.eval(SEEK(8));
|
||||
await sleep(100);
|
||||
check(await page.eval(`(() => {
|
||||
|
|
|
|||
|
|
@ -1650,6 +1650,7 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
|
|||
background-size: 8px 8px;
|
||||
background-position: 0 0, 4px 4px;
|
||||
}
|
||||
.palette .cell.clear { grid-column: span 2; width: 36px; }
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
on the stage: the tools' marks, in stage pixels (the SVG's viewBox), and their
|
||||
|
|
@ -1696,6 +1697,7 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
|
|||
background: linear-gradient(135deg, #3a3f52 0 50%, #e8d9a8 50% 100%);
|
||||
color: #fff; font-size: 11px; line-height: 1; text-shadow: 0 1px 1px rgba(0, 0, 0, .6);
|
||||
}
|
||||
.palette .cell.map { grid-column: span 2; width: 36px; }
|
||||
.palette .cell { position: relative; }
|
||||
.palette .cell .badge {
|
||||
position: absolute; right: -2px; bottom: -2px;
|
||||
|
|
@ -1724,87 +1726,32 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
|
|||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* The palette's own settings, at the foot of the strip: drawn as a tool button,
|
||||
because it is one more thing in the strip you press. */
|
||||
.palette-assets > button {
|
||||
width: 34px; height: 30px; padding: 0;
|
||||
display: grid; place-items: center;
|
||||
border: 1px solid transparent; border-radius: 4px; background: none;
|
||||
}
|
||||
.palette-assets > button:hover:not(:disabled) { background: var(--sunk); border-color: var(--hair); }
|
||||
.palette-assets > button.on { background: var(--sel-bg); border-color: var(--sel); }
|
||||
.palette-assets svg path { fill: var(--fg); }
|
||||
.palette-assets svg circle { fill: var(--chrome); }
|
||||
.palette-assets > button.on svg circle { fill: var(--sel-bg); }
|
||||
|
||||
/* A popover's panel: settings, not commands, so narrower than a menu and laid
|
||||
out as a form. Placed and dismissed by `ui/menu`. */
|
||||
.menu.popout { width: 240px; min-width: 0; padding: 10px; line-height: 1.4; }
|
||||
.menu.popout:has(.palette-popout) { width: 280px; }
|
||||
.palette-popout { display: grid; gap: 8px; }
|
||||
.palette-popout .row { margin: 0; }
|
||||
|
||||
/* Every palette as its strip and its name: what you would switch to, seen
|
||||
before you switch. The open one is the selected row. */
|
||||
.palette-list { display: grid; gap: 1px; max-height: 50vh; overflow: auto; }
|
||||
.palette-row { display: flex; align-items: center; gap: 2px; border-radius: 3px; }
|
||||
.palette-row.on { background: var(--sel-bg); }
|
||||
.palette-row .choose {
|
||||
flex: 1; min-width: 0;
|
||||
display: grid; grid-template-columns: 1fr auto; align-items: center; gap: 3px 8px;
|
||||
padding: 5px 6px; border: 1px solid transparent; border-radius: 3px; background: none; text-align: left;
|
||||
}
|
||||
.palette-row button.choose:hover:not(:disabled) { background: var(--sunk); border-color: var(--hair); }
|
||||
.palette-row.on button.choose { border-color: var(--sel); }
|
||||
.palette-row.on button.choose:hover { background: var(--sel-bg); }
|
||||
.palette-row .strip { grid-column: 1 / -1; }
|
||||
.palette-row .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.palette-row.on .name { font-weight: 600; color: #14395f; }
|
||||
.palette-row .palette-name { grid-column: 1 / -1; width: 100%; min-width: 0; }
|
||||
.palette-row .rename { align-self: stretch; padding: 0 7px; border-color: transparent; background: none; }
|
||||
.palette-row .rename:hover:not(:disabled) { border-color: var(--sel); background: #fff; }
|
||||
|
||||
.strip { display: flex; height: 14px; border: 1px solid rgba(0, 0, 0, .35); border-radius: 2px; overflow: hidden; }
|
||||
.strip i { flex: 1; }
|
||||
|
||||
/* On the stage at the playhead. */
|
||||
.on-stage { width: 7px; height: 7px; border-radius: 50%; background: var(--live); }
|
||||
|
||||
/* A palette being edited that the stage is not drawing in here. Information,
|
||||
not alarm: the accent of a selection, not of a warning. */
|
||||
.palette-assets-wrap { position: relative; }
|
||||
.palette-info {
|
||||
position: absolute; top: -3px; right: -5px;
|
||||
width: 13px; height: 13px; border-radius: 50%;
|
||||
display: grid; place-items: center;
|
||||
background: var(--sel); color: #fff;
|
||||
font: italic 600 9px/1 Georgia, serif;
|
||||
cursor: help;
|
||||
box-shadow: 0 0 0 1.5px var(--chrome);
|
||||
}
|
||||
.info-card {
|
||||
position: fixed; z-index: 60;
|
||||
width: 240px; padding: 8px 10px;
|
||||
display: grid; gap: 3px;
|
||||
background: var(--pane); color: var(--fg);
|
||||
font: 11px/1.45 "Lucida Grande", "Segoe UI", system-ui, sans-serif;
|
||||
border: 1px solid var(--sel); border-left-width: 3px; border-radius: 3px;
|
||||
box-shadow: 0 3px 10px rgba(0, 0, 0, .25);
|
||||
cursor: default;
|
||||
}
|
||||
.info-card strong { color: #14395f; }
|
||||
/* Palette assets stay in the existing narrow tool strip. */
|
||||
.toolbox .palette-assets { display: block; width: 44px; flex: 0 0 auto; }
|
||||
.palette-assets summary { cursor: pointer; font-size: 10px; line-height: 1.25; overflow-wrap: anywhere; padding: 3px; }
|
||||
.palette-popout, .view-popout { position: fixed; z-index: 100; background: var(--pane); color: var(--fg); border: 1px solid var(--line); padding: 10px; box-shadow: 0 4px 16px #0008; line-height: 1.4; }
|
||||
.palette-popout { left: 52px; width: 230px; display: flex; flex-wrap: wrap; gap: 7px; }
|
||||
.palette-popout strong { width: 100%; overflow-wrap: anywhere; }
|
||||
.palette-popout select { width: 100%; }
|
||||
.view-popout { right: 16px; width: 240px; }
|
||||
.view-popout label { display: block; margin-bottom: 8px; }
|
||||
.view-popout label:last-child { margin-bottom: 0; }
|
||||
.view-popout input, .view-popout select { width: 100%; }
|
||||
|
||||
/* An overlay switch and its settings: one object, two parts. */
|
||||
.overlay > .menu-wrap > button { margin-left: -1px; padding: 1px 5px; border-radius: 0 2px 2px 0; }
|
||||
.overlay > button { border-radius: 2px 0 0 2px; }
|
||||
.view-popout input { width: 100%; }
|
||||
.passepartout-controls summary { cursor: pointer; }
|
||||
.stage-wrap { flex: 0 0 auto; }
|
||||
|
||||
|
||||
.onion-skin { position: absolute; inset: 0; pointer-events: none; image-rendering: pixelated; }
|
||||
|
||||
.onion-controls { display: inline-flex; align-items: center; }
|
||||
.onion-controls > button { border-radius: 2px 0 0 2px; }
|
||||
.onion-controls summary {
|
||||
display: block; cursor: pointer; list-style: none;
|
||||
color: var(--fg); background: var(--pane); border: 1px solid var(--line);
|
||||
border-radius: 0 2px 2px 0; margin-left: -1px; padding: 1px 5px;
|
||||
}
|
||||
.onion-controls summary:hover, .onion-controls details[open] > summary { background: #fff; }
|
||||
.onion-controls summary::-webkit-details-marker { display: none; }
|
||||
.onion-controls .view-popout { line-height: 1.4; }
|
||||
.onion-controls select { width: 100%; }
|
||||
|
||||
/* Expanded automation lanes expose editable keyframe dots. */
|
||||
button.tl-key { pointer-events: auto; padding: 0; border: 0; width: 11px; height: 11px; margin: -5.5px; cursor: ew-resize; z-index: 4; opacity: 1; border-radius: 50%; }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue