diff --git a/clips/tests/test_api.py b/clips/tests/test_api.py index f01e47a..2d58317 100644 --- a/clips/tests/test_api.py +++ b/clips/tests/test_api.py @@ -416,19 +416,6 @@ class DocumentTests(TestCase): self.assertEqual({str(self.project.id)}, {r["project"] 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): response = self.save() self.assertEqual(200, response.status_code, response.content) diff --git a/clips/views.py b/clips/views.py index 4092a0e..11c785e 100644 --- a/clips/views.py +++ b/clips/views.py @@ -357,7 +357,6 @@ def footage_list(request): _SYMBOL_LEAF = re.compile(r"^clip/([^/]+)/symbol/([^/]+)$") -_PALETTE_LEAF = re.compile(r"^clip/([^/]+)/palette/([^/]+)$") def _transit_fields(value, *keys): @@ -392,21 +391,7 @@ def symbols(request): "frames": fields.get("frames"), }) rows.sort(key=lambda r: (r["project_name"], r["project"], r["name"])) - 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}) + return JsonResponse({"symbols": rows}) @require_http_methods(["GET", "PATCH"]) diff --git a/docs/time.md b/docs/time.md index c0d9205..643dc17 100644 --- a/docs/time.md +++ b/docs/time.md @@ -4,10 +4,6 @@ Project `:fps` is the playback and export grid. Each symbol has its own native `:fps` and `:frames`; keys, spans, trace choices and corrections stay in that native space. A symbol without an explicit rate inherits the document rate; changing project fps first records that rate so its existing timing stays put. -The untouched symbol in a new document is deliberately different: it has no -authored timing to preserve, so it stays on the project grid and its empty frame -extent is rescaled to keep the same duration. This makes changing fps before -authoring establish the editor's grid instead of preserving the 30fps default. An output frame selects the latest native frame at or before its time: `floor(output-frame * native-fps / output-fps)`. Thus 30fps content in a 12fps diff --git a/frontend/src/arthur/db.cljs b/frontend/src/arthur/db.cljs index b53f3c8..bba47ec 100644 --- a/frontend/src/arthur/db.cljs +++ b/frontend/src/arthur/db.cljs @@ -82,7 +82,7 @@ ;; 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 ;; it is dropped. - :assets {:symbols [] :palettes [] :loading? false} + :assets {:symbols [] :loading? false} ;; 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 @@ -159,8 +159,7 @@ :ui {:open nil :tabs [] :selection nil - :selections [] - :tone 1 + :tone :skin-base :tool nil :auto-key? false :draft [] diff --git a/frontend/src/arthur/domain/clip.cljs b/frontend/src/arthur/domain/clip.cljs index f313b7b..5f5f807 100644 --- a/frontend/src/arthur/domain/clip.cljs +++ b/frontend/src/arthur/domain/clip.cljs @@ -51,8 +51,7 @@ 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 `leaf/clip` in the same commit." - #{:name :fps :analysis :subjects :features :groups :width :height :symbols - :palettes :default-palette}) + #{:name :fps :analysis :subjects :features :groups :width :height :symbols}) (defn symbol "One of the clip's symbols, by id." @@ -72,25 +71,12 @@ (defn fps [clip sid] (or (:fps (symbol clip sid)) (:fps clip))) (defn set-fps - "Change the output grid without rewriting authored content's frames. - - A new document's empty symbol is the one exception: it has no native rate yet, - so it follows the project grid and its empty extent is rescaled to preserve its - duration. Once a symbol contains anything, changing the project rate records - the old effective rate on it before changing the output grid." + "Change the output grid without rewriting any content's frames." [clip rate] - (let [old (:fps clip)] - (-> clip - (update :symbols - #(into {} - (map (fn [[sid sym]] - [sid (cond - (:fps sym) sym - (empty? (:nodes sym)) - (update sym :frames cadence/frames rate old) - :else (assoc sym :fps old))])) - %)) - (assoc :fps rate)))) + (-> clip + (update :symbols #(into {} (map (fn [[sid sym]] + [sid (assoc sym :fps (fps clip sid))])) %)) + (assoc :fps rate))) (defn output-frames [clip sid] (cadence/frames (frames clip sid) (:fps clip) (fps clip sid))) @@ -179,18 +165,6 @@ (first (sort-by (fn [sid] [(- (or (frames clip sid) 0)) (str sid)]) (unplaced clip)))) -(defn set-root-fps - "Set the document/output rate and the root symbol's editing rate together. - - Project FPS is the root timeline's clock. Nested symbols keep their own native - rates and are sampled when placed across that boundary; only the root changes - here. Frame numbers are authored positions, so changing the rate does not - rewrite them or silently move cuts and keys." - [clip rate] - (let [root (opens-on clip)] - (cond-> (assoc clip :fps rate) - root (assoc-in [:symbols root :fps] rate)))) - (def ^:const blank-frames "How long a new document is before anything says otherwise. Four seconds at 30, which is long enough to key something into and short enough to scrub by hand." @@ -214,11 +188,7 @@ {:name "untitled" :fps 30 :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 - ;; and generated symbols carry their own rate explicitly. - :symbols {:main {:id :main :frames blank-frames :nodes {}}}}) + :symbols {:main {:id :main :fps 30 :frames blank-frames :nodes {}}}}) (defn- transform-op "Put a symbol's already resolved mark into its instance's parent space. Its @@ -248,27 +218,15 @@ Every instance owns its cursors and buffers. The IResolver queries return native node frames and world matrices for the last rendered output frame." [clip sid store palette opts] - (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] + (letfn [(build [sid chain pose-tracks] (when (some #{sid} chain) (throw (ex-info "symbol cycle" {:chain (conj chain sid)}))) (let [sym (or (symbol clip sid) (throw (ex-info "an instance names a missing symbol" {:symbol sid}))) - active (volatile! (when context? (:default palette))) nodes (:nodes sym) rank (symbol/draw-rank nodes (symbol/order nodes)) ids (sort-by rank (keys nodes)) - own (symbol/resolver sym store (if context? - #(pal/render-index palette @active %) - palette) + own (symbol/resolver sym store palette (assoc opts :pose-tracks pose-tracks)) ;; Each cel owns its source resolver and mutable buffers. children (into {} @@ -283,10 +241,7 @@ ;; there used to be. Their resolvers still hold the frame ;; before whenever they were not on. entered (volatile! {}) - step (fn [f pre inherited forced] - (when context? - (vreset! active (or forced - (selection-at sym (js/Math.floor f) inherited)))) + step (fn [f pre] (vreset! entered {}) (let [by-id (into {} (map (juxt :node identity)) (own (js/Math.floor f) (js/Math.floor pre)))] @@ -319,18 +274,14 @@ ;; previous frame, and so no gap. (or (:frame (when (number? prior) (placed-frame clip sid n prior))) - (dec frame)) - @active - (when (and context? (:palette n)) - (selection-at n frame nil))))) + (dec frame))))) [])) (when-let [op (get by-id id)] [op])))) ids))))] (reify IFn - (-invoke [_ f] (step f (dec f) nil nil)) - (-invoke [_ f pre] (step f pre nil nil)) - (-invoke [_ f pre inherited forced] (step f pre inherited forced)) + (-invoke [_ f] (step f (dec f))) + (-invoke [_ f pre] (step f pre)) symbol/IResolver (world-of [_ [id & more]] (if more @@ -367,13 +318,11 @@ ;; 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 ;; group, so it is seated where groups exist. - (if (pos? f) (cadence/frame (dec f) grid native) -1) - (when context? (:default palette)) - nil)) + (if (pos? f) (cadence/frame (dec f) grid native) -1))) symbol/IResolver (world-of [_ path] (symbol/world-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 "The middle of everything symbol `sid` draws, over all its frames, in its own @@ -523,14 +472,6 @@ (str "clip has a field with no leaf to save it in: " (pr-str k))) (when-not (map? (:symbols clip)) [":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)))) [(str ":fps is " (pr-str (:fps clip)) " — a rate is a positive number")]) (for [[id sym] (:symbols clip) diff --git a/frontend/src/arthur/domain/gesture.cljs b/frontend/src/arthur/domain/gesture.cljs index 9850dff..7da71bd 100644 --- a/frontend/src/arthur/domain/gesture.cljs +++ b/frontend/src/arthur/domain/gesture.cljs @@ -29,19 +29,14 @@ xy #(let [v (at %)] [(ch/component v 0) (ch/component v 1)])] {:pos (xy :pos) :rot (at :rot) :scale (xy :scale) :anchor (xy :anchor)})) -(defn- measured-channel? [n path] - (let [c (get-in n [:channels path])] - (boolean (or (:dense c) (:generated c))))) - (defn refusal - "Why `kind` cannot transform node `n`, or nil. Measured position and rotation - accept authored correction layers; measured scale cannot yet be decomposed - safely. The one-argument form asks whether any stage gesture is possible." - ([n] (when (node/measured? n) - "its transform is measured — use a correction or place the instance it is in")) - ([n kind] - (when (and (= :scale kind) (measured-channel? n [:xform :scale])) - "its scale is measured — place the instance it is in"))) + "Why node `n`'s transform cannot be set by hand, or nil when it can. A + measured transform is regenerated from the footage, and writing a value over + it would throw the measurement away." + [n] + (cond + (node/measured? n) + "its transform is measured — place the instance it is in")) (defn- through [m [x y]] (let [out (js/Float64Array. 2)] @@ -81,57 +76,24 @@ nil)] {[:xform :scale] (if r (mapv #(* r %) s) (mapv * s (map k a b)))}))) -(def ^:private manual-layer ::manual-transform) - -(defn- plus [a b] - (if (number? a) (+ a b) (mapv + (vec a) (vec b)))) - -(defn- minus [a b] - (if (number? a) (- a b) (mapv - (vec a) (vec b)))) - -(defn- corrected-channel - "Set the effective value of a measured channel without replacing its base." - [channel f target store extent auto-key?] - (let [current (ch/value-at channel f store) - layers (vec (:over channel)) - i (first (keep-indexed #(when (= manual-layer (:id %2)) %1) layers)) - old (when i (ch/value-at (get-in layers [i :values]) f store)) - zero (if (number? target) 0 (vec (repeat (count target) 0))) - value (plus (or old zero) (minus target current)) - values (or (when i (get-in layers [i :values])) (ch/framed zero)) - values ((if auto-key? node/set-keyed-channel node/set-channel) - {:kind :group :channels {[:xform :pos] values}} - [:xform :pos] f value) - values (get-in values [:channels [:xform :pos]]) - layer (ch/layer manual-layer [0 extent] :offset values)] - (assoc channel :over (if i (assoc layers i layer) (conj layers layer))))) - (defn apply-values "Clip with channel values `vs`, `{path value}`, written into node `id` of symbol `sid` on the node's own frame `f`, by the keying rule above. The sixth argument arms auto-key; the five-argument form retains the normal rule." - ([clip sid id f vs] (apply-values clip sid id f vs false nil)) - ([clip sid id f vs auto-key?] (apply-values clip sid id f vs auto-key? nil)) - ([clip sid id f vs auto-key? store] + ([clip sid id f vs] (apply-values clip sid id f vs false)) + ([clip sid id f vs auto-key?] (let [put (if auto-key? node/set-keyed-channel node/set-channel)] (update-in clip [:symbols sid :nodes id] - #(reduce-kv - (fn [n path v] - (if (measured-channel? n path) - (update-in n [:channels path] corrected-channel f v store - (get-in clip [:symbols sid :frames]) auto-key?) - (put n path f v))) - % vs))))) + #(reduce-kv (fn [n path v] (put n path f v)) % vs))))) (defn apply-take "Apply a buffered performance take. `take` is keyed by `[symbol node]`, then local frame, then channel path. It becomes ordinary authored keys in one document edit rather than making the edit pipeline run for every sample." - ([clip take] (apply-take clip take nil)) - ([clip take store] - (reduce-kv - (fn [c [sid id] frames] - (reduce-kv (fn [c f values] - (apply-values c sid id f values true store)) - c frames)) - clip take))) + [clip take] + (reduce-kv + (fn [c [sid id] frames] + (reduce-kv (fn [c f values] + (apply-values c sid id f values true)) + c frames)) + clip take)) diff --git a/frontend/src/arthur/domain/leaf.cljs b/frontend/src/arthur/domain/leaf.cljs index 49f0f7b..8fe84f1 100644 --- a/frontend/src/arthur/domain/leaf.cljs +++ b/frontend/src/arthur/domain/leaf.cljs @@ -10,8 +10,6 @@ clip//name a label clip//timing fps clip//stage width, height - clip//palette/ a named indexed palette asset - clip//palette-default the project fallback palette id clip//source the analysis record this came out of clip//subject/ a tracked subject and its params clip//feature/ one feature: area, nodes, params @@ -136,13 +134,11 @@ (some-leaf (at "name") (select-keys clip [:name])) (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 "source") (:analysis clip)) (concat (for [[id v] (:subjects clip)] {(at "subject" (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] (:palettes clip)] {(at "palette" (segment id)) v}) ;; 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 ;; disagree with itself about; `clip` puts it back. @@ -194,8 +190,6 @@ "timing" (merge acc v) "stage" (merge acc 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) "feature" (assoc-in acc [:features (unsegment a)] v) "group" (assoc-in acc [:groups (unsegment a)] v) @@ -236,8 +230,8 @@ false) (case (count p) ;; The clip's own facts carry no id. - 3 (#{"name" "timing" "stage" "source" "palette-default"} (nth p 2)) - 4 (#{"subject" "feature" "group" "palette"} (nth p 2)) + 3 (#{"name" "timing" "stage" "source"} (nth p 2)) + 4 (#{"subject" "feature" "group"} (nth p 2)) false))))] (vec (concat diff --git a/frontend/src/arthur/domain/nest.cljs b/frontend/src/arthur/domain/nest.cljs index 210cca7..8558ae8 100644 --- a/frontend/src/arthur/domain/nest.cljs +++ b/frontend/src/arthur/domain/nest.cljs @@ -454,7 +454,7 @@ ;; a boundary can be rolled depend on which of them `some` reaches ;; first. `finish` does the same `problems` check and the overlap one ;; too, so this is less code and one fewer invariant to remember. - (span/claim clip (:sid here) moved id :grow-symbol (random-uuid)))))) + (span/finish clip (:sid here) moved id :grow-symbol))))) (defn resize-out "Move the right edge of the node at `path` by `df` frames of `open`. diff --git a/frontend/src/arthur/domain/palette.cljs b/frontend/src/arthur/domain/palette.cljs index d6cdbf4..101496e 100644 --- a/frontend/src/arthur/domain/palette.cljs +++ b/frontend/src/arthur/domain/palette.cljs @@ -1,12 +1,15 @@ (ns arthur.domain.palette - "Project palette assets and their render-time index banks. + "The indexed palette. - Drawing data stores a dumb LOCAL SLOT NUMBER. A symbol or instance supplies - the palette context. `compile` gives project palettes disjoint ranges in the - raster index space, so differently-paletted subtrees can coexist. The ranges - are derived, never persisted: adding a palette never rewrites drawing data.") + THE RULE, and it is a rule rather than a default: a part carries a palette + INDEX, never a sampled RGB value. Sampling colour off the footage produces a + pixel-art filter, and it does so irrecoverably — once a shape holds a measured + colour there is no way back to an authored one, because the information that it + was ever a choice is gone. Every `[:style :color]` channel holds one of the + keywords below. -(def default-id :arthur/default) + Entries are ordered, and the order IS the index the raster writes. Inserting in + the middle renumbers every stored index, so new tones append.") (def entries [{:name :bg :hex "#12141c"} @@ -45,68 +48,3 @@ (def rgb "Index -> [r g b], precomputed." (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))) diff --git a/frontend/src/arthur/domain/pick.cljs b/frontend/src/arthur/domain/pick.cljs index 8d0abff..74ff3a0 100644 --- a/frontend/src/arthur/domain/pick.cljs +++ b/frontend/src/arthur/domain/pick.cljs @@ -55,31 +55,6 @@ [ops [x y]] (some #(when (on? % x y) (path-of %)) (rseq (vec ops)))) -(defn- op-bounds [{:keys [kind pts n cx cy r size]}] - (case kind - :poly (reduce (fn [b i] - (let [x (aget pts (* 2 i)) y (aget pts (inc (* 2 i)))] - (if b (let [[x0 y0 x1 y1] b] - [(min x0 x) (min y0 y) (max x1 x) (max y1 y)]) - [x y x y]))) nil (range n)) - :disc [(- cx r) (- cy r) (+ cx r) (+ cy r)] - :rect (let [h (/ size 2)] [(- cx h) (- cy h) (+ cx h) (+ cy h)]) - nil)) - -(defn in-rect - "Distinct row paths whose drawn bounds intersect `[x0 y0 x1 y1]`. `depth` - chooses objects at one hierarchy level, just as an ordinary stage click does." - [ops [ax ay bx by] depth] - (let [[rx0 rx1] [(min ax bx) (max ax bx)] - [ry0 ry1] [(min ay by) (max ay by)]] - (->> ops - (keep (fn [op] - (when-let [[x0 y0 x1 y1] (op-bounds op)] - (when (and (<= x0 rx1) (<= rx0 x1) (<= y0 ry1) (<= ry0 y1)) - (let [path (path-of op)] - (subvec path 0 (min (count path) (max 1 depth)))))))) - distinct vec))) - (defn- prefix? [a b] (and (<= (count a) (count b)) (= a (subvec b 0 (count a))))) diff --git a/frontend/src/arthur/domain/span.cljs b/frontend/src/arthur/domain/span.cljs index a578c41..735f2e8 100644 --- a/frontend/src/arthur/domain/span.cljs +++ b/frontend/src/arthur/domain/span.cljs @@ -49,28 +49,6 @@ [arthur.domain.node :as node] [arthur.domain.symbol :as symbol])) -(defn- fit-lanes - "Make direct lane children cover `sid`'s authored window. - - A lane has no independently authored extent: it is a view of its parent - symbol's timeline. Keep that invariant at the one commit point that can grow - a symbol, so neither the wrapper nor the lane symbol retains an old parent - length." - [clip sid] - (let [frames (clip/frames clip sid)] - (reduce - (fn [c [id n]] - (let [source (node/source n)] - (if (and (nil? (:parent n)) - (symbol/lane? (clip/symbol c source))) - (-> c - (assoc-in [:symbols sid :nodes id :span] [0 frames]) - (assoc-in [:symbols sid :nodes id :time :at] 0) - (assoc-in [:symbols source :frames] frames)) - c))) - clip - (get-in clip [:symbols sid :nodes])))) - (defn finish "Commit `nodes` as symbol `sid`'s, or refuse. @@ -116,11 +94,9 @@ (and (> needed (:frames sym)) (= :keep extent)) {:refused (str "the edit needs " needed " frames; extend the shot to continue") :required-frames needed} - :else {:clip (fit-lanes - (cond-> (assoc-in clip [:symbols sid :nodes] nodes) - (> needed (:frames sym)) - (assoc-in [:symbols sid :frames] needed)) - sid) + :else {:clip (cond-> (assoc-in clip [:symbols sid :nodes] nodes) + (> needed (:frames sym)) + (assoc-in [:symbols sid :frames] needed)) :selection selection}))) ;; --------------------------------------------------------------------------- @@ -144,47 +120,6 @@ [n which f] (assoc-in n [:span (case which :in 0 :out 1)] (local n f))) -(defn claim - "Commit proposed `nodes`, with `id` claiming its interval in lane mode. - - This is the one difference between editing a lane and a composition. In a - composition it is exactly `finish`. In a lane, immediately before that same - commit, clips covered by the edited one are removed and clips crossing either - edge are trimmed. A clip crossing both edges is split and therefore needs a - caller-supplied `remainder-id`." - [clip sid nodes id extent remainder-id] - (let [n (get nodes id) - lane-child? (and (symbol/lane? (clip/symbol clip sid)) - n (nil? (:parent n)) (node/placed-span n))] - (if-not lane-child? - (finish clip sid nodes id extent) - (let [[a b] (node/placed-span n) - others (remove #(= id (:id %)) (symbol/children nodes)) - spanning (first (filter #(let [[lo hi] (node/placed-span %)] - (and (< lo a) (> hi b))) - others))] - (if (and spanning (or (nil? remainder-id) (contains? nodes remainder-id))) - {:refused "claiming time inside one clip needs a free ID for its remainder"} - (finish - clip sid - (reduce - (fn [ns other] - (let [oid (:id other) - [lo hi] (node/placed-span other)] - (cond - (or (<= hi a) (>= lo b)) ns - (and (< lo a) (> hi b)) - (-> ns - (assoc oid (edged other :out a)) - (assoc remainder-id - (assoc (edged other :in b) :id remainder-id - :z (str "a-" remainder-id)))) - (and (>= lo a) (<= hi b)) (dissoc ns oid) - (< lo a) (assoc ns oid (edged other :out a)) - :else (assoc ns oid (edged other :in b))))) - nodes others) - id extent)))))) - (defn host-frame "Symbol frame `f` as a frame of the space node `id` is POSITIONED in — its parent's — which is the frame space every command here takes its coordinate @@ -376,7 +311,7 @@ (let [nodes (get-in clip [:symbols sid :nodes]) {:keys [node refused]} (subject nodes id) [lo old-out] (when node (node/placed-span node)) - later (when (and node ripple?) + later (when node (filter #(>= (first (node/placed-span %)) old-out) (siblings clip sid nodes id)))] (cond @@ -387,10 +322,22 @@ :else (let [delta (- to old-out) resized (assoc nodes id (edged node :out to)) - changed (reduce (fn [ns sibling] - (update-in ns [(:id sibling) :time :at] (fnil + 0) delta)) - resized later)] - (claim clip sid changed id extent nil))))) + changed + (if ripple? + (reduce (fn [ns sibling] + (update-in ns [(:id sibling) :time :at] (fnil + 0) delta)) + resized later) + (if (pos? delta) + (reduce + (fn [ns sibling] + (let [[s e] (node/placed-span sibling)] + (cond + (>= s to) ns + (<= e to) (dissoc ns (:id sibling)) + :else (assoc ns (:id sibling) (edged sibling :in to))))) + resized later) + resized))] + (finish clip sid changed id extent))))) (defn resize-in "Put clip `id`'s left edge at parent frame `to`. Shrinking leaves a gap; @@ -398,14 +345,29 @@ [clip sid id to] (let [nodes (get-in clip [:symbols sid :nodes]) {:keys [node refused]} (subject nodes id) - [old-in hi] (when node (node/placed-span node))] + [old-in hi] (when node (node/placed-span node)) + earlier (when node + (filter #(<= (second (node/placed-span %)) old-in) + (siblings clip sid nodes id)))] (cond refused {:refused refused} (not (integer? to)) {:refused "an edge goes to a whole frame"} (not (< to hi)) {:refused "a clip must keep at least one frame"} (neg? to) {:refused "a clip cannot begin before the shot"} (= to old-in) {:clip clip :selection id} - :else (claim clip sid (assoc nodes id (edged node :in to)) id :keep nil)))) + :else + (let [resized (assoc nodes id (edged node :in to)) + changed (if (< to old-in) + (reduce + (fn [ns sibling] + (let [[s e] (node/placed-span sibling)] + (cond + (<= e to) ns + (>= s to) (dissoc ns (:id sibling)) + :else (assoc ns (:id sibling) (edged sibling :out to))))) + resized earlier) + resized)] + (finish clip sid changed id :keep))))) (defn roll "Move the shared boundary between adjacent clips `left-id` and `right-id`. @@ -474,6 +436,17 @@ ;; selected rather than being handed a clip it did not ask for. (finish clip sid nodes (when spanning id) :keep))))) +(defn- cleared + "`clip` with frames `[at (+ at duration))` of `sid` emptied where `sid` is drawn + as a lane, and untouched where it is not: outside lane mode a placement does not + claim time, because being on screen together is what compositing IS. + + `{:clip c}` or `{:refused why}`, so one `if-let` covers both." + [clip sid at duration remainder-id] + (if-not (symbol/lane? (clip/symbol clip sid)) + {:clip clip} + (blank clip sid [at (+ at duration)] {:id remainder-id}))) + (defn extend-hold "Change one held clip's duration by `delta` frames and ripple its later siblings. Keys, source clocks and the clips' own channels stay put. @@ -531,8 +504,12 @@ (and remainder-id (contains? nodes remainder-id)) {:refused "the remainder clip needs a free ID"} :else - (let [placed (update-in n [:time :at] (fnil + 0) (- at lo))] - (claim clip sid (assoc nodes id placed) id extent remainder-id))))) + (let [room (cleared clip sid at duration remainder-id)] + (if (:refused room) + room + (let [placed (update-in n [:time :at] (fnil + 0) (- at lo)) + nodes (assoc (get-in (:clip room) [:symbols sid :nodes]) id placed)] + (finish (:clip room) sid nodes id extent))))))) (defn place-symbol "Materialize an instance of `source-id`, then place it into `sid` at `at`. @@ -745,11 +722,12 @@ "the remainder clip needs a free ID different from the new clip" (clip/symbol clip drawing-id) "the new drawing ID is already used")] {:refused why} - (let [c (assoc-in clip [:symbols drawing-id] - {:id drawing-id :name (name drawing-id) - :fps (clip/fps clip sid) :frames 1 :nodes {}}) - nodes (assoc nodes id (held id drawing-id at))] - (claim c sid nodes id extent remainder-id))))) + (let [room (cleared clip sid at 1 remainder-id)] + (if (:refused room) + room + (place (assoc-in (:clip room) [:symbols drawing-id] + {:id drawing-id :name (name drawing-id) :fps (clip/fps clip sid) :frames 1 :nodes {}}) + sid id drawing-id at extent false)))))) (defn make-unique "Point clip `id` at a private copy of its content, leaving every other diff --git a/frontend/src/arthur/domain/symbol.cljs b/frontend/src/arthur/domain/symbol.cljs index bc52961..d0c1a31 100644 --- a/frontend/src/arthur/domain/symbol.cljs +++ b/frontend/src/arthur/domain/symbol.cljs @@ -275,7 +275,6 @@ be impossible to miss and should not take the frame down." [palette k] (cond - (fn? palette) (palette k) (number? k) k (nil? k) 255 :else (get palette k 255))) diff --git a/frontend/src/arthur/events/export.cljs b/frontend/src/arthur/events/export.cljs index 34ac377..92be1a3 100644 --- a/frontend/src/arthur/events/export.cljs +++ b/frontend/src/arthur/events/export.cljs @@ -153,8 +153,9 @@ ;; The same palette and ramp the preview resolves and blits ;; through. Read here rather than in the fx so that the effect ;; takes data and nothing else. - :palette (pal/compile (:clip entry)) - :ramp (:ramp (pal/compile (:clip entry))) + :palette (get {:arthur/default pal/index-of} + (:palette db) pal/index-of) + :ramp (get {:arthur/default pal/rgb} (:palette db) pal/rgb) :zoom zoom :audio-url (:audio entry) :name (stem (:label entry) diff --git a/frontend/src/arthur/events/project.cljs b/frontend/src/arthur/events/project.cljs index bf63547..d4ada08 100644 --- a/frontend/src/arthur/events/project.cljs +++ b/frontend/src/arthur/events/project.cljs @@ -23,12 +23,10 @@ fx, which is the only thing in this namespace that is not pure." (:require [arthur.db :as db] [arthur.domain.bring :as bring] - [arthur.domain.channel :as ch] [arthur.domain.clip :as clip] [arthur.domain.span :as span] [arthur.domain.leaf :as leaf] [arthur.domain.node :as node] - [arthur.domain.palette :as pal] [arthur.events.edit :as edit] [arthur.audio.mix :as mix] [arthur.demo.stage :as stage] @@ -115,11 +113,7 @@ loaded (project/load cid #js {:leaves (.-leaves clip-json) :blocks blocks}) - ;; Project FPS and the root timeline are one clock. This - ;; also normalizes documents saved by the earlier model, - ;; where changing project FPS left the root on its old - ;; editing grid. - built (clip/set-root-fps (:clip loaded) (:fps (:clip loaded)))] + built (:clip loaded)] (let [entry (merge (select-keys built [:fps :width :height]) {:label (str (or (.-name clip-json) cid) " (saved)") :cid cid @@ -167,34 +161,6 @@ :host (get-in db [:ui :open]) :frame frame :point point :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 ::imported ;; As drawing: its tracking stays with the analysis that measured it. See @@ -326,11 +292,7 @@ {:project (.-project r) :project-name (.-project_name r) :cid (.-cid r) :symbol (.-symbol r) :name (.-name r) :frames (.-frames r)}) - (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)))]))) + (array-seq (.-symbols listed)))]))) (.catch (fn [error] (rf/dispatch [::failed (or (ex-message error) (str error))])))))) @@ -342,8 +304,7 @@ (rf/reg-event-db ::symbols-listed - (fn [db [_ rows palettes]] - (assoc db :assets {:symbols rows :palettes palettes :loading? false}))) + (fn [db [_ rows]] (assoc db :assets {:symbols rows :loading? false}))) (rf/reg-sub ::assets (fn [db _] (:assets db))) @@ -645,7 +606,7 @@ (if (or (not (#{:fps :width :height} key)) (not (and (integer? value) (pos? value)))) {} - (let [db' (edit/edit db #(if (= key :fps) (clip/set-root-fps % value) (assoc % key value))) + (let [db' (edit/edit db #(if (= key :fps) (clip/set-fps % value) (assoc % key value))) db' (assoc-in db' [:clip key] value) frame (min (dec (pb/frames db')) (js/Math.floor (* (get-in db [:playback :frame]) @@ -675,7 +636,7 @@ (rf/reg-event-db ::symbol-setting (fn [db [_ sid key value]] - (if-not (and (#{:frames :fps :width :height} key) + (if-not (and (#{:frames :width :height} key) (or (nil? value) (and (integer? value) (pos? value)))) db (edit/edit db @@ -684,70 +645,6 @@ (update-in c [:symbols sid] dissoc key) (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 ::set-channel ;; `frame` is the node's own, as for a drawing key. @@ -755,16 +652,6 @@ (let [put (if (get-in db [:ui :auto-key?]) node/set-keyed-channel node/set-channel)] (edit/edit db #(update-in % [:symbols sid :nodes id] put path frame value))))) -(rf/reg-event-db - ::set-channels - ;; One property assignment over a stage selection is one document edit. - (fn [db [_ edits]] - (let [put (if (get-in db [:ui :auto-key?]) node/set-keyed-channel node/set-channel)] - (edit/edit db - #(reduce (fn [c {:keys [sid id path frame value]}] - (update-in c [:symbols sid :nodes id] put path frame value)) - % edits))))) - (rf/reg-event-db ::toggle-key (fn [db [_ sid id path frame]] diff --git a/frontend/src/arthur/events/ui.cljs b/frontend/src/arthur/events/ui.cljs index 80a8ac0..ceefbb0 100644 --- a/frontend/src/arthur/events/ui.cljs +++ b/frontend/src/arthur/events/ui.cljs @@ -78,7 +78,6 @@ [:clip :symbols sid :nodes id :kind]))] (cond-> (-> db (assoc-in [:ui :selection] selection) - (assoc-in [:ui :selections] (if selection [selection] [])) (update :ui dissoc :points :retry)) (and (= :node kind) (seq path) (not sound?)) (update-in [:ui :expanded] (fnil into #{}) (rest (reductions conj [] (pop path))))))) @@ -104,29 +103,6 @@ (rf/reg-event-db ::select (fn [db [_ selection]] (selected db selection))) -(rf/reg-event-db - ::select-many - ;; `:selection` remains the primary address used by the inspector and timeline; - ;; `:selections` is the ordered set used by the stage and batch commands. - (fn [db [_ selections]] - (let [xs (vec (distinct (keep identity selections)))] - (if-let [primary (peek xs)] - (-> (selected db primary) (assoc-in [:ui :selections] xs)) - (selected db nil))))) - -(rf/reg-event-db - ::toggle-selection - (fn [db [_ selection]] - (let [primary (get-in db [:ui :selection]) - many (vec (get-in db [:ui :selections])) - old (if (some #{primary} many) many (if primary [primary] [])) - xs (if (some #{selection} old) - (vec (remove #{selection} old)) - (conj old selection))] - (if-let [primary (peek xs)] - (-> (selected db primary) (assoc-in [:ui :selections] xs)) - (selected db nil))))) - (rf/reg-event-db ::aim ;; The gestures that name a PLACE in the document rather than a thing on @@ -648,12 +624,6 @@ (let [{document :clip st :store} (store/entry (:clip/current db)) open (get-in db [:ui :open]) frame (editing-frame db document) - ;; A lane is the open symbol's timeline, not an insert that happens to - ;; begin where the playhead was when it was made. Giving that wrapper - ;; the whole open-symbol window makes the row's promise true: it is a - ;; destination at every frame. Ordinary symbols remain clips created at - ;; the playhead. - frame (if lane? 0 frame) into (if (= :top where) (assoc (nest/inside document st open [] frame) :path []) (aimed-symbol document st db frame)) @@ -695,10 +665,8 @@ (rf/reg-event-db ::new-lane (fn [db _] - ;; A lane is an explicit, persistent top-level track of the open symbol. It - ;; must not become nested merely because the previously created lane is - ;; still aimed, and it spans the open symbol rather than starting at the - ;; current playhead. + ;; A lane is an explicit top-level track of the open symbol. It must not + ;; become nested merely because the previously created lane is still aimed. (create-container db :top true))) ;; --------------------------------------------------------------------------- @@ -720,9 +688,9 @@ ::drop-clear (fn [db _] (update db :ui dissoc :drop))) -(defn drop-destination-at - "Which symbol a drop on `target` lands in and on which of its frames: - `{:clip :sid :at :path}`, or `{:refused why}`. +(defn drop-destination + "Which symbol a drop lands in and on which of its frames: `{:clip :sid :at + :path}`, or `{:refused why}`. ONE RULE AND EVERY DROP ASKS IT — a symbol from the pool, a sound, and a video brought in as a take alike. The pointer names a ROW, `target`, and a row leads @@ -738,11 +706,12 @@ `:clip` is handed back unchanged and is in the result only so the callers that used to be given a document with a freshly made lane in it go on reading one thing." - [document st open frame target] + [db document st frame target] (let [target (if (vector? target) (let [[_ sid id path] target] {:sid sid :id id :path path}) target) + open (get-in db [:ui :open]) path (cond (nil? target) [] (= :instance (get-in document [:symbols (:sid target) @@ -755,11 +724,6 @@ (not (integer? at)) {:refused "the drop is not on one frame of that symbol"} :else {:clip document :sid sid :at at :path path}))) -(defn drop-destination - "`drop-destination-at` from the symbol currently open in `db`." - [db document st frame target] - (drop-destination-at document st (get-in db [:ui :open]) frame target)) - (defn landed "`db` after a drop that produced `result`, with `uuid` selected." [db {:keys [sid path]} uuid result] @@ -939,44 +903,26 @@ ::transform ;; The drag let go: one edit, so one undo step and one write to collaborators. (fn [db [_ g]] - (let [auto? (boolean (get-in db [:ui :gesture :auto-key?])) - st (:store (store/entry (:clip/current db)))] + (let [auto? (boolean (get-in db [:ui :gesture :auto-key?]))] (if auto? (let [db (-> db (update-in [:ui :gesture] merge g) (record-auto-frame (get-in db [:playback :frame]))) take (get-in db [:ui :gesture :take])] (cond-> (update db :ui dissoc :gesture) - (seq take) (edit/edit #(gesture/apply-take % take st)))) + (seq take) (edit/edit #(gesture/apply-take % take)))) (let [{:keys [sid id frame values]} g] (cond-> (update db :ui dissoc :gesture) - (seq values) (edit/edit #(gesture/apply-values % sid id frame values false st)))))))) - -(rf/reg-event-db - ::transform-many - ;; Every visible member of a stage selection is one edit and therefore one - ;; undo step. Members absent at this playhead are intentionally not in `edits`. - (fn [db [_ edits]] - (let [st (:store (store/entry (:clip/current db)))] - (cond-> (update db :ui dissoc :gesture) - (seq edits) - (edit/edit #(reduce (fn [c {:keys [sid id frame values]}] - (gesture/apply-values c sid id frame values - (boolean (get-in db [:ui :auto-key?])) st)) - % edits)))))) + (seq values) (edit/edit #(gesture/apply-values % sid id frame values false)))))))) (rf/reg-event-db ::delete-selected (fn [db _] - (let [primary (get-in db [:ui :selection]) - many (vec (get-in db [:ui :selections])) - selections (if (some #{primary} many) many (if primary [primary] [])) - nodes (distinct (keep (fn [[kind sid id]] (when (= :node kind) [sid id])) selections))] - (if (seq nodes) + (let [[kind sid id] (get-in db [:ui :selection])] + (if (= :node kind) (-> db - (edit/edit #(reduce (fn [c [sid id]] (nest/delete-node c sid id)) % nodes)) - (assoc-in [:ui :selection] nil) - (assoc-in [:ui :selections] [])) + (edit/edit #(nest/delete-node % sid id)) + (assoc-in [:ui :selection] nil)) db)))) (rf/reg-event-db diff --git a/frontend/src/arthur/flow/freeze.cljs b/frontend/src/arthur/flow/freeze.cljs index c83efa5..9efccf2 100644 --- a/frontend/src/arthur/flow/freeze.cljs +++ b/frontend/src/arthur/flow/freeze.cljs @@ -38,7 +38,6 @@ [arthur.domain.geom :as geom] [arthur.domain.node :as node] [arthur.domain.pick :as pick] - [arthur.domain.palette :as pal] [arthur.domain.ring :as ring] [arthur.domain.trace :as trace] [arthur.flow.address :as address])) @@ -465,12 +464,12 @@ {:nodes {:mouth {:id :mouth :name "mouth" :kind :poly :parent :head :z "a1" :channels {[:geom :pts] (dense rings 0 (prov :roto/lips-outer)) - [:style :color] (ch/framed 2)}} + [:style :color] (ch/framed :skin-dark)}} :mouth-in {:id :mouth-in :name "mouth interior" :kind :poly :parent :mouth :z "a2" :channels {[:geom :pts] (dense rings 1 (prov :roto/lips-inner)) - [:style :color] (ch/framed 3) + [:style :color] (ch/framed :mouth-dark) [:vis] (visibility params inputs {:by :roto/mouth-aperture :analysis (:id analysis) @@ -524,13 +523,13 @@ {:id id :name (clojure.core/name id) :kind :poly :parent :head :z z :channels {[:geom :pts] (dense eye-block track (provenance :roto/eyelid {:verts eye-verts})) - [:style :color] (ch/framed 2)}}) + [:style :color] (ch/framed :skin-dark)}}) inner-node (fn [id parent z track shut] {:id id :name (clojure.core/name id) :kind :poly :parent parent :z z :channels {[:geom :pts] (dense eye-block track (provenance :roto/eye-opening {:verts eye-verts})) - [:style :color] (ch/framed 5) + [:style :color] (ch/framed :eye-white) [:vis] (keyed-visibility (mapv not shut) (provenance :roto/blink nil))}}) iris-node (fn [id parent track radius] @@ -542,7 +541,7 @@ :generated (provenance :roto/iris-size {:iris-size (:iris-size params)})) - [:style :color] (ch/framed 6)}}) + [:style :color] (ch/framed :iris)}}) pupil-node (fn [id parent] {:id id :name (clojure.core/name id) :kind :rect :parent parent :z "a1" :stencil parent @@ -550,14 +549,14 @@ :generated (provenance :roto/pupil-size {:pupil-size (:pupil-size params)})) - [:style :color] (ch/framed 7)}}) + [:style :color] (ch/framed :pupil)}}) brow-node (fn [id z track] {:id id :name (clojure.core/name id) :kind :poly :parent :head :z z :channels {[:geom :pts] (dense brow-block track (provenance :roto/brow {:verts brow-verts})) [:xform :pos] (dense brow-pos-block track (provenance :roto/brow-raise nil)) - [:style :color] (ch/framed 8)}})] + [:style :color] (ch/framed :brow)}})] {:nodes (merge (when eyes {:eye-r (eye-node :eye-r "a2" 0) @@ -605,7 +604,7 @@ :parent :mouth-in :z "a1" :stencil :mouth-in :channels {[:geom :pts] (dense blk 0 generated) - [:style :color] (ch/framed 4) + [:style :color] (ch/framed :teeth) [:vis] (keyed-visibility shown generated)}}} :store (stored blk)})) @@ -764,8 +763,6 @@ merged (fn [k] (into {} (mapcat (comp k second)) parts)) built {:name name :fps fps :analysis (:analysis params) :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) :features (merged :features) :groups (merged :groups) :symbols diff --git a/frontend/src/arthur/subs/render.cljs b/frontend/src/arthur/subs/render.cljs index 94bf24a..b4df292 100644 --- a/frontend/src/arthur/subs/render.cljs +++ b/frontend/src/arthur/subs/render.cljs @@ -43,24 +43,20 @@ ;; With a timeline bar being slid, the document as it will be when the drag ;; lets go, so the stage and the rows follow the pointer. Nothing is written ;; until then: one drag is one undo step and one write to collaborators. - (let [{c :clip st :store} (footage/entry id)] + (let [c (:clip (footage/entry id))] (or (when-let [{:keys [path df kind ripple? other]} sliding] (:clip (case kind :out (nest/resize-out c open path df ripple?) :in (nest/resize-in c open path df) :roll (nest/roll c open other path df) (nest/slide c open path df)))) - (when-let [{:keys [sid id frame values edits]} gesture] + (when-let [{:keys [sid id frame values]} gesture] ;; A held stage control owns the touched parameters completely. Make ;; them temporary static channels for the preview, so their existing ;; automation cannot pull against the pointer while the performance ;; recorder samples the live value in the background. Pointer-up ;; removes this preview and commits the buffered take as real keys. - (when c - (if (seq edits) - (reduce (fn [out {:keys [sid id frame values]}] - (gesture/apply-values out sid id frame values false st)) c edits) - (gesture/apply-values c sid id frame values false st)))) + (when c (gesture/apply-values c sid id frame values false))) c)))) (rf/reg-sub @@ -122,13 +118,21 @@ (rf/reg-sub ::palette - :<- [::clip] - (fn [clip _] (when clip (pal/compile clip)))) + (fn [db _] + ;; A NAME resolves to a ramp. One today; when timelines carry a `:palette` + ;; 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 ::ramp - :<- [::palette] - (fn [compiled _] (:ramp compiled))) + (fn [db _] + ;; index -> [r g b]. The other half of the palette: `::palette` says which + ;; 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 ::store diff --git a/frontend/src/arthur/subs/ui.cljs b/frontend/src/arthur/subs/ui.cljs index 21e7fcd..65fd08c 100644 --- a/frontend/src/arthur/subs/ui.cljs +++ b/frontend/src/arthur/subs/ui.cljs @@ -12,17 +12,6 @@ [re-frame.core :as rf])) (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 ::retry (fn [db _] (get-in db [:ui :retry]))) (rf/reg-sub ::tone (fn [db _] (get-in db [:ui :tone]))) @@ -111,23 +100,6 @@ (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)))))))) -(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 ::settled-clip :<- [::render/clip-id] diff --git a/frontend/src/arthur/ui/palette.cljs b/frontend/src/arthur/ui/palette.cljs index cf12058..2a333d1 100644 --- a/frontend/src/arthur/ui/palette.cljs +++ b/frontend/src/arthur/ui/palette.cljs @@ -2,73 +2,52 @@ "The bar above the stage: the tone a new shape gets, and the tool that makes one. - Shapes store only the selected local slot number. Palette identity is supplied - by the symbol tree, and the colour input edits the project palette asset. + SIXTEEN SLOTS, and the palette supplies nine of them. The count is the format's + and not the data's — an indexed 320x200 picture in the Animator Pro idiom this + 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 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 with the selection. See `ui/params`." (:require [arthur.domain.palette :as pal] - [arthur.domain.node :as node] [arthur.events.ui :as ui] - [arthur.events.project :as project] - [arthur.subs.render :as render] [arthur.subs.ui :as sub] [arthur.ui.layout :as layout] [re-frame.core :as rf])) -(rf/reg-sub ::chosen (fn [db _] (get-in db [:ui :palette]))) +(def ^:const slots 16) -(defn- swatch [pid i {:keys [name hex]} tone placements] - (let [picker-id (str "palette-picker-" pid "-" i)] - [:div {:key i :class "palette-slot"} - [:button {:class (str "swatch" (when (= i tone) " on")) - :style {:background hex} - :title (str i (when name (str " · " (clojure.core/name name))) - " · " hex " · double-click to edit") - :on-click #(do (rf/dispatch [::ui/set-tone i]) - (let [edits (into [] (keep (fn [{:keys [sid id node frame]}] - (when (contains? (get node/valid-paths (:kind node)) - [:style :color]) - {:sid sid :id id :path [:style :color] - :frame frame :value i}))) - 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- swatch [i tone] + (let [{slot-tone :name :keys [hex]} (get pal/entries i) + bg? (zero? i) + pick (and slot-tone (not bg?))] + [:button + {:key i + :class (str "swatch" + (when-not slot-tone " empty") + (when bg? " bg") + (when (and pick (= slot-tone tone)) " on")) + :style (when hex {:background hex}) + :title (if slot-tone (str i " · " (name slot-tone) " " hex) (str i " · empty")) + :disabled (not pick) + :on-click #(rf/dispatch [::ui/set-tone slot-tone])}])) (defn bar [] (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]) auto? @(rf/subscribe [::sub/auto-key?]) draft @(rf/subscribe [::sub/draft])] [:div.palette-bar - [: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 [: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")]) + [:div.swatches (doall (map #(swatch % tone) (range slots)))] + [:span.dim (name tone)] [:span {:style {:flex 1}}] [:button.auto-key {:class (when auto? "on") :aria-pressed auto? @@ -83,12 +62,8 @@ [:button {:disabled (< (count draft) 6) :on-click #(rf/dispatch [::ui/finish-polygon])} "finish"] [:button {:on-click #(rf/dispatch [::ui/cancel-polygon])} "cancel"]] - [:button {:title "pen tool — click points on the stage" - :on-click #(rf/dispatch [::ui/begin-polygon])} "pen"]) + [:button {:on-click #(rf/dispatch [::ui/begin-polygon])} "polygon"]) ;; 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 ;; own chrome rather than in the inspector. [layout/zoomer :stage "the stage"]])) - -(rf/reg-event-db :arthur.ui.palette/select - (fn [db [_ id]] (assoc-in db [:ui :palette] id))) diff --git a/frontend/src/arthur/ui/params.cljs b/frontend/src/arthur/ui/params.cljs index 3ca75a4..b01f246 100644 --- a/frontend/src/arthur/ui/params.cljs +++ b/frontend/src/arthur/ui/params.cljs @@ -11,7 +11,6 @@ [arthur.domain.channel :as channel] [arthur.domain.feature :as feature] [arthur.domain.node :as node] - [arthur.domain.palette :as pal] [arthur.domain.paint :as paint] [arthur.domain.params :as params] [arthur.domain.pose :as pose] @@ -202,10 +201,7 @@ (defn- node-section [[sid id n]] (let [[start end] (:span n) - auto-key? @(rf/subscribe [::sub/auto-key?]) - clip @(rf/subscribe [::render/clip]) - local @(rf/subscribe [::sub/selected-local]) - frame (:frame local)] + auto-key? @(rf/subscribe [::sub/auto-key?])] [section (str (name (:kind n)) " · in " (name sid)) [facts "name" (or (:name n) (brief id)) @@ -218,18 +214,8 @@ "span" (when start (str start " … " end)) "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 (= :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"]] - (let [{:keys [frame]} local] + (let [{:keys [frame]} @(rf/subscribe [::sub/selected-local])] [:dl.facts (doall (for [[path ch] (sort-by (comp str key) (node/channels n))] @@ -541,9 +527,6 @@ (defn- symbol-section [sid] (let [clip @(rf/subscribe [::render/clip]) 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]))] [section "symbol" [facts @@ -554,32 +537,10 @@ [:div.inspector-form [number-field "length (frames)" (:frames sym) #(rf/dispatch [::project/symbol-setting sid :frames %]) nil busy?] - [number-field "fps" (clip-domain/fps clip sid) - #(rf/dispatch (if (= sid (clip-domain/opens-on clip)) - [::project/project-setting :fps %] - [::project/symbol-setting sid :fps %])) - nil busy?] [number-field "width" (:width sym) #(rf/dispatch [::project/symbol-setting sid :width %]) "project default" busy?] [number-field "height" (:height sym) #(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)) [:div.row [:button {:disabled busy? :on-click #(do diff --git a/frontend/src/arthur/ui/player.cljs b/frontend/src/arthur/ui/player.cljs index a9d752f..5452f67 100644 --- a/frontend/src/arthur/ui/player.cljs +++ b/frontend/src/arthur/ui/player.cljs @@ -152,9 +152,6 @@ [point] (pick/hit (:ops @state) point)) -(defn in-rect [rect depth] - (pick/in-rect (:ops @state) rect depth)) - (defn paint! "Resolve `f` and put it on the canvas. `ops` are consumed here and only here — the resolver reuses its point buffers between frames, so they have to be diff --git a/frontend/src/arthur/ui/pool.cljs b/frontend/src/arthur/ui/pool.cljs index aeea14e..ed1828b 100644 --- a/frontend/src/arthur/ui/pool.cljs +++ b/frontend/src/arthur/ui/pool.cljs @@ -45,7 +45,6 @@ footage stay separate records on the server, so dropping the same file twice does not decode it twice." (:require [arthur.domain.clip :as clip] - [arthur.domain.palette :as pal] [arthur.domain.raster :as raster] [arthur.events.footage :as footage] [arthur.events.playback :as pb] @@ -270,23 +269,6 @@ (carrying (str "symbol:" (subs (str sid) 1)) #(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] ^{:key id} [row (merge {:label label @@ -345,13 +327,6 @@ #(drag/other! {:kind :import :label name :frames frames :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 @@ -400,28 +375,21 @@ 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." - [{:keys [document query searching? media sounds chosen rename main palette-choice] :as ctx}] + [{:keys [document query searching? media sounds chosen rename main] :as ctx}] (let [named? #(hit? query (clip/symbol-name document %)) top (when (and main (named? main)) main) rest (filterv #(and (named? %) (not= main %)) (sort-by str (keys (:symbols document)))) media (filterv #(hit? query (:label %)) media) - sounds (filterv #(hit? query (:label %)) sounds) - palettes (filterv #(hit? query (:name (val %))) - (sort-by (comp str :name val) (pal/palettes document)))] + sounds (filterv #(hit? query (:label %)) sounds)] [sections searching? - (+ (if top 1 0) (count rest) (count media) (count sounds) (count palettes)) + (+ (if top 1 0) (count rest) (count media) (count sounds)) [{:title "project" :searching? searching? :blank "nothing to open yet" :rows (when top [(symbol-row document top ctx)])} {:title "symbols" :searching? searching? :blank "nothing else in the library" :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? :blank "drop a video here" :rows (mapv #(footage-row % chosen rename) media)} @@ -433,25 +401,21 @@ "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 the one you want." - [{:keys [document query searching? rename chosen available all-sounds symbols palettes + [{:keys [document query searching? rename chosen available all-sounds symbols project-id]}] (let [media (filterv #(hit? query (:label %)) available) sounds (filterv #(hit? query (:label %)) all-sounds) others (filterv #(and (not= project-id (:project %)) (hit? query (:name %))) symbols) - 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)] + grouped (sort-by (comp str second key) (group-by (juxt :project :project-name) others))] [sections searching? - (+ (count media) (count sounds) (count others) (count palettes)) + (+ (count media) (count sounds) (count others)) [{:title "media" :searching? searching? :blank "nothing uploaded yet" :rows (mapv #(footage-row % chosen rename) media)} {:title "sounds" :searching? searching? :blank "no sounds uploaded yet" :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? :blank "no other saved projects" :rows (for [[[pid pname] rows] grouped] @@ -473,15 +437,13 @@ 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 labels the rows are filtered by." - [{:keys [document query media sounds available all-sounds symbols palettes project-id]}] + [{:keys [document query media sounds available all-sounds symbols project-id]}] (let [n (fn [labels] (count (filter #(hit? query %) labels)))] {:project (+ (n (map #(clip/symbol-name document %) (keys (:symbols document)))) - (n (map :name (vals (pal/palettes document)))) (n (map :label media)) (n (map :label sounds))) :assets (+ (n (map :label available)) (n (map :label all-sounds)) - (n (map :name (remove #(= project-id (:project %)) palettes))) (n (map :name (remove #(= project-id (:project %)) symbols))))})) (defn view [] @@ -503,10 +465,9 @@ palette @(rf/subscribe [::render/palette]) ramp @(rf/subscribe [::render/ramp]) selection @(rf/subscribe [::sub/selection]) - palette-choice @(rf/subscribe [:arthur.ui.palette/chosen]) open @(rf/subscribe [::render/open]) media @(rf/subscribe [::sub/project-footage]) - {:keys [symbols palettes]} @(rf/subscribe [::project/assets]) + {:keys [symbols]} @(rf/subscribe [::project/assets]) {project-id :id} @(rf/subscribe [::playback/project]) ;; The project's videos' sounds first, then its uploaded ones. own-sounds (into (mapv (fn [{:keys [id label frames fps]}] @@ -520,8 +481,7 @@ :ramp ramp :selection selection :open open :media media :sounds own-sounds :chosen chosen :available (vec available) :all-sounds (vec sounds) - :symbols symbols :palettes palettes :project-id project-id - :palette-choice palette-choice + :symbols symbols :project-id project-id :query needle :searching? searching? ;; `clip/opens-on` and not `:main`: the longest symbol nothing ;; else places is the timeline the work happens in, and it is the diff --git a/frontend/src/arthur/ui/stage.cljs b/frontend/src/arthur/ui/stage.cljs index c022429..8c419cf 100644 --- a/frontend/src/arthur/ui/stage.cljs +++ b/frontend/src/arthur/ui/stage.cljs @@ -25,8 +25,7 @@ [arthur.ui.layout :as layout] [arthur.ui.player :as player] [arthur.ui.underlay :as underlay] - [re-frame.core :as rf] - [reagent.core :as r])) + [re-frame.core :as rf])) ;; THE ZOOM IS NOT A CONSTANT ANY MORE — it is `ui/layout`'s, an integer in ;; [1 8], 2 to begin with. It scales the canvas with CSS and never its backing @@ -119,7 +118,6 @@ ;; placement and transform it started from, and where. What it makes of the ;; pointer goes out as `::ui/gesture`, and on the way up as one `::ui/transform`. (defonce ^:private gesture (atom nil)) -(defonce ^:private marquee (r/atom nil)) (defn- loaded "The document and its tier-2 store, read WHEN THE POINTER GOES DOWN. @@ -139,15 +137,12 @@ (defn- select! "Select the node at row path `path` of the open symbol — the selection a timeline row makes, so the row, the inspector and the stage all show it — or - the open symbol itself when `path` is empty. Blank stage is an explicit place, - not merely an absence of a picked shape, so it clears a stale drawing target - as well as the inspector selection." + nothing." [{:keys [open f] :as ctx} path] (let [{document :clip st :store} (loaded ctx)] - (if-let [{:keys [sid id]} (when (seq path) - (nest/placement document st open path f))] - (rf/dispatch [::ui/select [:node sid id path]]) - (rf/dispatch [::ui/aim nil])))) + (rf/dispatch [::ui/select (when-let [{:keys [sid id]} (when (seq path) + (nest/placement document st open path f))] + [:node sid id path])]))) (defn- begin! "Start dragging `kind` of the node at `path` from stage point `p`." @@ -159,62 +154,12 @@ (reset! gesture {:kind kind :pl pl :path path :open open :v0 v0 :p0 p :n n :a (gesture/angle pl v0 p) :turned 0}))))) -(defn- stage-bounds [{:keys [world bounds]}] - (when (and world bounds) - (let [[x0 y0 x1 y1] bounds - ps (pairs (through world [x0 y0 x1 y0 x1 y1 x0 y1]))] - [(apply min (map first ps)) (apply min (map second ps)) - (apply max (map first ps)) (apply max (map second ps))]))) - -(defn- begin-many! [ctx kind placements p] - (let [{document :clip st :store} (loaded ctx) - members (into [] (keep (fn [{:keys [path]}] - (when-let [{:keys [sid id frame] :as pl} - (nest/placement document st (:open ctx) path (:f ctx))] - (let [n (get-in document [:symbols sid :nodes id])] - {:pl pl :path path :node n - :v0 (gesture/values n frame st)})))) - placements) - why (some #(gesture/refusal (:node %) kind) members) - boxes (keep stage-bounds placements) - box (when (seq boxes) - (reduce (fn [[ax ay bx by] [cx cy dx dy]] - [(min ax cx) (min ay cy) (max bx dx) (max by dy)]) boxes))] - (cond - why (rf/dispatch [::ui/refuse (str "the selection cannot move together: " why)]) - (and (seq members) box) - (reset! gesture {:kind kind :members members :p0 p :box box})))) - -(defn- multi-values [kind {:keys [pl v0]} [cx cy] p0 p] - (case kind - :move (gesture/move pl v0 p0 p) - :scale - (let [d0 (max 1e-6 (js/Math.hypot (- (first p0) cx) (- (second p0) cy))) - k (/ (js/Math.hypot (- (first p) cx) (- (second p) cy)) d0) - [px py] (through (:world pl) (:anchor v0)) - target [(+ cx (* k (- px cx))) (+ cy (* k (- py cy)))] - inv (node/invert (:parent pl))] - (when inv - (let [[a b] (through inv [px py]) [c d] (through inv target)] - {[:xform :pos] (mapv + (:pos v0) [(- c a) (- d b)]) - [:xform :scale] (mapv #(* k %) (:scale v0))}))) - nil)) - (defn- drag! [p ^js event] (let [{:keys [kind pl path open v0 p0 n a turned values]} @gesture shift? (.-shiftKey event) moved? (or values (< 1 (js/Math.hypot (- (first p) (first p0)) (- (second p) (second p0)))))] (when moved? - (if-let [members (:members @gesture)] - (let [[x0 y0 x1 y1] (:box @gesture) - center [(/ (+ x0 x1) 2) (/ (+ y0 y1) 2)] - edits (into [] (keep (fn [{:keys [pl path] :as member}] - (when-let [vs (multi-values kind member center p0 p)] - (assoc (select-keys pl [:sid :id :frame]) - :path path :values vs)))) members)] - (swap! gesture assoc :values edits) - (rf/dispatch [::ui/gesture {:edits edits}])) - (if-let [why (gesture/refusal n kind)] + (if-let [why (gesture/refusal n)] (do (reset! gesture nil) (rf/dispatch [::ui/refuse why])) (let [vs (case kind :move (gesture/move pl v0 p0 p) @@ -232,17 +177,16 @@ (swap! gesture assoc :values vs) (rf/dispatch [::ui/gesture (assoc (select-keys pl [:sid :id :frame]) - :path path :open open :values vs)])))))))) + :path path :open open :values vs)]))))))) (defn- let-go! [commit?] - (when-let [{:keys [pl path open values members]} @gesture] + (when-let [{:keys [pl path open values]} @gesture] (reset! gesture nil) (when values - (rf/dispatch (cond - (not commit?) [::ui/gesture nil] - members [::ui/transform-many values] - :else [::ui/transform (assoc (select-keys pl [:sid :id :frame]) - :path path :open open :values values)]))))) + (rf/dispatch (if commit? + [::ui/transform (assoc (select-keys pl [:sid :id :frame]) + :path path :open open :values values)] + [::ui/gesture nil]))))) (defn- handles "The selected node's box, drawn through its own transform so it turns with @@ -280,30 +224,6 @@ [:path.pivot {:d (str "M " (- px 3) " " py " H " (+ px 3) " M " px " " (- py 3) " V " (+ py 3))}]]))) -(defn- group-handles [ctx placements] - (let [boxes (keep stage-bounds placements)] - (when (seq boxes) - (let [[x0 y0 x1 y1] (reduce (fn [[ax ay bx by] [cx cy dx dy]] - [(min ax cx) (min ay cy) (max bx dx) (max by dy)]) boxes) - grab (fn [kind] - (fn [^js event] - (.stopPropagation event) - (.preventDefault event) - (let [svg (.-ownerSVGElement (.-currentTarget event))] - (.setPointerCapture svg (.-pointerId event)) - (begin-many! ctx kind placements (xy svg event (:w ctx) (:h ctx))))))] - [:g.handles.multi - [:rect.box {:x x0 :y y0 :width (- x1 x0) :height (- y1 y0)}] - (doall (for [[i [x y]] (map-indexed vector [[x0 y0] [x1 y0] [x1 y1] [x0 y1]])] - ^{:key i} [:rect.corner {:x (- x 1.8) :y (- y 1.8) - :width 3.6 :height 3.6 - :on-pointer-down (grab :scale)}]))])))) - -(defn- address-for [{:keys [open f] :as ctx} path] - (let [{document :clip st :store} (loaded ctx)] - (when-let [{:keys [sid id]} (and (seq path) (nest/placement document st open path f))] - [:node sid id path]))) - (defn- aim-box "The TARGET's outline: where a new polygon or symbol would be parented, drawn around the thing that would be its parent. @@ -348,9 +268,6 @@ draft @(rf/subscribe [::sub/draft]) drawing? (= :polygon tool) [_ _ _ selected] @(rf/subscribe [::sub/selection]) - selections @(rf/subscribe [::sub/selections]) - placements @(rf/subscribe [::sub/selected-placements]) - selected-paths (set (keep #(nth % 3 nil) selections)) clip-id @(rf/subscribe [::render/clip-id]) ctx {:clip-id clip-id ;; The open symbol's OWN frame, which is what `nest/placement` @@ -361,8 +278,7 @@ points? @(rf/subscribe [::sub/points]) [sid id geom active editable? frame matrix] (when points? (editing)) pts (when geom (through matrix (channel/value-at geom frame - (:store (store/entry clip-id))))) - mark @marquee] + (:store (store/entry clip-id)))))] [:svg {:class (str "paint-overlay" (when drawing? " drawing")) :width (* zoom w) :height (* zoom h) :view-box (str "0 0 " w " " h) @@ -376,22 +292,10 @@ path (pick/choose selected (player/at p) (or (.-metaKey event) (.-ctrlKey event)))] (.focus svg) - (cond - (and (.-shiftKey event) path) - (when-let [address (address-for ctx path)] - (rf/dispatch [::ui/toggle-selection address])) - - path - (when-not (contains? selected-paths path) (select! ctx path)) - - :else - (do (.setPointerCapture svg (.-pointerId event)) - (reset! marquee {:p0 p :p p :more? (.-shiftKey event)}))) - (when (and path (not (.-shiftKey event))) + (when (not= path selected) (select! ctx path)) + (when path (.setPointerCapture svg (.-pointerId event)) - (if (and (> (count placements) 1) (contains? selected-paths path)) - (begin-many! ctx :move placements p) - (begin! ctx :move path p)))))) + (begin! ctx :move path p))))) :on-double-click (fn [^js event] (when-not drawing? (let [p (xy (.-currentTarget event) event w h) @@ -414,38 +318,19 @@ (rf/dispatch [::paint-events/set-vertex sid node key-frame vertex (through inv (stage-point event w h))]) - (let [p (xy (.-currentTarget event) event w h)] - (if @marquee - (swap! marquee assoc :p p) - (when @gesture (drag! p event)))))) - :on-pointer-up (fn [_] - (reset! dragging nil) - (if-let [{:keys [p0 p more?]} @marquee] - (let [depth (or (some-> selected count) 1) - paths (player/in-rect [(first p0) (second p0) - (first p) (second p)] depth) - addresses (into [] (keep #(address-for ctx %)) paths) - old (if more? selections [])] - (reset! marquee nil) - (rf/dispatch [::ui/select-many (into old addresses)])) - (let-go! true))) - :on-pointer-cancel (fn [_] - (reset! dragging nil) (reset! marquee nil) (let-go! false))} + (when @gesture + (drag! (xy (.-currentTarget event) event w h) event)))) + :on-pointer-up (fn [_] (reset! dragging nil) (let-go! true)) + :on-pointer-cancel (fn [_] (reset! dragging nil) (let-go! false))} [ghost] (when (seq draft) [:polyline {:points (points-text draft) :fill "none" :stroke "#d0ba86" :stroke-width 1}]) - (when mark - (let [[[x0 y0] [x1 y1]] [(:p0 mark) (:p mark)]] - [:rect.marquee {:x (min x0 x1) :y (min y0 y1) - :width (js/Math.abs (- x1 x0)) - :height (js/Math.abs (- y1 y0))}])) ;; DRAWN WHILE DRAWING, unlike the handles. "Where will this polygon land" ;; is the question the outline exists to answer, and the moment it is being ;; asked is mid-draft. (when-not points? [aim-box]) - (when-not (or drawing? points?) - (if (> (count placements) 1) [group-handles ctx placements] [handles ctx])) + (when-not (or drawing? points?) [handles ctx]) (when (and id pts (not drawing?) (not (channel/nothing? pts))) [:g [:polygon {:points (points-text pts) :fill "none" diff --git a/frontend/src/arthur/ui/timeline.cljs b/frontend/src/arthur/ui/timeline.cljs index d87b9d6..8ce0833 100644 --- a/frontend/src/arthur/ui/timeline.cljs +++ b/frontend/src/arthur/ui/timeline.cljs @@ -43,16 +43,9 @@ ;; --------------------------------------------------------------------------- ;; the rows -(defn- time->parent - "The inverse of a time map: where one of its output frames sits in its input." - [{:keys [at rate]}] - (if (and (zero? at) (= 1 rate)) - identity - (fn [f] (+ at (/ f rate))))) - (defn- local->parent - "Where a frame of a node's OWN time sits in the symbol it lives in. - See `node/time-of`. + "The inverse of a node's time map: where a frame of its OWN time sits in the + symbol it lives in. See `node/time-of`. Two frame spaces meet at every node and mixing them up is the bug this exists to prevent: a placement `:at 48` whose scale is keyed at 0 has that key on @@ -63,7 +56,10 @@ frame the exposure grid never samples is still authored on that frame, and that is where the row should show it." [n] - (time->parent (node/time-of n))) + (let [{:keys [at rate]} (node/time-of n)] + (if (and (zero? at) (= 1 rate)) + identity + (fn [f] (+ at (/ f rate)))))) (defn- keyed-frames [ch] (some-> (:keys ch) keys sort)) @@ -135,8 +131,8 @@ ;; the key positions, and the bars that would imply them. Refuse ;; rather than guess, without refusing the whole subtree. (when-let [source (and (= :instance (:kind n)) (node/source n))] - (if-let [source-time (clip/source-time clip sid n)] - (walk source path depth (comp self (time->parent source-time))) + (if-let [{:keys [at rate]} (clip/source-time clip sid n)] + (walk source path depth (comp self #(+ at (/ % rate)))) (mapv (fn [row] (-> row (assoc :keys [] :unmapped? true) @@ -248,13 +244,6 @@ self (comp parent-map (local->parent n)) source-sym (when (= :instance (:kind n)) (clip/symbol clip (node/source n))) - ;; `self` maps the instance's local clock. Rows of - ;; the symbol it places use its source clock too, - ;; including the native-FPS ratio. - source-self (if-let [t (and source-sym - (clip/source-time clip sid n))] - (comp self (time->parent t)) - self) lane? (symbol/lane? source-sym) clips (when lane? (symbol/children (:nodes source-sym))) span (mapv parent-map @@ -294,13 +283,13 @@ :label (or (get-in clip [:symbols (node/source child) :name]) (some-> (node/source child) name)) :source (node/source child) - :span (mapv source-self (node/placed-span child)) + :span (mapv self (node/placed-span child)) ;; The clip's own keys, on the ;; block, so a collapsed lane ;; still says where it changes. :keys (into [] (comp (mapcat keyed-frames) - (map (comp source-self (local->parent child))) + (map (comp self (local->parent child))) (distinct)) (vals (node/channels child))) :select [:node (node/source n) (:id child) @@ -313,7 +302,7 @@ ;; The one clip an expanded lane opens. (into (when lane? (if-let [child (first (filter #(under? (conj rpath (:id %))) clips))] - (portal (node/source n) rpath (inc depth) source-self child) + (portal (node/source n) rpath (inc depth) self child) [{:path (conj rpath ::portal) :depth (inc depth) :kind :hint @@ -598,6 +587,7 @@ (if (= :instance node-kind) " drop-into" " drop-group")))) :style {:padding-left (str (+ 4 (* 11 depth)) "px")} :title label + :tab-index (when lane? 0) :ref (when (and select (= select selection)) (reveal selection)) ;; A LABEL AIMS. Clicking a row's name says "I am working ;; here", which is a statement about a place in the document; @@ -607,8 +597,13 @@ :on-click #(when select (rf/dispatch [::ui/aim select])) ;; An instance's row opens the symbol it places, as a tab. :on-double-click (fn [^js e] - (when of - (rf/dispatch [::pb/open-symbol of])))} + (cond lane? (do (.stopPropagation e) (begin-rename!)) + of (rf/dispatch [::pb/open-symbol of]))) + :on-key-down (when lane? + (fn [^js e] + (when (= "F2" (.-key e)) + (.preventDefault e) + (begin-rename!))))} ;; A node's row can be dragged onto another: onto an instance's, to ;; go inside the symbol it places; onto any other node's, to be ;; grouped with it into a new one; onto an edge of either, to be @@ -672,7 +667,7 @@ node? [:span.kind (if via (str "· in " via) (str "·" (name node-kind)))]) (when lane? [:button.tl-rename - {:title "rename lane" + {:title "rename lane (F2)" :on-click (fn [^js e] (.stopPropagation e) (begin-rename!))} "✎"]) ;; A face's row is where its own footage is switched on, next to solo @@ -739,22 +734,10 @@ (not= (:select under) (:selection drag))) under) [target-el target] (when (and drag (not shift?)) (lane-under e)) - target-at (when target - (frame-under e frames target-el)) - ;; A cel moved on its OWN lane is a normal slide. - ;; Resolve the row under the pointer before calling - ;; it a transfer: comparing the displayed rows is - ;; not enough when the lane is reached through an - ;; instance. This leaves one slide gesture and one - ;; coordinate conversion; lane overlap trimming is - ;; the domain command's only additional policy. - target-sid (when target - (:sid (ui/drop-destination-at - clip store open target-at target))) - landing? (and target-sid - (not= target-sid (nth (:selection drag) 1))) + landing? (some? target) target-frame (when landing? - (max 0 (- target-at (:grab drag))))] + (max 0 (- (frame-under e frames target-el) + (:grab drag))))] (when (and drag hint) (reset! hint {:x (.-clientX e) :y (.-clientY e) @@ -1068,11 +1051,8 @@ visible (cond-> (vec picture) (seq sounds) (-> (conj {:path [::sounds] :kind :section :label "audio"}) (into sounds))) - ;; Major marks are whole seconds in the OPEN symbol's clock. On long - ;; timelines use a whole-number multiple of a second to keep roughly - ;; ten labels; never invent an FPS-blind 20/40/60 ruler. - fps (max 1 (or (clip/fps clip open) 1)) - step (* fps (max 1 (js/Math.ceil (/ frames (* fps 10))))) + ;; Roughly ten labels, on a round number of frames. + step (* 10 (js/Math.ceil (/ frames 100))) ;; THE OTHER DIRECTION, AND THE ONLY PLACE THIS PANE GOES THERE. The ;; ruler is in the open symbol's frames and the transport counts output ;; frames, so scrubbing names a mark and seeks to the output frame that @@ -1083,19 +1063,9 @@ [:section.pane.time [transport] [:div.tl-body - ;; Blank timeline space means the open symbol. Use the same `aim` - ;; gesture as its breadcrumb so both the visible selection and the - ;; drawing destination return to the root. Child controls stop their - ;; own events; the target check keeps ordinary row/bar clicks local. - {:on-click (fn [^js e] - (when (= (.-target e) (.-currentTarget e)) - (rf/dispatch [::ui/aim nil])))} [:div.tl-labels ;; Empty label space takes a row back out to the top of the open symbol. - {:on-click (fn [^js e] - (when (= (.-target e) (.-currentTarget e)) - (rf/dispatch [::ui/aim nil]))) - :on-drag-over (fn [^js e] + {:on-drag-over (fn [^js e] (when (drag/row) (.preventDefault e) (set! (.. e -dataTransfer -dropEffect) "move"))) @@ -1113,10 +1083,7 @@ [label-cell row selection target over solo tracing renaming draft]) {:key (str (:path row))})))] [:div.tl-tracks - {:on-click (fn [^js e] - (when (= (.-target e) (.-currentTarget e)) - (rf/dispatch [::ui/aim nil]))) - :on-drag-enter (fn [^js e] (when (drag/accepts?) (.preventDefault e))) + {:on-drag-enter (fn [^js e] (when (drag/accepts?) (.preventDefault e))) :on-drag-over (fn [^js e] (when (drag/accepts?) (.preventDefault e) @@ -1138,10 +1105,10 @@ :on-drop (fn [^js e] (.preventDefault e) (drag/land! (frame-at e frames) nil)) - ;; The rows and ruler share this exact major interval. Keeping a - ;; second, hard-coded five-frame grid here made its lines disagree - ;; with the numbered marks whenever the symbol's FPS changed. - :style {"--tick" (str (* 100 (/ step frames)) "%")}} + ;; Five frames as a percentage of the whole span, handed to the + ;; stylesheet so the frame grid can be a repeating background instead + ;; of a div per frame. A 900-frame take is 900 elements nobody needs. + :style {"--tick" (str (* 100 (/ 5 frames)) "%")}} [:div.tl-ruler {:on-pointer-down (fn [^js e] (rf/dispatch [::pb/seek (seek-to e frames)]) diff --git a/frontend/test/arthur/domain/cadence_test.cljs b/frontend/test/arthur/domain/cadence_test.cljs index c32002b..80be494 100644 --- a/frontend/test/arthur/domain/cadence_test.cljs +++ b/frontend/test/arthur/domain/cadence_test.cljs @@ -45,36 +45,6 @@ (is (= 3 (clip/first-output-frame doc :main 7))) (is (= [0 60] (:span (first (timeline/rows doc :main #{}))))))) -(deftest changing-fps-before-authoring-moves-the-empty-canvas-to-that-grid - (let [doc (clip/set-fps (clip/blank) 12)] - (is (= 12 (:fps doc))) - (is (nil? (get-in doc [:symbols :main :fps]))) - (is (= 48 (clip/frames doc :main))) - (is (= 48 (clip/output-frames doc :main))) - (is (= 8 (clip/shown-frame doc :main 8))) - (is (= 8 (clip/first-output-frame doc :main 8))))) - -(deftest changing-fps-after-authoring-preserves-the-symbols-native-grid - (let [started (assoc-in (clip/blank) [:symbols :main :nodes :mark] - {:id :mark :kind :rect :z "a"}) - doc (clip/set-fps started 12)] - (is (= 30 (get-in doc [:symbols :main :fps]))) - (is (= 120 (clip/frames doc :main))) - (is (= 48 (clip/output-frames doc :main))))) - -(deftest project-fps-is-the-root-symbols-editing-grid - (let [doc (-> (clip/blank) - (assoc-in [:symbols :main :nodes :child] - {:id :child :kind :instance :z "a" - :source {:symbol :nested}}) - (assoc-in [:symbols :nested] - {:id :nested :fps 30 :frames 90 :nodes {}}) - (clip/set-root-fps 12))] - (is (= 12 (:fps doc)) "the output grid") - (is (= 12 (clip/fps doc :main)) "is also the root editing grid") - (is (= 30 (clip/fps doc :nested)) "while a nested symbol keeps its own grid") - (is (= 120 (clip/frames doc :main)) "frame positions are not rewritten"))) - (deftest crossing-to-the-output-grid-and-back-lands-on-the-frame-it-names ;; `first-output-frame` is the inverse of `shown-frame` as far as a floor has ;; one: seeking to the output frame it names puts the playhead on a frame at or diff --git a/frontend/test/arthur/domain/gesture_test.cljs b/frontend/test/arthur/domain/gesture_test.cljs index f64be4a..c87cd19 100644 --- a/frontend/test/arthur/domain/gesture_test.cljs +++ b/frontend/test/arthur/domain/gesture_test.cljs @@ -274,22 +274,6 @@ (is (string? (gesture/refusal {:channels {[:xform :pos] {:animated? true :dense {:stride 2}}}}))) (is (nil? (gesture/refusal {:channels {[:xform :pos] (ch/keyed {0 [1 1]} :hold)}})))) -(deftest moving-a-measured-part-adds-an-authored-offset - (let [key "measured-position" - st {key {:data (js/Float32Array. #js [1 2 2 3])}} - measured {:animated? true :dense {:store key :offset 0 :stride 2 :frames 2}} - c (-> (clip/blank) - (paint/new-shape :main :brow 0 [0 0 10 0 5 3] :brow) - (assoc-in [:symbols :main :nodes :brow :channels [:xform :pos]] measured)) - once (gesture/apply-values c :main :brow 0 {[:xform :pos] [4 6]} false st) - twice (gesture/apply-values once :main :brow 0 {[:xform :pos] [5 8]} false st) - pos #(get-in % [:symbols :main :nodes :brow :channels [:xform :pos]])] - (is (= (:dense measured) (:dense (pos twice))) "the measured base survives") - (is (= [5 8] (ch/value-at (pos twice) 0 st)) "the brow lands under the pointer") - (is (= [6 9] (ch/value-at (pos twice) 1 st)) - "its measured motion continues underneath the authored offset") - (is (= 1 (count (:over (pos twice)))) "repeated drags update one correction layer"))) - (deftest a-click-selects-the-level-figma-would (let [hit [:a :b :c :shape]] (testing "choose" @@ -313,18 +297,6 @@ (is (= [:dot] (pick/hit ops [52.5 50])) "a few-pixel shape can be missed by a little") (is (nil? (pick/hit ops [30 30]))))) -(deftest a-marquee-selects-visible-objects-at-one-depth - (let [sq (fn [path x0] {:kind :poly :node path :n 4 - :pts (js/Float64Array. - #js [x0 0 (+ x0 10) 0 (+ x0 10) 10 x0 10])}) - ops [(sq [:left :inside] 0) (sq [:right :inside] 20) (sq [:away] 80)]] - (is (= [[:left] [:right]] (pick/in-rect ops [-2 -2 35 12] 1)) - "a fresh marquee chooses objects in the open symbol") - (is (= [[:left :inside] [:right :inside]] (pick/in-rect ops [-2 -2 35 12] 2)) - "an existing deep selection keeps the marquee at that depth") - (is (= [[:left]] (pick/in-rect ops [5 5 6 6] 1)) - "intersection, rather than full containment, makes small objects selectable"))) - (deftest an-instances-box-is-what-its-symbol-draws (let [c (two-down) {:keys [frame]} (nest/placement c nil :main [u v] 16)] diff --git a/frontend/test/arthur/domain/instance_test.cljs b/frontend/test/arthur/domain/instance_test.cljs index d8b0ce3..80c5a29 100644 --- a/frontend/test/arthur/domain/instance_test.cljs +++ b/frontend/test/arthur/domain/instance_test.cljs @@ -288,33 +288,3 @@ (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])) "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))))) diff --git a/frontend/test/arthur/domain/nest_test.cljs b/frontend/test/arthur/domain/nest_test.cljs index aba75ff..d224a4f 100644 --- a/frontend/test/arthur/domain/nest_test.cljs +++ b/frontend/test/arthur/domain/nest_test.cljs @@ -356,15 +356,3 @@ (is (= [22 23] (node/placed-span (get-in slid [:symbols :lane :nodes :b])))) (is (= 1 (:rate (get-in slid [:symbols :lane :nodes :b :time]))) "and the rest of its map is still there"))) - -(deftest sliding-in-a-lane-claims-overlapped-time - ;; The gesture is `nest/slide` in both display modes. Lane mode contributes - ;; only the placement rule at commit: the moved cel replaces what it covers. - (let [c (on-twos) - r (nest/slide c :lane [:b] -14) - slid (:clip r)] - (is (nil? (:refused r)) (:refused r)) - (is (= [5 6] (node/placed-span (get-in slid [:symbols :lane :nodes :b])))) - (is (nil? (get-in slid [:symbols :lane :nodes :a])) - "a fully covered neighbor is removed") - (is (empty? (clip/problems slid))))) diff --git a/frontend/test/arthur/events/lane_test.cljs b/frontend/test/arthur/events/lane_test.cljs index 5c8f4e0..8aade43 100644 --- a/frontend/test/arthur/events/lane_test.cljs +++ b/frontend/test/arthur/events/lane_test.cljs @@ -3,7 +3,6 @@ [arthur.domain.clip :as clip] [arthur.domain.history :as history] [arthur.domain.leaf :as leaf] - [arthur.domain.node :as node] [arthur.domain.sequence-test :as fixture] [arthur.domain.span :as span] [arthur.events.ui :as ui] @@ -17,41 +16,20 @@ (let [doc (clip/blank) id (store/install! {:clip doc :store {}} key)] (reset! rf-db/app-db {:clip/current id :paint/revision 0 - :ui {:open :main} :playback {:frame 6}}) + :ui {:open :main} :playback {:frame 0}}) (rf/dispatch-sync event) (let [db @rf-db/app-db saved (:clip (store/entry id)) [_ _ instance-id] (get-in db [:ui :selection]) sid (get-in saved [:symbols :main :nodes instance-id :source :symbol])] - {:db db :document saved :instance-id instance-id - :symbol (clip/symbol saved sid)})))] + {:db db :symbol (clip/symbol saved sid)})))] (let [{ordinary :symbol} (run [::ui/new-symbol :inside] "explicit-symbol") - {lane :symbol lane-db :db document :document instance-id :instance-id} - (run [::ui/new-lane] "explicit-lane")] + {lane :symbol lane-db :db} (run [::ui/new-lane] "explicit-lane")] (is (nil? (:display ordinary)) "new symbol means ordinary symbol") (is (= :lane (:display lane)) "only the lane command creates a lane") - (is (= [0 (clip/frames document :main)] - (node/placed-span (get-in document [:symbols :main :nodes instance-id]))) - "a lane exists across the open symbol, independent of the playhead") - (let [longer (assoc-in document [:symbols :main :frames] 300) - fitted (:clip (span/finish longer :main - (get-in longer [:symbols :main :nodes]) - nil :keep)) - lane-id (node/source (get-in fitted [:symbols :main :nodes instance-id]))] - (is (= [0 300] - (node/placed-span (get-in fitted [:symbols :main :nodes instance-id]))) - "the lane follows a later change to its parent's extent") - (is (= 300 (clip/frames fitted lane-id)))) (is (some? (get-in lane-db [:ui :target])) "the new lane is aimed so drawing and pool drops can go into it")))) -(deftest aiming-the-root-clears-selection-and-drawing-target - (let [db {:ui {:selection [:node :main :shape [:lane :shape]] - :target {:sid :main :id :lane :path [:lane]}}} - after (ui/aimed db nil)] - (is (nil? (get-in after [:ui :selection]))) - (is (nil? (get-in after [:ui :target]))))) - (deftest an-explicit-lane-is-one-row-of-clips (let [doc (fixture/document) rows (timeline/rows doc :main #{}) @@ -63,29 +41,6 @@ [:node :main :insert [:insert]]] (mapv :select (:cels lane)))))) -(deftest a-nested-lane-is-drawn-in-the-open-symbols-frame-rate - (let [child {:id :cel :kind :instance :z "a" :span [0 2] - :time {:at 4 :rate 1} - :source {:symbol :drawing} - :playback {:in 0 :speed 0 :end :stop}} - placed {:id :take :kind :instance :z "a" :span [0 12] - :time {:at 0 :rate 1} - :source {:symbol :lane} - :playback {:in 0 :speed 1 :end :stop}} - doc (-> (clip/blank) - (assoc :fps 12) - (assoc-in [:symbols :main :fps] 12) - (assoc-in [:symbols :main :frames] 12) - (assoc-in [:symbols :main :nodes] {:take placed}) - (assoc-in [:symbols :lane] - {:id :lane :fps 24 :frames 24 :display :lane - :nodes {:cel child}}) - (assoc-in [:symbols :drawing] - {:id :drawing :fps 24 :frames 1 :nodes {}})) - lane (first (filter :lane? (timeline/rows doc :main #{})))] - (is (= [[2 3]] (mapv :span (:cels lane))) - "native frames 4–6 occupy ruler frames 2–3 at twice the frame rate"))) - (deftest an-ordinary-symbol-keeps-a-row-per-node (let [doc (update-in (fixture/document) [:symbols :main] dissoc :display) rows (timeline/rows doc :main #{})] @@ -182,14 +137,6 @@ (is (empty? (get-in after [:ui :expanded])) "and there are no rows above the top to open"))) -(deftest a-primary-selection-also-starts-the-stage-selection-set - (let [doc (fixture/document) - id (store/install! {:clip doc :store {}} "primary-stage-selection") - address [:node :main :insert [:insert]] - after (ui/selected {:clip/current id :ui {}} address)] - (is (= address (get-in after [:ui :selection]))) - (is (= [address] (get-in after [:ui :selections]))))) - (deftest finishing-a-polygon-opens-no-rows ;; Expansion is the twist triangle's business. Finishing a shape used to open ;; every row down to it, which inside a lane meant tearing its one row into a diff --git a/static/arthur/app.css b/static/arthur/app.css index c6ab16a..80a3afa 100644 --- a/static/arthur/app.css +++ b/static/arthur/app.css @@ -800,9 +800,6 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); } } .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 a row of dots says that where a row of tiles says "swatch book". */ @@ -1029,9 +1026,10 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); } min-width: max(340px, calc((100% - var(--label)) * var(--tl-zoom, 1))); position: relative; background: #fff; - /* The major frame grid, using the same whole-second interval as the numbered - ruler. It is a background rather than an element per mark; `--tick` is set - by the component because only it knows the open symbol's length and FPS. */ + /* The frame grid, five frames to a division, as a background rather than as + an element per frame: a 900-frame take is 900 divs nobody needs in the DOM. + `--tick` is five frames as a percentage of the span, set from the component + because only it knows how long the clip is. */ background-image: repeating-linear-gradient(90deg, var(--grid-5) 0 1px, transparent 1px var(--tick, 10%)); @@ -1380,7 +1378,6 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); } .paint-overlay .handles .knob { fill: #161820; stroke: #fff1be; stroke-width: 0.6; cursor: grab; } .paint-overlay .handles .corner { fill: #fff1be; stroke: #161820; stroke-width: 0.5; cursor: nwse-resize; } .paint-overlay .handles .pivot { stroke: #fff1be; stroke-width: 0.6; pointer-events: none; } -.paint-overlay .marquee { fill: rgba(230, 202, 139, 0.12); stroke: #e6ca8b; stroke-width: 0.6; stroke-dasharray: 2 1; pointer-events: none; } /* The aim outline and its name tag. Solid, no handles, and a tag pinned just outside the bottom-right corner — see `ui/stage/aim-box` for why the box