From be9073f0b79383264f4b1e9883dfbf2015548cb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Moya?= Date: Mon, 16 Aug 2021 12:56:44 +0200 Subject: [PATCH] :tada: Add stroke caps to path ends --- common/src/app/common/pages/spec.cljc | 14 +++ frontend/resources/images/icons/switch.svg | 5 + .../app/main/ui/components/numeric_input.cljs | 2 +- frontend/src/app/main/ui/icons.cljs | 1 + frontend/src/app/main/ui/shapes/attrs.cljs | 25 +++- .../src/app/main/ui/shapes/custom_stroke.cljs | 109 +++++++++++++++++- .../sidebar/options/menus/stroke.cljs | 77 ++++++++++++- .../sidebar/options/shapes/multiple.cljs | 2 +- .../sidebar/options/shapes/path.cljs | 1 + frontend/translations/en.po | 32 +++++ frontend/translations/es.po | 32 +++++ 11 files changed, 292 insertions(+), 8 deletions(-) create mode 100644 frontend/resources/images/icons/switch.svg diff --git a/common/src/app/common/pages/spec.cljc b/common/src/app/common/pages/spec.cljc index 9c445ce6de..aec1120161 100644 --- a/common/src/app/common/pages/spec.cljc +++ b/common/src/app/common/pages/spec.cljc @@ -10,6 +10,7 @@ [app.common.geom.point :as gpt] [app.common.spec :as us] [app.common.uuid :as uuid] + [clojure.set :as set] [clojure.spec.alpha :as s])) ;; --- Specs @@ -254,6 +255,17 @@ (s/def :internal.shape/stroke-color-ref-id (s/nilable uuid?)) (s/def :internal.shape/stroke-opacity ::safe-number) (s/def :internal.shape/stroke-style #{:solid :dotted :dashed :mixed :none :svg}) + +(def stroke-caps-line #{:round :square}) +(def stroke-caps-marker #{:line-arrow :triangle-arrow :square-marker :circle-marker :diamond-marker}) +(def stroke-caps (set/union stroke-caps-line stroke-caps-marker)) +(s/def :internal.shape/stroke-cap-start stroke-caps) +(s/def :internal.shape/stroke-cap-end stroke-caps) + +(defn has-caps? + [shape] + (= (:type shape) :path)) + (s/def :internal.shape/stroke-width ::safe-number) (s/def :internal.shape/stroke-alignment #{:center :inner :outer}) (s/def :internal.shape/text-align #{"left" "right" "center" "justify"}) @@ -342,6 +354,8 @@ :internal.shape/stroke-style :internal.shape/stroke-width :internal.shape/stroke-alignment + :internal.shape/stroke-cap-start + :internal.shape/stroke-cap-end :internal.shape/text-align :internal.shape/transform :internal.shape/transform-inverse diff --git a/frontend/resources/images/icons/switch.svg b/frontend/resources/images/icons/switch.svg new file mode 100644 index 0000000000..d6bf0ab85e --- /dev/null +++ b/frontend/resources/images/icons/switch.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/frontend/src/app/main/ui/components/numeric_input.cljs b/frontend/src/app/main/ui/components/numeric_input.cljs index a6bbb02e3c..54154c5479 100644 --- a/frontend/src/app/main/ui/components/numeric_input.cljs +++ b/frontend/src/app/main/ui/components/numeric_input.cljs @@ -31,7 +31,7 @@ local-ref (mf/use-ref) ref (or external-ref local-ref) - value (d/parse-integer value-str) + value (d/parse-integer value-str 0) min-val (when (string? min-val-str) (d/parse-integer min-val-str)) diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index b4b4cd7126..47738e45a2 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -124,6 +124,7 @@ (def sort-descending (icon-xref :sort-descending)) (def strikethrough (icon-xref :strikethrough)) (def stroke (icon-xref :stroke)) +(def switch (icon-xref :switch)) (def text (icon-xref :text)) (def text-align-center (icon-xref :text-align-center)) (def text-align-justify (icon-xref :text-align-justify)) diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index dcb26a3695..85ac6e16e2 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.shapes.attrs (:require + [app.common.pages.spec :as spec] [app.main.ui.context :as muc] [app.util.object :as obj] [app.util.svg :as usvg] @@ -117,7 +118,29 @@ (assoc :strokeOpacity (:stroke-opacity shape nil)) (not= stroke-style :svg) - (assoc :strokeDasharray (stroke-type->dasharray stroke-style)))] + (assoc :strokeDasharray (stroke-type->dasharray stroke-style)) + + ;; For simple line caps we use svg stroke-line-cap attribute. This + ;; only works if all caps are the same and we are not using the tricks + ;; for inner or outer strokes. + (and (spec/stroke-caps-line (:stroke-cap-start shape)) + (= (:stroke-cap-start shape) (:stroke-cap-end shape)) + (= (:stroke-alignment shape) :center)) + (assoc :strokeLinecap (:stroke-cap-start shape)) + + ;; For other cap types we use markers. + (and (or (spec/stroke-caps-marker (:stroke-cap-start shape)) + (and (spec/stroke-caps-line (:stroke-cap-start shape)) + (not= (:stroke-cap-start shape) (:stroke-cap-end shape)))) + (= (:stroke-alignment shape) :center)) + (assoc :markerStart (str "url(#marker-" render-id "-" (name (:stroke-cap-start shape)))) + + (and (or (spec/stroke-caps-marker (:stroke-cap-end shape)) + (and (spec/stroke-caps-line (:stroke-cap-end shape)) + (not= (:stroke-cap-start shape) (:stroke-cap-end shape)))) + (= (:stroke-alignment shape) :center)) + (assoc :markerEnd (str "url(#marker-" render-id "-" (name (:stroke-cap-end shape)))))] + (obj/merge! attrs (clj->js stroke-attrs))) attrs))) diff --git a/frontend/src/app/main/ui/shapes/custom_stroke.cljs b/frontend/src/app/main/ui/shapes/custom_stroke.cljs index 741494946c..6a5f92dcbf 100644 --- a/frontend/src/app/main/ui/shapes/custom_stroke.cljs +++ b/frontend/src/app/main/ui/shapes/custom_stroke.cljs @@ -42,6 +42,107 @@ [:use {:xlinkHref (str "#" shape-id) :style #js {:fill "black"}}]])) +(mf/defc cap-markers + [{:keys [shape render-id]}] + (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)) + (:stroke-color shape)) + + stroke-opacity (when-not (:stroke-color-gradient shape) + (:stroke-opacity shape))] + [:* + (when (or (= cap-start :line-arrow) (= cap-end :line-arrow)) + [:marker {:id (str marker-id-prefix "-line-arrow") + :viewBox "0 0 3 6" + :refX "2" + :refY "3" + :markerWidth "3" + :markerHeight "6" + :orient "auto-start-reverse" + :fill stroke-color + :fillOpacity stroke-opacity} + [:path {:d "M 0.5 0.5 L 3 3 L 0.5 5.5 L 0 5 L 2 3 L 0 1 z"}]]) + + (when (or (= cap-start :triangle-arrow) (= cap-end :triangle-arrow)) + [:marker {:id (str marker-id-prefix "-triangle-arrow") + :viewBox "0 0 3 6" + :refX "2" + :refY "3" + :markerWidth "3" + :markerHeight "6" + :orient "auto-start-reverse" + :fill stroke-color + :fillOpacity stroke-opacity} + [:path {:d "M 0 0 L 3 3 L 0 6 z"}]]) + + (when (or (= cap-start :square-marker) (= cap-end :square-marker)) + [:marker {:id (str marker-id-prefix "-square-marker") + :viewBox "0 0 6 6" + :refX "5" + :refY "3" + :markerWidth "6" + :markerHeight "6" + :orient "auto-start-reverse" + :fill stroke-color + :fillOpacity stroke-opacity} + [:rect {:x 0 :y 0 :width 6 :height 6}]]) + + (when (or (= cap-start :circle-marker) (= cap-end :circle-marker)) + [:marker {:id (str marker-id-prefix "-circle-marker") + :viewBox "0 0 6 6" + :refX "5" + :refY "3" + :markerWidth "6" + :markerHeight "6" + :orient "auto-start-reverse" + :fill stroke-color + :fillOpacity stroke-opacity} + [:circle {:cx "3" :cy "3" :r "3"}]]) + + (when (or (= cap-start :diamond-marker) (= cap-end :diamond-marker)) + [:marker {:id (str marker-id-prefix "-diamond-marker") + :viewBox "0 0 6 6" + :refX "5" + :refY "3" + :markerWidth "6" + :markerHeight "6" + :orient "auto-start-reverse" + :fill stroke-color + :fillOpacity stroke-opacity} + [:path {:d "M 3 0 L 6 3 L 3 6 L 0 3 z"}]]) + + ;; If the user wants line caps but different in each end, + ;; simulate it with markers. + (when (and (or (= cap-start :round) (= cap-end :round)) + (not= cap-start cap-end)) + [:marker {:id (str marker-id-prefix "-round") + :viewBox "0 0 6 6" + :refX "3" + :refY "3" + :markerWidth "6" + :markerHeight "6" + :orient "auto-start-reverse" + :fill stroke-color + :fillOpacity stroke-opacity} + [:path {:d "M 3 2.5 A 0.5 0.5 0 0 1 3 3.5 "}]]) + + (when (and (or (= cap-start :square) (= cap-end :square)) + (not= cap-start cap-end)) + [:marker {:id (str marker-id-prefix "-square") + :viewBox "0 0 6 6" + :refX "3" + :refY "3" + :markerWidth "6" + :markerHeight "6" + :orient "auto-start-reverse" + :fill stroke-color + :fillOpacity stroke-opacity} + [:rect {:x 3 :y 2.5 :width 0.5 :height 1}]])])) + (mf/defc stroke-defs [{:keys [shape render-id]}] (cond @@ -53,7 +154,13 @@ (and (= :outer (:stroke-alignment shape :center)) (> (:stroke-width shape 0) 0)) [:& outer-stroke-mask {:shape shape - :render-id render-id}])) + :render-id render-id}] + + (and (or (some? (:stroke-cap-start shape)) + (some? (:stroke-cap-end shape))) + (= (:stroke-alignment shape) :center)) + [:& cap-markers {: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 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 beee39a191..39c996bc17 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,6 +8,7 @@ (:require [app.common.data :as d] [app.common.math :as math] + [app.common.pages.spec :as spec] [app.main.data.workspace.changes :as dch] [app.main.data.workspace.colors :as dc] [app.main.data.workspace.undo :as dwu] @@ -27,7 +28,9 @@ :stroke-color-ref-id :stroke-color-ref-file :stroke-opacity - :stroke-color-gradient]) + :stroke-color-gradient + :stroke-cap-start + :stroke-cap-end]) (defn- width->string [width] (if (= width :multiple) @@ -42,8 +45,8 @@ (pr-str value))) (mf/defc stroke-menu - {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type"]))]} - [{:keys [ids type values] :as props}] + {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values" "type" "show-caps"]))]} + [{:keys [ids type values show-caps] :as props}] (let [label (case type :multiple (tr "workspace.options.selection-stroke") :group (tr "workspace.options.group-stroke") @@ -51,6 +54,8 @@ show-options (not= (:stroke-style values :none) :none) + show-caps (and show-caps (= (:stroke-alignment values) :center)) + current-stroke-color {:color (:stroke-color values) :opacity (:stroke-opacity values) :id (:stroke-color-ref-id values) @@ -94,6 +99,38 @@ (when-not (str/empty? value) (st/emit! (dch/update-shapes ids #(assoc % :stroke-width value)))))) + update-cap-attr + (fn [& kvs] + #(if (spec/has-caps? %) + (apply (partial assoc %) kvs) + %)) + + on-stroke-cap-start-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 (update-cap-attr :stroke-cap-start value)))))) + + on-stroke-cap-end-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 (update-cap-attr :stroke-cap-end value)))))) + + on-stroke-cap-switch + (fn [_] + (let [stroke-cap-start (:stroke-cap-start values) + stroke-cap-end (:stroke-cap-end values)] + (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)))))) + on-add-stroke (fn [_] (st/emit! (dch/update-shapes ids #(assoc % @@ -157,7 +194,39 @@ [: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")]]]]] + [:option {:value ":mixed"} (tr "workspace.options.stroke.mixed")]]] + + ;; Stroke Caps + (when show-caps + [:div.row-flex + [:select#style.input-select {:value (enum->string (:stroke-cap-start values)) + :on-change on-stroke-cap-start-change} + (when (= (:stroke-cap-start values) :multiple) + [:option {:value ""} "--"]) + [:option {:value ""} (tr "workspace.options.stroke-cap.none")] + [:option {:value ":line-arrow"} (tr "workspace.options.stroke-cap.line-arrow")] + [:option {:value ":triangle-arrow"} (tr "workspace.options.stroke-cap.triangle-arrow")] + [:option {:value ":square-marker"} (tr "workspace.options.stroke-cap.square-marker")] + [:option {:value ":circle-marker"} (tr "workspace.options.stroke-cap.circle-marker")] + [:option {:value ":diamond-marker"} (tr "workspace.options.stroke-cap.diamond-marker")] + [:option {:value ":round"} (tr "workspace.options.stroke-cap.round")] + [:option {:value ":square"} (tr "workspace.options.stroke-cap.square")]] + + [:div.element-set-actions-button {:on-click on-stroke-cap-switch} + i/switch] + + [:select#style.input-select {:value (enum->string (:stroke-cap-end values)) + :on-change on-stroke-cap-end-change} + (when (= (:stroke-cap-end values) :multiple) + [:option {:value ""} "--"]) + [:option {:value ""} (tr "workspace.options.stroke-cap.none")] + [:option {:value ":line-arrow"} (tr "workspace.options.stroke-cap.line-arrow")] + [:option {:value ":triangle-arrow"} (tr "workspace.options.stroke-cap.triangle-arrow")] + [:option {:value ":square-marker"} (tr "workspace.options.stroke-cap.square-marker")] + [:option {:value ":circle-marker"} (tr "workspace.options.stroke-cap.circle-marker")] + [:option {:value ":diamond-marker"} (tr "workspace.options.stroke-cap.diamond-marker")] + [:option {:value ":round"} (tr "workspace.options.stroke-cap.round")] + [:option {:value ":square"} (tr "workspace.options.stroke-cap.square")]]])]] ;; NO STROKE [:div.element-set 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 5f69d1ea9a..07dbf09fb1 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 @@ -215,7 +215,7 @@ [:& blur-menu {:type type :ids blur-ids :values blur-values}]) (when-not (empty? stroke-ids) - [:& stroke-menu {:type type :ids stroke-ids :values stroke-values}]) + [:& 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/main/ui/workspace/sidebar/options/shapes/path.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs index 9d8727a028..cd911b1b12 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/shapes/path.cljs @@ -38,6 +38,7 @@ :values (select-keys shape fill-attrs)}] [:& stroke-menu {:ids ids :type type + :show-caps true :values stroke-values}] [:& shadow-menu {:ids ids :values (select-keys shape [:shadow])}] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 2b54ff9448..f3361ab5bb 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -2320,6 +2320,38 @@ msgstr "Outside" msgid "workspace.options.stroke.solid" msgstr "Solid" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.none" +msgstr "None" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow" +msgstr "Line arrow" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow" +msgstr "Triangle arrow" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker" +msgstr "Square marker" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker" +msgstr "Circle marker" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker" +msgstr "Diamond marker" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.round" +msgstr "Round" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square" +msgstr "Square" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-bottom" msgstr "Align bottom" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 0fa15cde3f..e853dc6151 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -2318,6 +2318,38 @@ msgstr "Exterior" msgid "workspace.options.stroke.solid" msgstr "Sólido" +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.none" +msgstr "Ninguno" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.line-arrow" +msgstr "Flecha de línea" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.triangle-arrow" +msgstr "Flecha triángulo" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square-marker" +msgstr "Marcador cuadrado" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.circle-marker" +msgstr "Marcador círculo" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.diamond-marker" +msgstr "Marcador diamante" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.round" +msgstr "Redondo" + +#: src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +msgid "workspace.options.stroke-cap.square" +msgstr "Cuadrado" + #: src/app/main/ui/workspace/sidebar/options/menus/text.cljs msgid "workspace.options.text-options.align-bottom" msgstr "Alinear abajo"