diff --git a/common/src/app/common/geom/image_crop.cljc b/common/src/app/common/geom/image_crop.cljc index fdd9f89527..15ad24b9bd 100644 --- a/common/src/app/common/geom/image_crop.cljc +++ b/common/src/app/common/geom/image_crop.cljc @@ -9,6 +9,7 @@ image fill). The shape is a window over a picture that keeps its size and position; the window never flips." (:require + [app.common.geom.matrix :as gmt] [app.common.math :as mth])) (defn crop-axis @@ -36,3 +37,18 @@ :y (/ (- (* y height) dy) height') :width (/ (* w width) width') :height (/ (* h height) height')})) + +(defn preview-image-matrix + "Return the matrix that takes the picture of a `width` x `height` window + from its box under `transform` to its box under `transform'`, both in + the coordinates of the window before the resize. While dragging, the + SVG preview resizes the whole shape with one matrix; this one, applied + to the picture first, undoes that resize so the picture stays in place." + [transform transform' width height] + (let [x (* (get transform :x 0.0) width) + y (* (get transform :y 0.0) height) + x' (* (get transform' :x 0.0) width) + y' (* (get transform' :y 0.0) height) + sx (/ (get transform' :width 1.0) (get transform :width 1.0)) + sy (/ (get transform' :height 1.0) (get transform :height 1.0))] + (gmt/matrix sx 0 0 sy (- x' (* x sx)) (- y' (* y sy))))) diff --git a/common/test/common_tests/geom_image_crop_test.cljc b/common/test/common_tests/geom_image_crop_test.cljc index cdd30e444f..31c686fbab 100644 --- a/common/test/common_tests/geom_image_crop_test.cljc +++ b/common/test/common_tests/geom_image_crop_test.cljc @@ -7,6 +7,8 @@ (ns common-tests.geom-image-crop-test (:require [app.common.geom.image-crop :as gic] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] [app.common.math :as mth] [clojure.test :as t])) @@ -65,3 +67,29 @@ (t/is (close-vals? [(- dy) height] [dy2 h2]) "window back at its original place") (t/is (close-vals? ((juxt :x :y :width :height) tf) ((juxt :x :y :width :height) back))))))) + +(t/deftest preview-image-matrix-test + (let [width 400 + height 300 + tf {:x 0 :y -0.2 :width 1 :height 1.4}] + + (t/testing "the picture keeps its place once the window is resized" + (doseq [[dx dy w' h'] [[0 60 400 240] + [0 300 400 100] + [-50 75 500 150]]] + (let [tf' (gic/update-image-transform tf width height dx dy w' h') + matrix (gic/preview-image-matrix tf tf' width height) + ;; Resize the preview applies to the whole shape: the old + ;; window goes to the new one. + window (-> (gmt/matrix) + (gmt/translate (gpt/point dx dy)) + (gmt/scale (gpt/point (/ w' width) (/ h' height)))) + [x y w h] (picture-box tf [0 0] [width height]) + p1 (gpt/transform (gpt/point x y) (gmt/multiply window matrix)) + p2 (gpt/transform (gpt/point (+ x w) (+ y h)) (gmt/multiply window matrix))] + (t/is (close-vals? [x y (+ x w) (+ y h)] [(:x p1) (:y p1) (:x p2) (:y p2)]) + (str "window " [dx dy w' h']))))) + + (t/testing "no change gives the identity" + (t/is (gmt/close? (gmt/matrix) (gic/preview-image-matrix tf tf width height))) + (t/is (gmt/close? (gmt/matrix) (gic/preview-image-matrix nil nil width height)))))) diff --git a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs index 237ce41434..ace4f726a7 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame/dynamic_modifiers.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] + [app.common.geom.image-crop :as gic] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] [app.common.geom.rect :as grc] @@ -20,6 +21,7 @@ [app.util.debug :as dbg] [app.util.dom :as dom] [app.util.timers :as ts] + [cuerdas.core :as str] [rumext.v2 :as mf])) (defn get-shape-node @@ -31,6 +33,14 @@ base-node (dom/query base-node (dm/str "#shape-" id))))) +(defn- get-fill-image-nodes + "Retrieve the pictures of the shape image fills, inside its own fill + patterns" + [shape shape-node] + (when (or (some :fill-image (:fills shape)) + (some? (:fill-image shape))) + (dom/query-all shape-node ":scope > defs > pattern image"))) + (defn get-nodes "Retrieve the DOM nodes to apply the matrix transformation" [base-node {:keys [id parent-id] :as shape}] @@ -43,11 +53,13 @@ masking-child? (:masking-child? (meta shape))] (cond frame? - [shape-node - (dom/query shape-node ".frame-children") - (dom/query (dm/str "#thumbnail-container-" id)) - (dom/query (dm/str "#thumbnail-" id)) - (dom/query (dm/str "#frame-title-" id))] + (d/concat-vec + [shape-node + (dom/query shape-node ".frame-children") + (dom/query (dm/str "#thumbnail-container-" id)) + (dom/query (dm/str "#thumbnail-" id)) + (dom/query (dm/str "#frame-title-" id))] + (get-fill-image-nodes shape shape-node)) ;; For groups we don't want to transform the whole group but only ;; its filters/masks @@ -72,9 +84,38 @@ (dom/query (dm/str "#frame-title-" id)))] :else - [shape-node - (when (dbg/enabled? :shape-titles) - (dom/query (dm/str "#frame-title-" id)))])))) + (d/concat-vec + [shape-node + (when (dbg/enabled? :shape-titles) + (dom/query (dm/str "#frame-title-" id)))] + (get-fill-image-nodes shape shape-node)))))) + +(defn- fill-image-matrix + "Return the matrix that keeps the picture of an image fill `node` in + place while `modifiers` change the fill transform (resize with the mod + key). Return nil when the modifiers do not change the fill." + [shape modifiers node] + (let [changes (->> (:structure-parent modifiers) + (filter #(= :change-property (:type %))) + (d/index-by :property :value)) + selrect (dm/get-prop shape :selrect) + id (dom/get-attribute node "id") + + [transform transform' :as change] + (if (some? id) + ;; Fill images have an id ending with the fill index + (when-let [fills' (get changes :fills)] + (let [index (d/parse-integer (last (str/split id "-")))] + [(dm/get-in (nth (:fills shape) index nil) [:fill-image :transform]) + (dm/get-in (nth fills' index nil) [:fill-image :transform])])) + (when-let [fill-image' (get changes :fill-image)] + [(dm/get-in shape [:fill-image :transform]) + (get fill-image' :transform)]))] + + (when (some? change) + (gic/preview-image-matrix transform transform' + (dm/get-prop selrect :width) + (dm/get-prop selrect :height))))) (defn transform-region! [node modifiers] @@ -167,6 +208,13 @@ (= (dom/get-tag-name node) "pattern") (set-transform-att! node "patternTransform" transform) + ;; Picture of an image fill: it gets the shape transform + ;; through the shape node, and only needs its own when the + ;; fill transform changes + (= (dom/get-tag-name node) "image") + (when-let [matrix (fill-image-matrix shape modifiers node)] + (set-transform-att! node "transform" matrix)) + (and (some? transform) (some? node)) (set-transform-att! node "transform" transform)))))))