From 8886db7453a1180c84a694286f3beae413178ccf Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Wed, 22 Apr 2020 18:24:54 +0200 Subject: [PATCH] :tada: New stretch method --- frontend/src/uxbox/main/data/workspace.cljs | 4 +- frontend/src/uxbox/main/geom.cljs | 185 ++++++++++++------ .../uxbox/main/ui/shapes/bounding_box.cljs | 19 +- frontend/src/uxbox/util/data.cljs | 3 + frontend/src/uxbox/util/debug.cljs | 2 +- frontend/src/uxbox/util/geom/matrix.cljs | 36 ++-- frontend/src/uxbox/util/geom/point.cljs | 19 ++ 7 files changed, 179 insertions(+), 89 deletions(-) diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 7f50afc17..084fbbd0f 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -348,12 +348,12 @@ (ptk/type? ::initialize-page %)) stream) notifier (->> stream - (rx/filter (ptk/type? ::commit-changes)) + (rx/filter (ptk/type? ::common/commit-changes)) (rx/debounce 2000) (rx/merge stoper))] (rx/merge (->> stream - (rx/filter (ptk/type? ::commit-changes)) + (rx/filter (ptk/type? ::common/commit-changes)) (rx/map deref) (rx/buffer-until notifier) (rx/map vec) diff --git a/frontend/src/uxbox/main/geom.cljs b/frontend/src/uxbox/main/geom.cljs index f98adbf65..448f0b7b0 100644 --- a/frontend/src/uxbox/main/geom.cljs +++ b/frontend/src/uxbox/main/geom.cljs @@ -11,6 +11,8 @@ [uxbox.util.geom.matrix :as gmt] [uxbox.util.geom.point :as gpt] [uxbox.util.math :as mth] + [uxbox.util.data :as d] + [uxbox.util.debug :as debug] [uxbox.main.data.helpers :as helpers])) ;; --- Relative Movement @@ -149,6 +151,14 @@ maxy (apply max (map :y segments))] (gpt/point (/ (+ minx maxx) 2) (/ (+ miny maxy) 2)))) +(defn center->rect + "Creates a rect given a center and a width and height" + [center width height] + {:x (- (:x center) (/ width 2)) + :y (- (:y center) (/ height 2)) + :width width + :height height}) + ;; --- Proportions (declare assign-proportions-path) @@ -343,7 +353,8 @@ (let [points [(gpt/point x y) (gpt/point (+ x width) y) (gpt/point (+ x width) (+ y height)) - (gpt/point x (+ y height))]] + (gpt/point x (+ y height)) + (gpt/point x y)]] (-> shape (assoc :type :path) (assoc :segments points)))) @@ -409,6 +420,15 @@ (transform-rect shape xfmt)) shape)) +(defn center-transform [shape matrix] + (let [shape-center (center shape)] + (-> shape + (transform + (-> (gmt/matrix) + (gmt/translate shape-center) + (gmt/multiply matrix) + (gmt/translate (gpt/negate shape-center))))))) + (defn- transform-rect [{:keys [x y width height] :as shape} mx] (let [tl (gpt/transform (gpt/point x y) mx) @@ -613,6 +633,39 @@ :type :rect}] (overlaps? shape selrect))) + +(defn calculate-rec-path-skew-angle + [path-shape] + (let [p1 (get-in path-shape [:segments 2]) + p2 (get-in path-shape [:segments 3]) + p3 (get-in path-shape [:segments 4]) + v1 (gpt/to-vec p1 p2) + v2 (gpt/to-vec p2 p3)] + (- 90 (gpt/angle-with-other v1 v2)))) + +(defn calculate-rec-path-height + "Calculates the height of a paralelogram given by the path" + [path-shape] + + (let [p1 (get-in path-shape [:segments 2]) + p2 (get-in path-shape [:segments 3]) + p3 (get-in path-shape [:segments 4]) + v1 (gpt/to-vec p1 p2) + v2 (gpt/to-vec p2 p3) + angle (gpt/angle-with-other v1 v2)] + (* (gpt/length v2) (mth/sin (mth/radians angle))))) + +(defn calculate-rec-path-rotation + [center path-shape1 path-shape2] + (let [p1 (get-in path-shape1 [:segments 0]) + p2 (get-in path-shape2 [:segments 0]) + v1 (gpt/to-vec center p1) + v2 (gpt/to-vec center p2) + + rot-angle (gpt/angle-with-other v1 v2) + rot-sign (if (> (* (:y v1) (:x v2)) (* (:x v1) (:y v2))) -1 1)] + (* rot-sign rot-angle))) + (defn transform-shape-point "Transform a point around the shape center" [point shape transform] @@ -624,19 +677,6 @@ transform (gmt/translate-matrix (gpt/negate shape-center))))))) - -(defn- add-rotate-transform [shape rotation] - (let [rotation (or rotation 0)] - (-> shape - (update :transform #(gmt/multiply (gmt/rotate-matrix rotation) (or % (gmt/matrix)))) - (update :transform-inverse #(gmt/multiply (or % (gmt/matrix)) (gmt/rotate-matrix (- rotation))))))) - -(defn- add-stretch-transform [shape stretch] - (let [stretch (or stretch (gpt/point 1 1))] - (-> shape - (update :transform #(gmt/multiply (gmt/scale-matrix stretch) (or % (gmt/matrix)))) - (update :transform-inverse #(gmt/multiply (or % (gmt/matrix)) (gmt/scale-matrix (gpt/inverse stretch))))))) - (defn- transform-apply-modifiers [shape] (let [ds-modifier (:displacement-modifier shape (gmt/matrix)) @@ -667,19 +707,32 @@ (gmt/multiply (:transform shape (gmt/matrix))) (gmt/translate (gpt/negate shape-center))))))) -(defn calculate-stretch - [shape-path shape] - (let [{:keys [width height] :as selrect} (shape->rect-shape shape-path) - {width' :width height' :height :as selrect'} (selection-rect [shape]) - shape-path-size (gpt/point width height) - shape-size (gpt/point width' height')] - (gpt/divide shape-path-size shape-size))) +(defn rect-path-dimensions [rect-path] + (let [seg (:segments rect-path) + [width height] (mapv (fn [[c1 c2]] (gpt/distance c1 c2)) (take 2 (d/zip seg (rest seg))))] + {:width width + :height height})) + +(defn calculate-stretch [shape-path transform-inverse] + (let [shape-center (center shape-path) + shape-path-temp (transform + shape-path + (-> (gmt/matrix) + (gmt/translate shape-center) + (gmt/multiply transform-inverse) + (gmt/translate (gpt/negate shape-center)))) + + shape-path-temp-rec (shape->rect-shape shape-path-temp) + shape-path-temp-dim (rect-path-dimensions shape-path-temp)] + (gpt/divide (gpt/point (:width shape-path-temp-rec) (:height shape-path-temp-rec)) + (gpt/point (:width shape-path-temp-dim) (:height shape-path-temp-dim))))) (defn transform-selrect [frame shape] - (-> (shape->rect-shape (transform-apply-modifiers shape)) - (update :x - (:x frame 0)) - (update :y - (:y frame 0)))) + (-> shape + (transform-apply-modifiers) + (translate-to-frame frame) + (shape->rect-shape))) (defn dissoc-modifiers [shape] (-> shape @@ -692,58 +745,74 @@ (defn transform-rect-shape [shape] - (let [shape-path (transform-apply-modifiers shape) - shape-path-center (center shape-path) + (let [;; Apply modifiers to the rect as a path so we have the end shape expected + shape-path (transform-apply-modifiers shape) + shape-center (center shape-path) - shape-transform-inverse' (-> (gmt/matrix) - (gmt/translate shape-path-center) - (gmt/multiply (:transform-inverse shape (gmt/matrix))) - (gmt/multiply (gmt/rotate-matrix (- (:rotation-modifier shape 0)))) - (gmt/translate (gpt/negate shape-path-center))) + ;; Reverse the current transformation stack to get the base rectangle + shape-path-temp (center-transform shape-path (:transform-inverse shape (gmt/matrix))) + shape-path-temp-dim (rect-path-dimensions shape-path-temp) + shape-path-temp-rec (shape->rect-shape shape-path-temp) - ;; Revert the transformation so we can calculate the rectangle properties: x, y, width, height - changes (-> shape-path - (transform shape-transform-inverse') - (path->rect-shape) - (select-keys [:x :y :width :height])) + ;; This rectangle is the new data for the current rectangle. We want to change our rectangle + ;; to have this width, height, x, y + rec (center->rect shape-center (:width shape-path-temp-dim) (:height shape-path-temp-dim)) + rec-path (rect->path rec) + + ;; The next matrix is a series of transformations we have to do to the previous rec so that + ;; after applying them the end result is the `shape-path-temp` + ;; This is compose of three transformations: skew, resize and rotation + stretch-matrix (gmt/matrix) + + skew-angle (calculate-rec-path-skew-angle shape-path-temp) + stretch-matrix (gmt/multiply stretch-matrix (gmt/skew-matrix skew-angle 0)) + + h1 (calculate-rec-path-height shape-path-temp) + h2 (calculate-rec-path-height (center-transform rec-path stretch-matrix)) + stretch-matrix (gmt/multiply stretch-matrix (gmt/scale-matrix (gpt/point 1 (/ h1 h2)))) + + rotation-angle (calculate-rec-path-rotation shape-center (center-transform rec-path stretch-matrix) shape-path-temp) + stretch-matrix (gmt/multiply (gmt/rotate-matrix rotation-angle) stretch-matrix) + + ;; This is the inverse to be able to remove the transformation + stretch-matrix-inverse (-> (gmt/matrix) + (gmt/scale (gpt/point 1 (/ h2 h1))) + (gmt/skew (- skew-angle) 0) + (gmt/rotate (- rotation-angle))) - ;; Merges the rect values and updates de transformation matrix before calculating the stretch new-shape (-> shape - (merge changes) - (add-rotate-transform (:rotation-modifier shape 0))) - - ;; Calculate the stretch deformation with the resize and rotation aplied - stretch (calculate-stretch shape-path (-> new-shape dissoc-modifiers))] + (merge rec) + (update :transform #(gmt/multiply (or % (gmt/matrix)) stretch-matrix)) + (update :transform-inverse #(gmt/multiply stretch-matrix-inverse (or % (gmt/matrix)))))] - (-> new-shape - (add-stretch-transform stretch)))) + new-shape)) (defn transform-path-shape [shape] (transform-apply-modifiers shape) ;; TODO: Addapt for paths is not working #_(let [shape-path (transform-apply-modifiers shape) - shape-path-center (center shape-path) + shape-path-center (center shape-path) - shape-transform-inverse' (-> (gmt/matrix) - (gmt/translate shape-path-center) - (gmt/multiply (:transform-inverse shape (gmt/matrix))) - (gmt/multiply (gmt/rotate-matrix (- (:rotation-modifier shape 0)))) - (gmt/translate (gpt/negate shape-path-center)))] - (-> shape-path - (transform shape-transform-inverse') - (add-rotate-transform (:rotation-modifier shape 0))))) + shape-transform-inverse' (-> (gmt/matrix) + (gmt/translate shape-path-center) + (gmt/multiply (:transform-inverse shape (gmt/matrix))) + (gmt/multiply (gmt/rotate-matrix (- (:rotation-modifier shape 0)))) + (gmt/translate (gpt/negate shape-path-center)))] + (-> shape-path + (transform shape-transform-inverse') + (add-rotate-transform (:rotation-modifier shape 0))))) (defn transform-shape "Transform the shape properties given the modifiers" ([shape] (transform-shape nil shape)) ([frame shape] - (let [new-shape (cond - (#{:path :curve} (:type shape)) (transform-path-shape shape) - :else (transform-rect-shape shape))] + (let [new-shape (case (:type shape) + :path (transform-path-shape shape) + :curve (transform-path-shape shape) + (transform-rect-shape shape))] (-> new-shape - (update :x - (:x frame 0)) - (update :y - (:y frame 0)) + (translate-to-frame frame) (update :rotation #(mod (+ % (:rotation-modifier shape)) 360)) (dissoc-modifiers))))) diff --git a/frontend/src/uxbox/main/ui/shapes/bounding_box.cljs b/frontend/src/uxbox/main/ui/shapes/bounding_box.cljs index 84642706c..886dba60f 100644 --- a/frontend/src/uxbox/main/ui/shapes/bounding_box.cljs +++ b/frontend/src/uxbox/main/ui/shapes/bounding_box.cljs @@ -9,7 +9,10 @@ [cuerdas.core :as str] [rumext.alpha :as mf] [uxbox.main.geom :as geom] - [uxbox.util.debug :refer [debug?] ])) + [uxbox.util.debug :as debug] + [uxbox.util.geom.matrix :as gmt] + [uxbox.util.geom.point :as gpt] + [uxbox.util.debug :refer [debug?]])) (defn fix [num] (when num (.toFixed num 2))) @@ -20,7 +23,11 @@ (when (debug? :bounding-boxes) (let [shape (unchecked-get props "shape") frame (unchecked-get props "frame") - selrect (geom/transform-selrect frame shape)] + selrect (geom/transform-selrect frame shape) + shape-path (-> shape + (geom/transform-apply-modifiers) + (geom/translate-to-frame frame)) + shape-center (geom/center shape-path)] [:g [:text {:x (:x selrect) :y (- (:y selrect) 5) @@ -30,12 +37,8 @@ :stroke-width 0.1} (str/format "%s - (%s, %s)" (str/slice (str (:id shape)) 0 8) (fix (:x shape)) (fix (:y shape)))] - [:text {:x (-> selrect geom/center :x) - :y (-> selrect geom/center :y) - :fill "red" - :font-size 15 - :text-anchor "middle"} - (str/format "%s°" (fix (+ (:rotation shape) (:rotation-modifier shape))))] + [:circle {:cx (:x shape-center) :cy (:y shape-center) :r 5 :fill "yellow"}] + [:rect {:x (:x selrect) :y (:y selrect) :width (:width selrect) diff --git a/frontend/src/uxbox/util/data.cljs b/frontend/src/uxbox/util/data.cljs index 61145306d..d252743f8 100644 --- a/frontend/src/uxbox/util/data.cljs +++ b/frontend/src/uxbox/util/data.cljs @@ -111,6 +111,9 @@ not-found)) not-found coll))) +(defn zip [col1 col2] + (map vector col1 col2)) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Numbers Parsing ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/uxbox/util/debug.cljs b/frontend/src/uxbox/util/debug.cljs index 5aeef407d..18f0a48f7 100644 --- a/frontend/src/uxbox/util/debug.cljs +++ b/frontend/src/uxbox/util/debug.cljs @@ -3,7 +3,7 @@ (def debug-options #{:bounding-boxes :group :events #_:simple-selection}) -(defonce ^:dynamic *debug* (atom #{})) +(defonce ^:dynamic *debug* (atom #{:bounding-boxes})) (defn debug-all! [] (reset! *debug* debug-options)) (defn debug-none! [] (reset! *debug* #{})) diff --git a/frontend/src/uxbox/util/geom/matrix.cljs b/frontend/src/uxbox/util/geom/matrix.cljs index 7a49cdc58..18bee57ee 100644 --- a/frontend/src/uxbox/util/geom/matrix.cljs +++ b/frontend/src/uxbox/util/geom/matrix.cljs @@ -79,6 +79,16 @@ 0 0)))) +(defn skew-matrix + ([angle-x angle-y point] + (multiply (translate-matrix point) + (skew-matrix angle-y angle-y) + (translate-matrix (gpt/negate point)))) + ([angle-x angle-y] + (let [m1 (mth/tan (mth/radians angle-x)) + m2 (mth/tan (mth/radians angle-y))] + (Matrix. 1 m2 m1 1 0 0)))) + (defn rotate "Apply rotation transformation to the matrix." ([m angle] @@ -98,6 +108,12 @@ [m pt] (multiply m (translate-matrix pt))) +(defn skew + "Apply translate transformation to the matrix." + ([m angle-x angle-y] + (multiply m (skew-matrix angle-x angle-y))) + ([m angle-x angle-y p] + (multiply m (skew-matrix angle-x angle-y p)))) ;; --- Transit Adapter @@ -110,23 +126,3 @@ (t/read-handler (fn [value] (map->Matrix value)))) - -;; Calculates the delta vector to move the figure when scaling after rotation -;; https://math.stackexchange.com/questions/1449672/determine-shift-between-scaled-rotated-object-and-additional-scale-step -(defn correct-rotation [handler lx ly kx ky angle] - (let [[s1 s2 s3] - ;; Different sign configurations change the anchor corner - (cond - (#{:right :bottom :bottom-right} handler) [-1 1 1] - (#{:left :top :top-left} handler) [1 -1 1] - (#{:bottom-left} handler) [-1 -1 -1] - (#{:top-right} handler) [1 1 -1]) - rad (* (or angle 0) (/ Math/PI 180)) - kx' (* (/ (- kx 1.) 2.) lx) - ky' (* (/ (- ky 1.) 2.) ly) - dx (+ (* s3 (* kx' (- 1 (Math/cos rad)))) - (* ky' (Math/sin rad))) - dy (+ (* (- s3) (* ky' (- 1 (Math/cos rad)))) - (* kx' (Math/sin rad)))] - (translate-matrix - (gpt/point (* s1 dx) (* s2 dy))))) diff --git a/frontend/src/uxbox/util/geom/point.cljs b/frontend/src/uxbox/util/geom/point.cljs index b73ef25c8..4e9bc7e88 100644 --- a/frontend/src/uxbox/util/geom/point.cljs +++ b/frontend/src/uxbox/util/geom/point.cljs @@ -171,3 +171,22 @@ (fn [value] (map->Point value)))) + +;; Vector functions +(defn to-vec [p1 p2] + (subtract p2 p1)) + +(defn dot [{x1 :x y1 :y} {x2 :x y2 :y}] + (+ (* x1 x2) (* y1 y2))) + +(defn unit [v] + (let [v-length (length v)] + (divide v (point v-length v-length)))) + +(defn project [v1 v2] + (let [v2-unit (unit v2) + scalar-projection (dot v1 (unit v2))] + (multiply + v2-unit + (point scalar-projection scalar-projection)))) +