Animate seven stage symbols and bound playback memory

This commit is contained in:
Olive Vaughn 2026-09-28 15:29:31 -04:00
parent ffb95543a3
commit 39ee37db02
8 changed files with 126 additions and 27 deletions

View file

@ -44,10 +44,13 @@
`:head` written as a dense track in one and as framed identity in the other, so
the button that switches between them switches a document field and nothing
else."
{:demo (entry :demo "demo" demo/clip nil)
:swarm (entry :swarm "swarm" @swarm/clip @swarm/store)
:take (entry :take "take" @take/clip @take/store)
:take-locked (entry :take-locked "locked" @take/locked @take/store)})
{:demo {:label "demo" :entry (delay (entry :demo "demo" demo/clip nil))}
:swarm {:label "swarm" :entry (delay (entry :swarm "swarm" @swarm/clip @swarm/store))}
:take {:label "take" :entry (delay (entry :take "take" @take/clip @take/store))}
:take-locked {:label "locked" :entry (delay (entry :take-locked "locked" @take/locked @take/store))}})
(defn clip-entry [id]
(some-> (get-in clips [id :entry]) deref))
(def default
{;; --- the document ---
@ -60,7 +63,7 @@
;; footage's. That is what deleting `makeXform` buys — the framing became a
;; transform on a node, so nothing downstream of the freeze knows the frame
;; size — and it is why ui/player no longer hardcodes 320x200.
:clip (select-keys (:take clips) [:fps :frames :width :height :audio :display-fps])
:clip (select-keys (clip-entry :take) [:fps :frames :width :height :audio :display-fps])
;; Which ingested footage to detect, and what the last load said. The list
;; comes from the server — tier 3 is the backend's since step 9 — so there is

View file

@ -1,21 +1,41 @@
(ns arthur.demo.stage
"A saved take placed twice on a stage. The EDN is the authored layout."
(:require [cljs.reader :as reader]
"A saved take placed seven times on a stage. The EDN is the authored layout."
(:require [arthur.domain.channel :as ch]
[cljs.reader :as reader]
[shadow.resource :as rc]))
(def layout (reader/read-string (rc/inline "arthur/demo/stage_8625.edn")))
(defn- position-track [center anchor drift phase frames]
(let [base (mapv - center anchor)
[dx dy] drift
wave (fn [f period] (js/Math.sin (* 2 js/Math.PI (/ (+ f phase) period))))
x0 (wave 0 96)
y0 (wave 0 132)]
(ch/keyed
(into {}
(for [f (conj (vec (range 0 frames 20)) (dec frames))]
[f [(+ (first base) (* dx (- (wave f 96) x0)))
(+ (second base) (* dy (- (wave f 132) y0)))]]))
:linear)))
(defn compose [source]
(let [{:keys [name width height frames symbol instances audio]} layout
(let [{:keys [name width height frames symbol instances audio scale]} layout
default-anchor (or (:anchor layout)
[(/ (:width source) 2) (/ (:height source) 2)])
original (get-in source [:timelines :main])
nodes (into
{:root {:id :root :name "stage" :kind :group :z "a1"}}
(map (fn [{:keys [id name z span at in pos scale]}]
[id {:id id :name name :kind :symbol :of symbol
:parent :root :z z :span span
:time {:mode :map :at at :in in :rate 1}
:channels {[:xform :pos] {:animated? false :value pos}
[:xform :scale] {:animated? false :value scale}}}])
(map (fn [{:keys [id name z span at in center anchor drift phase]}]
(let [anchor (or anchor default-anchor)]
[id {:id id :name name :kind :symbol :of symbol
:parent :root :z z :span span
:time {:mode :map :at at :in in :rate 1}
:channels {[:xform :pos] (if drift
(position-track center anchor drift phase frames)
(ch/framed (mapv - center anchor)))
[:xform :anchor] {:animated? false :value anchor}
[:xform :scale] scale}}]))
instances))
nodes (into nodes
(map (fn [{:keys [id linked-to z source span at in gain pan]}]

View file

@ -1,11 +1,22 @@
;; A local stage sketch. The source is the saved, post-processed IMG_8625.MOV
;; clip in the project store; its dense channel blocks remain shared by both
;; instances. Positions and timing are authored here in stage pixels and frames.
;; clip in the project store; its dense channel blocks are shared by all seven
;; instances. Centers, drift and timing are authored in stage pixels and frames.
{:source-project "4379f900-bdd2-409b-acf6-32081f8ce01f"
:source-cid "f8cace9e-4ad3-4796-973c-c62eeebe3d01"
:symbol :sym/face-8625
:name "8625 stage study"
:width 320 :height 200 :frames 280
;; Each :center below places the source clip's center on the stage. A symbol
;; can author :anchor to override that default for an off-center drawing.
;; Each placement reads this pulse in its own local time, so the staggered
;; entrances start their growth at different moments on the master timeline.
:scale {:animated? true :interp :linear
:keys {0 [0.4 0.4], 12 [0.56 0.56], 24 [0.48 0.48],
48 [0.52 0.52], 72 [0.48 0.48], 96 [0.52 0.52],
120 [0.48 0.48], 144 [0.52 0.52], 168 [0.48 0.48],
192 [0.52 0.52], 216 [0.48 0.48], 240 [0.52 0.52],
279 [0.48 0.48]}
:over []}
;; Audio placements are ordinary timeline nodes with channel parameters.
;; :linked-to is an editorial link; their spans and time maps are independent.
:audio
@ -26,7 +37,22 @@
:instances
[{:id :left :name "8625 left" :z "a1"
:span [0 280] :at 0 :in 0
:pos [12 38] :scale [0.62 0.62]}
:center [40 40] :drift [3 2] :phase 0}
{:id :right :name "8625 right" :z "a2"
:span [48 280] :at 48 :in 0
:pos [108 38] :scale [0.62 0.62]}]}
:center [120 40] :drift [-3 2] :phase 17}
{:id :top-third :name "8625 top third" :z "a5"
:span [24 280] :at 24 :in 0
:center [200 40] :drift [2 -3] :phase 31}
{:id :top-fourth :name "8625 top fourth" :z "a6"
:span [72 280] :at 72 :in 0
:center [280 40] :drift [-2 -2] :phase 49}
{:id :bottom-left :name "8625 bottom left" :z "a7"
:span [96 280] :at 96 :in 0
:center [70 135] :drift [3 -2] :phase 63}
{:id :bottom-middle :name "8625 bottom middle" :z "a8"
:span [120 280] :at 120 :in 0
:center [160 135] :drift [-2 3] :phase 81}
{:id :bottom-right :name "8625 bottom right" :z "a9"
:span [144 280] :at 144 :in 0
:center [250 135] :drift [2 2] :phase 107}]}

View file

@ -172,8 +172,11 @@
(defn- interpolate [ch f left right]
(let [a (get (:keys ch) left)]
(if (and (= :linear (:interp ch)) right (>= f left) (> right left))
(let [b (get (:keys ch) right)]
(+ a (* (/ (- f left) (- right left)) (- b a))))
(let [b (get (:keys ch) right)
t (/ (- f left) (- right left))]
(if (vector? a)
(mapv (fn [x y] (+ x (* t (- y x)))) a b)
(+ a (* t (- b a)))))
a)))
(defn- keyed-at
@ -291,7 +294,16 @@
(defn problems
"Human-readable reasons this map is not a channel. Empty means it is one."
[ch]
(cond-> []
(let [values (when (map? (:keys ch)) (vals (:keys ch)))
first-value (first values)
linear-values? (or (every? number? values)
(and (vector? first-value)
(pos? (count first-value))
(every? (fn [v] (and (vector? v)
(= (count v) (count first-value))
(every? number? v)))
values)))]
(cond-> []
(not (map? ch))
(conj "not a map")
@ -315,8 +327,8 @@
(conj (str ":interp " (:interp ch) " is not implemented"))
(and (map? ch) (= :linear (:interp ch))
(or (:dense ch) (not (every? number? (vals (:keys ch))))))
(conj ":linear interpolation needs numeric keyed values")
(or (:dense ch) (not linear-values?)))
(conj ":linear interpolation needs numeric keys of one shape")
(and (map? ch) (seq (:over ch)))
(conj ":over layers are not implemented (port-plan step 2 scope)")
@ -328,4 +340,4 @@
(not (and (number? (:scale (:dense ch))) (pos? (:scale (:dense ch))))))
(conj (str ":dense :scale is " (pr-str (:scale (:dense ch)))
" — a fixed-point scale is a positive number the stored integers"
" were multiplied by"))))
" were multiplied by")))))

View file

@ -29,4 +29,4 @@
(defn entry [id]
(if (= id (:id @loaded))
@loaded
(get db/clips id)))
(db/clip-entry id)))

View file

@ -151,7 +151,14 @@
(js/performance.mark "arthur/blit:start")
(canvas/blit! canvas ras ramp))
(js/performance.measure "arthur/resolve+draw" "arthur/paint:start" "arthur/blit:start")
(js/performance.measure "arthur/paint" "arthur/paint:start"))))
(js/performance.measure "arthur/paint" "arthur/paint:start")
;; User Timing entries otherwise accumulate forever in the browser's
;; performance timeline during playback. DevTools captures the events as
;; they happen; keeping copies on the page serves no purpose.
(js/performance.clearMarks "arthur/paint:start")
(js/performance.clearMarks "arthur/blit:start")
(js/performance.clearMeasures "arthur/resolve+draw")
(js/performance.clearMeasures "arthur/paint"))))
(defonce ^:private watch
;; A scene swap changes the resolver and not the frame number, so the loop