Add project palette assets and overrides

This commit is contained in:
Your Name 2026-10-02 09:08:53 -04:00
parent 8f09b7b47f
commit b41180db08
17 changed files with 470 additions and 87 deletions

View file

@ -416,6 +416,19 @@ class DocumentTests(TestCase):
self.assertEqual({str(self.project.id)}, {r["project"] for r in rows}) self.assertEqual({str(self.project.id)}, {r["project"] for r in rows})
self.assertEqual({"c1"}, {r["cid"] for r in rows}) self.assertEqual({"c1"}, {r["cid"] for r in rows})
def test_saved_palettes_are_listed_as_assets(self):
leaves = self.leaves()
leaves["clip/c1/palette/night"] = [
"^ ", "~:id", "~:night", "~:name", "Moonlit",
"~:slots", ["~#list", [["^ ", "~:hex", "#001122"]]],
]
self.assertEqual(200, self.save(leaves).status_code)
rows = self.client.get("/api/symbols").json()["palettes"]
self.assertEqual(
[("Moonlit", "night", "c1")],
[(r["name"], r["palette"], r["cid"]) for r in rows],
)
def test_a_document_comes_back_exactly(self): def test_a_document_comes_back_exactly(self):
response = self.save() response = self.save()
self.assertEqual(200, response.status_code, response.content) self.assertEqual(200, response.status_code, response.content)

View file

@ -357,6 +357,7 @@ def footage_list(request):
_SYMBOL_LEAF = re.compile(r"^clip/([^/]+)/symbol/([^/]+)$") _SYMBOL_LEAF = re.compile(r"^clip/([^/]+)/symbol/([^/]+)$")
_PALETTE_LEAF = re.compile(r"^clip/([^/]+)/palette/([^/]+)$")
def _transit_fields(value, *keys): def _transit_fields(value, *keys):
@ -391,7 +392,21 @@ def symbols(request):
"frames": fields.get("frames"), "frames": fields.get("frames"),
}) })
rows.sort(key=lambda r: (r["project_name"], r["project"], r["name"])) rows.sort(key=lambda r: (r["project_name"], r["project"], r["name"]))
return JsonResponse({"symbols": rows}) palettes = []
for leaf in Leaf.objects.filter(path__contains="/palette/").select_related("project"):
m = _PALETTE_LEAF.match(leaf.path)
if not m:
continue
fields = _transit_fields(leaf.value, "name")
palettes.append({
"project": str(leaf.project_id),
"project_name": leaf.project.name,
"cid": m.group(1),
"palette": m.group(2),
"name": fields.get("name") or m.group(2).replace("~", "/"),
})
palettes.sort(key=lambda r: (r["project_name"], r["project"], r["name"]))
return JsonResponse({"symbols": rows, "palettes": palettes})
@require_http_methods(["GET", "PATCH"]) @require_http_methods(["GET", "PATCH"])

View file

@ -82,7 +82,7 @@
;; Every symbol in every saved project, for the pool's all-assets folder. Rows ;; Every symbol in every saved project, for the pool's all-assets folder. Rows
;; from `/api/symbols`, nothing loaded: a symbol from elsewhere is fetched when ;; from `/api/symbols`, nothing loaded: a symbol from elsewhere is fetched when
;; it is dropped. ;; it is dropped.
:assets {:symbols [] :loading? false} :assets {:symbols [] :palettes [] :loading? false}
;; The document's own identity on the server. `:seq` is the monotonic project ;; The document's own identity on the server. `:seq` is the monotonic project
;; version: a client that sees a delta with `seq > local + 1` refetches, which ;; version: a client that sees a delta with `seq > local + 1` refetches, which
@ -159,7 +159,8 @@
:ui {:open nil :ui {:open nil
:tabs [] :tabs []
:selection nil :selection nil
:tone :skin-base :selections []
:tone 1
:tool nil :tool nil
:auto-key? false :auto-key? false
:draft [] :draft []

View file

@ -51,7 +51,8 @@
that loses something on every round trip, which is the one bug a persistence that loses something on every round trip, which is the one bug a persistence
layer must not be able to have. Add the field here and to `leaf/leaves` and layer must not be able to have. Add the field here and to `leaf/leaves` and
`leaf/clip` in the same commit." `leaf/clip` in the same commit."
#{:name :fps :analysis :subjects :features :groups :width :height :symbols}) #{:name :fps :analysis :subjects :features :groups :width :height :symbols
:palettes :default-palette})
(defn symbol (defn symbol
"One of the clip's symbols, by id." "One of the clip's symbols, by id."
@ -213,6 +214,8 @@
{:name "untitled" {:name "untitled"
:fps 30 :fps 30
:width 320 :height 200 :width 320 :height 200
:palettes {pal/default-id pal/default-palette}
:default-palette pal/default-id
;; No native fps yet: an untouched canvas follows the project grid. Imported ;; No native fps yet: an untouched canvas follows the project grid. Imported
;; and generated symbols carry their own rate explicitly. ;; and generated symbols carry their own rate explicitly.
:symbols {:main {:id :main :frames blank-frames :nodes {}}}}) :symbols {:main {:id :main :frames blank-frames :nodes {}}}})
@ -245,15 +248,27 @@
Every instance owns its cursors and buffers. The IResolver queries return Every instance owns its cursors and buffers. The IResolver queries return
native node frames and world matrices for the last rendered output frame." native node frames and world matrices for the last rendered output frame."
[clip sid store palette opts] [clip sid store palette opts]
(letfn [(build [sid chain pose-tracks] (let [context? (and (map? palette) (:palettes palette) (:offsets palette))]
(letfn [(selection-at [owner frame inherited]
(let [selection (:palette owner)
chosen (cond
(nil? selection) nil
(and (map? selection) (contains? selection :animated?))
(ch/value-at selection frame store)
:else selection)]
(or chosen inherited (:default palette))))
(build [sid chain pose-tracks]
(when (some #{sid} chain) (when (some #{sid} chain)
(throw (ex-info "symbol cycle" {:chain (conj chain sid)}))) (throw (ex-info "symbol cycle" {:chain (conj chain sid)})))
(let [sym (or (symbol clip sid) (let [sym (or (symbol clip sid)
(throw (ex-info "an instance names a missing symbol" {:symbol sid}))) (throw (ex-info "an instance names a missing symbol" {:symbol sid})))
active (volatile! (when context? (:default palette)))
nodes (:nodes sym) nodes (:nodes sym)
rank (symbol/draw-rank nodes (symbol/order nodes)) rank (symbol/draw-rank nodes (symbol/order nodes))
ids (sort-by rank (keys nodes)) ids (sort-by rank (keys nodes))
own (symbol/resolver sym store palette own (symbol/resolver sym store (if context?
#(pal/render-index palette @active %)
palette)
(assoc opts :pose-tracks pose-tracks)) (assoc opts :pose-tracks pose-tracks))
;; Each cel owns its source resolver and mutable buffers. ;; Each cel owns its source resolver and mutable buffers.
children (into {} children (into {}
@ -268,7 +283,10 @@
;; there used to be. Their resolvers still hold the frame ;; there used to be. Their resolvers still hold the frame
;; before whenever they were not on. ;; before whenever they were not on.
entered (volatile! {}) entered (volatile! {})
step (fn [f pre] step (fn [f pre inherited forced]
(when context?
(vreset! active (or forced
(selection-at sym (js/Math.floor f) inherited))))
(vreset! entered {}) (vreset! entered {})
(let [by-id (into {} (map (juxt :node identity)) (let [by-id (into {} (map (juxt :node identity))
(own (js/Math.floor f) (js/Math.floor pre)))] (own (js/Math.floor f) (js/Math.floor pre)))]
@ -301,14 +319,18 @@
;; previous frame, and so no gap. ;; previous frame, and so no gap.
(or (:frame (when (number? prior) (or (:frame (when (number? prior)
(placed-frame clip sid n prior))) (placed-frame clip sid n prior)))
(dec frame))))) (dec frame))
@active
(when (and context? (:palette n))
(selection-at n frame nil)))))
[])) []))
(when-let [op (get by-id id)] [op])))) (when-let [op (get by-id id)] [op]))))
ids))))] ids))))]
(reify (reify
IFn IFn
(-invoke [_ f] (step f (dec f))) (-invoke [_ f] (step f (dec f) nil nil))
(-invoke [_ f pre] (step f pre)) (-invoke [_ f pre] (step f pre nil nil))
(-invoke [_ f pre inherited forced] (step f pre inherited forced))
symbol/IResolver symbol/IResolver
(world-of [_ [id & more]] (world-of [_ [id & more]]
(if more (if more
@ -345,11 +367,13 @@
;; the WHOLE PICTURE reading 13 instead of 15 — a head going two frames ;; the WHOLE PICTURE reading 13 instead of 15 — a head going two frames
;; stale, a 67ms hitch at 12fps, to fix one group's mouth. It is per ;; stale, a 67ms hitch at 12fps, to fix one group's mouth. It is per
;; group, so it is seated where groups exist. ;; group, so it is seated where groups exist.
(if (pos? f) (cadence/frame (dec f) grid native) -1))) (if (pos? f) (cadence/frame (dec f) grid native) -1)
(when context? (:default palette))
nil))
symbol/IResolver symbol/IResolver
(world-of [_ path] (symbol/world-of r path)) (world-of [_ path] (symbol/world-of r path))
(frame-of [_ path] (symbol/frame-of r path)) (frame-of [_ path] (symbol/frame-of r path))
(pre-frame-of [_ path] (symbol/pre-frame-of r path)))))) (pre-frame-of [_ path] (symbol/pre-frame-of r path)))))))
(defn center (defn center
"The middle of everything symbol `sid` draws, over all its frames, in its own "The middle of everything symbol `sid` draws, over all its frames, in its own
@ -499,6 +523,14 @@
(str "clip has a field with no leaf to save it in: " (pr-str k))) (str "clip has a field with no leaf to save it in: " (pr-str k)))
(when-not (map? (:symbols clip)) (when-not (map? (:symbols clip))
[":symbols must be a map of id -> symbol"]) [":symbols must be a map of id -> symbol"])
(when (and (contains? clip :palettes) (not (map? (:palettes clip))))
[":palettes must be a map of id -> palette"])
(when (and (contains? clip :default-palette) (map? (:palettes clip))
(not (contains? (:palettes clip) (:default-palette clip))))
[":default-palette must name a project palette"])
(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"))
(when-not (or (nil? (:fps clip)) (and (number? (:fps clip)) (pos? (:fps clip)))) (when-not (or (nil? (:fps clip)) (and (number? (:fps clip)) (pos? (:fps clip))))
[(str ":fps is " (pr-str (:fps clip)) " — a rate is a positive number")]) [(str ":fps is " (pr-str (:fps clip)) " — a rate is a positive number")])
(for [[id sym] (:symbols clip) (for [[id sym] (:symbols clip)

View file

@ -10,6 +10,8 @@
clip/<cid>/name a label clip/<cid>/name a label
clip/<cid>/timing fps clip/<cid>/timing fps
clip/<cid>/stage width, height 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>/source the analysis record this came out of clip/<cid>/source the analysis record this came out of
clip/<cid>/subject/<subj> a tracked subject and its params clip/<cid>/subject/<subj> a tracked subject and its params
clip/<cid>/feature/<fid> one feature: area, nodes, params clip/<cid>/feature/<fid> one feature: area, nodes, params
@ -134,11 +136,13 @@
(some-leaf (at "name") (select-keys clip [:name])) (some-leaf (at "name") (select-keys clip [:name]))
(some-leaf (at "timing") (select-keys clip [:fps])) (some-leaf (at "timing") (select-keys clip [:fps]))
(some-leaf (at "stage") (select-keys clip [:width :height])) (some-leaf (at "stage") (select-keys clip [:width :height]))
(some-leaf (at "palette-default") (select-keys clip [:default-palette]))
(some-leaf (at "source") (:analysis clip)) (some-leaf (at "source") (:analysis clip))
(concat (concat
(for [[id v] (:subjects clip)] {(at "subject" (segment id)) v}) (for [[id v] (:subjects clip)] {(at "subject" (segment id)) v})
(for [[id v] (:features clip)] {(at "feature" (segment id)) v}) (for [[id v] (:features clip)] {(at "feature" (segment id)) v})
(for [[id v] (:groups clip)] {(at "group" (segment id)) v}) (for [[id v] (:groups clip)] {(at "group" (segment id)) v})
(for [[id v] (:palettes clip)] {(at "palette" (segment id)) v})
;; The symbol's own facts. `:id` is the path segment, so writing it ;; The symbol's own facts. `:id` is the path segment, so writing it
;; into the value as well would be the one field a rename could ;; into the value as well would be the one field a rename could
;; disagree with itself about; `clip` puts it back. ;; disagree with itself about; `clip` puts it back.
@ -190,6 +194,8 @@
"timing" (merge acc v) "timing" (merge acc v)
"stage" (merge acc v) "stage" (merge acc v)
"source" (assoc acc :analysis v) "source" (assoc acc :analysis v)
"palette-default" (merge acc v)
"palette" (assoc-in acc [:palettes (unsegment a)] v)
"subject" (assoc-in acc [:subjects (unsegment a)] v) "subject" (assoc-in acc [:subjects (unsegment a)] v)
"feature" (assoc-in acc [:features (unsegment a)] v) "feature" (assoc-in acc [:features (unsegment a)] v)
"group" (assoc-in acc [:groups (unsegment a)] v) "group" (assoc-in acc [:groups (unsegment a)] v)
@ -230,8 +236,8 @@
false) false)
(case (count p) (case (count p)
;; The clip's own facts carry no id. ;; The clip's own facts carry no id.
3 (#{"name" "timing" "stage" "source"} (nth p 2)) 3 (#{"name" "timing" "stage" "source" "palette-default"} (nth p 2))
4 (#{"subject" "feature" "group"} (nth p 2)) 4 (#{"subject" "feature" "group" "palette"} (nth p 2))
false))))] false))))]
(vec (vec
(concat (concat

View file

@ -1,15 +1,12 @@
(ns arthur.domain.palette (ns arthur.domain.palette
"The indexed palette. "Project palette assets and their render-time index banks.
THE RULE, and it is a rule rather than a default: a part carries a palette Drawing data stores a dumb LOCAL SLOT NUMBER. A symbol or instance supplies
INDEX, never a sampled RGB value. Sampling colour off the footage produces a the palette context. `compile` gives project palettes disjoint ranges in the
pixel-art filter, and it does so irrecoverably — once a shape holds a measured raster index space, so differently-paletted subtrees can coexist. The ranges
colour there is no way back to an authored one, because the information that it are derived, never persisted: adding a palette never rewrites drawing data.")
was ever a choice is gone. Every `[:style :color]` channel holds one of the
keywords below.
Entries are ordered, and the order IS the index the raster writes. Inserting in (def default-id :arthur/default)
the middle renumbers every stored index, so new tones append.")
(def entries (def entries
[{:name :bg :hex "#12141c"} [{:name :bg :hex "#12141c"}
@ -48,3 +45,68 @@
(def rgb (def rgb
"Index -> [r g b], precomputed." "Index -> [r g b], precomputed."
(mapv hex->rgb hexes)) (mapv hex->rgb hexes))
(def default-palette
{:id default-id :name "Arthur"
:slots (into entries (repeat 7 {:hex "#000000"}))})
(defn palettes [clip]
(if (seq (:palettes clip)) (:palettes clip) {default-id default-palette}))
(defn default-palette-id [clip]
(let [ps (palettes clip)]
(or (:default-palette clip)
(when (contains? ps default-id) default-id)
(first (sort-by str (keys ps))))))
(defn- slot-index [p value]
(cond
(and (integer? value) (<= 0 value) (< value (count (:slots p)))) value
;; Compatibility for existing documents. New drawing data is numeric.
(keyword? value) (first (keep-indexed #(when (= value (:name %2)) %1) (:slots p)))
:else nil))
(defn compile
"Compile the project's palette assets into the one ramp used by the raster.
Index 255 remains the conspicuous bad-data sentinel."
[clip]
(let [selection-values (fn [x]
(cond
(nil? x) []
(and (map? x) (:keys x)) (vals (:keys x))
(and (map? x) (contains? x :value)) [(:value x)]
:else [x]))
used (into #{(default-palette-id clip)}
(mapcat selection-values)
(concat (map :palette (vals (:symbols clip)))
(for [sym (vals (:symbols clip))
n (vals (:nodes sym))]
(:palette n))))
ps (select-keys (palettes clip) used)
ordered (sort-by (comp str key) ps)
offsets (loop [xs ordered at 0 out {}]
(if-let [[id p] (first xs)]
(recur (next xs) (+ at (count (:slots p))) (assoc out id at))
out))
total (reduce + (map #(count (:slots (val %))) ordered))]
(when (> total 255)
(throw (ex-info "palettes visible in one render use more than 255 slots"
{:slots total :palettes (count ps)})))
{:palettes ps
:default (default-palette-id clip)
:offsets offsets
:ramp (vec (mapcat (fn [[_ p]] (map #(hex->rgb (:hex %)) (:slots p))) ordered))
:bg (get offsets (default-palette-id clip) 0)}))
(defn render-index
"A local slot (or a legacy tone keyword) in palette `id` -> raster index."
[{:keys [palettes offsets]} id value]
(let [p (get palettes id)
i (and p (slot-index p value))]
(if (some? i) (+ (get offsets id 0) i) 255)))
(defn valid-palette? [{:keys [id name slots]}]
(and id (string? name) (seq name) (vector? slots) (pos? (count slots))
(<= (count slots) 255)
(every? #(and (string? (:hex %))
(boolean (re-matches #"#[0-9a-fA-F]{6}" (:hex %)))) slots)))

View file

@ -275,6 +275,7 @@
be impossible to miss and should not take the frame down." be impossible to miss and should not take the frame down."
[palette k] [palette k]
(cond (cond
(fn? palette) (palette k)
(number? k) k (number? k) k
(nil? k) 255 (nil? k) 255
:else (get palette k 255))) :else (get palette k 255)))

View file

@ -153,9 +153,8 @@
;; The same palette and ramp the preview resolves and blits ;; The same palette and ramp the preview resolves and blits
;; through. Read here rather than in the fx so that the effect ;; through. Read here rather than in the fx so that the effect
;; takes data and nothing else. ;; takes data and nothing else.
:palette (get {:arthur/default pal/index-of} :palette (pal/compile (:clip entry))
(:palette db) pal/index-of) :ramp (:ramp (pal/compile (:clip entry)))
:ramp (get {:arthur/default pal/rgb} (:palette db) pal/rgb)
:zoom zoom :zoom zoom
:audio-url (:audio entry) :audio-url (:audio entry)
:name (stem (:label entry) :name (stem (:label entry)

View file

@ -23,10 +23,12 @@
fx, which is the only thing in this namespace that is not pure." fx, which is the only thing in this namespace that is not pure."
(:require [arthur.db :as db] (:require [arthur.db :as db]
[arthur.domain.bring :as bring] [arthur.domain.bring :as bring]
[arthur.domain.channel :as ch]
[arthur.domain.clip :as clip] [arthur.domain.clip :as clip]
[arthur.domain.span :as span] [arthur.domain.span :as span]
[arthur.domain.leaf :as leaf] [arthur.domain.leaf :as leaf]
[arthur.domain.node :as node] [arthur.domain.node :as node]
[arthur.domain.palette :as pal]
[arthur.events.edit :as edit] [arthur.events.edit :as edit]
[arthur.audio.mix :as mix] [arthur.audio.mix :as mix]
[arthur.demo.stage :as stage] [arthur.demo.stage :as stage]
@ -165,6 +167,34 @@
:host (get-in db [:ui :open]) :frame frame :point point :host (get-in db [:ui :open]) :frame frame :point point
:target target)})) :target target)}))
(rf/reg-event-fx
::import-palette
(fn [{:keys [db]} [_ {:keys [project cid palette name]}]]
{:db (update db :project merge {:status (str "fetching " name "…")})
::import-palette! {:project project :cid cid :palette palette}}))
(rf/reg-fx
::import-palette!
(fn [{:keys [project cid palette]}]
(-> (saved-clip! project cid)
(.then (fn [{other :clip}]
(let [source (leaf/unsegment palette)
p (get-in other [:palettes source])]
(if p
(rf/dispatch [::palette-imported p])
(rf/dispatch [::failed "that palette no longer exists"])))))
(.catch (fn [error]
(rf/dispatch [::failed (or (ex-message error) (str error))]))))))
(rf/reg-event-db
::palette-imported
(fn [db [_ palette]]
(let [id (random-uuid)]
(-> (edit/edit db #(assoc-in % [:palettes id]
(assoc palette :id id :name (str (:name palette) " copy"))))
(assoc-in [:ui :palette] id)
(assoc-in [:project :status] (str "imported " (:name palette)))))))
(rf/reg-event-fx (rf/reg-event-fx
::imported ::imported
;; As drawing: its tracking stays with the analysis that measured it. See ;; As drawing: its tracking stays with the analysis that measured it. See
@ -296,7 +326,11 @@
{:project (.-project r) :project-name (.-project_name r) {:project (.-project r) :project-name (.-project_name r)
:cid (.-cid r) :symbol (.-symbol r) :cid (.-cid r) :symbol (.-symbol r)
:name (.-name r) :frames (.-frames r)}) :name (.-name r) :frames (.-frames r)})
(array-seq (.-symbols listed)))]))) (array-seq (.-symbols listed)))
(mapv (fn [^js r]
{:project (.-project r) :project-name (.-project_name r)
:cid (.-cid r) :palette (.-palette r) :name (.-name r)})
(array-seq (.-palettes listed)))])))
(.catch (fn [error] (.catch (fn [error]
(rf/dispatch [::failed (or (ex-message error) (str error))])))))) (rf/dispatch [::failed (or (ex-message error) (str error))]))))))
@ -308,7 +342,8 @@
(rf/reg-event-db (rf/reg-event-db
::symbols-listed ::symbols-listed
(fn [db [_ rows]] (assoc db :assets {:symbols rows :loading? false}))) (fn [db [_ rows palettes]]
(assoc db :assets {:symbols rows :palettes palettes :loading? false})))
(rf/reg-sub ::assets (fn [db _] (:assets db))) (rf/reg-sub ::assets (fn [db _] (:assets db)))
@ -649,6 +684,70 @@
(update-in c [:symbols sid] dissoc key) (update-in c [:symbols sid] dissoc key)
(assoc-in c [:symbols sid key] value))))))) (assoc-in c [:symbols sid key] value)))))))
(rf/reg-event-db
::new-palette
(fn [db _]
(let [id (random-uuid)
p {:id id :name "Untitled palette"
:slots (mapv #(select-keys % [:name :hex]) (:slots pal/default-palette))}]
(-> (edit/edit db #(assoc-in % [:palettes id] p))
(assoc-in [:ui :palette] id)))))
(rf/reg-event-db
::palette-name
(fn [db [_ id value]]
(let [value (str/trim (str value))]
(if (str/blank? value) db
(edit/edit db #(assoc-in % [:palettes id :name] value))))))
(rf/reg-event-db
::palette-color
(fn [db [_ id slot hex]]
(if-not (re-matches #"#[0-9a-fA-F]{6}" (str hex))
db
(edit/edit db #(assoc-in % [:palettes id :slots slot :hex] (str/lower-case hex))))))
(rf/reg-event-db
::default-palette
(fn [db [_ id]]
(edit/edit db #(if (get-in % [:palettes id]) (assoc % :default-palette id) %))))
(rf/reg-event-db
::symbol-palette
(fn [db [_ sid id frame]]
(edit/edit db #(if id
(let [old (get-in % [:symbols sid :palette])]
(assoc-in % [:symbols sid :palette]
(if (:keys old)
(assoc-in old [:keys frame] id)
(ch/framed id))))
(update-in % [:symbols sid] dissoc :palette)))))
(rf/reg-event-db
::key-symbol-palette
(fn [db [_ sid frame id]]
(edit/edit db
(fn [c]
(let [old (get-in c [:symbols sid :palette])
base (or (when old (ch/value-at old frame nil))
(pal/default-palette-id c))
keyed (if (:keys old) old (ch/keyed {0 base} :hold))]
(assoc-in c [:symbols sid :palette]
(assoc-in keyed [:keys frame] id)))))))
(rf/reg-event-db
::instance-palette
(fn [db [_ sid node-id frame id]]
(edit/edit db
(fn [c]
(if-not id
(update-in c [:symbols sid :nodes node-id] dissoc :palette)
(let [old (get-in c [:symbols sid :nodes node-id :palette])]
(assoc-in c [:symbols sid :nodes node-id :palette]
(if (:keys old)
(assoc-in old [:keys frame] id)
(ch/framed id)))))))))
(rf/reg-event-db (rf/reg-event-db
::set-channel ::set-channel
;; `frame` is the node's own, as for a drawing key. ;; `frame` is the node's own, as for a drawing key.

View file

@ -38,6 +38,7 @@
[arthur.domain.geom :as geom] [arthur.domain.geom :as geom]
[arthur.domain.node :as node] [arthur.domain.node :as node]
[arthur.domain.pick :as pick] [arthur.domain.pick :as pick]
[arthur.domain.palette :as pal]
[arthur.domain.ring :as ring] [arthur.domain.ring :as ring]
[arthur.domain.trace :as trace] [arthur.domain.trace :as trace]
[arthur.flow.address :as address])) [arthur.flow.address :as address]))
@ -464,12 +465,12 @@
{:nodes {:mouth {:nodes {:mouth
{:id :mouth :name "mouth" :kind :poly :parent :head :z "a1" {:id :mouth :name "mouth" :kind :poly :parent :head :z "a1"
:channels {[:geom :pts] (dense rings 0 (prov :roto/lips-outer)) :channels {[:geom :pts] (dense rings 0 (prov :roto/lips-outer))
[:style :color] (ch/framed :skin-dark)}} [:style :color] (ch/framed 2)}}
:mouth-in :mouth-in
{:id :mouth-in :name "mouth interior" :kind :poly {:id :mouth-in :name "mouth interior" :kind :poly
:parent :mouth :z "a2" :parent :mouth :z "a2"
:channels {[:geom :pts] (dense rings 1 (prov :roto/lips-inner)) :channels {[:geom :pts] (dense rings 1 (prov :roto/lips-inner))
[:style :color] (ch/framed :mouth-dark) [:style :color] (ch/framed 3)
[:vis] (visibility params inputs [:vis] (visibility params inputs
{:by :roto/mouth-aperture {:by :roto/mouth-aperture
:analysis (:id analysis) :analysis (:id analysis)
@ -523,13 +524,13 @@
{:id id :name (clojure.core/name id) :kind :poly :parent :head :z z {:id id :name (clojure.core/name id) :kind :poly :parent :head :z z
:channels {[:geom :pts] (dense eye-block track :channels {[:geom :pts] (dense eye-block track
(provenance :roto/eyelid {:verts eye-verts})) (provenance :roto/eyelid {:verts eye-verts}))
[:style :color] (ch/framed :skin-dark)}}) [:style :color] (ch/framed 2)}})
inner-node (fn [id parent z track shut] inner-node (fn [id parent z track shut]
{:id id :name (clojure.core/name id) :kind :poly :parent parent :z z {:id id :name (clojure.core/name id) :kind :poly :parent parent :z z
:channels {[:geom :pts] (dense eye-block track :channels {[:geom :pts] (dense eye-block track
(provenance :roto/eye-opening (provenance :roto/eye-opening
{:verts eye-verts})) {:verts eye-verts}))
[:style :color] (ch/framed :eye-white) [:style :color] (ch/framed 5)
[:vis] (keyed-visibility (mapv not shut) [:vis] (keyed-visibility (mapv not shut)
(provenance :roto/blink nil))}}) (provenance :roto/blink nil))}})
iris-node (fn [id parent track radius] iris-node (fn [id parent track radius]
@ -541,7 +542,7 @@
:generated :generated
(provenance :roto/iris-size (provenance :roto/iris-size
{:iris-size (:iris-size params)})) {:iris-size (:iris-size params)}))
[:style :color] (ch/framed :iris)}}) [:style :color] (ch/framed 6)}})
pupil-node (fn [id parent] pupil-node (fn [id parent]
{:id id :name (clojure.core/name id) :kind :rect :parent parent :z "a1" {:id id :name (clojure.core/name id) :kind :rect :parent parent :z "a1"
:stencil parent :stencil parent
@ -549,14 +550,14 @@
:generated :generated
(provenance :roto/pupil-size (provenance :roto/pupil-size
{:pupil-size (:pupil-size params)})) {:pupil-size (:pupil-size params)}))
[:style :color] (ch/framed :pupil)}}) [:style :color] (ch/framed 7)}})
brow-node (fn [id z track] brow-node (fn [id z track]
{:id id :name (clojure.core/name id) :kind :poly :parent :head :z z {:id id :name (clojure.core/name id) :kind :poly :parent :head :z z
:channels {[:geom :pts] (dense brow-block track :channels {[:geom :pts] (dense brow-block track
(provenance :roto/brow {:verts brow-verts})) (provenance :roto/brow {:verts brow-verts}))
[:xform :pos] (dense brow-pos-block track [:xform :pos] (dense brow-pos-block track
(provenance :roto/brow-raise nil)) (provenance :roto/brow-raise nil))
[:style :color] (ch/framed :brow)}})] [:style :color] (ch/framed 8)}})]
{:nodes (merge {:nodes (merge
(when eyes (when eyes
{:eye-r (eye-node :eye-r "a2" 0) {:eye-r (eye-node :eye-r "a2" 0)
@ -604,7 +605,7 @@
:parent :mouth-in :z "a1" :parent :mouth-in :z "a1"
:stencil :mouth-in :stencil :mouth-in
:channels {[:geom :pts] (dense blk 0 generated) :channels {[:geom :pts] (dense blk 0 generated)
[:style :color] (ch/framed :teeth) [:style :color] (ch/framed 4)
[:vis] (keyed-visibility shown generated)}}} [:vis] (keyed-visibility shown generated)}}}
:store (stored blk)})) :store (stored blk)}))
@ -763,6 +764,8 @@
merged (fn [k] (into {} (mapcat (comp k second)) parts)) merged (fn [k] (into {} (mapcat (comp k second)) parts))
built {:name name :fps fps :analysis (:analysis params) built {:name name :fps fps :analysis (:analysis params)
:width (first stage) :height (second stage) :width (first stage) :height (second stage)
:palettes {pal/default-id pal/default-palette}
:default-palette pal/default-id
:subjects (into {} (map (fn [[id _]] [id {:id id :params {}}])) ordered) :subjects (into {} (map (fn [[id _]] [id {:id id :params {}}])) ordered)
:features (merged :features) :groups (merged :groups) :features (merged :features) :groups (merged :groups)
:symbols :symbols

View file

@ -118,21 +118,13 @@
(rf/reg-sub (rf/reg-sub
::palette ::palette
(fn [db _] :<- [::clip]
;; A NAME resolves to a ramp. One today; when timelines carry a `:palette` (fn [clip _] (when clip (pal/compile clip))))
;; channel this becomes the project's table and the walk carries the ramp in
;; scope, which is why domain/symbol takes the palette as a parameter rather
;; than reaching for a global.
(get {:arthur/default pal/index-of} (:palette db) pal/index-of)))
(rf/reg-sub (rf/reg-sub
::ramp ::ramp
(fn [db _] :<- [::palette]
;; index -> [r g b]. The other half of the palette: `::palette` says which (fn [compiled _] (:ramp compiled)))
;; INDEX a tone resolves to, this says what that index LOOKS LIKE. Two subs
;; because two different consumers — evaluation needs the first, the blit
;; needs the second, and neither wants the other's map.
(get {:arthur/default pal/rgb} (:palette db) pal/rgb)))
(rf/reg-sub (rf/reg-sub
::store ::store

View file

@ -12,6 +12,17 @@
[re-frame.core :as rf])) [re-frame.core :as rf]))
(rf/reg-sub ::selection (fn [db _] (get-in db [:ui :selection]))) (rf/reg-sub ::selection (fn [db _] (get-in db [:ui :selection])))
(rf/reg-sub
::selections
(fn [db _]
(let [primary (get-in db [:ui :selection])
many (vec (get-in db [:ui :selections]))]
;; Older structural commands replace `:selection` directly. Treat that as
;; an intentional single selection unless it is still the primary member
;; of the multi-selection.
(cond (nil? primary) []
(some #{primary} many) many
:else [primary]))))
(rf/reg-sub ::target (fn [db _] (get-in db [:ui :target]))) (rf/reg-sub ::target (fn [db _] (get-in db [:ui :target])))
(rf/reg-sub ::retry (fn [db _] (get-in db [:ui :retry]))) (rf/reg-sub ::retry (fn [db _] (get-in db [:ui :retry])))
(rf/reg-sub ::tone (fn [db _] (get-in db [:ui :tone]))) (rf/reg-sub ::tone (fn [db _] (get-in db [:ui :tone])))
@ -100,6 +111,23 @@
(when-let [pl (nest/placement clip st open (or path [id]) f)] (when-let [pl (nest/placement clip st open (or path [id]) f)]
(assoc pl :node n :bounds ((pick/bounds-of clip st (:sid pl) n) (:frame pl)))))))) (assoc pl :node n :bounds ((pick/bounds-of clip st (:sid pl) n) (:frame pl))))))))
(rf/reg-sub
::selected-placements
:<- [::selections]
:<- [::render/clip]
:<- [::render/clip-id]
:<- [::render/open]
:<- [::render/open-frame]
(fn [[selections clip clip-id open f] _]
(let [st (:store (store/entry clip-id))]
(into [] (keep (fn [[_ _ _ path :as selection]]
(when (and clip (= :node (first selection)) (seq path))
(when-let [{:keys [sid id] :as pl} (nest/placement clip st open path f)]
(when-let [n (get-in clip [:symbols sid :nodes id])]
(assoc pl :selection selection :path path :node n
:bounds ((pick/bounds-of clip st sid n) (:frame pl))))))))
selections))))
(rf/reg-sub (rf/reg-sub
::settled-clip ::settled-clip
:<- [::render/clip-id] :<- [::render/clip-id]

View file

@ -2,52 +2,73 @@
"The bar above the stage: the tone a new shape gets, and the tool that makes "The bar above the stage: the tone a new shape gets, and the tool that makes
one. one.
SIXTEEN SLOTS, and the palette supplies nine of them. The count is the format's Shapes store only the selected local slot number. Palette identity is supplied
and not the data's — an indexed 320x200 picture in the Animator Pro idiom this by the symbol tree, and the colour input edits the project palette asset.
tool inherits has a fixed-size table, and a strip that grew and shrank as tones
were added would make the palette look like a list of colours rather than like
a table with room in it. So the empty slots are drawn, hatched, and refuse the
click.
Slot 0 is the background, which is why it is shown and not selectable: a
polygon filled with index 0 is invisible against a stage cleared to index 0, so
offering it as a fill is offering a shape that vanishes on creation.
The footage switch is NOT here. It is a viewing aid rather than something you 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 — one that is there set before you draw, so it is a section of the inspector — one that is there
for whatever the open symbol has faces of, so it still does not come and go for whatever the open symbol has faces of, so it still does not come and go
with the selection. See `ui/params`." with the selection. See `ui/params`."
(:require [arthur.domain.palette :as pal] (:require [arthur.domain.palette :as pal]
[arthur.domain.node :as node]
[arthur.events.ui :as ui] [arthur.events.ui :as ui]
[arthur.events.project :as project]
[arthur.subs.render :as render]
[arthur.subs.ui :as sub] [arthur.subs.ui :as sub]
[arthur.ui.layout :as layout] [arthur.ui.layout :as layout]
[re-frame.core :as rf])) [re-frame.core :as rf]))
(def ^:const slots 16) (rf/reg-sub ::chosen (fn [db _] (get-in db [:ui :palette])))
(defn- swatch [i tone] (defn- swatch [pid i {:keys [name hex]} tone placements]
(let [{slot-tone :name :keys [hex]} (get pal/entries i) (let [picker-id (str "palette-picker-" pid "-" i)]
bg? (zero? i) [:div {:key i :class "palette-slot"}
pick (and slot-tone (not bg?))] [:button {:class (str "swatch" (when (= i tone) " on"))
[:button :style {:background hex}
{:key i :title (str i (when name (str " · " (clojure.core/name name)))
:class (str "swatch" " · " hex " · double-click to edit")
(when-not slot-tone " empty") :on-click #(do (rf/dispatch [::ui/set-tone i])
(when bg? " bg") (let [edits (into [] (keep (fn [{:keys [sid id node frame]}]
(when (and pick (= slot-tone tone)) " on")) (when (contains? (get node/valid-paths (:kind node))
:style (when hex {:background hex}) [:style :color])
:title (if slot-tone (str i " · " (name slot-tone) " " hex) (str i " · empty")) {:sid sid :id id :path [:style :color]
:disabled (not pick) :frame frame :value i})))
:on-click #(rf/dispatch [::ui/set-tone slot-tone])}])) placements)]
(when (seq edits)
(rf/dispatch [::project/set-channels edits]))))
:on-double-click (fn [e]
(.preventDefault e)
(some-> (js/document.getElementById picker-id) (.click)))}]
[:input {:id picker-id :class "palette-picker" :type "color" :value hex
:aria-label (str "edit palette slot " i)
:on-change #(rf/dispatch [::project/palette-color pid i (.. % -target -value)])}]]))
(defn bar [] (defn bar []
(let [tone @(rf/subscribe [::sub/tone]) (let [tone @(rf/subscribe [::sub/tone])
clip @(rf/subscribe [::render/clip])
chosen @(rf/subscribe [::chosen])
pid (if (contains? (:palettes clip) chosen) chosen (pal/default-palette-id clip))
palette (get (pal/palettes clip) pid)
placements @(rf/subscribe [::sub/selected-placements])
selections @(rf/subscribe [::sub/selections])
tool @(rf/subscribe [::sub/tool]) tool @(rf/subscribe [::sub/tool])
auto? @(rf/subscribe [::sub/auto-key?]) auto? @(rf/subscribe [::sub/auto-key?])
draft @(rf/subscribe [::sub/draft])] draft @(rf/subscribe [::sub/draft])]
[:div.palette-bar [:div.palette-bar
[:div.swatches (doall (map #(swatch % tone) (range slots)))] [:select {:value (str pid)
[:span.dim (name tone)] :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 [:arthur.ui.palette/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])} "+"]
[:div.swatches (doall (map-indexed #(swatch pid %1 %2 tone placements) (:slots palette)))]
[:span.dim (str tone)]
(when (> (count selections) 1)
[:span.dim (str (count selections) " selected")])
[:span {:style {:flex 1}}] [:span {:style {:flex 1}}]
[:button.auto-key {:class (when auto? "on") [:button.auto-key {:class (when auto? "on")
:aria-pressed auto? :aria-pressed auto?
@ -62,8 +83,12 @@
[:button {:disabled (< (count draft) 6) [:button {:disabled (< (count draft) 6)
:on-click #(rf/dispatch [::ui/finish-polygon])} "finish"] :on-click #(rf/dispatch [::ui/finish-polygon])} "finish"]
[:button {:on-click #(rf/dispatch [::ui/cancel-polygon])} "cancel"]] [:button {:on-click #(rf/dispatch [::ui/cancel-polygon])} "cancel"]]
[:button {:on-click #(rf/dispatch [::ui/begin-polygon])} "polygon"]) [:button {:title "pen tool — click points on the stage"
:on-click #(rf/dispatch [::ui/begin-polygon])} "pen"])
;; The stage's zoom, at the right end of the bar above the stage: it is a ;; The stage's zoom, at the right end of the bar above the stage: it is a
;; property of the view and not of the document, so it sits in the view's ;; property of the view and not of the document, so it sits in the view's
;; own chrome rather than in the inspector. ;; own chrome rather than in the inspector.
[layout/zoomer :stage "the stage"]])) [layout/zoomer :stage "the stage"]]))
(rf/reg-event-db :arthur.ui.palette/select
(fn [db [_ id]] (assoc-in db [:ui :palette] id)))

View file

@ -11,6 +11,7 @@
[arthur.domain.channel :as channel] [arthur.domain.channel :as channel]
[arthur.domain.feature :as feature] [arthur.domain.feature :as feature]
[arthur.domain.node :as node] [arthur.domain.node :as node]
[arthur.domain.palette :as pal]
[arthur.domain.paint :as paint] [arthur.domain.paint :as paint]
[arthur.domain.params :as params] [arthur.domain.params :as params]
[arthur.domain.pose :as pose] [arthur.domain.pose :as pose]
@ -201,7 +202,10 @@
(defn- node-section [[sid id n]] (defn- node-section [[sid id n]]
(let [[start end] (:span n) (let [[start end] (:span n)
auto-key? @(rf/subscribe [::sub/auto-key?])] auto-key? @(rf/subscribe [::sub/auto-key?])
clip @(rf/subscribe [::render/clip])
local @(rf/subscribe [::sub/selected-local])
frame (:frame local)]
[section (str (name (:kind n)) " · in " (name sid)) [section (str (name (:kind n)) " · in " (name sid))
[facts [facts
"name" (or (:name n) (brief id)) "name" (or (:name n) (brief id))
@ -214,8 +218,18 @@
"span" (when start (str start " … " end)) "span" (when start (str start " … " end))
"at" (when (node/mapped-time? n) (str (get-in n [:time :at] 0)))] "at" (when (node/mapped-time? n) (str (get-in n [:time :at] 0)))]
(when (:paint? n) [drawing-keys sid id n @(rf/subscribe [::sub/selected-local])]) (when (:paint? n) [drawing-keys sid id n @(rf/subscribe [::sub/selected-local])])
(when (= :instance (:kind n))
[:label.inspector-field "palette override"
[:select {:value (str (or (some-> (:palette n) (channel/value-at frame nil)) ""))
:on-change (fn [e]
(let [v (.. e -target -value)
pid (first (filter #(= v (str %)) (keys (pal/palettes clip))))]
(rf/dispatch [::project/instance-palette sid id frame pid])))}
[:option {:value ""} "inherit"]
(for [[pid p] (sort-by (comp str :name val) (pal/palettes clip))]
^{:key (str pid)} [:option {:value (str pid)} (:name p)])]])
[:div.row {:style {:margin-top "6px"}} [:span.dim "channels"]] [:div.row {:style {:margin-top "6px"}} [:span.dim "channels"]]
(let [{:keys [frame]} @(rf/subscribe [::sub/selected-local])] (let [{:keys [frame]} local]
[:dl.facts [:dl.facts
(doall (doall
(for [[path ch] (sort-by (comp str key) (node/channels n))] (for [[path ch] (sort-by (comp str key) (node/channels n))]
@ -527,6 +541,9 @@
(defn- symbol-section [sid] (defn- symbol-section [sid]
(let [clip @(rf/subscribe [::render/clip]) (let [clip @(rf/subscribe [::render/clip])
sym (get-in clip [:symbols sid]) sym (get-in clip [:symbols sid])
frame @(rf/subscribe [::render/open-frame])
palette-value (or (some-> (:palette sym) (channel/value-at frame nil))
(pal/default-palette-id clip))
busy? (:busy? @(rf/subscribe [::playback/project]))] busy? (:busy? @(rf/subscribe [::playback/project]))]
[section "symbol" [section "symbol"
[facts [facts
@ -546,6 +563,23 @@
#(rf/dispatch [::project/symbol-setting sid :width %]) "project default" busy?] #(rf/dispatch [::project/symbol-setting sid :width %]) "project default" busy?]
[number-field "height" (:height sym) [number-field "height" (:height sym)
#(rf/dispatch [::project/symbol-setting sid :height %]) "project default" busy?] #(rf/dispatch [::project/symbol-setting sid :height %]) "project default" busy?]
[:label.inspector-field "palette"
[:select {:value (str palette-value) :disabled busy?
:on-change (fn [e]
(let [v (.. e -target -value)
id (first (filter #(= v (str %)) (keys (pal/palettes clip))))]
(rf/dispatch [::project/symbol-palette sid id frame])))}
[:option {:value ""} "inherit"]
(for [[id p] (sort-by (comp str :name val) (pal/palettes clip))]
^{:key (str id)} [:option {:value (str id)} (:name p)])]]
[:div.row
[:button {:disabled busy?
:title "hold this palette from this frame"
:on-click #(rf/dispatch [::project/key-symbol-palette sid frame palette-value])}
"◆ key palette"]
[:button {:disabled (or busy? (nil? (:palette sym)))
:on-click #(rf/dispatch [::project/symbol-palette sid nil])}
"inherit"]]
(when (or (:width sym) (:height sym)) (when (or (:width sym) (:height sym))
[:div.row [:div.row
[:button {:disabled busy? :on-click #(do [:button {:disabled busy? :on-click #(do

View file

@ -45,6 +45,7 @@
footage stay separate records on the server, so dropping the same file twice footage stay separate records on the server, so dropping the same file twice
does not decode it twice." does not decode it twice."
(:require [arthur.domain.clip :as clip] (:require [arthur.domain.clip :as clip]
[arthur.domain.palette :as pal]
[arthur.domain.raster :as raster] [arthur.domain.raster :as raster]
[arthur.events.footage :as footage] [arthur.events.footage :as footage]
[arthur.events.playback :as pb] [arthur.events.playback :as pb]
@ -269,6 +270,23 @@
(carrying (str "symbol:" (subs (str sid) 1)) (carrying (str "symbol:" (subs (str sid) 1))
#(drag/symbol! clip-id sid open)))])) #(drag/symbol! clip-id sid open)))]))
(defn- palette-row [id p default-id chosen rename]
^{:key (str id)}
[row {:label (:name p)
:sub (str (count (:slots p)) " colors")
:title (str (:name p) " · " (count (:slots p)) " indexed colors")
:thumb [:span.thumb {:style {:display "grid"
:grid-template-columns "repeat(4,1fr)"}}
(for [[i s] (map-indexed vector (take 16 (:slots p)))]
^{:key i} [:i {:style {:background (:hex s)}}])]
:on? (= id chosen)
:rename (assoc rename :key [:palette id] :value (:name p)
:commit! (fn [value]
((:begin! rename) nil)
(rf/dispatch [::project/palette-name id value])))
:on-click #(rf/dispatch [:arthur.ui.palette/select id])
:on-double-click #(rf/dispatch [::project/default-palette id])}])
(defn- footage-row [{:keys [id label frames fps video] :as f} chosen rename] (defn- footage-row [{:keys [id label frames fps video] :as f} chosen rename]
^{:key id} ^{:key id}
[row (merge {:label label [row (merge {:label label
@ -327,6 +345,13 @@
#(drag/other! {:kind :import :label name :frames frames #(drag/other! {:kind :import :label name :frames frames
:project pid :cid cid :symbol symbol})))]) :project pid :cid cid :symbol symbol})))])
(defn- import-palette-row [{:keys [project palette name] :as asset}]
^{:key (str project palette)}
[row {:label name :sub "palette"
:title (str name " — click to copy this palette into the project")
:thumb [picture nil]
:on-click #(rf/dispatch [::project/import-palette asset])}])
;; --------------------------------------------------------------------------- ;; ---------------------------------------------------------------------------
;; sections ;; sections
@ -375,21 +400,28 @@
Still not `:main` being special. The document says which symbol that is by its Still not `:main` being special. The document says which symbol that is by its
structure; rename it, place it inside something else, and the pool follows." structure; rename it, place it inside something else, and the pool follows."
[{:keys [document query searching? media sounds chosen rename main] :as ctx}] [{:keys [document query searching? media sounds chosen rename main palette-choice] :as ctx}]
(let [named? #(hit? query (clip/symbol-name document %)) (let [named? #(hit? query (clip/symbol-name document %))
top (when (and main (named? main)) main) top (when (and main (named? main)) main)
rest (filterv #(and (named? %) (not= main %)) rest (filterv #(and (named? %) (not= main %))
(sort-by str (keys (:symbols document)))) (sort-by str (keys (:symbols document))))
media (filterv #(hit? query (:label %)) media) media (filterv #(hit? query (:label %)) media)
sounds (filterv #(hit? query (:label %)) sounds)] sounds (filterv #(hit? query (:label %)) sounds)
palettes (filterv #(hit? query (:name (val %)))
(sort-by (comp str :name val) (pal/palettes document)))]
[sections searching? [sections searching?
(+ (if top 1 0) (count rest) (count media) (count sounds)) (+ (if top 1 0) (count rest) (count media) (count sounds) (count palettes))
[{:title "project" :searching? searching? [{:title "project" :searching? searching?
:blank "nothing to open yet" :blank "nothing to open yet"
:rows (when top [(symbol-row document top ctx)])} :rows (when top [(symbol-row document top ctx)])}
{:title "symbols" :searching? searching? {:title "symbols" :searching? searching?
:blank "nothing else in the library" :blank "nothing else in the library"
:rows (mapv #(symbol-row document % ctx) rest)} :rows (mapv #(symbol-row document % ctx) rest)}
{:title "palettes" :searching? searching?
:blank "no palettes"
:rows (mapv (fn [[id p]] (palette-row id p (:default-palette document)
(or palette-choice (pal/default-palette-id document))
rename)) palettes)}
{:title "media" :searching? searching? {:title "media" :searching? searching?
:blank "drop a video here" :blank "drop a video here"
:rows (mapv #(footage-row % chosen rename) media)} :rows (mapv #(footage-row % chosen rename) media)}
@ -401,21 +433,25 @@
"Everything the server holds. Other projects' symbols stay grouped by project "Everything the server holds. Other projects' symbols stay grouped by project
and closed: a server holds many, and a wall of every symbol in every one buries and closed: a server holds many, and a wall of every symbol in every one buries
the one you want." the one you want."
[{:keys [document query searching? rename chosen available all-sounds symbols [{:keys [document query searching? rename chosen available all-sounds symbols palettes
project-id]}] project-id]}]
(let [media (filterv #(hit? query (:label %)) available) (let [media (filterv #(hit? query (:label %)) available)
sounds (filterv #(hit? query (:label %)) all-sounds) sounds (filterv #(hit? query (:label %)) all-sounds)
others (filterv #(and (not= project-id (:project %)) (hit? query (:name %))) others (filterv #(and (not= project-id (:project %)) (hit? query (:name %)))
symbols) symbols)
grouped (sort-by (comp str second key) (group-by (juxt :project :project-name) others))] grouped (sort-by (comp str second key) (group-by (juxt :project :project-name) others))
palettes (filterv #(and (not= project-id (:project %)) (hit? query (:name %))) palettes)]
[sections searching? [sections searching?
(+ (count media) (count sounds) (count others)) (+ (count media) (count sounds) (count others) (count palettes))
[{:title "media" :searching? searching? [{:title "media" :searching? searching?
:blank "nothing uploaded yet" :blank "nothing uploaded yet"
:rows (mapv #(footage-row % chosen rename) media)} :rows (mapv #(footage-row % chosen rename) media)}
{:title "sounds" :searching? searching? {:title "sounds" :searching? searching?
:blank "no sounds uploaded yet" :blank "no sounds uploaded yet"
:rows (mapv #(sound-row % (:fps document) rename) sounds)} :rows (mapv #(sound-row % (:fps document) rename) sounds)}
{:title "palettes" :searching? searching?
:blank "no palettes in other saved projects"
:rows (mapv import-palette-row palettes)}
{:title "symbols" :searching? searching? {:title "symbols" :searching? searching?
:blank "no other saved projects" :blank "no other saved projects"
:rows (for [[[pid pname] rows] grouped] :rows (for [[[pid pname] rows] grouped]
@ -437,13 +473,15 @@
one cost of collapsing two folders into two tabs — that a hit could be behind one cost of collapsing two folders into two tabs — that a hit could be behind
the tab you did not pick — is paid off by a number, counted over the same the tab you did not pick — is paid off by a number, counted over the same
labels the rows are filtered by." labels the rows are filtered by."
[{:keys [document query media sounds available all-sounds symbols project-id]}] [{:keys [document query media sounds available all-sounds symbols palettes project-id]}]
(let [n (fn [labels] (count (filter #(hit? query %) labels)))] (let [n (fn [labels] (count (filter #(hit? query %) labels)))]
{:project (+ (n (map #(clip/symbol-name document %) (keys (:symbols document)))) {:project (+ (n (map #(clip/symbol-name document %) (keys (:symbols document))))
(n (map :name (vals (pal/palettes document))))
(n (map :label media)) (n (map :label media))
(n (map :label sounds))) (n (map :label sounds)))
:assets (+ (n (map :label available)) :assets (+ (n (map :label available))
(n (map :label all-sounds)) (n (map :label all-sounds))
(n (map :name (remove #(= project-id (:project %)) palettes)))
(n (map :name (remove #(= project-id (:project %)) symbols))))})) (n (map :name (remove #(= project-id (:project %)) symbols))))}))
(defn view [] (defn view []
@ -465,9 +503,10 @@
palette @(rf/subscribe [::render/palette]) palette @(rf/subscribe [::render/palette])
ramp @(rf/subscribe [::render/ramp]) ramp @(rf/subscribe [::render/ramp])
selection @(rf/subscribe [::sub/selection]) selection @(rf/subscribe [::sub/selection])
palette-choice @(rf/subscribe [:arthur.ui.palette/chosen])
open @(rf/subscribe [::render/open]) open @(rf/subscribe [::render/open])
media @(rf/subscribe [::sub/project-footage]) media @(rf/subscribe [::sub/project-footage])
{:keys [symbols]} @(rf/subscribe [::project/assets]) {:keys [symbols palettes]} @(rf/subscribe [::project/assets])
{project-id :id} @(rf/subscribe [::playback/project]) {project-id :id} @(rf/subscribe [::playback/project])
;; The project's videos' sounds first, then its uploaded ones. ;; The project's videos' sounds first, then its uploaded ones.
own-sounds (into (mapv (fn [{:keys [id label frames fps]}] own-sounds (into (mapv (fn [{:keys [id label frames fps]}]
@ -481,7 +520,8 @@
:ramp ramp :selection selection :open open :ramp ramp :selection selection :open open
:media media :sounds own-sounds :chosen chosen :media media :sounds own-sounds :chosen chosen
:available (vec available) :all-sounds (vec sounds) :available (vec available) :all-sounds (vec sounds)
:symbols symbols :project-id project-id :symbols symbols :palettes palettes :project-id project-id
:palette-choice palette-choice
:query needle :searching? searching? :query needle :searching? searching?
;; `clip/opens-on` and not `:main`: the longest symbol nothing ;; `clip/opens-on` and not `:main`: the longest symbol nothing
;; else places is the timeline the work happens in, and it is the ;; else places is the timeline the work happens in, and it is the

View file

@ -288,3 +288,33 @@
(is (= [55 35] (clip/center grown nil :box)) "the symbol's middle moved") (is (= [55 35] (clip/center grown nil :box)) "the symbol's middle moved")
(is (= [25 35] (get-in grown [:symbols :main :nodes u :channels [:xform :anchor] :value])) (is (= [25 35] (get-in grown [:symbols :main :nodes u :channels [:xform :anchor] :value]))
"the instance's did not"))))) "the instance's did not")))))
(deftest palette-context-is-inherited-keyed-and-overridable
(let [palette (fn [id name a b]
{:id id :name name :slots [{:hex a} {:hex b}]})
mark {:id :mark :kind :rect :z "a1"
:channels {[:geom :size] (ch/framed 2)
[:style :color] (ch/framed 1)}}
instance (fn [id child z]
{:id id :kind :instance :z z :source {:symbol child}})
document {:fps 30 :width 20 :height 20
:palettes {:day (palette :day "Day" "#000000" "#112233")
:flash (palette :flash "Flash" "#ffffff" "#aabbcc")}
:default-palette :day
:symbols
{:main {:id :main :frames 4
:palette (ch/keyed {0 :day 2 :flash} :hold)
:nodes {:inherited (instance :inherited :drawing "a1")
:fixed (assoc (instance :fixed :fixed "a2")
:palette (ch/framed :day))}}
:drawing {:id :drawing :frames 4 :nodes {:mark mark}}
:fixed {:id :fixed :frames 4 :palette (ch/framed :flash)
:nodes {:mark mark}}}}
context (pal/compile document)
resolve (clip/resolver document :main nil context nil)
colors #(mapv :color (resolve %))]
(is (= [1 1] (colors 0)) "both children start in the day bank")
(is (= [3 1] (colors 2))
"the inheriting child follows the lightning cut; an instance override wins")
(is (= [17 34 51] (nth (:ramp context) 1)))
(is (= [170 187 204] (nth (:ramp context) 3)))))

View file

@ -800,6 +800,9 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
} }
.swatches { display: flex; gap: 3px; } .swatches { display: flex; gap: 3px; }
.palette-slot { display: flex; align-items: center; }
.palette-picker { display: none; }
.thumb i { display: block; min-width: 1px; min-height: 1px; }
/* Circles. A palette entry is one indivisible tone, not an area of coverage, and /* Circles. A palette entry is one indivisible tone, not an area of coverage, and
a row of dots says that where a row of tiles says "swatch book". */ a row of dots says that where a row of tiles says "swatch book". */