diff --git a/frontend/src/arthur/domain/clip.cljs b/frontend/src/arthur/domain/clip.cljs index 1c551f2..c7b93c3 100644 --- a/frontend/src/arthur/domain/clip.cljs +++ b/frontend/src/arthur/domain/clip.cljs @@ -68,6 +68,14 @@ [clip sid] (:frames (symbol clip sid))) +(defn stage + "A symbol's stage as `[width height]`: its own, or the clip's where it has none. + Absent rather than copied in at creation, so a symbol nobody has sized follows + the project's size when that changes." + [clip sid] + (let [sym (symbol clip sid)] + [(or (:width sym) (:width clip)) (or (:height sym) (:height clip))])) + (defn update-symbol "Apply f to one symbol in place." [clip sid f & args] @@ -223,7 +231,7 @@ (mapcat resolve (range (frames clip sid))))] (if x0 [(/ (+ x0 x1) 2) (/ (+ y0 y1) 2)] - [(/ (:width clip) 2) (/ (:height clip) 2)]))) + (mapv #(/ % 2) (stage clip sid))))) (defn place-symbol "An instance of symbol `sid`, inside symbol `host`, at `frame` of `host`. diff --git a/frontend/src/arthur/domain/leaf.cljs b/frontend/src/arthur/domain/leaf.cljs index 48eb44b..6269917 100644 --- a/frontend/src/arthur/domain/leaf.cljs +++ b/frontend/src/arthur/domain/leaf.cljs @@ -145,7 +145,7 @@ ;; disagree with itself about; `clip` puts it back. (for [[sid sym] (:symbols clip)] {(at "symbol" (segment sid)) - (select-keys sym [:name :frames :palette])}) + (select-keys sym [:name :frames :width :height :palette])}) (for [[sid sym] (:symbols clip) [id n] (:nodes sym)] {(at "symbol" (segment sid) "node" (segment id)) diff --git a/frontend/src/arthur/domain/symbol.cljs b/frontend/src/arthur/domain/symbol.cljs index 54485b5..23fbb70 100644 --- a/frontend/src/arthur/domain/symbol.cljs +++ b/frontend/src/arthur/domain/symbol.cljs @@ -542,8 +542,11 @@ write. `:name` is what a person calls it, and is not its id: an id is what instances - and saved leaves point at, so renaming a symbol must not change it." - #{:id :name :frames :nodes :palette}) + and saved leaves point at, so renaming a symbol must not change it. + + `:width` and `:height` are the symbol's own stage, and are absent until someone + sets them: a symbol without them uses the clip's — see `clip/stage`." + #{:id :name :frames :width :height :nodes :palette}) (defn problems "Human-readable reasons this symbol will not evaluate. Empty means it will. @@ -593,6 +596,10 @@ (into (when-not (or (nil? (:frames sym)) (and (integer? (:frames sym)) (pos? (:frames sym)))) [(str ":frames is " (pr-str (:frames sym)) " — a symbol is a frame SPACE, so its length is a positive integer")])) + (into (for [k [:width :height] + :let [v (get sym k)] + :when (and (some? v) (not (and (integer? v) (pos? v))))] + (str k " is " (pr-str v) " — a symbol stage dimension must be a positive integer"))) (into (try (doall (map #(depth nodes %) (keys nodes))) nil diff --git a/frontend/src/arthur/events/project.cljs b/frontend/src/arthur/events/project.cljs index 8b239c9..bc9ce5c 100644 --- a/frontend/src/arthur/events/project.cljs +++ b/frontend/src/arthur/events/project.cljs @@ -580,6 +580,31 @@ :label (or (:label clip) (name id)) :clip clip}})))) +(rf/reg-event-fx + ::project-setting + (fn [{:keys [db]} [_ key value]] + (if (or (not (#{:fps :width :height} key)) + (not (and (integer? value) (pos? value)))) + {} + (let [db' (-> (edit/edit db #(assoc % key value)) + (assoc-in [:clip key] value) + (cond-> (= key :fps) (assoc-in [:clip :display-fps] value))) + frame (get-in db [:playback :frame])] + (cond-> {:db db'} + (= key :fps) (assoc ::pb/seek! [value (pb/frames db') frame])))))) + +(rf/reg-event-db + ::symbol-setting + (fn [db [_ sid key value]] + (if-not (and (#{:frames :width :height} key) + (or (nil? value) (and (integer? value) (pos? value)))) + db + (edit/edit db + (fn [c] + (if (nil? value) + (update-in c [:symbols sid] dissoc key) + (assoc-in c [:symbols sid key] value))))))) + (rf/reg-event-db ::set-channel ;; `frame` is the node's own, as for a drawing key. diff --git a/frontend/src/arthur/export.cljs b/frontend/src/arthur/export.cljs index 552c60d..2aee8d2 100644 --- a/frontend/src/arthur/export.cljs +++ b/frontend/src/arthur/export.cljs @@ -160,13 +160,14 @@ commit to, and so the arithmetic is assertable without a sink." [{:keys [clip zoom picture-fps] sid :symbol isolate-id :isolate}] (let [sym (some-> (clip/symbol clip sid) (isolate isolate-id)) + [width height] (clip/stage clip sid) zoom (max 1 (js/Math.floor (or zoom 1)))] (when sym {:frames (:frames sym) :fps (:fps clip) :zoom zoom - :width (* (:width clip) zoom) - :height (* (:height clip) zoom) + :width (* width zoom) + :height (* height zoom) :seconds (/ (:frames sym) (:fps clip)) ;; The unedited picture-grid count. A per-instance pose track can add or ;; remove changes, so this is only the grid's nominal count. @@ -189,19 +190,20 @@ :symbols (vec (sort-by str (keys (:symbols clip))))}))) (let [{:keys [frames fps zoom]} (plan {:clip clip :symbol sid :zoom zoom :isolate isolate-id}) + [width height] (clip/stage clip sid) ;; Rooted at the chosen symbol, as the stage is. Nested instances ;; inside it still resolve — clip/resolver is the function that knows ;; how. doc (assoc-in clip [:symbols sid] sym) resolve-frame (clip/resolver doc store palette sid {:picture-fps picture-fps}) - ras (raster/make (:width clip) (:height clip)) + ras (raster/make width height) bg (get palette :bg 0)] (-> (audio! doc sid store audio-url) (.then (fn [audio] (js/Promise.resolve - (begin! exporter {:name name :width (:width clip) - :height (:height clip) :zoom zoom + (begin! exporter {:name name :width width + :height height :zoom zoom :fps fps :frames frames :ramp ramp :audio audio})))) (.then (fn [_] diff --git a/frontend/src/arthur/subs/playback.cljs b/frontend/src/arthur/subs/playback.cljs index fd92557..a8ebf40 100644 --- a/frontend/src/arthur/subs/playback.cljs +++ b/frontend/src/arthur/subs/playback.cljs @@ -2,7 +2,9 @@ "Layer-2 extractors over the transport. Cheap by construction: each one reads a path and returns a value, so a tick that changes only `:frame` notifies only the things that asked for `:frame`." - (:require [re-frame.core :as rf])) + (:require [arthur.domain.clip :as clip] + [arthur.footage.store :as store] + [re-frame.core :as rf])) (rf/reg-sub ::frame (fn [db _] (get-in db [:playback :frame]))) (rf/reg-sub ::playing? (fn [db _] (get-in db [:playback :playing?]))) @@ -11,11 +13,19 @@ (rf/reg-sub ::muted? (fn [db _] (get-in db [:playback :muted?]))) (rf/reg-sub ::fps (fn [db _] (get-in db [:clip :fps]))) (rf/reg-sub ::display-fps (fn [db _] (get-in db [:clip :display-fps]))) -;; The stage, in pixels. On the clip because project dimensions are independent -;; of the footage — see flow/freeze/face-placement — so the canvas and the raster -;; take their size from the document rather than from a constant. -(rf/reg-sub ::width (fn [db _] (get-in db [:clip :width]))) -(rf/reg-sub ::height (fn [db _] (get-in db [:clip :height]))) +;; The open symbol can have a local stage size; otherwise it follows the project. +(rf/reg-sub ::stage-size + :<- [:arthur.subs.playback/clip-id] + :<- [:arthur.subs.playback/revision] + :<- [:arthur.subs.playback/open] + (fn [[id _ sid] _] + (when-let [document (:clip (store/entry id))] + (clip/stage document sid)))) +(rf/reg-sub ::clip-id (fn [db _] (:clip/current db))) +(rf/reg-sub ::revision (fn [db _] (:paint/revision db))) +(rf/reg-sub ::open (fn [db _] (get-in db [:ui :open]))) +(rf/reg-sub ::width :<- [::stage-size] (fn [[w _] _] w)) +(rf/reg-sub ::height :<- [::stage-size] (fn [[_ h] _] h)) (rf/reg-sub ::audio (fn [db _] (get-in db [:clip :audio]))) (rf/reg-sub ::footage (fn [db _] (:footage db))) ;; The document's identity on the server. Not derived and not large — an id, a diff --git a/frontend/src/arthur/ui/params.cljs b/frontend/src/arthur/ui/params.cljs index 2a2cc95..674614c 100644 --- a/frontend/src/arthur/ui/params.cljs +++ b/frontend/src/arthur/ui/params.cljs @@ -7,6 +7,7 @@ has analysis in it. Nothing here computes — every control dispatches an intent and every readout comes off a subscription." (:require [clojure.string :as str] + [arthur.domain.clip :as clip-domain] [arthur.domain.channel :as channel] [arthur.domain.feature :as feature] [arthur.domain.node :as node] @@ -38,6 +39,15 @@ (defn- section [title & body] (into [:section.section [:h2 title]] body)) +(defn- number-field [label value on-change & [placeholder disabled?]] + [:label.inspector-field label + [:input {:type "number" :min 1 :step 1 :value (or value "") + :placeholder placeholder + :disabled disabled? + :on-change (fn [e] + (let [v (.. e -target -value)] + (on-change (when (seq v) (js/parseInt v 10)))))}]]) + ;; --------------------------------------------------------------------------- ;; the clip @@ -47,15 +57,22 @@ picture @(rf/subscribe [::playback/display-fps]) open @(rf/subscribe [::render/open]) frames @(rf/subscribe [::render/frames]) - expose @(rf/subscribe [::render/exposure])] - [section "clip" + project @(rf/subscribe [::playback/project]) + busy? (:busy? project)] + [section "project" [facts - "name" (:name clip) + "name" (or (:name project) (:name clip)) "stage" (str (:width clip) "×" (:height clip)) "open" (some-> open name) "length" (str frames " frames") - "source" (str fps " fps") - "expose" (str "on " expose "s")] + "rate" (str fps " fps")] + [:div.inspector-form + [number-field "width" (:width clip) + #(rf/dispatch [::project/project-setting :width %]) nil busy?] + [number-field "height" (:height clip) + #(rf/dispatch [::project/project-setting :height %]) nil busy?] + [number-field "project fps" fps + #(rf/dispatch [::project/project-setting :fps %]) nil busy?]] ;; Sampling the frozen roto at a lower rate. The source track, the duration ;; and the audio clock are untouched — a drawing is HELD, the file is never ;; short — which is why this is a picture rate and not a playback rate. @@ -190,12 +207,28 @@ ;; a symbol (defn- symbol-section [sid] - (let [sym (get-in @(rf/subscribe [::render/clip]) [:symbols sid])] + (let [clip @(rf/subscribe [::render/clip]) + sym (get-in clip [:symbols sid]) + busy? (:busy? @(rf/subscribe [::playback/project]))] [section "symbol" [facts "id" (str sid) "length" (str (:frames sym) " frames") - "nodes" (str (count (:nodes sym)))]])) + "stage" (let [[w h] (clip-domain/stage clip sid)] (str w "×" h)) + "nodes" (str (count (:nodes sym)))] + [:div.inspector-form + [number-field "length (frames)" (:frames sym) + #(rf/dispatch [::project/symbol-setting sid :frames %]) 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?] + (when (or (:width sym) (:height sym)) + [:div.row + [:button {:disabled busy? :on-click #(do + (rf/dispatch [::project/symbol-setting sid :width nil]) + (rf/dispatch [::project/symbol-setting sid :height nil]))} + "use project stage"]])]])) ;; --------------------------------------------------------------------------- ;; tracked objects