This commit is contained in:
Your Name 2026-10-02 17:44:11 -04:00
parent edca82cd5d
commit 664252e0fc
17 changed files with 373 additions and 112 deletions

View file

@ -360,9 +360,15 @@
(if (and (= :linear (segment-interp ch left)) right (>= f left) (> right left))
(let [b (get (:keys ch) right)
t (/ (- f left) (- right left))]
(if (vector? a)
(cond
;; Palette-choice channels interpolate identities into a blend
;; descriptor. The renderer keeps indexed geometry in the left
;; palette's bank and blends that bank's ramp toward the right one.
(and (= :palette (:semantic ch)) (keyword? a) (keyword? b))
{:from a :to b :t t}
(vector? a)
(mapv (fn [x y] (+ x (* t (- y x)))) a b)
(+ a (* t (- b a)))))
:else (+ a (* t (- b a)))))
a)))
(defn- keyed-at
@ -514,6 +520,7 @@
(let [values (when (map? (:keys ch)) (vals (:keys ch)))
first-value (first values)
linear-values? (or (every? number? values)
(and (= :palette (:semantic ch)) (every? keyword? values))
(and (vector? first-value)
(pos? (count first-value))
(every? (fn [v] (and (vector? v)

View file

@ -280,9 +280,14 @@
(and a (<= a frame) (< frame b))))
(symbol/children (:nodes track-symbol))))
palette-symbol (some-> palette-clip node/source
(#(symbol clip %)))]
(when (= :palette (:type palette-symbol))
(:palette-ref palette-symbol))))
(#(symbol clip %)))
fallback (when (= :palette (:type palette-symbol))
(:palette-ref palette-symbol))
choice (get-in palette-clip [:channels [:palette]])
start (some-> palette-clip node/placed-span first)]
(or (when (and choice start)
(channel-value choice (- frame start)))
fallback)))
track-value
(channel-value (:palette owner) frame)
inherited

View file

@ -45,7 +45,10 @@
change to this spec silently change what gets drawn."
(let [base (into #{[:vis]} xform-paths)]
{:group base
:instance base
;; A palette-track placement is still an instance. Its palette choice is
;; a parameter channel on that instance, so the generic span commands can
;; move and trim it without knowing what kind of lane owns it.
:instance (conj base [:palette])
;; A sound is not in the picture: no transform, no visibility. After
;; Effects' audio-only layer has no Transform group for the same reason.
:audio #{[:audio :gain] [:audio :pan] [:audio :rate]}
@ -102,7 +105,9 @@
[n path f v]
(let [c (get (channels n) path)]
(assoc-in n [:channels path]
(if (:keys c) (assoc-in c [:keys f] v) (ch/framed v)))))
(if (:keys c)
(assoc-in c [:keys f] v)
(merge (select-keys c [:semantic]) (ch/framed v))))))
(defn set-keyed-channel
"Write `v` as a key at `f`, starting an animated channel when needed. This is
@ -113,7 +118,8 @@
(assoc-in n [:channels path]
(if (:keys c)
(assoc-in c [:keys f] v)
(ch/keyed {f v} (if (boolean? v) :hold :linear))))))
(merge (select-keys c [:semantic])
(ch/keyed {f v} (if (or (boolean? v) (keyword? v)) :hold :linear)))))))
(defn toggle-key
"Key channel `path` on the node's own frame `f` with the value it has there, or
@ -128,7 +134,8 @@
ks (dissoc (:keys c) f)]
(assoc-in n [:channels path]
(cond
(not (:keys c)) (ch/keyed {f v} (if (boolean? v) :hold :linear))
(not (:keys c)) (merge (select-keys c [:semantic])
(ch/keyed {f v} (if (or (boolean? v) (keyword? v)) :hold :linear)))
(not (contains? (:keys c) f)) (assoc-in c [:keys f] v)
(seq ks) (cond-> (assoc c :keys ks)
(:segments c) (update :segments dissoc f))

View file

@ -85,7 +85,10 @@
(vals (:symbols clip)))
(for [sym (vals (:symbols clip))
n (vals (:nodes sym))]
(:palette n))))
(:palette n))
(for [sym (vals (:symbols clip))
n (vals (:nodes sym))]
(get-in n [:channels [:palette]]))))
ps (select-keys (palettes clip) used)
ordered (sort-by (comp str key) ps)
offsets (loop [xs ordered at 0 out {}]
@ -105,7 +108,8 @@
(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)
(let [id (if (map? id) (:from id) id)
p (get palettes id)
i (and p (slot-index p value))]
(if (some? i) (+ (get offsets id 0) i) 255)))
@ -119,6 +123,26 @@
(render-index palette active 0)
(get palette :bg 0)))
(defn effective-ramp
"The compiled ramp with the active palette blend applied. A hold returns the
original vector; a linear palette segment rewrites only the left palette's
bank, which is the bank the indexed raster was drawn into."
[{:keys [palettes offsets ramp]} active]
(if-let [{:keys [from to t]} (when (map? active) active)]
(let [a (get palettes from)
b (get palettes to)
at (get offsets from)
n (min (count (:slots a)) (count (:slots b)))]
(if (and a b (number? at) (number? t))
(reduce (fn [out i]
(let [x (hex->rgb (get-in a [:slots i :hex]))
y (hex->rgb (get-in b [:slots i :hex]))]
(assoc out (+ at i)
(mapv #(js/Math.round (+ %1 (* t (- %2 %1)))) x y))))
ramp (range n))
ramp))
ramp))
(defn valid-palette? [{:keys [id name slots]}]
(and id (string? name) (seq name) (vector? slots) (pos? (count slots))
(<= (count slots) 255)

View file

@ -751,7 +751,12 @@
:frames 1 :fps (clip/fps document root-sid) :nodes {}}))
id (random-uuid)
result (span/place-symbol document st track-id id palette-sid frame
{:extent :grow-symbol :remainder-id (random-uuid)})]
{:extent :grow-symbol :remainder-id (random-uuid)})
result (if-let [placed (:clip result)]
(assoc result :clip
(assoc-in placed [:symbols track-id :nodes id :channels [:palette]]
(assoc (ch/framed palette-id) :semantic :palette)))
result)]
(if-let [why (:refused result)]
(update db :project merge {:status why})
(-> db
@ -765,6 +770,34 @@
(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)))))
(defn- seed-palette-choice [document sid id]
(if (get-in document [:symbols sid :nodes id :channels [:palette]])
document
(let [source (get-in document [:symbols sid :nodes id :source :symbol])
palette-id (get-in document [:symbols source :palette-ref])]
(assoc-in document [:symbols sid :nodes id :channels [:palette]]
(assoc (ch/framed palette-id) :semantic :palette)))))
(rf/reg-event-db
::set-palette-choice
(fn [db [_ sid id frame palette-id]]
(let [put (if (get-in db [:ui :auto-key?]) node/set-keyed-channel node/set-channel)]
(edit/edit db
(fn [document]
(update-in (seed-palette-choice document sid id)
[:symbols sid :nodes id]
put [:palette] frame palette-id))))))
(rf/reg-event-db
::toggle-palette-key
(fn [db [_ sid id frame]]
(let [st (:store (store/entry (:clip/current db)))]
(edit/edit db
(fn [document]
(update-in (seed-palette-choice document sid id)
[:symbols sid :nodes id]
node/toggle-key [:palette] frame st))))))
(rf/reg-event-db
::set-channels
;; One property assignment over a stage selection is one document edit.

View file

@ -63,26 +63,16 @@
(defn selected
"`db` with `selection` selected, and nothing aimed.
The rows above a selection are opened, so one made deep on the stage is seen
in the timeline. Not a sound's: its row is always in the audio section, and
opening the placement it is heard through would bury it.
AN EMPTY PATH IS A REAL SELECTION, not a missing one. The open symbol's own
lane row names itself with `[]`, and `(pop [])` THROWS — so clicking that row
aborted the whole event: nothing was selected, nothing was aimed, and the next
polygon went wherever the stale target still pointed, which is most of what
made aiming a lane feel random. There is nothing above a selection at the top
to open, so the guard is `seq` rather than `some?`."
Selection and row disclosure are independent. A click or trim can select a
nested item, but only the row's twist button opens or closes it."
[db selection]
(let [[kind sid id path] selection
sound? (= :audio (get-in (store/entry (:clip/current db))
[: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)))))))
;; A captured trim gesture also selects on pointer-up. It must not acquire the
;; unrelated side effect of opening every row above the edited clip.
(cond-> (-> db
(assoc-in [:ui :selection] selection)
(assoc-in [:ui :selections] (if selection [selection] []))
(update :ui dissoc :points :retry))
(nil? selection) (assoc-in [:ui :target] nil)))
(defn target-of
"The target `selection` names: `{:sid :id :path}`, or nil for the open symbol
@ -487,8 +477,14 @@
question `symbol/lane?` answers."
[clip st db frame]
(let [open (get-in db [:ui :open])
down (where-new-goes clip db)]
(assoc (nest/inside clip st open down frame) :path down)))
down (where-new-goes clip db)
inside (nest/inside clip st open down frame)]
;; A stale or presently invisible target is no target. Creation must remain
;; a total operation, so it falls back to the document's main symbol.
(if (:sid inside)
(assoc inside :path down)
{:sid (if (get-in clip [:symbols :main]) :main open)
:path [] :frame frame})))
;; ---------------------------------------------------------------------------
;; clipboard and local duplication
@ -562,7 +558,7 @@
{:sid open :path [] :frame frame}
(nil? n)
{:refused "the paste target no longer exists"}
{:sid (if (get-in clip [:symbols :main]) :main open) :path [] :frame frame}
(= :instance (:kind n))
(let [visible (nest/inside clip st open path frame)
@ -576,12 +572,12 @@
at (or (:frame visible) (:frame shown) mapped held)]
(if (number? at)
{:sid (node/source n) :path (vec path) :frame (js/Math.floor at)}
{:refused "the playhead has no single frame inside the paste target"}))
{:sid (if (get-in clip [:symbols :main]) :main open) :path [] :frame frame}))
:else
(if (number? owner-frame)
{:sid sid :path (vec (butlast path)) :frame (js/Math.floor owner-frame)}
{:refused "the playhead has no single frame in the paste target"}))))
{:sid (if (get-in clip [:symbols :main]) :main open) :path [] :frame frame}))))
(rf/reg-event-db
::paste

View file

@ -210,16 +210,19 @@
(fn [chain i]
(.then chain
(fn [_]
(let [ops (resolve-frame i)]
(let [ops (resolve-frame i)
active (clip/active-palette resolve-frame)]
(-> ras
(raster/clear! (pal/background-index palette
(clip/active-palette resolve-frame)))
(raster/draw-ops! ops)))
(-> (js/Promise.resolve (frame! exporter i ras))
(raster/clear! (pal/background-index palette active))
(raster/draw-ops! ops))
(-> (js/Promise.resolve
(frame! exporter i
(assoc ras :palette-ramp
(pal/effective-ramp palette active))))
(.then (fn [_]
(when on-progress
(on-progress (inc i) frames))
(yield!)))))))
(yield!))))))))
(js/Promise.resolve)
(range frames))))
(.then (fn [_] (finish! exporter)))))))

View file

@ -63,7 +63,7 @@
;; raster: keeping a reference to it and encoding later would encode
;; the last frame N times, and every frame would be a valid PNG of the
;; wrong picture.
(-> (encode raster ramp)
(-> (encode raster (or (:palette-ramp raster) ramp))
(.then (fn [bytes]
(swap! state update :entries conj
{:name (str name "/" (pad (inc i) digits) ".png")

View file

@ -77,6 +77,13 @@
:parse #(if (seq %) (js/parseInt % 10) nil)
:on-number on-change}]])
(defn- name-field [label value commit!]
[:label.inspector-field label
[:input {:type "text" :default-value value
:key value
:on-key-down #(when (= "Enter" (.-key %)) (.. % -target blur))
:on-blur #(commit! (.. % -target -value))}]])
;; ---------------------------------------------------------------------------
;; the clip
@ -89,12 +96,13 @@
busy? (:busy? project)]
[section "project"
[facts
"name" (or (:name project) (:name clip))
"stage" (str (:width clip) "×" (:height clip))
"open" (some-> open name)
"length" (str frames " frames")
"rate" (str fps " fps")]
[:div.inspector-form
[name-field "name" (or (:name project) (:name clip) "")
#(rf/dispatch [::project/rename %])]
[number-field "width" (:width clip)
#(rf/dispatch [::project/project-setting :width %]) nil busy?]
[number-field "height" (:height clip)
@ -208,7 +216,6 @@
frame (:frame local)]
[section (str (name (:kind n)) " · in " (name sid))
[facts
"name" (or (:name n) (brief id))
"id" (brief id)
;; Which symbol a cel places. The one fact that makes an instance
;; legible as an instance rather than as a node.
@ -217,6 +224,9 @@
;; in this symbol. See `node/placed-span`.
"span" (when start (str start " … " end))
"at" (when (node/mapped-time? n) (str (get-in n [:time :at] 0)))]
[:div.inspector-form
[name-field "name" (or (:name n) "")
#(rf/dispatch [::ui/rename-node sid id %])]]
(when (:paint? n) [drawing-keys sid id n @(rf/subscribe [::sub/selected-local])])
[:div.row {:style {:margin-top "6px"}} [:span.dim "channels"]]
(let [{:keys [frame]} local]
@ -230,6 +240,58 @@
[channel-control sid id path ch frame auto-key?]
[:dd (channel-state ch)])]))])]))
(defn- palette-placement-section [[sid id n]]
(let [clip @(rf/subscribe [::render/clip])
transition (node/source n)
open-frame @(rf/subscribe [::render/open-frame])
[start _] (node/placed-span n)
frame (max 0 (- open-frame (or start 0)))
ch (or (get-in n [:channels [:palette]])
(assoc (channel/framed
(get-in clip [:symbols (node/source n) :palette-ref]))
:semantic :palette))
value (channel/value-at ch frame nil)
chosen (if (map? value) (:from value) value)
keyframes (sort (keys (:keys ch)))
left (when (seq keyframes)
(or (last (filter #(<= % frame) keyframes)) (first keyframes)))
right (when left (first (filter #(< left %) keyframes)))
palettes (sort-by (comp str :name val) (pal/palettes clip))]
[section "palette transition"
[:div.inspector-form
[name-field "name" (clip-domain/symbol-name clip transition)
#(rf/dispatch [::project/rename-symbol transition %])]]
[:div.inspector-form
[:label.inspector-field "palette"
[:span.row
[:button.key {:class (cond (contains? (:keys ch) frame) "on" (:keys ch) "keyed")
:title (if (:keys ch) (str (count (:keys ch)) " keys") "key this here")
:on-click #(rf/dispatch [::project/toggle-palette-key sid id frame])}
"◆"]
[:select {:value (str chosen)
:on-change
(fn [e]
(let [v (.. e -target -value)
palette-id (first (filter #(= v (str %)) (keys (pal/palettes clip))))]
(rf/dispatch [::project/set-palette-choice sid id frame palette-id])))}
(for [[palette-id p] palettes]
^{:key (str palette-id)}
[:option {:value (str palette-id)} (:name p)])]]]
(when right
[:label.inspector-field (str "key " left " → " right)
[segment-select sid id [:palette] ch left]])]]))
(defn- palette-symbol-section [sid]
(let [clip @(rf/subscribe [::render/clip])
sym (get-in clip [:symbols sid])]
[section "palette transition"
[:div.inspector-form
[name-field "name" (clip-domain/symbol-name clip sid)
#(rf/dispatch [::project/rename-symbol sid %])]]
[facts "palette" (or (get-in clip [:palettes (:palette-ref sym) :name])
(:palette-ref sym))]
[:div.dim "Keyframes and cut/tween interpolation are edited on a placement in the palette lane."]]))
;; ---------------------------------------------------------------------------
;; corrections
@ -540,6 +602,8 @@
"stage" (let [[w h] (clip-domain/stage clip sid)] (str w "×" h))
"nodes" (str (count (:nodes sym)))]
[:div.inspector-form
[name-field "name" (clip-domain/symbol-name clip sid)
#(rf/dispatch [::project/rename-symbol sid %])]
[number-field "length (frames)" (:frames sym)
#(rf/dispatch [::project/symbol-setting sid :frames %]) nil busy?]
[number-field "fps" (clip-domain/fps clip sid)
@ -567,28 +631,6 @@
(rf/dispatch [::project/symbol-setting sid :height nil]))}
"use project stage"]])]]))
(defn- symbol-parameters-section [sid frame title]
(let [clip @(rf/subscribe [::render/clip])
auto-key? @(rf/subscribe [::sub/auto-key?])
nodes (get-in clip [:symbols sid :nodes])]
(when (and sid (number? frame) (seq nodes))
(into
[section title]
(for [[id n] (sort-by (comp str key) nodes)
:let [editable (filter (fn [[path ch]]
(and (contains? (node/defaults-of n) path)
(not (:dense ch))))
(node/channels n))]
:when (seq editable)]
^{:key (str id)}
(into
[:div.inspector-form
[:div.row [:span.dim (or (:name n) (brief id))]]]
(for [[path ch] editable]
^{:key (str path)}
[:label.inspector-field (str/join " " (map name path))
[channel-control sid id path ch frame auto-key?]])))))))
;; ---------------------------------------------------------------------------
;; tracked objects
;;
@ -692,6 +734,10 @@
;; lane traces is not a question with one answer, and naming the drawing
;; showing now would move the section under the playhead.
placed (node/source (peek node))
palette-placement? (and node
(= :palette (get-in clip [:symbols placed :type])))
palette-symbol? (and (= :symbol (first selection))
(= :palette (get-in clip [:symbols (second selection) :type])))
face (or placed (when (trace/traceable? clip open) open))
faces (when face (trace/faces clip face))
;; Every face the open symbol has, which is what the footage switch is
@ -706,23 +752,19 @@
[:section.pane.params
[:div.pane-head "inspector"]
[:div {:style {:min-height 0}}
[clip-section]
(when node [node-section node])
(when placed [symbol-section placed "source symbol"])
(when placed
(let [{source-sid :sid source-frame :frame} @(rf/subscribe [::sub/selected-local])]
(when (= placed source-sid)
[symbol-parameters-section placed source-frame "source parameters"])))
(when node ^{:key (str (first node) "/" (second node))}
(if palette-placement?
[palette-placement-section node]
[clip-section])
(when (and node (not palette-placement?)) [node-section node])
(when (and placed (not palette-placement?)) [symbol-section placed "source symbol"])
(when (and node (not palette-placement?)) ^{:key (str (first node) "/" (second node))}
[correction-section node])
(when (seq here) [footage-section here])
(when (and (not palette-placement?) (seq here)) [footage-section here])
(when (and face (or (trace/traceable? clip face) (seq faces)))
[tracing-section face faces path])
(when face ^{:key (str "perf/" face)} [performance-section face])
(when (= :symbol (first selection)) [symbol-section (second selection)])
(when (= :symbol (first selection))
(let [sid (second selection)
frame (min (max 0 @(rf/subscribe [::render/open-frame]))
(max 0 (dec (or (clip-domain/frames clip sid) 1))))]
[symbol-parameters-section sid frame "symbol parameters"]))
(when tracked? [tracking-section])]]))
(when palette-symbol? [palette-symbol-section (second selection)])
(when (and (= :symbol (first selection)) (not palette-symbol?))
[symbol-section (second selection)])
(when (and tracked? (not palette-placement?) (not palette-symbol?))
[tracking-section])]]))

View file

@ -172,15 +172,16 @@
;; THE STAGE IS THE CLIP'S, not a constant. Project dimensions are
;; independent of the footage, so the size the frame is rasterised at comes
;; out of the document like everything else.
(let [ras (raster-for width height)]
(let [ops (resolver f)
bg (pal/background-index palette (clip/active-palette resolver))]
(swap! state assoc :ops ops)
(-> ras
(raster/clear! bg)
(raster/draw-ops! ops)))
(let [ras (raster-for width height)
ops (resolver f)
active (clip/active-palette resolver)
bg (pal/background-index palette active)]
(swap! state assoc :ops ops)
(-> ras
(raster/clear! bg)
(raster/draw-ops! ops))
(js/performance.mark "arthur/blit:start")
(canvas/blit! canvas ras ramp)
(canvas/blit! canvas ras (pal/effective-ramp palette active))
(underlay/paint! (assoc underlay :width width :playing? playing?)
resolver repaint!))
(js/performance.measure "arthur/resolve+draw" "arthur/paint:start" "arthur/blit:start")

View file

@ -290,6 +290,22 @@
:on-double-click #(rf/dispatch [::project/default-palette id])}
(carrying (str "palette:" id) #(drag/palette! id (:name p))))])
(defn- palette-transition-row [document sid selection rename]
(let [sym (clip/symbol document sid)
p (get (pal/palettes document) (:palette-ref sym))]
^{:key (str sid)}
[row {:label (clip/symbol-name document sid)
:sub (str "uses " (or (:name p) (:palette-ref sym)))
:title "Palette transition definition"
:on? (= selection [:symbol sid])
:rename (assoc rename
:key [:symbol sid]
:value (clip/symbol-name document sid)
:commit! (fn [value]
((:begin! rename) nil)
(rf/dispatch [::project/rename-symbol sid value])))
:on-click #(rf/dispatch [::ui/select [:symbol sid]])}]))
(defn- footage-row [{:keys [id label frames fps video] :as f} chosen rename]
^{:key id}
[row (merge {:label label
@ -403,23 +419,32 @@
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 palette-choice selection] :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))))
symbol-ids (sort-by str (keys (:symbols document)))
transition? #(= :palette (:type (clip/symbol document %)))
transitions (filterv #(and (named? %) (transition? %)) symbol-ids)
rest (filterv #(and (named? %) (not= main %)
(not (#{:palette :palette-track}
(:type (clip/symbol document %)))))
symbol-ids)
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)))]
[sections searching?
(+ (if top 1 0) (count rest) (count media) (count sounds) (count palettes))
(+ (if top 1 0) (count rest) (count transitions)
(count media) (count sounds) (count palettes))
[{: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 "palette transitions" :searching? searching?
:blank "no palette transitions"
:rows (mapv #(palette-transition-row document % selection rename) transitions)}
{:title "palettes" :searching? searching?
:blank "no palettes"
:rows (mapv (fn [[id p]] (palette-row id p (:default-palette document)

View file

@ -10,6 +10,7 @@
THE CANVAS IS THE RASTER'S OWN SIZE, scaled by CSS. See `ui/canvas` for why
that is load-bearing rather than convenient."
(:require [arthur.domain.channel :as channel]
[arthur.domain.clip :as clip]
[arthur.domain.gesture :as gesture]
[arthur.domain.nest :as nest]
[arthur.domain.node :as node]
@ -473,6 +474,15 @@
;; A drop lands at the playhead IN THE OPEN SYMBOL's frames, the same
;; unit a drop on the timeline's ruler lands in.
frame @(rf/subscribe [::render/open-frame])
clip @(rf/subscribe [::render/clip])
clip-id @(rf/subscribe [::render/clip-id])
db-target @(rf/subscribe [::sub/target])
open @(rf/subscribe [::render/open])
destination (when clip
(ui/aimed-symbol clip (:store (store/entry clip-id))
{:ui {:open open :target db-target}} frame))
destination-name (when-let [sid (:sid destination)]
(clip/symbol-name clip sid))
zoom @(rf/subscribe [::layout/zoom :stage])]
[:div.stage-area
[:div.stage-wrap
@ -498,4 +508,6 @@
:height (str (* zoom h) "px")}}]
[:canvas.underlay {:ref #(underlay/set-canvas! %)
:width (* zoom w) :height (* zoom h)}]
[overlay w h zoom]]]))
[overlay w h zoom]]
(when destination-name
[:div.stage-target "creating in " [:strong destination-name]])]))

View file

@ -339,6 +339,7 @@
(-> (first (rows clip track-id expanded chosen))
(assoc :path [::palette-track] :owner track-id
:label "palette" :kind :palette
:expandable? false :expanded? false
:select [::palette-track sid]))
{:path [::palette-track] :depth 0 :label "palette" :kind :palette
:owner nil :select [::palette-track sid]
@ -730,7 +731,11 @@
active-row (:row @sliding)
slide (fn [^js e]
(let [{from :row pressed :from} @sliding]
(when (= path from)
;; Pointer capture already makes this the one track receiving
;; the gesture. Do not compare its presentation path: the
;; palette lane is a view of another symbol and its synthetic
;; row address is not the authored clip path.
(when from
;; HOW MANY FRAMES THE POINTER HAS MOVED, as the difference
;; of two frames under it -- not a pixel distance scaled and
;; rounded by itself. The ghost below and the command on
@ -779,7 +784,14 @@
target-sid (when target
(:sid (ui/drop-destination-at
clip store open target-at target)))
;; Row identity answers before document addressing:
;; remaining on the DOM track that captured the
;; pointer is a slide. This is true for every lane
;; and avoids asking special rows (such as the
;; palette track) to counterfeit an ordinary target
;; address merely to recognize themselves.
landing? (and target-sid
(not (identical? target-el (.-currentTarget e)))
(not= target-sid (nth (:selection drag) 1)))
target-frame (when landing?
(max 0 (- target-at (:grab drag))))]
@ -814,7 +826,7 @@
(:kind @sliding) (:ripple? @sliding)
(:other @sliding) owner]))))))))
done (fn [commit?]
(when (= path (:row @sliding))
(when @sliding
(let [{:keys [path df kind ripple? other target-lane target-frame
drag nest on-click more?]} @sliding]
(reset! sliding nil)
@ -880,11 +892,18 @@
;; one, else this row's own instance. It opens as a tab, which is what
;; double-clicking the same symbol in the pool does.
:on-double-click (fn [^js e]
(when-let [source (or (:source (clip-under e)) of)]
(when-let [source (and (not= :palette kind)
(or (:source (clip-under e)) of))]
(.stopPropagation e)
(rf/dispatch [::pb/open-symbol source])))
:ref (when lane? (fn [el] (when el (aset el "arthurLane" select))))
:on-drag-enter (fn [^js e]
;; A palette hover belongs only to the palette row. If
;; the pointer leaves it for an ordinary row, clear its
;; last accepted preview even though this row correctly
;; declines the native drop.
(when (and (drag/palette?) (not= :palette kind))
(rf/dispatch [::ui/drop-clear]))
(cond
(and (= :palette kind) (drag/palette?))
(do (.preventDefault e) (.stopPropagation e))
@ -947,6 +966,9 @@
:on-pointer-down
(when (and select (not unmapped?))
#(begin! % (or slides path) :slide select nil nil))}
(when (and select (not unmapped?))
[:span.tl-edge.in {:title "Drag start"
:on-pointer-down #(begin! % path :in select nil nil)}])
(when (and select (not unmapped?))
[:span.tl-edge.out {:title "Drag endpoint · Shift-drag ripples later clips"
:on-pointer-down #(begin! % path :out select nil nil)}])]))
@ -966,8 +988,10 @@
:when (< in out)]
^{:key (str id)}
[:button.tl-cel
{:title (str label " · select clip; double-click to edit its symbol"
" · shift-drag another clip onto it to nest that clip inside")
{:title (if (= :palette kind)
(str label " · select palette transition")
(str label " · select clip; double-click to edit its symbol"
" · shift-drag another clip onto it to nest that clip inside"))
;; WHICH CLIP IS SELECTED IS THE QUESTION THIS ROW ANSWERS. An
;; expanded lane opens exactly the selected clip, so a block that
;; cannot say whether it is the one leaves the portal below it
@ -1114,19 +1138,19 @@
{:id ::drop :label (str "+ " (:label drop)) :ghost? true
:span drop-span})
chosen (when (= :node (first selection)) (nth selection 3))
picture (cond->> (cond->> (into [(palette-track-row clip open frames expanded chosen)]
(rows clip open expanded chosen))
lane-ghost
(mapv (fn [row]
(if (= drop-lane (:select row))
(update row :cels (fnil conj []) lane-ghost)
row))))
picture (cond-> (cond->> (into [(palette-track-row clip open frames expanded chosen)]
(rows clip open expanded chosen))
lane-ghost
(mapv (fn [row]
(if (= drop-lane (:select row))
(update row :cels (fnil conj []) lane-ghost)
row))))
;; An untargeted visual drop becomes a new ordinary row
;; at the bottom. Inserting it above the lanes shifts the
;; lane under the pointer and makes the browser alternate
;; between this ghost and that lane's drop target.
(and ghost (not (:sound? drop)))
(fn [rows] (conj (vec rows) ghost)))
(conj ghost))
sounds (cond->> (into (vec (filter :sound? picture))
(sound-rows clip open expanded))
(and ghost (:sound? drop)) (cons ghost))

View file

@ -384,6 +384,14 @@
(is (seq (ch/problems (ch/keyed {0 :a, 10 :b} :linear)))
"a cut or tone cannot interpolate"))
(deftest palette-choice-channels-can-cut-or-blend
(let [held (assoc (ch/keyed {0 :day 10 :night} :hold) :semantic :palette)
blend (assoc held :interp :linear)]
(is (empty? (ch/problems blend)))
(is (= :day (ch/value-at held 5 nil)))
(is (= {:from :day :to :night :t 0.5}
(ch/value-at blend 5 nil)))))
(deftest numeric-channels-can-ramp-between-keys
(let [c (ch/keyed {0 0.0, 10 1.0} :linear)
cursor (ch/cursor c nil)]

View file

@ -4,6 +4,7 @@
[arthur.domain.channel :as ch]
[arthur.domain.clip :as clip]
[arthur.domain.leaf :as leaf]
[arthur.domain.nest :as nest]
[arthur.domain.node :as node]
[arthur.domain.paint :as paint]
[arthur.domain.pose :as pose]
@ -362,3 +363,46 @@
(is (= :night (clip/active-palette main)))
(is (= (pal/render-index context :night 0)
(pal/background-index context (clip/active-palette main))))))
(deftest a-palette-lane-can-blend-between-keyed-palettes
(let [palette (fn [id color]
{:id id :name (name id) :slots [{:hex color} {:hex color}]})
mark {:id :mark :kind :rect :z "a1"
:channels {[:geom :size] (ch/framed 2)
[:style :color] (ch/framed 1)}}
choice (assoc (ch/keyed {0 :day 4 :night} :linear) :semantic :palette)
document {:fps 30 :width 20 :height 20
:palettes {:day (palette :day "#000000")
:night (palette :night "#ffffff")}
:default-palette :day
:symbols
{:main {:id :main :frames 5 :palette-track :palettes
:nodes {:drawing {:id :drawing :kind :instance :z "a1"
:source {:symbol :drawing}}}}
:palettes {:id :palettes :type :palette-track :display :lane :frames 5
:nodes {:change {:id :change :kind :instance :z "a1"
:source {:symbol :day-palette}
:span [0 5] :time {:at 0}
:channels {[:palette] choice}}}}
:day-palette {:id :day-palette :type :palette :palette-ref :day
:frames 1 :nodes {}}
:drawing {:id :drawing :frames 5 :nodes {:mark mark}}}}
context (pal/compile document)
resolve (clip/resolver document :main nil context nil)
slid (nest/slide document :palettes [:change] 1)
trimmed (nest/resize-in document :palettes [:change] 1)
extended (nest/resize-out document :palettes [:change] 1 false)]
(resolve 2)
(is (= {:from :day :to :night :t 0.5} (clip/active-palette resolve)))
(is (= [128 128 128]
(nth (pal/effective-ramp context (clip/active-palette resolve))
(pal/render-index context :day 1))))
(is (nil? (:refused slid)) (:refused slid))
(is (= [1 6] (node/placed-span
(get-in slid [:clip :symbols :palettes :nodes :change]))))
(is (nil? (:refused trimmed)) (:refused trimmed))
(is (= [1 5] (node/placed-span
(get-in trimmed [:clip :symbols :palettes :nodes :change]))))
(is (nil? (:refused extended)) (:refused extended))
(is (= [0 6] (node/placed-span
(get-in extended [:clip :symbols :palettes :nodes :change]))))))

View file

@ -52,6 +52,13 @@
(is (nil? (get-in after [:ui :selection])))
(is (nil? (get-in after [:ui :target])))))
(deftest clearing-selection-also-clears-the-creation-target
(let [db {:ui {:selection [:symbol :drawing]
:target {:sid :main :id :lane :path [:lane]}}}
after (ui/selected 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 #{})
@ -206,6 +213,14 @@
(is (= address (get-in after [:ui :selection])))
(is (= [address] (get-in after [:ui :selections])))))
(deftest selecting-or-trimming-a-nested-lane-item-does-not-open-its-row
(let [selection [:node :inner :clip [:lane :clip]]
before {:ui {:expanded #{[:already-open]}}}
after (ui/selected before selection)]
(is (= selection (get-in after [:ui :selection])))
(is (= #{[:already-open]} (get-in after [:ui :expanded]))
"disclosure is changed only by the twist control")))
(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