Simplify stage passepartout control

This commit is contained in:
Olive Vaughn 2026-10-04 00:49:28 -04:00
parent 981bef98f9
commit ee603a351b
4 changed files with 25 additions and 37 deletions

View file

@ -164,7 +164,7 @@
:on-click #(rf/dispatch [::set k (stepped k z false)])} "+"]])) :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))) (rf/reg-event-db ::mat (fn [db [_ k v]] (assoc-in db [:ui :passepartout k] v)))
(defn fit-stage! [] (defn fit-stage! []
@ -177,17 +177,12 @@
(js/requestAnimationFrame #(do (set! (.-scrollLeft area) 0) (set! (.-scrollTop area) 0))))))) (js/requestAnimationFrame #(do (set! (.-scrollLeft area) 0) (set! (.-scrollTop area) 0)))))))
(defn stage-controls [] (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"] [:button {:on-click fit-stage! :title "Fit stage and surrounding workspace in view"} "fit"]
[:details.passepartout-controls [:details.passepartout-controls
[:summary "passepartout"] [:summary "passepartout"]
[:div.view-popout [:div.view-popout
[:label "Outside margin (px)" [:label "Passepartout"
[: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"
[:input {:type "range" :min 0 :max 1 :step 0.05 :value opacity [:input {:type "range" :min 0 :max 1 :step 0.05 :value opacity
:on-change #(rf/dispatch [::mat :opacity (js/parseFloat (.. % -target -value))])}]] :on-change #(rf/dispatch [::mat :opacity (js/parseFloat (.. % -target -value))])}]]]]]))
[:p "Draw outside the rectangle. Exports trim to the symbol’s dimensions."]]]]))

View file

@ -261,14 +261,6 @@
(as-> ops (reduce #(clip/in-layer %1 target (poly (outline/join %2) {:color 0 :knock knock})) (as-> ops (reduce #(clip/in-layer %1 target (poly (outline/join %2) {:color 0 :knock knock}))
ops rings))))) 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! (defn paint!
"Resolve `f` and put it on the canvas. `ops` are consumed here and only here — "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 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 ;; 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 ;; 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 [margin (:margin mat 100) (let [ras (raster-for width height)
ras (raster-for (+ width (* 2 margin)) (+ height (* 2 margin)))
{traces true picture false} (group-by #(= :trace (:kind %)) (resolver f)) {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: ;; A layer switched off, or all of them, is not on the stage at all:
;; not painted and not there to be clicked. ;; not painted and not there to be clicked.
@ -296,10 +287,10 @@
(swap! state assoc :ops (into (vec picture) traces)) (swap! state assoc :ops (into (vec picture) traces))
(-> ras (-> ras
(raster/clear! bg) (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") (js/performance.mark "arthur/blit:start")
(canvas/blit! canvas ras (pal/effective-ramp palette active)) (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/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 ;; User Timing entries otherwise accumulate forever in the browser's

View file

@ -32,7 +32,7 @@
[re-frame.core :as rf] [re-frame.core :as rf]
[reagent.core :as r])) [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] (defn- event-point [element event]
(let [svg (if (= "svg" (.-tagName element)) element (.querySelector element "svg.paint-overlay")) (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))) (when (and (some? c) (not (symbol/knockout? c)) (or all? (= c start)))
q)))) q))))
(get-in document [:symbols sid :nodes])))) (get-in document [:symbols sid :nodes]))))
m (outline/stamp! (let [margin (:margin @(rf/subscribe [::layout/mat]))] m (outline/stamp! (outline/mask (:w ctx) (:h ctx)) p p size)
(assoc (outline/mask (+ (:w ctx) (* 2 margin)) (+ (:h ctx) (* 2 margin))) :origin [(- margin) (- margin)])) p p size)
s (cond s (cond
;; The brush's target is the creation target, which the player ;; The brush's target is the creation target, which the player
;; reads for itself. ;; reads for itself.
@ -499,7 +498,7 @@
[:circle {:class (str "first" (when (and hover (closes? draft hover)) " closing")) [:circle {:class (str "first" (when (and hover (closes? draft hover)) " closing"))
:cx fx :cy fy :r 1.8}]]))) :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]) (let [tool @(rf/subscribe [::sub/tool])
draft @(rf/subscribe [::sub/draft]) draft @(rf/subscribe [::sub/draft])
hover @(rf/subscribe [::sub/hover]) hover @(rf/subscribe [::sub/hover])
@ -528,8 +527,8 @@
mark @marquee] mark @marquee]
[:svg {:class (str "paint-overlay tool-" (name tool) [:svg {:class (str "paint-overlay tool-" (name tool)
(when (and pen? hover (closes? draft hover)) " closing")) (when (and pen? hover (closes? draft hover)) " closing"))
:width (* zoom (+ w (* 2 margin))) :height (* zoom (+ h (* 2 margin))) :width (* zoom 3 w) :height (* zoom 3 h)
:view-box (str (- margin) " " (- margin) " " (+ w (* 2 margin)) " " (+ h (* 2 margin))) :view-box (str (- w) " " (- h) " " (* 3 w) " " (* 3 h))
:tab-index -1 :tab-index -1
:on-pointer-down :on-pointer-down
(fn [^js event] (fn [^js event]
@ -615,9 +614,8 @@
(reset! dragging nil) (reset! marquee nil) (reset! dragging nil) (reset! marquee nil)
(reset! painting nil) (player/stroke! nil) (reset! painting nil) (player/stroke! nil)
(let-go! false))} (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"}] :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] [ghost]
(when (seq selected-pts) (when (seq selected-pts)
[:polygon.selected-paint-outline [:polygon.selected-paint-outline
@ -723,8 +721,7 @@
destination-name (when-let [sid (:sid destination)] destination-name (when-let [sid (:sid destination)]
(clip/symbol-name clip sid)) (clip/symbol-name clip sid))
zoom @(rf/subscribe [::layout/zoom :stage]) zoom @(rf/subscribe [::layout/zoom :stage])
{:keys [margin opacity]} @(rf/subscribe [::layout/mat]) {:keys [opacity]} @(rf/subscribe [::layout/mat])]
vw (+ w (* 2 margin)) vh (+ h (* 2 margin))]
[:div.stage-area [:div.stage-area
(navigation) (navigation)
[:div.stage-wrap [:div.stage-wrap
@ -733,7 +730,7 @@
;; `dragenter` is cancelled as well as `dragover`: a drop target has to ;; `dragenter` is cancelled as well as `dragover`: a drop target has to
;; accept on BOTH, and the element under the pointer changes whenever the ;; accept on BOTH, and the element under the pointer changes whenever the
;; preview re-renders beneath it, which fires a fresh `dragenter`. ;; 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-enter (fn [^js event] (when (drag/accepts?) (.preventDefault event)))
:on-drag-over (fn [^js event] :on-drag-over (fn [^js event]
(when (drag/accepts?) (when (drag/accepts?)
@ -746,12 +743,12 @@
(.preventDefault event) (.preventDefault event)
(drag/land! frame (stage-point event w h)))} (drag/land! frame (stage-point event w h)))}
[:canvas.stage {:ref #(player/set-canvas! %) [:canvas.stage {:ref #(player/set-canvas! %)
:width vw :height vh :width w :height h
:style {:width (str (* zoom vw) "px") :style {:width (str (* zoom w) "px")
:height (str (* zoom vh) "px")}}] :height (str (* zoom h) "px")}}]
[:canvas.tracing {:ref #(tracing/set-canvas! %) [:canvas.tracing {:ref #(tracing/set-canvas! %)
:width (* zoom vw) :height (* zoom vh)}] :width (* zoom w) :height (* zoom h)}]
[overlay w h zoom margin opacity]] [:div.stage-overlay-wrap [overlay w h zoom opacity]]]
(when destination-name (when destination-name
[:div.stage-target "creating in " [:strong destination-name]]) [:div.stage-target "creating in " [:strong destination-name]])
[tools/adjust-last]])) [tools/adjust-last]]))

View file

@ -874,6 +874,11 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); }
.stage { display: block; background: var(--stage); } .stage { display: block; background: var(--stage); }
.paint-overlay { position: absolute; inset: 0; touch-action: none; } .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. */ /* The footage traced over: a reference above the picture, never part of it. */
.tracing { position: absolute; inset: 0; pointer-events: none; } .tracing { position: absolute; inset: 0; pointer-events: none; }