From 346fb8fb112053db41d0cd03687e3b64a9399e87 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 13 May 2021 14:50:28 +0200 Subject: [PATCH] :sparkles: Transform simple shapes to path on double click --- CHANGES.md | 3 + frontend/src/app/config.cljs | 7 +- frontend/src/app/main/data/workspace.cljs | 3 +- .../app/main/data/workspace/transforms.cljs | 9 ++ frontend/src/app/main/ui/shapes/attrs.cljs | 4 + .../src/app/main/ui/shapes/fill_image.cljs | 38 +++++ frontend/src/app/main/ui/shapes/image.cljs | 23 ++- frontend/src/app/main/ui/shapes/shape.cljs | 10 +- .../main/ui/workspace/viewport/actions.cljs | 4 +- .../src/app/util/path/shapes_to_path.cljs | 145 ++++++++++++++++++ 10 files changed, 227 insertions(+), 19 deletions(-) create mode 100644 frontend/src/app/main/ui/shapes/fill_image.cljs create mode 100644 frontend/src/app/util/path/shapes_to_path.cljs diff --git a/CHANGES.md b/CHANGES.md index f7d697582..640f17c7f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,9 @@ ## :rocket: Next ### :sparkles: New features + +- Transform shapes to path on double click + ### :bug: Bugs fixed ### :arrow_up: Deps updates ### :boom: Breaking changes diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index 55e800fe5..87dcf4431 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -121,9 +121,10 @@ (defn resolve-file-media ([media] (resolve-file-media media false)) - ([{:keys [id] :as media} thumnail?] + + ([{:keys [id]} thumbnail?] (str (cond-> (u/join public-uri "assets/by-file-media-id/") - (true? thumnail?) (u/join (str id "/thumbnail")) - (false? thumnail?) (u/join (str id)))))) + (true? thumbnail?) (u/join (str id "/thumbnail")) + (false? thumbnail?) (u/join (str id)))))) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 53097aac6..4f5bcae82 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1797,6 +1797,7 @@ (d/export dwt/update-dimensions) (d/export dwt/flip-horizontal-selected) (d/export dwt/flip-vertical-selected) +(d/export dwt/selected-to-path) ;; Persistence @@ -1820,12 +1821,10 @@ (d/export dws/duplicate-selected) (d/export dws/handle-selection) (d/export dws/select-inside-group) -;;(d/export dws/select-last-layer) (d/export dwd/select-for-drawing) (d/export dwc/clear-edition-mode) (d/export dwc/add-shape) (d/export dwc/start-edition-mode) -#_(d/export dwc/start-path-edit) ;; Groups diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index e2c0ad42f..cc0270a95 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -23,6 +23,7 @@ [app.main.snap :as snap] [app.main.store :as st] [app.main.streams :as ms] + [app.util.path.shapes-to-path :as ups] [beicon.core :as rx] [beicon.core :as rx] [cljs.spec.alpha :as s] @@ -626,3 +627,11 @@ (-> state (dissoc :workspace-modifiers) (update :workspace-local dissoc :modifiers :current-move-selected))))) + +(defn selected-to-path + [] + (ptk/reify ::selected-to-path + ptk/WatchEvent + (watch [_ state stream] + (let [ids (wsh/lookup-selected state)] + (rx/of (dch/update-shapes ids ups/convert-to-path)))))) diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index 8573682fd..bbd09a262 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -81,6 +81,10 @@ (contains? shape :fill-color) {:fill (:fill-color shape)} + (contains? shape :fill-image) + (let [fill-image-id (str "fill-image-" render-id)] + {:fill (str/format "url(#%s)" fill-image-id) }) + ;; If contains svg-attrs the origin is svg. If it's not svg origin ;; we setup the default fill as transparent (instead of black) (and (not (contains? shape :svg-attrs)) diff --git a/frontend/src/app/main/ui/shapes/fill_image.cljs b/frontend/src/app/main/ui/shapes/fill_image.cljs new file mode 100644 index 000000000..17d13291b --- /dev/null +++ b/frontend/src/app/main/ui/shapes/fill_image.cljs @@ -0,0 +1,38 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.ui.shapes.fill-image + (:require + [app.common.geom.shapes :as gsh] + [app.config :as cfg] + [app.util.object :as obj] + [rumext.alpha :as mf] + [app.common.geom.point :as gpt] + [app.main.ui.shapes.image :as image])) + +(mf/defc fill-image-pattern + {::mf/wrap-props false} + [props] + + (let [shape (obj/get props "shape") + render-id (obj/get props "render-id")] + (when (contains? shape :fill-image) + (let [{:keys [x y width height]} (:selrect shape) + fill-image-id (str "fill-image-" render-id) + media (:fill-image shape) + uri (image/use-image-uri media) + transform (gsh/transform-matrix shape)] + + [:pattern {:id fill-image-id + :patternUnits "userSpaceOnUse" + :x x + :y y + :height height + :width width + :patternTransform transform} + [:image {:xlinkHref uri + :width width + :height height}]])))) diff --git a/frontend/src/app/main/ui/shapes/image.cljs b/frontend/src/app/main/ui/shapes/image.cljs index 4dd1e3e41..45ceb303b 100644 --- a/frontend/src/app/main/ui/shapes/image.cljs +++ b/frontend/src/app/main/ui/shapes/image.cljs @@ -17,13 +17,10 @@ [beicon.core :as rx] [rumext.alpha :as mf])) -(mf/defc image-shape - {::mf/wrap-props false} - [props] - - (let [shape (unchecked-get props "shape") - {:keys [id x y width height rotation metadata]} shape - uri (cfg/resolve-file-media metadata) +(defn use-image-uri + [media] + (let [uri (mf/use-memo (mf/deps (:id media)) + #(cfg/resolve-file-media media)) embed-resources? (mf/use-ctx muc/embed-ctx) data-uri (mf/use-state (when (not embed-resources?) uri))] @@ -38,6 +35,16 @@ (rx/mapcat wapi/read-file-as-data-url) (rx/subs #(reset! data-uri %)))))) + (or @data-uri uri))) + +(mf/defc image-shape + {::mf/wrap-props false} + [props] + + (let [shape (unchecked-get props "shape") + {:keys [id x y width height rotation metadata]} shape + uri (use-image-uri metadata)] + (let [transform (geom/transform-matrix shape) props (-> (attrs/extract-style-attrs shape) (obj/merge! @@ -53,5 +60,5 @@ [:> "image" (obj/merge! props - #js {:xlinkHref (or @data-uri uri) + #js {:xlinkHref uri :onDragStart on-drag-start})]))) diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index 793c168fb..82dddd238 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.uuid :as uuid] [app.main.ui.context :as muc] + [app.main.ui.shapes.fill-image :as fim] [app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.gradients :as grad] [app.main.ui.shapes.svg-defs :as defs] @@ -51,8 +52,9 @@ [:& (mf/provider muc/render-ctx) {:value render-id} [:> wrapper-tag group-props [:defs - [:& defs/svg-defs {:shape shape :render-id render-id}] - [:& filters/filters {:shape shape :filter-id filter-id}] - [:& grad/gradient {:shape shape :attr :fill-color-gradient}] - [:& grad/gradient {:shape shape :attr :stroke-color-gradient}]] + [:& defs/svg-defs {:shape shape :render-id render-id}] + [:& filters/filters {:shape shape :filter-id filter-id}] + [:& grad/gradient {:shape shape :attr :fill-color-gradient}] + [:& grad/gradient {:shape shape :attr :stroke-color-gradient}] + [:& fim/fill-image-pattern {:shape shape :render-id render-id}]] children]])) diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 2b6d7d25b..a32ae106e 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -179,8 +179,8 @@ (dw/start-editing-selected)) :else - ;; Do nothing - nil)))))) + (st/emit! (dw/selected-to-path) + (dw/start-editing-selected)))))))) (defn on-context-menu [hover] diff --git a/frontend/src/app/util/path/shapes_to_path.cljs b/frontend/src/app/util/path/shapes_to_path.cljs new file mode 100644 index 000000000..b260821e7 --- /dev/null +++ b/frontend/src/app/util/path/shapes_to_path.cljs @@ -0,0 +1,145 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.util.path.shapes-to-path + (:require + [app.common.data :as d] + [app.common.geom.matrix :as gmt] + [app.common.geom.point :as gpt] + [app.common.geom.shapes :as gsh] + [app.common.geom.shapes.path :as gsp] + [app.util.path.commands :as pc])) + +(def bezier-circle-c 0.551915024494) +(def dissoc-attrs [:x :y :width :height + :rx :ry :r1 :r2 :r3 :r4 + :medata]) +(def allowed-transform-types #{:rect + :circle + :image}) + +(defn make-corner-arc + "Creates a curvle corner for border radius" + [from to corner radius] + (let [x (case corner + :top-left (:x from) + :top-right (- (:x from) radius) + :bottom-right (- (:x to) radius) + :bottom-left (:x to)) + + y (case corner + :top-left (- (:y from) radius) + :top-right (:y from) + :bottom-right (- (:y to) (* 2 radius)) + :bottom-left (- (:y to) radius)) + + width (* radius 2) + height (* radius 2) + + c bezier-circle-c + c1x (+ x (* (/ width 2) (- 1 c))) + c2x (+ x (* (/ width 2) (+ 1 c))) + c1y (+ y (* (/ height 2) (- 1 c))) + c2y (+ y (* (/ height 2) (+ 1 c))) + + h1 (case corner + :top-left (assoc from :y c1y) + :top-right (assoc from :x c2x) + :bottom-right (assoc from :y c2y) + :bottom-left (assoc from :x c1x)) + + h2 (case corner + :top-left (assoc to :x c1x) + :top-right (assoc to :y c1y) + :bottom-right (assoc to :x c2x) + :bottom-left (assoc to :y c2y))] + + (pc/make-curve-to to h1 h2))) + +(defn circle->path + "Creates the bezier curves to approximate a circle shape" + [x y width height] + (let [mx (+ x (/ width 2)) + my (+ y (/ height 2)) + ex (+ x width) + ey (+ y height) + + pc (gpt/point mx my) + p1 (gpt/point mx y) + p2 (gpt/point ex my) + p3 (gpt/point mx ey) + p4 (gpt/point x my) + + c bezier-circle-c + c1x (+ x (* (/ width 2) (- 1 c))) + c2x (+ x (* (/ width 2) (+ 1 c))) + c1y (+ y (* (/ height 2) (- 1 c))) + c2y (+ y (* (/ height 2) (+ 1 c)))] + + [(pc/make-move-to p1) + (pc/make-curve-to p2 (assoc p1 :x c2x) (assoc p2 :y c1y)) + (pc/make-curve-to p3 (assoc p2 :y c2y) (assoc p3 :x c2x)) + (pc/make-curve-to p4 (assoc p3 :x c1x) (assoc p4 :y c2y)) + (pc/make-curve-to p1 (assoc p4 :y c1y) (assoc p1 :x c1x))])) + +(defn rect->path + "Creates a bezier curve that approximates a rounded corner rectangle" + [x y width height r1 r2 r3 r4] + (let [p1 (gpt/point x (+ y r1)) + p2 (gpt/point (+ x r1) y) + + p3 (gpt/point (+ width x (- r2)) y) + p4 (gpt/point (+ width x) (+ y r2)) + + p5 (gpt/point (+ width x) (+ height y (- r3))) + p6 (gpt/point (+ width x (- r3)) (+ height y)) + + p7 (gpt/point (+ x r4) (+ height y)) + p8 (gpt/point x (+ height y (- r4)))] + (-> [] + (conj (pc/make-move-to p1)) + (cond-> (not= p1 p2) + (conj (make-corner-arc p1 p2 :top-left r1))) + (conj (pc/make-line-to p3)) + (cond-> (not= p3 p4) + (conj (make-corner-arc p3 p4 :top-right r2))) + (conj (pc/make-line-to p5)) + (cond-> (not= p5 p6) + (conj (make-corner-arc p5 p6 :bottom-right r3))) + (conj (pc/make-line-to p7)) + (cond-> (not= p7 p8) + (conj (make-corner-arc p7 p8 :bottom-left r4))) + (conj (pc/make-line-to p1))))) + +(defn convert-to-path + "Transforms the given shape to a path" + [{:keys [type x y width height r1 r2 r3 r4 rx metadata] :as shape}] + + (if (contains? allowed-transform-types type) + (let [r1 (or r1 rx 0) + r2 (or r2 rx 0) + r3 (or r3 rx 0) + r4 (or r4 rx 0) + + new-content + (case type + :circle + (circle->path x y width height) + (rect->path x y width height r1 r2 r3 r4)) + + ;; Apply the transforms that had the shape + transform (gmt/transform-in (gsh/center-shape shape) (:transform shape)) + new-content (gsp/transform-content new-content transform)] + + (-> shape + (d/without-keys dissoc-attrs) + (assoc :type :path) + (assoc :content new-content) + (cond-> (= :image type) + (assoc :fill-image metadata)))) + ;; Do nothing if the shape is not of a correct type + shape)) +