0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-12 07:41:43 -05:00

🎉 Add stroke caps to path ends

This commit is contained in:
Andrés Moya 2021-08-16 12:56:44 +02:00 committed by Andrey Antukh
parent ac6c07b771
commit be9073f0b7
11 changed files with 292 additions and 8 deletions

View file

@ -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

View file

@ -0,0 +1,5 @@
<svg viewBox="0 0 500 500" width="500" height="500" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="M374.8 238.3l-19.6 18.5 94.8 97.3-437.2.3V383l437.2.3-94.8 97.3 18.8 19 126.4-130.8zM126 260.9l19.6-18.6L50.8 145H488v-28.8L50.8 116l94.8-97.2L126.8-.4.4 130.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 285 B

View file

@ -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))

View file

@ -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))

View file

@ -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)))

View file

@ -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

View file

@ -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

View file

@ -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}])]))

View file

@ -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])}]

View file

@ -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"

View file

@ -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"