From a73a393e261ef4fc2174bf1b4ece665641a14c5b Mon Sep 17 00:00:00 2001 From: Alejandro Alonso Date: Thu, 17 Feb 2022 15:19:36 +0100 Subject: [PATCH] :sparkles: Ability to add multiple strokes to a shape --- CHANGES.md | 1 + common/src/app/common/pages/common.cljc | 23 +- common/src/app/common/pages/init.cljc | 28 +- common/src/app/common/pages/migrations.cljc | 41 ++- .../partials/sidebar-element-options.scss | 6 + .../src/app/main/data/workspace/colors.cljs | 89 ++++-- .../app/main/data/workspace/svg_upload.cljs | 22 +- frontend/src/app/main/ui/shapes/attrs.cljs | 19 +- frontend/src/app/main/ui/shapes/circle.cljs | 4 +- .../src/app/main/ui/shapes/custom_stroke.cljs | 128 ++++++-- frontend/src/app/main/ui/shapes/export.cljs | 36 +++ frontend/src/app/main/ui/shapes/fills.cljs | 2 +- frontend/src/app/main/ui/shapes/filters.cljs | 10 +- frontend/src/app/main/ui/shapes/frame.cljs | 11 +- frontend/src/app/main/ui/shapes/image.cljs | 11 +- frontend/src/app/main/ui/shapes/path.cljs | 4 +- frontend/src/app/main/ui/shapes/rect.cljs | 4 +- frontend/src/app/main/ui/shapes/shape.cljs | 2 - .../src/app/main/ui/shapes/text/svg_text.cljs | 6 +- .../main/ui/viewer/handoff/attributes.cljs | 2 +- .../ui/viewer/handoff/attributes/stroke.cljs | 22 +- .../app/main/ui/workspace/colorpalette.cljs | 2 +- .../app/main/ui/workspace/sidebar/assets.cljs | 2 +- .../main/ui/workspace/sidebar/options.cljs | 3 +- .../workspace/sidebar/options/menus/fill.cljs | 17 +- .../sidebar/options/menus/stroke.cljs | 291 +++++++----------- .../sidebar/options/rows/color_row.cljs | 12 +- .../sidebar/options/rows/stroke_row.cljs | 159 ++++++++++ .../sidebar/options/shapes/multiple.cljs | 10 +- frontend/src/app/util/import/parser.cljs | 66 +++- 30 files changed, 680 insertions(+), 353 deletions(-) create mode 100644 frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs diff --git a/CHANGES.md b/CHANGES.md index af8d8f019f..4f1dd6b229 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ ### :boom: Breaking changes ### :sparkles: New features +- Ability to add multiple strokes to a shape [Taiga #2778](https://tree.taiga.io/project/penpot/us/2778) - Scroll to selected size in font size selector [Taiga #2825](https://tree.taiga.io/project/penpot/us/2825) - Duplicate artboards create new flows if needed [Taiga #2221](https://tree.taiga.io/project/penpot/issue/2221) - Add new invitations section [Taiga #2797](https://tree.taiga.io/project/penpot/us/2797) diff --git a/common/src/app/common/pages/common.cljc b/common/src/app/common/pages/common.cljc index 70e17e37bf..98102fc94e 100644 --- a/common/src/app/common/pages/common.cljc +++ b/common/src/app/common/pages/common.cljc @@ -9,7 +9,7 @@ [app.common.colors :as clr] [app.common.uuid :as uuid])) -(def file-version 14) +(def file-version 15) (def default-color clr/gray-20) (def root uuid/zero) @@ -32,6 +32,7 @@ :letter-spacing :text-display-group :line-height :text-display-group :text-align :text-display-group + :strokes :stroke-group :stroke-color :stroke-group :stroke-color-gradient :stroke-group :stroke-color-ref-file :stroke-group @@ -84,7 +85,19 @@ :fill-color-ref-id :fill-color-ref-file :fill-color-gradient - :hide-fill-on-export} + :hide-fill-on-export + + :strokes + :stroke-style + :stroke-alignment + :stroke-width + :stroke-color + :stroke-color-ref-id + :stroke-color-ref-file + :stroke-opacity + :stroke-color-gradient + :stroke-cap-start + :stroke-cap-end} :group #{:proportion-lock :width :height @@ -131,7 +144,8 @@ :fill-color-ref-id :fill-color-ref-file :fill-color-gradient - + + :strokes :stroke-style :stroke-alignment :stroke-width @@ -171,6 +185,7 @@ :fill-color-ref-file :fill-color-gradient + :strokes :stroke-style :stroke-alignment :stroke-width @@ -210,6 +225,7 @@ :fill-color-ref-file :fill-color-gradient + :strokes :stroke-style :stroke-alignment :stroke-width @@ -339,6 +355,7 @@ :fill-color-ref-file :fill-color-gradient + :strokes :stroke-style :stroke-alignment :stroke-width diff --git a/common/src/app/common/pages/init.cljc b/common/src/app/common/pages/init.cljc index 536fc51725..ac9bd085c3 100644 --- a/common/src/app/common/pages/init.cljc +++ b/common/src/app/common/pages/init.cljc @@ -35,6 +35,7 @@ {:frame-id uuid/zero :fills [{:fill-color clr/white :fill-opacity 1}] + :strokes [] :shapes [] :hide-fill-on-export false}) @@ -43,47 +44,32 @@ :name "Rect-1" :fills [{:fill-color default-color :fill-opacity 1}] - :stroke-style :none - :stroke-alignment :center - :stroke-width 0 - :stroke-color clr/black - :stroke-opacity 0 + :strokes [] :rx 0 :ry 0} {:type :image :rx 0 :ry 0 - :fills []} + :fills [] + :strokes []} {:type :circle :name "Circle-1" :fills [{:fill-color default-color :fill-opacity 1}] - :stroke-style :none - :stroke-alignment :center - :stroke-width 0 - :stroke-color clr/black - :stroke-opacity 0} + :strokes []} {:type :path :name "Path-1" :fills [] - :stroke-style :solid - :stroke-alignment :center - :stroke-width 2 - :stroke-color clr/black - :stroke-opacity 1} + :strokes []} {:type :frame :name "Artboard-1" :fills [{:fill-color clr/white :fill-opacity 1}] - :stroke-style :none - :stroke-alignment :center - :stroke-width 0 - :stroke-color clr/black - :stroke-opacity 0} + :strokes []} {:type :text :name "Text-1" diff --git a/common/src/app/common/pages/migrations.cljc b/common/src/app/common/pages/migrations.cljc index 464df6630f..87f7e4af30 100644 --- a/common/src/app/common/pages/migrations.cljc +++ b/common/src/app/common/pages/migrations.cljc @@ -310,13 +310,9 @@ :fill-opacity (:fill-opacity shape)} clean-attrs (d/without-nils attrs)] - (-> shape - (assoc :fills [clean-attrs]) - (dissoc :fill-color) - (dissoc :fill-color-gradient) - (dissoc :fill-color-ref-file) - (dissoc :fill-color-ref-id) - (dissoc :fill-opacity)))) + (cond-> shape + (not (empty? clean-attrs)) + (assoc :fills [clean-attrs])))) ;; Add fills to shapes (defmethod migrate 14 @@ -328,5 +324,34 @@ (update-page [_ page] (update page :objects #(d/mapm update-object %)))] - + (update data :pages-index #(d/mapm update-page %)))) + +(defn set-strokes + [shape] + (let [attrs {:stroke-style (:stroke-style shape) + :stroke-alignment (:stroke-alignment shape) + :stroke-width (:stroke-width shape) + :stroke-color (:stroke-color shape) + :stroke-color-ref-id (:stroke-color-ref-id shape) + :stroke-color-ref-file (:stroke-color-ref-file shape) + :stroke-opacity (:stroke-opacity shape) + :stroke-color-gradient (:stroke-color-gradient shape) + :stroke-cap-start (:stroke-cap-start shape) + :stroke-cap-end (:stroke-cap-end shape)} + + clean-attrs (d/without-nils attrs)] + (cond-> shape + (not (empty? clean-attrs)) + (assoc :strokes [clean-attrs])))) + +;; Add strokes to shapes +(defmethod migrate 15 + [data] + (letfn [(update-object [_ object] + (cond-> object + (and (not (= :text (:type object))) (nil? (:strokes object))) + (set-strokes))) + + (update-page [_ page] + (update page :objects #(d/mapm update-object %)))] (update data :pages-index #(d/mapm update-page %)))) diff --git a/frontend/resources/styles/main/partials/sidebar-element-options.scss b/frontend/resources/styles/main/partials/sidebar-element-options.scss index 49c2beb301..1e182e9ac3 100644 --- a/frontend/resources/styles/main/partials/sidebar-element-options.scss +++ b/frontend/resources/styles/main/partials/sidebar-element-options.scss @@ -463,6 +463,12 @@ } } +.element-set-content .border-data { + &[draggable="true"] { + cursor: pointer; + } +} + .element-set-content .grid-option-main { .editable-select { height: 2rem; diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 1021995585..dd0b89a8e4 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -135,12 +135,12 @@ (rx/from (map #(dwt/update-text-with-function % transform-attrs) text-ids)) (rx/of (dch/update-shapes shape-ids transform-attrs))))) -(defn swap-fills [shape index new-index] - (let [first (get-in shape [:fills index]) - second (get-in shape [:fills new-index])] +(defn swap-attrs [shape attr index new-index] + (let [first (get-in shape [attr index]) + second (get-in shape [attr new-index])] (-> shape - (assoc-in [:fills index] second) - (assoc-in [:fills new-index] first)))) + (assoc-in [attr index] second) + (assoc-in [attr new-index] first)))) (defn reorder-fills [ids index new-index] @@ -152,7 +152,7 @@ is-text? #(= :text (:type (get objects %))) text-ids (filter is-text? ids) shape-ids (remove is-text? ids) - transform-attrs #(swap-fills % index new-index)] + transform-attrs #(swap-attrs % :fills index new-index)] (rx/concat (rx/from (map #(dwt/update-text-with-function % transform-attrs) text-ids)) @@ -225,37 +225,72 @@ shape)))))))) (defn change-stroke - [ids color] + [ids attrs index] (ptk/reify ::change-stroke ptk/WatchEvent (watch [_ _ _] - (let [attrs (cond-> {:stroke-color nil - :stroke-color-ref-id nil - :stroke-color-ref-file nil - :stroke-color-gradient nil - :stroke-opacity nil} - (contains? color :color) - (assoc :stroke-color (:color color)) + (let [color-attrs (cond-> {} + (contains? attrs :color) + (assoc :stroke-color (:color attrs)) - (contains? color :id) - (assoc :stroke-color-ref-id (:id color)) + (contains? attrs :id) + (assoc :stroke-color-ref-id (:id attrs)) - (contains? color :file-id) - (assoc :stroke-color-ref-file (:file-id color)) + (contains? attrs :file-id) + (assoc :stroke-color-ref-file (:file-id attrs)) - (contains? color :gradient) - (assoc :stroke-color-gradient (:gradient color)) + (contains? attrs :gradient) + (assoc :stroke-color-gradient (:gradient attrs)) - (contains? color :opacity) - (assoc :stroke-opacity (:opacity color)))] + (contains? attrs :opacity) + (assoc :stroke-opacity (:opacity attrs))) + attrs (merge attrs color-attrs)] (rx/of (dch/update-shapes ids (fn [shape] - (cond-> (d/merge shape attrs) - (= (:stroke-style shape) :none) - (assoc :stroke-style :solid - :stroke-width 1 - :stroke-opacity 1))))))))) + (assoc-in shape [:strokes index] (merge (get-in shape [:strokes index]) attrs))))))))) +(defn add-stroke + [ids stroke] + (ptk/reify ::add-stroke + ptk/WatchEvent + (watch [_ _ _] + (let [add (fn [shape attrs] (assoc shape :strokes (into [attrs] (:strokes shape))))] + (rx/of (dch/update-shapes + ids + #(add % stroke))))))) + +(defn remove-stroke + [ids position] + (ptk/reify ::remove-stroke + ptk/WatchEvent + (watch [_ _ _] + (let [remove-fill-by-index (fn [values index] (->> (d/enumerate values) + (filterv (fn [[idx _]] (not= idx index))) + (mapv second))) + + remove (fn [shape] (update shape :strokes remove-fill-by-index position))] + (rx/of (dch/update-shapes + ids + #(remove %))))))) + +(defn remove-all-strokes + [ids] + (ptk/reify ::remove-all-strokes + ptk/WatchEvent + (watch [_ _ _] + (let [remove-all (fn [shape] (assoc shape :strokes []))] + (rx/of (dch/update-shapes + ids + #(remove-all %))))))) + +(defn reorder-strokes + [ids index new-index] + (ptk/reify ::reorder-strokes + ptk/WatchEvent + (watch [_ _ _] + (rx/of (dch/update-shapes + ids + #(swap-attrs % :strokes index new-index)))))) (defn picker-for-selected-shape [] diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index cd456cce67..b90258b1f3 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -101,25 +101,26 @@ (get-in shape [:svg-attrs :style :stroke-linecap])) ((d/nilf str/trim)) ((d/nilf keyword))) + shape (cond-> shape - (uc/color? (get-in shape [:svg-attrs :stroke])) + (uc/color? (str/trim (get-in shape [:svg-attrs :stroke]))) (-> (update :svg-attrs dissoc :stroke) - (assoc :stroke-color (get-in shape [:svg-attrs :stroke]))) + (assoc-in [:strokes 0 :stroke-color] (get-in shape [:svg-attrs :stroke]))) - (uc/color? (get-in shape [:svg-attrs :style :stroke])) + (uc/color? (str/trim (get-in shape [:svg-attrs :style :stroke]))) (-> (update-in [:svg-attrs :style] dissoc :stroke) - (assoc :stroke-color (get-in shape [:svg-attrs :style :stroke]))) + (assoc-in [:strokes 0 :stroke-color] (get-in shape [:svg-attrs :style :stroke]))) (get-in shape [:svg-attrs :stroke-width]) (-> (update :svg-attrs dissoc :stroke-width) - (assoc :stroke-width (-> (get-in shape [:svg-attrs :stroke-width]) - (d/parse-double)))) + (assoc-in [:strokes 0 :stroke-width] (-> (get-in shape [:svg-attrs :stroke-width]) + (d/parse-double)))) (get-in shape [:svg-attrs :style :stroke-width]) (-> (update-in [:svg-attrs :style] dissoc :stroke-width) - (assoc :stroke-width (-> (get-in shape [:svg-attrs :style :stroke-width]) - (d/parse-double)))) + (assoc-in [:strokes 0 :stroke-width] (-> (get-in shape [:svg-attrs :style :stroke-width]) + (d/parse-double)))) (and stroke-linecap (= (:type shape) :path)) (-> (update-in [:svg-attrs :style] dissoc :stroke-linecap) @@ -128,8 +129,8 @@ (assoc :stroke-cap-start stroke-linecap :stroke-cap-end stroke-linecap))))] - (if (d/any-key? shape :stroke-color :stroke-opacity :stroke-width :stroke-cap-start :stroke-cap-end) - (merge {:stroke-style :svg} shape) + (if (d/any-key? (get-in [:strokes 0] shape) :stroke-color :stroke-opacity :stroke-width :stroke-cap-start :stroke-cap-end) + (assoc-in shape [:strokes 0 :stroke-style] :svg) shape))) (defn setup-opacity [shape] @@ -383,6 +384,7 @@ #_other (create-raw-svg name frame-id svg-data element-data))) shape (assoc shape :fills []) + shape (assoc shape :strokes []) shape (when (some? shape) (-> shape diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index 60370404d8..dfe08205ec 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -113,9 +113,9 @@ (obj/merge! attrs (clj->js fill-attrs))))) -(defn add-stroke [attrs shape render-id] +(defn add-stroke [attrs shape render-id index] (let [stroke-style (:stroke-style shape :none) - stroke-color-gradient-id (str "stroke-color-gradient_" render-id) + stroke-color-gradient-id (str "stroke-color-gradient_" render-id "_" index) stroke-width (:stroke-width shape 1)] (if (not= stroke-style :none) (let [stroke-attrs @@ -198,14 +198,13 @@ styles (-> (obj/get props "style" (obj/new)) (obj/merge! svg-styles) - (add-stroke shape render-id) (add-layer-props shape)) styles (cond (or (some? (:fill-image shape)) (= :image (:type shape)) (> (count (:fills shape)) 1) (some #(some? (:fill-color-gradient %)) (:fills shape))) - (obj/set! styles "fill" (str "url(#fill-0-" render-id ")")) + (obj/set! styles "fill" (str "url(#fill-0-" render-id ")")) ;; imported svgs can have fill and fill-opacity attributes (obj/contains? svg-styles "fill") @@ -233,7 +232,15 @@ (-> (obj/new) (obj/set! "style" fill-styles)))) +(defn extract-stroke-attrs + [shape index] + (let [render-id (mf/use-ctx muc/render-ctx) + stroke-styles (-> (obj/get shape "style" (obj/new)) + (add-stroke shape render-id index))] + (-> (obj/new) + (obj/set! "style" stroke-styles)))) + (defn extract-border-radius-attrs [shape] - (-> (obj/new) - (add-border-radius shape))) + (-> (obj/new) + (add-border-radius shape))) diff --git a/frontend/src/app/main/ui/shapes/circle.cljs b/frontend/src/app/main/ui/shapes/circle.cljs index 74a4084fbd..5924765404 100644 --- a/frontend/src/app/main/ui/shapes/circle.cljs +++ b/frontend/src/app/main/ui/shapes/circle.cljs @@ -8,7 +8,7 @@ (:require [app.common.geom.shapes :as geom] [app.main.ui.shapes.attrs :as attrs] - [app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]] + [app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]] [app.util.object :as obj] [rumext.alpha :as mf])) @@ -32,5 +32,5 @@ :ry ry :transform transform}))] - [:& shape-custom-stroke {:shape shape} + [:& shape-custom-strokes {: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 3300bda301..1b8cd3f2a2 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -6,8 +6,11 @@ (ns app.main.ui.shapes.custom-stroke (:require + [app.common.data :as d] [app.common.geom.shapes :as gsh] [app.main.ui.context :as muc] + [app.main.ui.shapes.attrs :as attrs] + [app.main.ui.shapes.gradients :as grad] [app.util.object :as obj] [cuerdas.core :as str] [rumext.alpha :as mf])) @@ -39,8 +42,20 @@ stroke-width (case (:stroke-alignment shape :center) :center (/ (:stroke-width shape 0) 2) :outer (:stroke-width shape 0) - 0)] - [:mask {:id stroke-mask-id} + 0) + margin (gsh/shape-stroke-margin shape stroke-width) + bounding-box (-> (gsh/points->selrect (:points shape)) + (update :x - (+ stroke-width margin)) + (update :y - (+ stroke-width margin)) + (update :width + (* 2 (+ stroke-width margin))) + (update :height + (* 2 (+ stroke-width margin))))] + + [:mask {:id stroke-mask-id + :x (:x bounding-box) + :y (:y bounding-box) + :width (:width bounding-box) + :height (:height bounding-box) + :maskUnits "userSpaceOnUse"} [:use {:xlinkHref (str "#" shape-id) :style #js {:fill "none" :stroke "white" :strokeWidth (* stroke-width 2)}}] @@ -49,13 +64,13 @@ :stroke "none"}}]])) (mf/defc cap-markers - [{:keys [shape render-id]}] + [{:keys [shape render-id index]}] (let [marker-id-prefix (str "marker-" render-id) cap-start (:stroke-cap-start shape) cap-end (:stroke-cap-end shape) stroke-color (if (:stroke-color-gradient shape) - (str/format "url(#%s)" (str "stroke-color-gradient_" render-id)) + (str/format "url(#%s)" (str "stroke-color-gradient_" render-id "_" index)) (:stroke-color shape)) stroke-opacity (when-not (:stroke-color-gradient shape) @@ -154,26 +169,35 @@ [{:keys [shape render-id index]}] (let [open-path? (and (= :path (:type shape)) (gsh/open-path? shape))] - (cond - (and (not open-path?) - (= :inner (:stroke-alignment shape :center)) - (> (:stroke-width shape 0) 0)) - [:& inner-stroke-clip-path {:shape shape - :render-id render-id - :index index}] + [:* + (cond (some? (:stroke-color-gradient shape)) + (case (:type (:stroke-color-gradient shape)) + :linear [:> grad/linear-gradient #js {:id (str (name :stroke-color-gradient) "_" render-id "_" index) + :gradient (:stroke-color-gradient shape) + :shape shape}] + :radial [:> grad/radial-gradient #js {:id (str (name :stroke-color-gradient) "_" render-id "_" index) + :gradient (:stroke-color-gradient shape) + :shape shape}])) + (cond + (and (not open-path?) + (= :inner (:stroke-alignment shape :center)) + (> (:stroke-width shape 0) 0)) + [:& inner-stroke-clip-path {:shape shape + :render-id render-id + :index index}] - (and (not open-path?) - (= :outer (:stroke-alignment shape :center)) - (> (:stroke-width shape 0) 0)) - [:& outer-stroke-mask {:shape shape - :render-id render-id - :index index}] + (and (not open-path?) + (= :outer (:stroke-alignment shape :center)) + (> (:stroke-width shape 0) 0)) + [:& outer-stroke-mask {:shape shape + :render-id render-id + :index index}] - (or (some? (:stroke-cap-start shape)) - (some? (:stroke-cap-end shape))) - [:& cap-markers {:shape shape - :render-id render-id - :index index}]))) + (or (some? (:stroke-cap-start shape)) + (some? (:stroke-cap-end shape))) + [:& cap-markers {:shape shape + :render-id render-id + :index index}])])) ;; Outer alignment: display the shape in two layers. One ;; without stroke (only fill), and another one only with stroke @@ -265,6 +289,7 @@ (let [child (obj/get props "children") shape (obj/get props "shape") + render-id (mf/use-ctx muc/render-ctx) index (obj/get props "index") stroke-width (:stroke-width shape 0) stroke-style (:stroke-style shape :none) @@ -286,5 +311,62 @@ child] :else - child))) + [:g.stroke-shape + [:defs + [:& stroke-defs {:shape shape :render-id render-id :index index}]] + child]))) +(defn build-stroke-props [position shape child value] + (let [render-id (mf/use-ctx muc/render-ctx) + url-fill? (or (some? (:fill-image shape)) + (= :image (:type shape)) + (> (count (:fills shape)) 1) + (some :fill-color-gradient (:fills shape))) + one-fill? (= (count (:fills shape)) 1) + no-fills? (= (count (:fills shape)) 0) + last-stroke? (= position (- (count (:strokes shape)) 1)) + + props (-> (obj/get child "props") + (obj/clone)) + + props (cond + (and last-stroke? url-fill?) + ;; TODO: check this zero + (obj/set! props "fill" (str "url(#fill-0-" render-id ")")) + + (and last-stroke? one-fill?) + (obj/merge! + props + (attrs/extract-fill-attrs (get-in shape [:fills 0]) render-id 0)) + + :else + (-> props + (obj/without ["fill" "fillOpacity"]) + (obj/set! + "style" + (-> (obj/get props "style") + (obj/set! "fill" "none") + (obj/set! "fillOpacity" "none"))))) + + props (-> props + (add-style + (obj/get (attrs/extract-stroke-attrs value position) "style")))] + props)) + +(mf/defc shape-custom-strokes + {::mf/wrap-props false} + [props] + (let [child (obj/get props "children") + shape (obj/get props "shape") + elem-name (obj/get child "type")] + + (cond + (seq (:strokes shape)) + [:* + (for [[index value] (-> (d/enumerate (:strokes shape)) reverse)] + [:& shape-custom-stroke {:shape (assoc value :points (:points shape)) :index index} + [:> elem-name (build-stroke-props index shape child value)]])] + + :else + [:& shape-custom-stroke {:shape shape :index 0} + child]))) diff --git a/frontend/src/app/main/ui/shapes/export.cljs b/frontend/src/app/main/ui/shapes/export.cljs index df9770836f..4a7180cdd3 100644 --- a/frontend/src/app/main/ui/shapes/export.cljs +++ b/frontend/src/app/main/ui/shapes/export.cljs @@ -11,6 +11,7 @@ (:require [app.common.data :as d] [app.common.geom.shapes :as gsh] + [app.main.ui.context :as muc] [app.util.json :as json] [app.util.object :as obj] [app.util.svg :as usvg] @@ -272,6 +273,39 @@ (for [leaf (->> shape :content :content (filter string?))] [:> "penpot:svg-child" {} leaf])]))])) + +(defn- export-fills-data [{:keys [fills]}] + (when-let [fills (seq fills)] + (mf/html + [:> "penpot:fills" #js {} + (for [[index fill] (d/enumerate fills)] + [:> "penpot:fill" + #js {:penpot:fill-color (if (some? (:fill-color-gradient fill)) + (str/format "url(#%s)" (str "fill-color-gradient_" (mf/use-ctx muc/render-ctx) "_" index)) + (d/name (:fill-color fill))) + :penpot:fill-color-ref-file (d/name (:fill-color-ref-file fill)) + :penpot:fill-color-ref-id (d/name (:fill-color-ref-id fill)) + :penpot:fill-opacity (d/name (:fill-opacity fill))}])]))) + +(defn- export-strokes-data [{:keys [strokes]}] + (when-let [strokes (seq strokes)] + (mf/html + [:> "penpot:strokes" #js {} + (for [[index stroke] (d/enumerate strokes)] + [:> "penpot:stroke" + #js {:penpot:stroke-color (if (some? (:stroke-color-gradient stroke)) + (str/format "url(#%s)" (str "stroke-color-gradient_" (mf/use-ctx muc/render-ctx) "_" index)) + (d/name (:stroke-color stroke))) + :penpot:stroke-color-ref-file (d/name (:stroke-color-ref-file stroke)) + :penpot:stroke-color-ref-id (d/name (:stroke-color-ref-id stroke)) + :penpot:stroke-opacity (d/name (:stroke-opacity stroke)) + :penpot:stroke-style (d/name (:stroke-style stroke)) + :penpot:stroke-width (d/name (:stroke-width stroke)) + :penpot:stroke-alignment (d/name (:stroke-alignment stroke)) + :penpot:stroke-cap-start (d/name (:stroke-cap-start stroke)) + :penpot:stroke-cap-end (d/name (:stroke-cap-end stroke))}])]))) + + (defn- export-interactions-data [{:keys [interactions]}] (when-let [interactions (seq interactions)] (mf/html @@ -300,5 +334,7 @@ (export-exports-data shape) (export-svg-data shape) (export-interactions-data shape) + (export-fills-data shape) + (export-strokes-data shape) (export-grid-data shape)])) diff --git a/frontend/src/app/main/ui/shapes/fills.cljs b/frontend/src/app/main/ui/shapes/fills.cljs index ab98cf8527..116797c8b7 100644 --- a/frontend/src/app/main/ui/shapes/fills.cljs +++ b/frontend/src/app/main/ui/shapes/fills.cljs @@ -29,7 +29,7 @@ (let [{:keys [x y width height]} (:selrect shape) {:keys [metadata]} shape - + has-image (or metadata (:fill-image shape)) uri (if metadata (cfg/resolve-file-media metadata) diff --git a/frontend/src/app/main/ui/shapes/filters.cljs b/frontend/src/app/main/ui/shapes/filters.cljs index 1bf804cce1..eadd1750d0 100644 --- a/frontend/src/app/main/ui/shapes/filters.cljs +++ b/frontend/src/app/main/ui/shapes/filters.cljs @@ -201,11 +201,11 @@ :height (- y2 y1)}))))) (defn calculate-padding [shape] - (let [stroke-width (case (:stroke-alignment shape :center) - :center (/ (:stroke-width shape 0) 2) - :outer (:stroke-width shape 0) - 0) - margin (gsh/shape-stroke-margin shape stroke-width)] + (let [stroke-width (apply max 0 (map #(case (:stroke-alignment % :center) + :center (/ (:stroke-width % 0) 2) + :outer (:stroke-width % 0) + 0) (:strokes shape))) + margin (apply max 0 (map #(gsh/shape-stroke-margin % stroke-width) (:strokes shape)))] (+ stroke-width margin))) (defn change-filter-in diff --git a/frontend/src/app/main/ui/shapes/frame.cljs b/frontend/src/app/main/ui/shapes/frame.cljs index 0555b83657..38ae85320b 100644 --- a/frontend/src/app/main/ui/shapes/frame.cljs +++ b/frontend/src/app/main/ui/shapes/frame.cljs @@ -8,6 +8,8 @@ (:require [app.common.data.macros :as dm] [app.main.ui.shapes.attrs :as attrs] + [app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]] + [app.main.ui.shapes.filters :as filters] [app.util.object :as obj] [debug :refer [debug?]] [rumext.alpha :as mf])) @@ -24,9 +26,10 @@ (mf/defc frame-clip-def [{:keys [shape render-id]}] (when (= :frame (:type shape)) - (let [{:keys [x y width height]} shape] + (let [{:keys [x y width height]} shape + padding (filters/calculate-padding shape)] [:clipPath {:id (frame-clip-id shape render-id) :class "frame-clip"} - [:rect {:x x :y y :width width :height height}]]))) + [:rect {:x (- x padding) :y (- y padding) :width (+ width (* 2 padding)) :height (+ height (* 2 padding))}]]))) (mf/defc frame-thumbnail {::mf/wrap-props false} @@ -59,8 +62,10 @@ :width width :height height :className "frame-background"}))] + [:* - [:> :rect props] + [:& shape-custom-strokes {:shape shape} + [:> :rect props]] (for [item childs] [:& shape-wrapper {:shape item diff --git a/frontend/src/app/main/ui/shapes/image.cljs b/frontend/src/app/main/ui/shapes/image.cljs index af784319f1..5a80f78444 100644 --- a/frontend/src/app/main/ui/shapes/image.cljs +++ b/frontend/src/app/main/ui/shapes/image.cljs @@ -8,7 +8,7 @@ (:require [app.common.geom.shapes :as gsh] [app.main.ui.shapes.attrs :as attrs] - [app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]] + [app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]] [app.util.object :as obj] [rumext.alpha :as mf])) @@ -29,8 +29,7 @@ :height height})) path? (some? (.-d props))] - [:g - [:& shape-custom-stroke {:shape shape} - (if path? - [:> :path props] - [:> :rect props])]])) + [:& shape-custom-strokes {:shape shape} + (if path? + [:> :path props] + [:> :rect props])])) diff --git a/frontend/src/app/main/ui/shapes/path.cljs b/frontend/src/app/main/ui/shapes/path.cljs index dc658c1197..896564b87f 100644 --- a/frontend/src/app/main/ui/shapes/path.cljs +++ b/frontend/src/app/main/ui/shapes/path.cljs @@ -8,7 +8,7 @@ (:require [app.common.logging :as log] [app.main.ui.shapes.attrs :as attrs] - [app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]] + [app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]] [app.util.object :as obj] [app.util.path.format :as upf] [rumext.alpha :as mf])) @@ -31,5 +31,5 @@ props (-> (attrs/extract-style-attrs shape) (obj/set! "d" pdata))] - [:& shape-custom-stroke {:shape shape} + [:& shape-custom-strokes {: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 a995f51ccb..8c8a24e792 100644 --- a/frontend/src/app/main/ui/shapes/rect.cljs +++ b/frontend/src/app/main/ui/shapes/rect.cljs @@ -8,7 +8,7 @@ (:require [app.common.geom.shapes :as gsh] [app.main.ui.shapes.attrs :as attrs] - [app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]] + [app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]] [app.util.object :as obj] [rumext.alpha :as mf])) @@ -29,7 +29,7 @@ path? (some? (.-d props))] - [:& shape-custom-stroke {:shape shape} + [:& shape-custom-strokes {: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 d81e986063..6d00dbe1ca 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -14,7 +14,6 @@ [app.main.ui.shapes.fills :as fills] [app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.frame :as frame] - [app.main.ui.shapes.gradients :as grad] [app.main.ui.shapes.svg-defs :as defs] [app.util.object :as obj] [rumext.alpha :as mf])) @@ -62,7 +61,6 @@ [:defs [:& defs/svg-defs {:shape shape :render-id render-id}] [:& filters/filters {:shape shape :filter-id filter-id}] - [:& grad/gradient {:shape shape :attr :stroke-color-gradient}] [:& fills/fills {:shape shape :render-id render-id}] [:& frame/frame-clip-def {:shape shape :render-id render-id}]] children]])) diff --git a/frontend/src/app/main/ui/shapes/text/svg_text.cljs b/frontend/src/app/main/ui/shapes/text/svg_text.cljs index 6bb26816b2..2c436ed6c7 100644 --- a/frontend/src/app/main/ui/shapes/text/svg_text.cljs +++ b/frontend/src/app/main/ui/shapes/text/svg_text.cljs @@ -10,7 +10,7 @@ [app.common.geom.shapes :as gsh] [app.main.ui.context :as muc] [app.main.ui.shapes.attrs :as attrs] - [app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]] + [app.main.ui.shapes.custom-stroke :refer [shape-custom-strokes]] [app.main.ui.shapes.gradients :as grad] [app.util.object :as obj] [rumext.alpha :as mf])) @@ -21,7 +21,7 @@ {::mf/wrap-props false ::mf/wrap [mf/memo]} [props] - + (let [render-id (mf/use-ctx muc/render-ctx) {:keys [x y width height position-data] :as shape} (obj/get props "shape") transform (str (gsh/transform-matrix shape)) @@ -60,7 +60,7 @@ :direction (if (:rtl data) "rtl" "ltr") :whiteSpace "pre"} (obj/set! "fill" (str "url(#fill-" index "-" render-id ")")))})] - [:& shape-custom-stroke {:shape shape :index index} + [:& shape-custom-strokes {:shape shape} [:> :text props (:text data)]]))]])) diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes.cljs index 7a09206895..824876efd5 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes.cljs @@ -21,7 +21,7 @@ (def type->options {:multiple [:fill :stroke :image :text :shadow :blur] - :frame [:layout :fill] + :frame [:layout :fill :stroke] :group [:layout :svg] :rect [:layout :fill :stroke :shadow :blur :svg] :circle [:layout :fill :stroke :shadow :blur :svg] diff --git a/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs b/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs index a41d11ad5c..d04f8832cb 100644 --- a/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs +++ b/frontend/src/app/main/ui/viewer/handoff/attributes/stroke.cljs @@ -30,10 +30,13 @@ color (-> shape shape->color uc/color->background)] (str/format "%spx %s %s" width style color))) -(defn has-stroke? [{:keys [stroke-style]}] - (and stroke-style - (and (not= stroke-style :none) - (not= stroke-style :svg)))) +(defn has-stroke? [shape] + (let [stroke-style (:stroke-style shape)] + (or + (and stroke-style + (and (not= stroke-style :none) + (not= stroke-style :svg))) + (seq (:strokes shape))))) (defn copy-stroke-data [shape] (cg/generate-css-props @@ -80,6 +83,11 @@ [:& copy-button {:data (copy-stroke-data (first shapes))}])] (for [shape shapes] - [:& stroke-block {:key (str "stroke-color-" (:id shape)) - :shape shape - :locale locale}])]))) + (if (seq (:strokes shape)) + (for [value (:strokes shape [])] + [:& stroke-block {:key (str "stroke-color-" (:id shape)) + :shape value + :locale locale}]) + [:& stroke-block {:key (str "stroke-color-" (:id shape)) + :shape shape + :locale locale}]))]))) diff --git a/frontend/src/app/main/ui/workspace/colorpalette.cljs b/frontend/src/app/main/ui/workspace/colorpalette.cljs index 4ed80552d1..813e5fbb7d 100644 --- a/frontend/src/app/main/ui/workspace/colorpalette.cljs +++ b/frontend/src/app/main/ui/workspace/colorpalette.cljs @@ -45,7 +45,7 @@ select-color (fn [event] (if (kbd/alt? event) - (st/emit! (mdc/change-stroke ids-with-children (merge uc/empty-color color))) + (st/emit! (mdc/change-stroke ids-with-children (merge uc/empty-color color) 0)) (st/emit! (mdc/change-fill ids-with-children (merge uc/empty-color color) 0))))] [:div.color-cell {:on-click select-color} diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 34e7a1eb0b..6c08fa5eee 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -803,7 +803,7 @@ (fn [_ event] (let [ids (wsh/lookup-selected @st/state)] (if (kbd/alt? event) - (st/emit! (dc/change-stroke ids color)) + (st/emit! (dc/change-stroke ids color 0)) (st/emit! (dc/change-fill ids color 0))))) rename-color diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index 0d9f452646..0254610848 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -78,7 +78,6 @@ [:div.element-options [:& interactions-menu {:shape (first shapes)}]]]]]]) - ;; TODO: this need optimizations, selected-objects and ;; selected-objects-with-children are derefed always but they only ;; need on multiple selection in majority of cases @@ -93,6 +92,8 @@ file-id (mf/use-ctx ctx/current-file-id) shapes (mf/deref refs/selected-objects) shapes-with-children (mf/deref refs/selected-shapes-with-children)] + ;; TODO: review performance] + [:& options-content {:shapes shapes :selected selected :shapes-with-children shapes-with-children diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index 8cda5adfa8..6cac844898 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -98,7 +98,17 @@ (mf/deps ids) (fn [event] (let [value (-> event dom/get-target dom/checked?)] - (st/emit! (dc/change-hide-fill-on-export ids (not value))))))] + (st/emit! (dc/change-hide-fill-on-export ids (not value)))))) + + disable-drag (mf/use-state false) + + select-all (fn [event] + (when (not @disable-drag) + (dom/select-text! (dom/get-target event))) + (reset! disable-drag true)) + + on-blur (fn [_] + (reset! disable-drag false))] (mf/use-layout-effect (mf/deps hide-fill-on-export?) @@ -139,7 +149,10 @@ :on-change (on-change index) :on-reorder (on-reorder index) :on-detach (on-detach index) - :on-remove (on-remove index)}])]) + :on-remove (on-remove index) + :disable-drag disable-drag + :select-all select-all + :on-blur on-blur}])]) (when (or (= type :frame) (and (= type :multiple) (some? hide-fill-on-export?))) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs index 4dd8c5c154..876b7f2c1d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs @@ -8,20 +8,19 @@ (:require [app.common.colors :as clr] [app.common.data :as d] - [app.main.data.workspace.changes :as dch] [app.main.data.workspace.colors :as dc] [app.main.store :as st] - [app.main.ui.components.dropdown :refer [dropdown]] - [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.hooks :as h] [app.main.ui.icons :as i] - [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] + [app.main.ui.workspace.sidebar.options.rows.stroke-row :refer [stroke-row]] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [cuerdas.core :as str] [rumext.alpha :as mf])) (def stroke-attrs - [:stroke-style + [:strokes + :stroke-style :stroke-alignment :stroke-width :stroke-color @@ -32,36 +31,6 @@ :stroke-cap-start :stroke-cap-end]) -(defn- width->string [width] - (if (= width :multiple) - "" - (str (or width 1)))) - -(defn- enum->string [value] - (if (= value :multiple) - "" - (pr-str value))) - -(defn- stroke-cap-names [] - [[nil (tr "workspace.options.stroke-cap.none") false] - [:line-arrow (tr "workspace.options.stroke-cap.line-arrow") true] - [:triangle-arrow (tr "workspace.options.stroke-cap.triangle-arrow") false] - [:square-marker (tr "workspace.options.stroke-cap.square-marker") false] - [:circle-marker (tr "workspace.options.stroke-cap.circle-marker") false] - [:diamond-marker (tr "workspace.options.stroke-cap.diamond-marker") false] - [:round (tr "workspace.options.stroke-cap.round") true] - [:square (tr "workspace.options.stroke-cap.square") false]]) - -(defn- value->name [value] - (if (= value :multiple) - "--" - (-> (d/seek #(= (first %) value) (stroke-cap-names)) - (second)))) - -(defn- value->img [value] - (when (and value (not= value :multiple)) - (str "images/cap-" (name value) ".svg"))) - (mf/defc stroke-menu {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type" "show-caps"]))]} [{:keys [ids type values show-caps] :as props}] @@ -70,65 +39,62 @@ :group (tr "workspace.options.group-stroke") (tr "workspace.options.stroke")) - show-options (not= (or (:stroke-style values) :none) :none) - show-caps (and show-caps - (not (#{:inner :outer} (:stroke-alignment values)))) - - start-caps-state (mf/use-state {:open? false - :top 0 - :left 0}) - end-caps-state (mf/use-state {:open? false - :top 0 - :left 0}) - - current-stroke-color {:color (:stroke-color values) - :opacity (:stroke-opacity values) - :id (:stroke-color-ref-id values) - :file-id (:stroke-color-ref-file values) - :gradient (:stroke-color-gradient values)} - handle-change-stroke-color (mf/use-callback - (mf/deps ids) - (fn [color] - (let [remove-multiple (fn [[_ value]] (not= value :multiple)) - color (into {} (filter remove-multiple) color)] - (st/emit! (dc/change-stroke ids color))))) + (mf/deps ids) + (fn [index] + (fn [color] + (st/emit! (dc/change-stroke ids color index))))) + + handle-remove + (mf/use-callback + (mf/deps ids) + (fn [index] + (fn [] + (st/emit! (dc/remove-stroke ids index))))) + + handle-remove-remove-all + (fn [_] + (st/emit! (dc/remove-all-strokes ids))) handle-detach (mf/use-callback - (mf/deps ids) - (fn [] - (let [remove-multiple (fn [[_ value]] (not= value :multiple)) - current-stroke-color (-> (into {} (filter remove-multiple) current-stroke-color) - (assoc :id nil :file-id nil))] - (st/emit! (dc/change-stroke ids current-stroke-color))))) + (mf/deps ids) + (fn [index] + (fn [color] + (let [color (-> color + (assoc :id nil :file-id nil))] + (st/emit! (dc/change-stroke ids color index)))))) + + handle-reorder + (mf/use-callback + (mf/deps ids) + (fn [new-index] + (fn [index] + (st/emit! (dc/reorder-strokes ids index new-index))))) on-stroke-style-change - (fn [event] - (let [value (-> (dom/get-target event) - (dom/get-value) - (d/read-string))] - (st/emit! (dch/update-shapes ids #(assoc % :stroke-style value))))) + (fn [index] + (fn [event] + (let [value (-> (dom/get-target event) + (dom/get-value) + (d/read-string))] + (st/emit! (dc/change-stroke ids {:stroke-style value} index))))) on-stroke-alignment-change - (fn [event] - (let [value (-> (dom/get-target event) - (dom/get-value) - (d/read-string))] - (when-not (str/empty? value) - (st/emit! (dch/update-shapes ids #(assoc % :stroke-alignment value)))))) + (fn [index] + (fn [event] + (let [value (-> (dom/get-target event) + (dom/get-value) + (d/read-string))] + (when-not (str/empty? value) + (st/emit! (dc/change-stroke ids {:stroke-alignment value} index)))))) on-stroke-width-change - (fn [value] - (when-not (str/empty? value) - (st/emit! (dch/update-shapes ids #(assoc % :stroke-width value))))) - - update-cap-attr - (fn [& kvs] - #(if (= :path (:type %)) - (apply (partial assoc %) kvs) - %)) + (fn [index] + (fn [value] + (when-not (str/empty? value) + (st/emit! (dc/change-stroke ids {:stroke-width value} index))))) open-caps-select (fn [caps-state] @@ -146,8 +112,8 @@ (:left rect) (- (:width window-size) 205))] (swap! caps-state assoc :open? true - :left left - :top top)))) + :left left + :top top)))) close-caps-select (fn [caps-state] @@ -155,119 +121,72 @@ (swap! caps-state assoc :open? false))) on-stroke-cap-start-change - (fn [value] - (st/emit! (dch/update-shapes ids (update-cap-attr :stroke-cap-start value)))) + (fn [index value] + (st/emit! (dc/change-stroke ids {:stroke-cap-start value} index))) on-stroke-cap-end-change - (fn [value] - (st/emit! (dch/update-shapes ids (update-cap-attr :stroke-cap-end value)))) + (fn [index value] + (st/emit! (dc/change-stroke ids {:stroke-cap-end value} index))) on-stroke-cap-switch - (fn [_] - (let [stroke-cap-start (:stroke-cap-start values) - stroke-cap-end (:stroke-cap-end values)] + (fn [index] + (let [stroke-cap-start (get-in values [:strokes index :stroke-cap-start]) + stroke-cap-end (get-in values [:strokes index :stroke-cap-end])] (when (and (not= stroke-cap-start :multiple) (not= stroke-cap-end :multiple)) - (st/emit! (dch/update-shapes ids (update-cap-attr - :stroke-cap-start stroke-cap-end - :stroke-cap-end stroke-cap-start)))))) - + (st/emit! (dc/change-stroke ids {:stroke-cap-start stroke-cap-end + :stroke-cap-end stroke-cap-start} index))))) on-add-stroke (fn [_] - (st/emit! (dch/update-shapes ids #(assoc % - :stroke-style :solid - :stroke-color clr/black - :stroke-opacity 1 - :stroke-width 1)))) + (st/emit! (dc/add-stroke ids {:stroke-style :solid + :stroke-color clr/black + :stroke-opacity 1 + :stroke-width 1}))) - on-del-stroke - (fn [_] - (st/emit! (dch/update-shapes ids #(assoc % :stroke-style :none))))] + disable-drag (mf/use-state false) - (if show-options - [:div.element-set - [:div.element-set-title - [:span label] - [:div.add-page {:on-click on-del-stroke} i/minus]] + select-all (fn [event] + (when (not @disable-drag) + (dom/select-text! (dom/get-target event))) + (reset! disable-drag true)) - [:div.element-set-content - ;; Stroke Color - [:& color-row {:color current-stroke-color - :title (tr "workspace.options.stroke-color") - :on-change handle-change-stroke-color - :on-detach handle-detach}] + on-blur (fn [_] + (reset! disable-drag false))] - ;; Stroke Width, Alignment & Style - [:div.row-flex - [:div.input-element - {:class (dom/classnames :pixels (not= (:stroke-width values) :multiple)) - :title (tr "workspace.options.stroke-width")} + [:div.element-set + [:div.element-set-title + [:span label] + [:div.add-page {:on-click on-add-stroke} i/close]] - [:> numeric-input - {:min 0 - :value (-> (:stroke-width values) width->string) - :precision 2 - :placeholder (tr "settings.multiple") - :on-change on-stroke-width-change}]] + [:div.element-set-content + (cond + (= :multiple (:strokes values)) + [:div.element-set-options-group + [:div.element-set-label (tr "settings.multiple")] + [:div.element-set-actions + [:div.element-set-actions-button {:on-click handle-remove-remove-all} + i/minus]]] - [:select#style.input-select {:value (enum->string (:stroke-alignment values)) - :on-change on-stroke-alignment-change} - (when (= (:stroke-alignment values) :multiple) - [:option {:value ""} "--"]) - [:option {:value ":center"} (tr "workspace.options.stroke.center")] - [:option {:value ":inner"} (tr "workspace.options.stroke.inner")] - [:option {:value ":outer"} (tr "workspace.options.stroke.outer")]] - - [:select#style.input-select {:value (enum->string (:stroke-style values)) - :on-change on-stroke-style-change} - (when (= (:stroke-style values) :multiple) - [:option {:value ""} "--"]) - [:option {:value ":solid"} (tr "workspace.options.stroke.solid")] - [:option {:value ":dotted"} (tr "workspace.options.stroke.dotted")] - [:option {:value ":dashed"} (tr "workspace.options.stroke.dashed")] - [:option {:value ":mixed"} (tr "workspace.options.stroke.mixed")]]] - - ;; Stroke Caps - (when show-caps - [:div.row-flex - [:div.cap-select {:tab-index 0 ;; tab-index to make the element focusable - :on-click (open-caps-select start-caps-state)} - (value->name (:stroke-cap-start values)) - [:span.cap-select-button - i/arrow-down]] - [:& dropdown {:show (:open? @start-caps-state) - :on-close (close-caps-select start-caps-state)} - [:ul.dropdown.cap-select-dropdown {:style {:top (:top @start-caps-state) - :left (:left @start-caps-state)}} - (for [[value label separator] (stroke-cap-names)] - (let [img (value->img value)] - [:li {:class (dom/classnames :separator separator) - :on-click #(on-stroke-cap-start-change value)} - (when img [:img {:src (value->img value)}]) - label]))]] - - [:div.element-set-actions-button {:on-click on-stroke-cap-switch} - i/switch] - - [:div.cap-select {:tab-index 0 - :on-click (open-caps-select end-caps-state)} - (value->name (:stroke-cap-end values)) - [:span.cap-select-button - i/arrow-down]] - [:& dropdown {:show (:open? @end-caps-state) - :on-close (close-caps-select end-caps-state)} - [:ul.dropdown.cap-select-dropdown {:style {:top (:top @end-caps-state) - :left (:left @end-caps-state)}} - (for [[value label separator] (stroke-cap-names)] - (let [img (value->img value)] - [:li {:class (dom/classnames :separator separator) - :on-click #(on-stroke-cap-end-change value)} - (when img [:img {:src (value->img value)}]) - label]))]]])]] - - ;; NO STROKE - [:div.element-set - [:div.element-set-title - [:span label] - [:div.add-page {:on-click on-add-stroke} i/close]]]))) + (seq (:strokes values)) + [:& h/sortable-container {} + (for [[index value] (d/enumerate (:strokes values []))] + [:& stroke-row {:stroke value + :title (tr "workspace.options.stroke-color") + :index index + :show-caps show-caps + :on-color-change handle-change-stroke-color + :on-color-detach handle-detach + :on-stroke-width-change on-stroke-width-change + :on-stroke-style-change on-stroke-style-change + :on-stroke-alignment-change on-stroke-alignment-change + :open-caps-select open-caps-select + :close-caps-select close-caps-select + :on-stroke-cap-start-change on-stroke-cap-start-change + :on-stroke-cap-end-change on-stroke-cap-end-change + :on-stroke-cap-switch on-stroke-cap-switch + :on-remove handle-remove + :on-reorder (handle-reorder index) + :disable-drag disable-drag + :select-all select-all + :on-blur on-blur}])])]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs index 8ab359d529..8628265755 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs @@ -61,12 +61,11 @@ (if (= v :multiple) nil v)) (mf/defc color-row - [{:keys [index color disable-gradient disable-opacity on-change on-reorder on-detach on-open on-close title on-remove]}] + [{:keys [index color disable-gradient disable-opacity on-change on-reorder on-detach on-open on-close title on-remove disable-drag select-all on-blur]}] (let [current-file-id (mf/use-ctx ctx/current-file-id) file-colors (mf/deref refs/workspace-file-colors) shared-libs (mf/deref refs/workspace-libraries) hover-detach (mf/use-state false) - disable-drag (mf/use-state false) get-color-name (fn [{:keys [id file-id]}] (let [src-colors (if (= file-id current-file-id) @@ -107,14 +106,6 @@ handle-opacity-change (fn [value] (change-opacity (/ value 100))) - select-all (fn [event] - (when (not @disable-drag) - (dom/select-text! (dom/get-target event))) - (reset! disable-drag true)) - - on-blur (fn [_] - (reset! disable-drag false)) - handle-click-color (mf/use-callback (mf/deps color) (color-picker-callback color @@ -194,6 +185,7 @@ [:> numeric-input {:value (-> color :opacity opacity->string) :placeholder (tr "settings.multiple") :on-click select-all + :on-blur on-blur :on-change handle-opacity-change :min 0 :max 100}]])]) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs new file mode 100644 index 0000000000..6f462ded7e --- /dev/null +++ b/frontend/src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs @@ -0,0 +1,159 @@ +;; 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.workspace.sidebar.options.rows.stroke-row + (:require + [app.common.data :as d] + [app.main.ui.components.dropdown :refer [dropdown]] + [app.main.ui.components.numeric-input :refer [numeric-input]] + [app.main.ui.hooks :as h] + [app.main.ui.icons :as i] + [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] + [app.util.dom :as dom] + [app.util.i18n :as i18n :refer [tr]] + [rumext.alpha :as mf])) + +(defn- width->string [width] + (if (= width :multiple) + "" + (str (or width 1)))) + +(defn- enum->string [value] + (if (= value :multiple) + "" + (pr-str value))) + +(defn- stroke-cap-names [] + [[nil (tr "workspace.options.stroke-cap.none") false] + [:line-arrow (tr "workspace.options.stroke-cap.line-arrow") true] + [:triangle-arrow (tr "workspace.options.stroke-cap.triangle-arrow") false] + [:square-marker (tr "workspace.options.stroke-cap.square-marker") false] + [:circle-marker (tr "workspace.options.stroke-cap.circle-marker") false] + [:diamond-marker (tr "workspace.options.stroke-cap.diamond-marker") false] + [:round (tr "workspace.options.stroke-cap.round") true] + [:square (tr "workspace.options.stroke-cap.square") false]]) + +(defn- value->img [value] + (when (and value (not= value :multiple)) + (str "images/cap-" (name value) ".svg"))) + +(defn- value->name [value] + (if (= value :multiple) + "--" + (-> (d/seek #(= (first %) value) (stroke-cap-names)) + (second)))) + +(mf/defc stroke-row + [{:keys [index stroke title show-caps on-color-change on-reorder on-color-detach on-remove on-stroke-width-change on-stroke-style-change on-stroke-alignment-change open-caps-select close-caps-select on-stroke-cap-start-change on-stroke-cap-end-change on-stroke-cap-switch disable-drag select-all on-blur]}] + (let [start-caps-state (mf/use-state {:open? false + :top 0 + :left 0}) + end-caps-state (mf/use-state {:open? false + :top 0 + :left 0}) + on-drop + (fn [_ data] + (on-reorder (:index data))) + + [dprops dref] (if (some? on-reorder) + (h/use-sortable + :data-type "penpot/stroke-row" + :on-drop on-drop + :disabled @disable-drag + :detect-center? false + :data {:id (str "stroke-row-" index) + :index index + :name (str "Border row" index)}) + [nil nil])] + + [:div.border-data {:class (dom/classnames + :dnd-over-top (= (:over dprops) :top) + :dnd-over-bot (= (:over dprops) :bot)) + :ref dref} + ;; Stroke Color + [:& color-row {:color {:color (:stroke-color stroke) + :opacity (:stroke-opacity stroke) + :id (:stroke-color-ref-id stroke) + :file-id (:stroke-color-ref-file stroke) + :gradient (:stroke-color-gradient stroke)} + :index index + :title title + :on-change (on-color-change index) + :on-detach (on-color-detach index) + :on-remove (on-remove index) + :disable-drag disable-drag + :select-all select-all + :on-blur on-blur}] + + ;; Stroke Width, Alignment & Style + [:div.row-flex + [:div.input-element + {:class (dom/classnames :pixels (not= (:stroke-width stroke) :multiple)) + :title (tr "workspace.options.stroke-width")} + + [:> numeric-input + {:min 0 + :value (-> (:stroke-width stroke) width->string) + :precision 2 + :placeholder (tr "settings.multiple") + :on-change (on-stroke-width-change index) + :on-click select-all + :on-blur on-blur}]] + + [:select#style.input-select {:value (enum->string (:stroke-alignment stroke)) + :on-change (on-stroke-alignment-change index)} + (when (= (:stroke-alignment stroke) :multiple) + [:option {:value ""} "--"]) + [:option {:value ":center"} (tr "workspace.options.stroke.center")] + [:option {:value ":inner"} (tr "workspace.options.stroke.inner")] + [:option {:value ":outer"} (tr "workspace.options.stroke.outer")]] + + [:select#style.input-select {:value (enum->string (:stroke-style stroke)) + :on-change (on-stroke-style-change index)} + (when (= (:stroke-style stroke) :multiple) + [:option {:value ""} "--"]) + [:option {:value ":solid"} (tr "workspace.options.stroke.solid")] + [:option {:value ":dotted"} (tr "workspace.options.stroke.dotted")] + [:option {:value ":dashed"} (tr "workspace.options.stroke.dashed")] + [:option {:value ":mixed"} (tr "workspace.options.stroke.mixed")]]] + + ;; Stroke Caps + (when show-caps + [:div.row-flex + [:div.cap-select {:tab-index 0 ;; tab-index to make the element focusable + :on-click (open-caps-select start-caps-state)} + (value->name (:stroke-cap-start stroke)) + [:span.cap-select-button + i/arrow-down]] + [:& dropdown {:show (:open? @start-caps-state) + :on-close (close-caps-select start-caps-state)} + [:ul.dropdown.cap-select-dropdown {:style {:top (:top @start-caps-state) + :left (:left @start-caps-state)}} + (for [[value label separator] (stroke-cap-names)] + (let [img (value->img value)] + [:li {:class (dom/classnames :separator separator) + :on-click #(on-stroke-cap-start-change index value)} + (when img [:img {:src (value->img value)}]) + label]))]] + + [:div.element-set-actions-button {:on-click #(on-stroke-cap-switch index)} + i/switch] + + [:div.cap-select {:tab-index 0 + :on-click (open-caps-select end-caps-state)} + (value->name (:stroke-cap-end stroke)) + [:span.cap-select-button + i/arrow-down]] + [:& dropdown {:show (:open? @end-caps-state) + :on-close (close-caps-select end-caps-state)} + [:ul.dropdown.cap-select-dropdown {:style {:top (:top @end-caps-state) + :left (:left @end-caps-state)}} + (for [[value label separator] (stroke-cap-names)] + (let [img (value->img value)] + [:li {:class (dom/classnames :separator separator) + :on-click #(on-stroke-cap-end-change index value)} + (when img [:img {:src (value->img value)}]) + label]))]]])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs index 5b1d31e95b..2c3fa4a0ab 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/multiple.cljs @@ -8,6 +8,7 @@ (:require [app.common.attrs :as attrs] [app.common.data :as d] + [app.common.geom.shapes :as gsh] [app.common.pages.common :as cpc] [app.common.text :as txt] [app.main.ui.hooks :as hooks] @@ -34,7 +35,7 @@ :fill :shape :shadow :children :blur :children - :stroke :children + :stroke :shape :text :children} :group @@ -205,6 +206,7 @@ (let [shapes (unchecked-get props "shapes") shapes-with-children (unchecked-get props "shapes-with-children") objects (->> shapes-with-children (group-by :id) (d/mapm (fn [_ v] (first v)))) + show-caps (some #(and (= :path (:type %)) (gsh/open-path? %)) shapes) ;; Selrect/points only used for measures and it's the one that changes the most. We separate it ;; so we can memoize it @@ -249,14 +251,14 @@ (when-not (empty? fill-ids) [:& fill-menu {:type type :ids fill-ids :values fill-values}]) + (when-not (empty? stroke-ids) + [:& stroke-menu {:type type :ids stroke-ids :show-caps show-caps :values stroke-values}]) + (when-not (empty? shadow-ids) [:& shadow-menu {:type type :ids shadow-ids :values shadow-values}]) (when-not (empty? blur-ids) [:& blur-menu {:type type :ids blur-ids :values blur-values}]) - (when-not (empty? stroke-ids) - [:& stroke-menu {:type type :ids stroke-ids :show-caps true :values stroke-values}]) - (when-not (empty? text-ids) [:& ot/text-menu {:type type :ids text-ids :values text-values}])])) diff --git a/frontend/src/app/util/import/parser.cljs b/frontend/src/app/util/import/parser.cljs index 70edb5ec40..123856b2b8 100644 --- a/frontend/src/app/util/import/parser.cljs +++ b/frontend/src/app/util/import/parser.cljs @@ -208,7 +208,11 @@ (reduce add-attrs node-attrs)) (= type :frame) - (let [svg-node (->> node :content (d/seek #(= "frame-background" (get-in % [:attrs :class]))))] + (let [;; The nodes with the "frame-background" class can have some anidation depending on the strokes they have + g-nodes (find-all-nodes node :g) + defs-nodes (flatten (map #(find-all-nodes % :defs) g-nodes)) + rect-nodes (flatten [(map #(find-all-nodes % :rect) defs-nodes) (map #(find-all-nodes % :rect) g-nodes)]) + svg-node (d/seek #(= "frame-background" (get-in % [:attrs :class])) rect-nodes)] (merge (add-attrs {} (:attrs svg-node)) node-attrs)) (= type :svg-raw) @@ -685,20 +689,48 @@ props))) -(defn add-fills - [props node svg-data] - (let [fills (-> node - (find-node :defs) - (find-node :pattern) - (find-node :g) - (find-all-nodes :rect) - (reverse)) - fills (if (= 0 (count fills)) - [(add-fill {} node svg-data)] - (map #(add-fill {} node (get-svg-data :rect %)) fills))] - (-> props - (assoc :fills fills)))) +(defn parse-fills + [node svg-data] + (let [fills-node (get-data node :penpot:fills) + fills (->> (find-all-nodes fills-node :penpot:fill) + (mapv (fn [fill-node] + {:fill-color (when (not (str/starts-with? (get-meta fill-node :fill-color) "url")) + (get-meta fill-node :fill-color)) + :fill-color-gradient (when (str/starts-with? (get-meta fill-node :fill-color) "url") + (parse-gradient node (get-meta fill-node :fill-color))) + :fill-color-ref-file (get-meta fill-node :fill-color-ref-file uuid/uuid) + :fill-color-ref-id (get-meta fill-node :fill-color-ref-id uuid/uuid) + :fill-opacity (get-meta fill-node :fill-opacity d/parse-double)})) + (mapv d/without-nils))] + (if (seq fills) + fills + (->> [(-> (add-fill {} node svg-data) + (d/without-nils))] + (filterv not-empty))))) +(defn parse-strokes + [node svg-data] + (let [strokes-node (get-data node :penpot:strokes) + strokes (->> (find-all-nodes strokes-node :penpot:stroke) + (mapv (fn [stroke-node] + {:stroke-color (when (not (str/starts-with? (get-meta stroke-node :stroke-color) "url")) + (get-meta stroke-node :stroke-color)) + :stroke-color-gradient (when (str/starts-with? (get-meta stroke-node :stroke-color) "url") + (parse-gradient node (get-meta stroke-node :stroke-color))) + :stroke-color-ref-file (get-meta stroke-node :stroke-color-ref-file uuid/uuid) + :stroke-color-ref-id (get-meta stroke-node :stroke-color-ref-id uuid/uuid) + :stroke-opacity (get-meta stroke-node :stroke-opacity d/parse-double) + :stroke-style (get-meta stroke-node :stroke-style keyword) + :stroke-width (get-meta stroke-node :stroke-width d/parse-double) + :stroke-alignment (get-meta stroke-node :stroke-alignment keyword) + :stroke-cap-start (get-meta stroke-node :stroke-cap-start keyword) + :stroke-cap-end (get-meta stroke-node :stroke-cap-end keyword)})) + (mapv d/without-nils))] + (if (seq strokes) + strokes + (->> [(-> (add-stroke {} node svg-data) + (d/without-nils))] + (filterv #(and (not-empty %) (not= (:stroke-style %) :none))))))) (defn add-svg-content [props node] @@ -765,14 +797,16 @@ (-> {} (add-common-data node) (add-position type node svg-data) - (add-stroke node svg-data) + (add-layer-options svg-data) (add-shadows node) (add-blur node) (add-exports node) (add-svg-attrs node svg-data) (add-library-refs node) - (add-fills node svg-data) + + (assoc :fills (parse-fills node svg-data)) + (assoc :strokes (parse-strokes node svg-data)) (cond-> (= :svg-raw type) (add-svg-content node))