From ee603a351bf6807d8422478dcc396fc289f5d726 Mon Sep 17 00:00:00 2001 From: Olive Vaughn Date: Sun, 4 Oct 2026 00:49:28 -0400 Subject: [PATCH] Simplify stage passepartout control --- frontend/src/arthur/ui/layout.cljs | 13 ++++--------- frontend/src/arthur/ui/player.cljs | 15 +++------------ frontend/src/arthur/ui/stage.cljs | 29 +++++++++++++---------------- static/arthur/app.css | 5 +++++ 4 files changed, 25 insertions(+), 37 deletions(-) diff --git a/frontend/src/arthur/ui/layout.cljs b/frontend/src/arthur/ui/layout.cljs index 836dbbe..3d60d33 100644 --- a/frontend/src/arthur/ui/layout.cljs +++ b/frontend/src/arthur/ui/layout.cljs @@ -164,7 +164,7 @@ :on-click #(rf/dispatch [::set k (stepped k z false)])} "+"]])) -(rf/reg-sub ::mat (fn [db _] (merge {:margin 100 :opacity 0.55} (get-in db [:ui :passepartout])))) +(rf/reg-sub ::mat (fn [db _] (merge {:opacity 0.55} (get-in db [:ui :passepartout])))) (rf/reg-event-db ::mat (fn [db [_ k v]] (assoc-in db [:ui :passepartout k] v))) (defn fit-stage! [] @@ -177,17 +177,12 @@ (js/requestAnimationFrame #(do (set! (.-scrollLeft area) 0) (set! (.-scrollTop area) 0))))))) (defn stage-controls [] - (let [{:keys [margin opacity]} @(rf/subscribe [::mat])] + (let [{:keys [opacity]} @(rf/subscribe [::mat])] [:<> [:button {:on-click fit-stage! :title "Fit stage and surrounding workspace in view"} "fit"] [:details.passepartout-controls [:summary "passepartout"] [:div.view-popout - [:label "Outside margin (px)" - [:input {:type "number" :min 0 :max 1000 :value margin - :on-change #(let [v (js/parseInt (.. % -target -value))] - (when (js/Number.isFinite v) (rf/dispatch [::mat :margin (max 0 (min 1000 v))])))}]] - [:label "Shade outside export bounds" + [:label "Passepartout" [:input {:type "range" :min 0 :max 1 :step 0.05 :value opacity - :on-change #(rf/dispatch [::mat :opacity (js/parseFloat (.. % -target -value))])}]] - [:p "Draw outside the rectangle. Exports trim to the symbol’s dimensions."]]]])) + :on-change #(rf/dispatch [::mat :opacity (js/parseFloat (.. % -target -value))])}]]]]])) diff --git a/frontend/src/arthur/ui/player.cljs b/frontend/src/arthur/ui/player.cljs index 7abbfd3..156f9c8 100644 --- a/frontend/src/arthur/ui/player.cljs +++ b/frontend/src/arthur/ui/player.cljs @@ -261,14 +261,6 @@ (as-> ops (reduce #(clip/in-layer %1 target (poly (outline/join %2) {:color 0 :knock knock})) ops rings))))) -(defn- shifted-op [op margin] - (cond-> op - (:pts op) (assoc :pts (let [pts (:pts op) n (* 2 (:n op)) shifted (js/Float64Array. n)] - (dotimes [i n] (aset shifted i (+ margin (aget pts i)))) - shifted)) - (:cx op) (update :cx + margin) - (:cy op) (update :cy + margin))) - (defn paint! "Resolve `f` and put it on the canvas. `ops` are consumed here and only here — the resolver reuses its point buffers between frames, so they have to be @@ -284,8 +276,7 @@ ;; 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 [margin (:margin mat 100) - ras (raster-for (+ width (* 2 margin)) (+ height (* 2 margin))) + (let [ras (raster-for width height) {traces true picture false} (group-by #(= :trace (:kind %)) (resolver f)) ;; A layer switched off, or all of them, is not on the stage at all: ;; not painted and not there to be clicked. @@ -296,10 +287,10 @@ (swap! state assoc :ops (into (vec picture) traces)) (-> ras (raster/clear! bg) - (raster/draw-ops! (map #(shifted-op % margin) (with-previews picture palette active)))) + (raster/draw-ops! (with-previews picture palette active))) (js/performance.mark "arthur/blit:start") (canvas/blit! canvas ras (pal/effective-ramp palette active)) - (tracing/paint! traces (assoc tracing :width (+ width (* 2 margin)) :origin margin :playing? playing?) repaint!)) + (tracing/paint! traces (assoc tracing :width width :origin 0 :playing? playing?) repaint!)) (js/performance.measure "arthur/resolve+draw" "arthur/paint:start" "arthur/blit:start") (js/performance.measure "arthur/paint" "arthur/paint:start") ;; User Timing entries otherwise accumulate forever in the browser's diff --git a/frontend/src/arthur/ui/stage.cljs b/frontend/src/arthur/ui/stage.cljs index 6da1299..c80d986 100644 --- a/frontend/src/arthur/ui/stage.cljs +++ b/frontend/src/arthur/ui/stage.cljs @@ -32,7 +32,7 @@ [re-frame.core :as rf] [reagent.core :as r])) -;; Pointer coordinates come from the SVG transform, including the outside margin. +;; Pointer coordinates come from the SVG transform, in stage pixels. (defn- event-point [element event] (let [svg (if (= "svg" (.-tagName element)) element (.querySelector element "svg.paint-overlay")) @@ -415,8 +415,7 @@ (when (and (some? c) (not (symbol/knockout? c)) (or all? (= c start))) q)))) (get-in document [:symbols sid :nodes])))) - m (outline/stamp! (let [margin (:margin @(rf/subscribe [::layout/mat]))] - (assoc (outline/mask (+ (:w ctx) (* 2 margin)) (+ (:h ctx) (* 2 margin))) :origin [(- margin) (- margin)])) p p size) + m (outline/stamp! (outline/mask (:w ctx) (:h ctx)) p p size) s (cond ;; The brush's target is the creation target, which the player ;; reads for itself. @@ -499,7 +498,7 @@ [:circle {:class (str "first" (when (and hover (closes? draft hover)) " closing")) :cx fx :cy fy :r 1.8}]]))) -(defn- overlay [w h zoom margin opacity] +(defn- overlay [w h zoom opacity] (let [tool @(rf/subscribe [::sub/tool]) draft @(rf/subscribe [::sub/draft]) hover @(rf/subscribe [::sub/hover]) @@ -528,8 +527,8 @@ mark @marquee] [:svg {:class (str "paint-overlay tool-" (name tool) (when (and pen? hover (closes? draft hover)) " closing")) - :width (* zoom (+ w (* 2 margin))) :height (* zoom (+ h (* 2 margin))) - :view-box (str (- margin) " " (- margin) " " (+ w (* 2 margin)) " " (+ h (* 2 margin))) + :width (* zoom 3 w) :height (* zoom 3 h) + :view-box (str (- w) " " (- h) " " (* 3 w) " " (* 3 h)) :tab-index -1 :on-pointer-down (fn [^js event] @@ -615,9 +614,8 @@ (reset! dragging nil) (reset! marquee nil) (reset! painting nil) (player/stroke! nil) (let-go! false))} - [:path {:d (str "M" (- margin) " " (- margin) "h" (+ w (* 2 margin)) "v" (+ h (* 2 margin)) "h" (- (+ w (* 2 margin))) "z M0 0v" h "h" w "v" (- h) "z") + [:path {:d (str "M" (- w) " " (- h) "h" (* 3 w) "v" (* 3 h) "h" (* -3 w) "z M0 0v" h "h" w "v" (- h) "z") :fill "#000" :fill-rule "evenodd" :opacity opacity :pointer-events "none"}] - [:rect {:x 0 :y 0 :width w :height h :fill "none" :stroke "#aaa" :vector-effect "non-scaling-stroke" :pointer-events "none"}] [ghost] (when (seq selected-pts) [:polygon.selected-paint-outline @@ -723,8 +721,7 @@ destination-name (when-let [sid (:sid destination)] (clip/symbol-name clip sid)) zoom @(rf/subscribe [::layout/zoom :stage]) - {:keys [margin opacity]} @(rf/subscribe [::layout/mat]) - vw (+ w (* 2 margin)) vh (+ h (* 2 margin))] + {:keys [opacity]} @(rf/subscribe [::layout/mat])] [:div.stage-area (navigation) [:div.stage-wrap @@ -733,7 +730,7 @@ ;; `dragenter` is cancelled as well as `dragover`: a drop target has to ;; accept on BOTH, and the element under the pointer changes whenever the ;; preview re-renders beneath it, which fires a fresh `dragenter`. - {:data-width vw :data-height vh + {:data-width w :data-height h :on-drag-enter (fn [^js event] (when (drag/accepts?) (.preventDefault event))) :on-drag-over (fn [^js event] (when (drag/accepts?) @@ -746,12 +743,12 @@ (.preventDefault event) (drag/land! frame (stage-point event w h)))} [:canvas.stage {:ref #(player/set-canvas! %) - :width vw :height vh - :style {:width (str (* zoom vw) "px") - :height (str (* zoom vh) "px")}}] + :width w :height h + :style {:width (str (* zoom w) "px") + :height (str (* zoom h) "px")}}] [:canvas.tracing {:ref #(tracing/set-canvas! %) - :width (* zoom vw) :height (* zoom vh)}] - [overlay w h zoom margin opacity]] + :width (* zoom w) :height (* zoom h)}] + [:div.stage-overlay-wrap [overlay w h zoom opacity]]] (when destination-name [:div.stage-target "creating in " [:strong destination-name]]) [tools/adjust-last]])) diff --git a/static/arthur/app.css b/static/arthur/app.css index e0d91b5..1044cc2 100644 --- a/static/arthur/app.css +++ b/static/arthur/app.css @@ -874,6 +874,11 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); } .stage { display: block; background: var(--stage); } .paint-overlay { position: absolute; inset: 0; touch-action: none; } +.stage-overlay-wrap { + position: absolute; + left: -100%; top: -100%; + width: 300%; height: 300%; +} /* The footage traced over: a reference above the picture, never part of it. */ .tracing { position: absolute; inset: 0; pointer-events: none; }