Fix palette swap backgrounds and timeline drop jitter

This commit is contained in:
Your Name 2026-10-02 10:24:23 -04:00
parent 26fab9392e
commit 7c49c08bc7
8 changed files with 55 additions and 20 deletions

View file

@ -243,12 +243,17 @@
(assoc op :cx x :cy y :size (* scale (:size op)))) (assoc op :cx x :cy y :size (* scale (:size op))))
op))) op)))
(defprotocol IActivePalette
(active-palette [this]
"The palette selected by this resolver's most recently resolved frame."))
(defn resolver (defn resolver
"Resolve an output frame, selecting native content at each symbol boundary. "Resolve an output frame, selecting native content at each symbol boundary.
Every instance owns its cursors and buffers. The IResolver queries return Every instance owns its cursors and buffers. The IResolver queries return
native node frames and world matrices for the last rendered output frame." native node frames and world matrices for the last rendered output frame."
[clip sid store palette opts] [clip sid store palette opts]
(let [context? (and (map? palette) (:palettes palette) (:offsets palette))] (let [context? (and (map? palette) (:palettes palette) (:offsets palette))
active-palette-state (atom (:default palette))]
(letfn [(channel-value [selection frame] (letfn [(channel-value [selection frame]
(cond (cond
(nil? selection) nil (nil? selection) nil
@ -323,6 +328,9 @@
(root-selection-at sym (js/Math.floor f) inherited) (root-selection-at sym (js/Math.floor f) inherited)
inherited) inherited)
(:default palette)))) (:default palette))))
;; The same decision that maps local drawing slots to
;; the active bank also names the clear colour.
(when root? (reset! active-palette-state @active))
(vreset! entered {}) (vreset! entered {})
(let [by-id (into {} (map (juxt :node identity)) (let [by-id (into {} (map (juxt :node identity))
(own (js/Math.floor f) (js/Math.floor pre)))] (own (js/Math.floor f) (js/Math.floor pre)))]
@ -409,7 +417,9 @@
symbol/IResolver symbol/IResolver
(world-of [_ path] (symbol/world-of r path)) (world-of [_ path] (symbol/world-of r path))
(frame-of [_ path] (symbol/frame-of r path)) (frame-of [_ path] (symbol/frame-of r path))
(pre-frame-of [_ path] (symbol/pre-frame-of r path))))))) (pre-frame-of [_ path] (symbol/pre-frame-of r path))
IActivePalette
(active-palette [_] @active-palette-state))))))
(defn center (defn center
"The middle of everything symbol `sid` draws, over all its frames, in its own "The middle of everything symbol `sid` draws, over all its frames, in its own

View file

@ -109,6 +109,16 @@
i (and p (slot-index p value))] i (and p (slot-index p value))]
(if (some? i) (+ (get offsets id 0) i) 255))) (if (some? i) (+ (get offsets id 0) i) 255)))
(defn background-index
"The raster index of slot zero in the resolver's active palette.
`index-of` remains a supported legacy palette map for domain callers; it has
no palette banks or active selection, so its established `:bg` index applies."
[palette active]
(if (and (:palettes palette) (:offsets palette))
(render-index palette active 0)
(get palette :bg 0)))
(defn valid-palette? [{:keys [id name slots]}] (defn valid-palette? [{:keys [id name slots]}]
(and id (string? name) (seq name) (vector? slots) (pos? (count slots)) (and id (string? name) (seq name) (vector? slots) (pos? (count slots))
(<= (count slots) 255) (<= (count slots) 255)

View file

@ -34,6 +34,7 @@
(:refer-clojure :exclude [run!]) (:refer-clojure :exclude [run!])
(:require [arthur.audio.mix :as mix] (:require [arthur.audio.mix :as mix]
[arthur.domain.clip :as clip] [arthur.domain.clip :as clip]
[arthur.domain.palette :as pal]
[arthur.domain.raster :as raster])) [arthur.domain.raster :as raster]))
(defprotocol Exporter (defprotocol Exporter
@ -191,8 +192,7 @@
;; how. ;; how.
doc (assoc-in clip [:symbols sid] sym) doc (assoc-in clip [:symbols sid] sym)
resolve-frame (clip/resolver doc sid store palette nil) resolve-frame (clip/resolver doc sid store palette nil)
ras (raster/make width height) ras (raster/make width height)]
bg (get palette :bg 0)]
(-> (audio! doc sid store audio-url) (-> (audio! doc sid store audio-url)
(.then (fn [audio] (.then (fn [audio]
(js/Promise.resolve (js/Promise.resolve
@ -210,9 +210,11 @@
(fn [chain i] (fn [chain i]
(.then chain (.then chain
(fn [_] (fn [_]
(let [ops (resolve-frame i)]
(-> ras (-> ras
(raster/clear! bg) (raster/clear! (pal/background-index palette
(raster/draw-ops! (resolve-frame i))) (clip/active-palette resolve-frame)))
(raster/draw-ops! ops)))
(-> (js/Promise.resolve (frame! exporter i ras)) (-> (js/Promise.resolve (frame! exporter i ras))
(.then (fn [_] (.then (fn [_]
(when on-progress (when on-progress

View file

@ -194,7 +194,9 @@
symbol/IResolver symbol/IResolver
(world-of [_ path] (symbol/world-of resolve path)) (world-of [_ path] (symbol/world-of resolve path))
(frame-of [_ path] (symbol/frame-of resolve path)) (frame-of [_ path] (symbol/frame-of resolve path))
(pre-frame-of [_ path] (symbol/pre-frame-of resolve path))))))) (pre-frame-of [_ path] (symbol/pre-frame-of resolve path))
clip/IActivePalette
(active-palette [_] (clip/active-palette resolve)))))))
(rf/reg-sub (rf/reg-sub
::underlay ::underlay

View file

@ -19,6 +19,8 @@
times a second at a 30fps clip on a 60Hz display, not sixty — and it carries times a second at a 30fps clip on a 60Hz display, not sixty — and it carries
no global interceptors." no global interceptors."
(:require [arthur.clock :as clock] (:require [arthur.clock :as clock]
[arthur.domain.clip :as clip]
[arthur.domain.palette :as pal]
[arthur.domain.pick :as pick] [arthur.domain.pick :as pick]
[arthur.domain.raster :as raster] [arthur.domain.raster :as raster]
[arthur.events.playback :as pb] [arthur.events.playback :as pb]
@ -171,10 +173,11 @@
;; independent of the footage, so the size the frame is rasterised at comes ;; independent of the footage, so the size the frame is rasterised at comes
;; out of the document like everything else. ;; out of the document like everything else.
(let [ras (raster-for width height)] (let [ras (raster-for width height)]
(let [ops (resolver f)] (let [ops (resolver f)
bg (pal/background-index palette (clip/active-palette resolver))]
(swap! state assoc :ops ops) (swap! state assoc :ops ops)
(-> ras (-> ras
(raster/clear! (get palette :bg 0)) (raster/clear! bg)
(raster/draw-ops! ops))) (raster/draw-ops! ops)))
(js/performance.mark "arthur/blit:start") (js/performance.mark "arthur/blit:start")
(canvas/blit! canvas ras ramp) (canvas/blit! canvas ras ramp)

View file

@ -98,10 +98,11 @@
(when (clip/symbol document sid) (when (clip/symbol document sid)
(let [resolve (clip/resolver document sid store palette {}) (let [resolve (clip/resolver document sid store palette {})
[w h] (clip/stage document sid)] [w h] (clip/stage document sid)]
(let [ops (resolve 0)]
(-> (raster/make w h) (-> (raster/make w h)
(raster/clear! (get palette :bg 0)) (raster/clear! (pal/background-index palette (clip/active-palette resolve)))
(raster/draw-ops! (resolve 0)) (raster/draw-ops! ops)
(canvas/->png ramp)))) (canvas/->png ramp)))))
(catch :default _ nil))) (catch :default _ nil)))
(defn- symbol-thumb (defn- symbol-thumb

View file

@ -1083,12 +1083,12 @@
(if (= drop-lane (:select row)) (if (= drop-lane (:select row))
(update row :cels (fnil conj []) lane-ghost) (update row :cels (fnil conj []) lane-ghost)
row)))) row))))
;; The palette track is a permanent master row. An ;; An untargeted visual drop becomes a new ordinary row
;; untargeted visual drop becomes a new ordinary row and ;; at the bottom. Inserting it above the lanes shifts the
;; therefore previews immediately BELOW it, where the ;; lane under the pointer and makes the browser alternate
;; committed row will appear—not above the master row. ;; between this ghost and that lane's drop target.
(and ghost (not (:sound? drop))) (and ghost (not (:sound? drop)))
((fn [rows] (into [(first rows) ghost] (rest rows))))) (fn [rows] (conj (vec rows) ghost)))
sounds (cond->> (into (vec (filter :sound? picture)) sounds (cond->> (into (vec (filter :sound? picture))
(sound-rows clip open expanded)) (sound-rows clip open expanded))
(and ghost (:sound? drop)) (cons ghost)) (and ghost (:sound? drop)) (cons ghost))

View file

@ -354,4 +354,11 @@
(is (= (pal/render-index context :day 1) (color main 2)) (is (= (pal/render-index context :day 1) (color main 2))
"a nil track interval is a real gap and restores the authoring palette") "a nil track interval is a real gap and restores the authoring palette")
(is (= (pal/render-index context :night 1) (color drawing 0)) (is (= (pal/render-index context :night 1) (color drawing 0))
"the same nested symbol uses its authoring palette when viewed directly"))) "the same nested symbol uses its authoring palette when viewed directly")
;; The renderer clears with this exact resolver state before rasterising the
;; returned ops, so a palette cut changes the stage background as well as
;; authored shapes.
(main 1)
(is (= :night (clip/active-palette main)))
(is (= (pal/render-index context :night 0)
(pal/background-index context (clip/active-palette main))))))