diff --git a/frontend/src/arthur/domain/clip.cljs b/frontend/src/arthur/domain/clip.cljs index a74c2f7..4352a43 100644 --- a/frontend/src/arthur/domain/clip.cljs +++ b/frontend/src/arthur/domain/clip.cljs @@ -243,12 +243,17 @@ (assoc op :cx x :cy y :size (* scale (:size op)))) op))) +(defprotocol IActivePalette + (active-palette [this] + "The palette selected by this resolver's most recently resolved frame.")) + (defn resolver "Resolve an output frame, selecting native content at each symbol boundary. 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))] + (let [context? (and (map? palette) (:palettes palette) (:offsets palette)) + active-palette-state (atom (:default palette))] (letfn [(channel-value [selection frame] (cond (nil? selection) nil @@ -323,6 +328,9 @@ (root-selection-at sym (js/Math.floor f) inherited) inherited) (: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 {}) (let [by-id (into {} (map (juxt :node identity)) (own (js/Math.floor f) (js/Math.floor pre)))] @@ -409,7 +417,9 @@ 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)) + IActivePalette + (active-palette [_] @active-palette-state)))))) (defn center "The middle of everything symbol `sid` draws, over all its frames, in its own diff --git a/frontend/src/arthur/domain/palette.cljs b/frontend/src/arthur/domain/palette.cljs index 3fe004c..5a45e1b 100644 --- a/frontend/src/arthur/domain/palette.cljs +++ b/frontend/src/arthur/domain/palette.cljs @@ -109,6 +109,16 @@ i (and p (slot-index p value))] (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]}] (and id (string? name) (seq name) (vector? slots) (pos? (count slots)) (<= (count slots) 255) diff --git a/frontend/src/arthur/export.cljs b/frontend/src/arthur/export.cljs index 0f41deb..16955a4 100644 --- a/frontend/src/arthur/export.cljs +++ b/frontend/src/arthur/export.cljs @@ -34,6 +34,7 @@ (:refer-clojure :exclude [run!]) (:require [arthur.audio.mix :as mix] [arthur.domain.clip :as clip] + [arthur.domain.palette :as pal] [arthur.domain.raster :as raster])) (defprotocol Exporter @@ -191,8 +192,7 @@ ;; how. doc (assoc-in clip [:symbols sid] sym) resolve-frame (clip/resolver doc sid store palette nil) - ras (raster/make width height) - bg (get palette :bg 0)] + ras (raster/make width height)] (-> (audio! doc sid store audio-url) (.then (fn [audio] (js/Promise.resolve @@ -210,9 +210,11 @@ (fn [chain i] (.then chain (fn [_] - (-> ras - (raster/clear! bg) - (raster/draw-ops! (resolve-frame i))) + (let [ops (resolve-frame i)] + (-> ras + (raster/clear! (pal/background-index palette + (clip/active-palette resolve-frame))) + (raster/draw-ops! ops))) (-> (js/Promise.resolve (frame! exporter i ras)) (.then (fn [_] (when on-progress diff --git a/frontend/src/arthur/subs/render.cljs b/frontend/src/arthur/subs/render.cljs index 1d91a6b..d225b09 100644 --- a/frontend/src/arthur/subs/render.cljs +++ b/frontend/src/arthur/subs/render.cljs @@ -194,7 +194,9 @@ symbol/IResolver (world-of [_ path] (symbol/world-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 ::underlay diff --git a/frontend/src/arthur/ui/player.cljs b/frontend/src/arthur/ui/player.cljs index a9d752f..41ef085 100644 --- a/frontend/src/arthur/ui/player.cljs +++ b/frontend/src/arthur/ui/player.cljs @@ -19,6 +19,8 @@ times a second at a 30fps clip on a 60Hz display, not sixty — and it carries no global interceptors." (:require [arthur.clock :as clock] + [arthur.domain.clip :as clip] + [arthur.domain.palette :as pal] [arthur.domain.pick :as pick] [arthur.domain.raster :as raster] [arthur.events.playback :as pb] @@ -171,10 +173,11 @@ ;; 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)] + (let [ops (resolver f) + bg (pal/background-index palette (clip/active-palette resolver))] (swap! state assoc :ops ops) (-> ras - (raster/clear! (get palette :bg 0)) + (raster/clear! bg) (raster/draw-ops! ops))) (js/performance.mark "arthur/blit:start") (canvas/blit! canvas ras ramp) diff --git a/frontend/src/arthur/ui/pool.cljs b/frontend/src/arthur/ui/pool.cljs index 256daeb..472d161 100644 --- a/frontend/src/arthur/ui/pool.cljs +++ b/frontend/src/arthur/ui/pool.cljs @@ -98,10 +98,11 @@ (when (clip/symbol document sid) (let [resolve (clip/resolver document sid store palette {}) [w h] (clip/stage document sid)] - (-> (raster/make w h) - (raster/clear! (get palette :bg 0)) - (raster/draw-ops! (resolve 0)) - (canvas/->png ramp)))) + (let [ops (resolve 0)] + (-> (raster/make w h) + (raster/clear! (pal/background-index palette (clip/active-palette resolve))) + (raster/draw-ops! ops) + (canvas/->png ramp))))) (catch :default _ nil))) (defn- symbol-thumb diff --git a/frontend/src/arthur/ui/timeline.cljs b/frontend/src/arthur/ui/timeline.cljs index 587b58f..722de9b 100644 --- a/frontend/src/arthur/ui/timeline.cljs +++ b/frontend/src/arthur/ui/timeline.cljs @@ -1083,12 +1083,12 @@ (if (= drop-lane (:select row)) (update row :cels (fnil conj []) lane-ghost) row)))) - ;; The palette track is a permanent master row. An - ;; untargeted visual drop becomes a new ordinary row and - ;; therefore previews immediately BELOW it, where the - ;; committed row will appear—not above the master 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] (into [(first rows) ghost] (rest rows))))) + (fn [rows] (conj (vec rows) ghost))) sounds (cond->> (into (vec (filter :sound? picture)) (sound-rows clip open expanded)) (and ghost (:sound? drop)) (cons ghost)) diff --git a/frontend/test/arthur/domain/instance_test.cljs b/frontend/test/arthur/domain/instance_test.cljs index efd7f6c..30eebc6 100644 --- a/frontend/test/arthur/domain/instance_test.cljs +++ b/frontend/test/arthur/domain/instance_test.cljs @@ -354,4 +354,11 @@ (is (= (pal/render-index context :day 1) (color main 2)) "a nil track interval is a real gap and restores the authoring palette") (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))))))