mirror of
https://github.com/penpot/penpot.git
synced 2025-03-11 23:31:21 -05:00
🎉 Add stroke caps to path ends
This commit is contained in:
parent
ac6c07b771
commit
be9073f0b7
11 changed files with 292 additions and 8 deletions
|
@ -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
|
||||
|
|
5
frontend/resources/images/icons/switch.svg
Normal file
5
frontend/resources/images/icons/switch.svg
Normal 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 |
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)))
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}])]))
|
||||
|
|
|
@ -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])}]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Reference in a new issue