From 6cbbfa6499d8ca6237a7b3327c2815c1fad995a1 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Mon, 31 May 2021 18:06:28 +0200 Subject: [PATCH] :recycle: Refactor custom stroke --- frontend/src/app/main/ui/shapes/circle.cljs | 5 +- .../src/app/main/ui/shapes/custom_stroke.cljs | 220 ++++++++++-------- frontend/src/app/main/ui/shapes/path.cljs | 14 +- frontend/src/app/main/ui/shapes/rect.cljs | 26 +-- frontend/src/app/main/ui/shapes/shape.cljs | 4 +- .../app/main/ui/workspace/shapes/path.cljs | 3 +- frontend/src/app/util/object.cljs | 31 ++- 7 files changed, 166 insertions(+), 137 deletions(-) diff --git a/frontend/src/app/main/ui/shapes/circle.cljs b/frontend/src/app/main/ui/shapes/circle.cljs index b3cdecbf0c..da19c2003e 100644 --- a/frontend/src/app/main/ui/shapes/circle.cljs +++ b/frontend/src/app/main/ui/shapes/circle.cljs @@ -32,6 +32,5 @@ :ry ry :transform transform}))] - [:& shape-custom-stroke {:shape shape - :base-props props - :elem-name "ellipse"}])) + [:& shape-custom-stroke {:shape shape} + [:> :ellipse props]])) diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index 643f074853..f3759ce0c2 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -9,7 +9,116 @@ [rumext.alpha :as mf] [app.common.uuid :as uuid] [app.common.geom.shapes :as geom] - [app.util.object :as obj])) + [app.util.object :as obj] + [app.main.ui.context :as muc])) + +(defn add-props + [props new-props] + (-> props + (obj/merge (clj->js new-props)))) + +(defn add-style + [props new-style] + (let [old-style (obj/get props "style") + style (obj/merge old-style (clj->js new-style))] + (-> props (obj/merge #js {:style style})))) + +(mf/defc inner-stroke-clip-path + [{:keys [shape render-id]}] + (let [clip-id (str "inner-stroke-" render-id) + shape-id (str "stroke-shape-" render-id)] + [:> "clipPath" #js {:id clip-id} + [:use {:href (str "#" shape-id)}]])) + +(mf/defc outer-stroke-mask + [{:keys [shape render-id]}] + (let [stroke-mask-id (str "outer-stroke-" render-id) + shape-id (str "stroke-shape-" render-id) + stroke-width (:stroke-width shape 0)] + [:mask {:id stroke-mask-id} + [:use {:href (str "#" shape-id) + :style #js {:fill "none" :stroke "white" :strokeWidth (* stroke-width 2)}}] + + [:use {:href (str "#" shape-id) + :style #js {:fill "black"}}]])) + +(mf/defc stroke-defs + [{:keys [shape render-id]}] + (cond + (and (= :inner (:stroke-alignment shape :center)) + (> (:stroke-width shape 0) 0)) + [:& inner-stroke-clip-path {:shape shape + :render-id render-id}] + + (and (= :outer (:stroke-alignment shape :center)) + (> (:stroke-width shape 0) 0)) + [:& outer-stroke-mask {:shape shape + :render-id render-id}])) + +;; Outer alingmnent: display the shape in two layers. One +;; without stroke (only fill), and another one only with stroke +;; at double width (transparent fill) and passed through a mask +;; that shows the whole shape, but hides the original shape +;; without stroke +(mf/defc outer-stroke + {::mf/wrap-props false} + [props] + + (let [render-id (mf/use-ctx muc/render-ctx) + child (obj/get props "children") + base-props (obj/get child "props") + elem-name (obj/get child "type") + shape (obj/get props "shape") + stroke-width (:stroke-width shape 0) + stroke-mask-id (str "outer-stroke-" render-id) + shape-id (str "stroke-shape-" render-id)] + + [:g.outer-stroke-shape + [:symbol + [:> elem-name (-> (obj/clone base-props) + (obj/set! "id" shape-id) + (obj/without ["style"]))]] + + [:use {:href (str "#" shape-id) + :mask (str "url(#" stroke-mask-id ")") + :style (-> (obj/get base-props "style") + (obj/clone) + (obj/update! "strokeWidth" * 2) + (obj/without ["fill" "fillOpacity"]) + (obj/set! "fill" "none"))}] + + [:use {:href (str "#" shape-id) + :style (-> (obj/get base-props "style") + (obj/clone) + (obj/without ["stroke" "strokeWidth" "strokeOpacity" "strokeStyle" "strokeDasharray"]))}]])) + + +;; Inner alignment: display the shape with double width stroke, +;; and clip the result with the original shape without stroke. +(mf/defc inner-stroke + {::mf/wrap-props false} + [props] + (let [render-id (mf/use-ctx muc/render-ctx) + child (obj/get props "children") + base-props (obj/get child "props") + elem-name (obj/get child "type") + shape (obj/get props "shape") + transform (obj/get base-props "transform") + + stroke-width (:stroke-width shape 0) + + clip-id (str "inner-stroke-" render-id) + shape-id (str "stroke-shape-" render-id) + + shape-props (-> base-props + (add-props {:id shape-id + :transform nil + :clipPath (str "url('#" clip-id "')")}) + (add-style {:strokeWidth (* stroke-width 2)}))] + + [:g.inner-stroke-shape {:transform transform} + [:> elem-name shape-props]])) + ; The SVG standard does not implement yet the 'stroke-alignment' ; attribute, to define the position of the stroke relative to the @@ -19,100 +128,25 @@ (mf/defc shape-custom-stroke {::mf/wrap-props false} [props] - (let [shape (unchecked-get props "shape") - base-props (unchecked-get props "base-props") - elem-name (unchecked-get props "elem-name") - base-style (obj/get base-props "style") - {:keys [x y width height]} (:selrect shape) - stroke-id (mf/use-var (uuid/next)) + (let [child (obj/get props "children") + shape (obj/get props "shape") + stroke-width (:stroke-width shape 0) stroke-style (:stroke-style shape :none) - stroke-position (:stroke-alignment shape :center)] + stroke-position (:stroke-alignment shape :center) + has-stroke? (and (and (> stroke-width 0) + (not= stroke-style :none))) + inner? (= :inner stroke-position) + outer? (= :outer stroke-position)] + (cond - ;; Center alignment (or no stroke): the default in SVG - (or (= stroke-style :none) (= stroke-position :center)) - [:> elem-name (obj/merge! #js {} base-props)] + (and has-stroke? inner?) + [:& inner-stroke {:shape shape} + child] - ;; Inner alignment: display the shape with double width stroke, - ;; and clip the result with the original shape without stroke. - (= stroke-position :inner) - (let [clip-id (str "clip-" @stroke-id) + (and has-stroke? outer?) + [:& outer-stroke {:shape shape} + child] - clip-props (obj/merge - base-props - #js {:transform nil - :style (obj/merge - base-style - #js {:stroke nil - :strokeWidth nil - :strokeOpacity nil - :strokeDasharray nil - :fill "white" - :fillOpacity 1})}) - - stroke-width (obj/get base-style "strokeWidth" 0) - shape-props (obj/merge - base-props - #js {:clipPath (str "url('#" clip-id "')") - :style (obj/merge - base-style - #js {:strokeWidth (* stroke-width 2)})})] - [:* - [:> "clipPath" #js {:id clip-id} - [:> elem-name clip-props]] - [:> elem-name shape-props]]) - - ;; Outer alingmnent: display the shape in two layers. One - ;; without stroke (only fill), and another one only with stroke - ;; at double width (transparent fill) and passed through a mask - ;; that shows the whole shape, but hides the original shape - ;; without stroke - - (= stroke-position :outer) - (let [stroke-mask-id (str "mask-" @stroke-id) - stroke-width (obj/get base-style "strokeWidth" 0) - mask-props1 (obj/merge - base-props - #js {:transform nil - :style (obj/merge - base-style - #js {:stroke "white" - :strokeWidth (* stroke-width 2) - :strokeOpacity 1 - :strokeDasharray nil - :fill "white" - :fillOpacity 1})}) - mask-props2 (obj/merge - base-props - #js {:transform nil - :style (obj/merge - base-style - #js {:stroke nil - :strokeWidth nil - :strokeOpacity nil - :strokeDasharray nil - :fill "black" - :fillOpacity 1})}) - - shape-props1 (obj/merge - base-props - #js {:style (obj/merge - base-style - #js {:stroke nil - :strokeWidth nil - :strokeOpacity nil - :strokeDasharray nil})}) - shape-props2 (obj/merge - base-props - #js {:mask (str "url('#" stroke-mask-id "')") - :style (obj/merge - base-style - #js {:strokeWidth (* stroke-width 2) - :fill "none" - :fillOpacity 0})})] - [:* - [:mask {:id stroke-mask-id} - [:> elem-name mask-props1] - [:> elem-name mask-props2]] - [:> elem-name shape-props1] - [:> elem-name shape-props2]])))) + :else + child))) diff --git a/frontend/src/app/main/ui/shapes/path.cljs b/frontend/src/app/main/ui/shapes/path.cljs index 256730eb22..9726803051 100644 --- a/frontend/src/app/main/ui/shapes/path.cljs +++ b/frontend/src/app/main/ui/shapes/path.cljs @@ -26,16 +26,6 @@ props (-> (attrs/extract-style-attrs shape) (obj/merge! #js {:d pdata}))] - (if background? - [:g - [:path {:stroke "none" - :fill "none" - :stroke-width "20px" - :d pdata}] - [:& shape-custom-stroke {:shape shape - :base-props props - :elem-name "path"}]] - [:& shape-custom-stroke {:shape shape - :base-props props - :elem-name "path"}]))) + [:& shape-custom-stroke {:shape shape} + [:> :path props]])) diff --git a/frontend/src/app/main/ui/shapes/rect.cljs b/frontend/src/app/main/ui/shapes/rect.cljs index aeb61b17a8..bb0ccd60a8 100644 --- a/frontend/src/app/main/ui/shapes/rect.cljs +++ b/frontend/src/app/main/ui/shapes/rect.cljs @@ -6,23 +6,19 @@ (ns app.main.ui.shapes.rect (:require - [rumext.alpha :as mf] + [app.common.geom.shapes :as gsh] [app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]] - [app.common.geom.shapes :as geom] - [app.util.object :as obj] [app.main.ui.shapes.gradients :refer [gradient]] - - [cuerdas.core :as str] - [app.common.uuid :as uuid] - [app.common.geom.point :as gpt])) + [app.util.object :as obj] + [rumext.alpha :as mf])) (mf/defc rect-shape {::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") {:keys [id x y width height]} shape - transform (geom/transform-matrix shape) + transform (gsh/transform-matrix shape) props (-> (attrs/extract-style-attrs shape) (obj/merge! @@ -30,11 +26,11 @@ :y y :transform transform :width width - :height height}))] + :height height})) - [:& shape-custom-stroke {:shape shape - :base-props props - :elem-name - (if (.-d props) - "path" - "rect")}])) + path? (some? (.-d props))] + + [:& shape-custom-stroke {:shape shape} + (if path? + [:> :path props] + [:> :rect props])])) diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index 82dddd2386..cd05e9d11b 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.custom-stroke :as cs] [app.main.ui.shapes.fill-image :as fim] [app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.gradients :as grad] @@ -56,5 +57,6 @@ [:& 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}]] + [:& fim/fill-image-pattern {:shape shape :render-id render-id}] + [:& cs/stroke-defs {:shape shape :render-id render-id}]] children]])) diff --git a/frontend/src/app/main/ui/workspace/shapes/path.cljs b/frontend/src/app/main/ui/workspace/shapes/path.cljs index 22232ca011..c9183c3fe9 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path.cljs @@ -28,5 +28,4 @@ [:> shape-container {:shape shape :pointer-events (when editing? "none")} - [:& path/path-shape {:shape shape - :background? true}]])) + [:& path/path-shape {:shape shape}]])) diff --git a/frontend/src/app/util/object.cljs b/frontend/src/app/util/object.cljs index ad6697a428..03c2447048 100644 --- a/frontend/src/app/util/object.cljs +++ b/frontend/src/app/util/object.cljs @@ -27,17 +27,18 @@ (js/Object.keys ^js obj)) (defn get-in - [obj keys] - (loop [key (first keys) - keys (rest keys) - res obj] - (if (nil? key) - res - (if (nil? res) - res - (recur (first keys) - (rest keys) - (unchecked-get res key)))))) + ([obj keys] + (get-in obj keys nil)) + + ([obj keys default] + (loop [key (first keys) + keys (rest keys) + res obj] + (if (or (nil? key) (nil? res)) + (or res default) + (recur (first keys) + (rest keys) + (unchecked-get res key)))))) (defn without [obj keys] @@ -68,6 +69,14 @@ (unchecked-set obj key value) obj) +(defn update! + [obj key f & args] + (let [found (get obj key ::not-found)] + (if-not (identical? ::not-found found) + (do (unchecked-set obj key (apply f found args)) + obj) + obj))) + (defn- props-key-fn [key] (if (or (= key :class) (= key :class-name))