0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-26 16:56:11 -05:00

Merge pull request #186 from uxbox/286/stroke-alignment

🎉 Add stroke-alignment option
This commit is contained in:
Andrey Antukh 2020-04-27 14:53:05 +02:00 committed by GitHub
commit c8c4bec316
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 170 additions and 27 deletions

View file

@ -61,6 +61,7 @@
(s/def ::stroke-opacity number?) (s/def ::stroke-opacity number?)
(s/def ::stroke-style #{:solid :dotted :dashed :mixed :none}) (s/def ::stroke-style #{:solid :dotted :dashed :mixed :none})
(s/def ::stroke-width number?) (s/def ::stroke-width number?)
(s/def ::stroke-alignment #{:center :inner :outer})
(s/def ::text-align #{"left" "right" "center" "justify"}) (s/def ::text-align #{"left" "right" "center" "justify"})
(s/def ::type #{:rect :path :circle :image :text :canvas :curve :icon :frame :group}) (s/def ::type #{:rect :path :circle :image :text :canvas :curve :icon :frame :group})
(s/def ::x number?) (s/def ::x number?)
@ -94,6 +95,7 @@
::stroke-opacity ::stroke-opacity
::stroke-style ::stroke-style
::stroke-width ::stroke-width
::stroke-alignment
::text-align ::text-align
::width ::height])) ::width ::height]))

View file

@ -984,21 +984,21 @@
"unused" : true "unused" : true
}, },
"workspace.options.position" : { "workspace.options.position" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:130", "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:126" ], "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:144", "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:126" ],
"translations" : { "translations" : {
"en" : "Position", "en" : "Position",
"fr" : "Position" "fr" : "Position"
} }
}, },
"workspace.options.radius" : { "workspace.options.radius" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:174" ], "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:188" ],
"translations" : { "translations" : {
"en" : "Radius", "en" : "Radius",
"fr" : "TODO" "fr" : "TODO"
} }
}, },
"workspace.options.rotation" : { "workspace.options.rotation" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:150" ], "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:164" ],
"translations" : { "translations" : {
"en" : "Rotation", "en" : "Rotation",
"fr" : "TODO" "fr" : "TODO"
@ -1012,7 +1012,7 @@
"unused" : true "unused" : true
}, },
"workspace.options.size" : { "workspace.options.size" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:78", "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:106", "src/uxbox/main/ui/workspace/sidebar/options/page.cljs:78", "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:101" ], "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/page.cljs:78", "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:92", "src/uxbox/main/ui/workspace/sidebar/options/measures.cljs:120", "src/uxbox/main/ui/workspace/sidebar/options/frame.cljs:101" ],
"translations" : { "translations" : {
"en" : "Size", "en" : "Size",
"fr" : "Taille" "fr" : "Taille"
@ -1025,35 +1025,53 @@
} }
}, },
"workspace.options.stroke" : { "workspace.options.stroke" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:74", "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:129" ], "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:81", "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:142" ],
"translations" : { "translations" : {
"en" : "Stroke", "en" : "Stroke",
"fr" : null "fr" : null
} }
}, },
"workspace.options.stroke.center" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:128" ],
"translations" : {
"en" : "Center"
}
},
"workspace.options.stroke.dashed" : { "workspace.options.stroke.dashed" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:115" ], "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:136" ],
"translations" : { "translations" : {
"en" : "Dashed", "en" : "Dashed",
"fr" : "Tiré" "fr" : "Tiré"
} }
}, },
"workspace.options.stroke.dotted" : { "workspace.options.stroke.dotted" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:114" ], "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:135" ],
"translations" : { "translations" : {
"en" : "Dotted", "en" : "Dotted",
"fr" : "Pointillé" "fr" : "Pointillé"
} }
}, },
"workspace.options.stroke.inner" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:129" ],
"translations" : {
"en" : "Inside"
}
},
"workspace.options.stroke.mixed" : { "workspace.options.stroke.mixed" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:116" ], "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:137" ],
"translations" : { "translations" : {
"en" : "Mixed", "en" : "Mixed",
"fr" : "Mixte" "fr" : "Mixte"
} }
}, },
"workspace.options.stroke.outer" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:130" ],
"translations" : {
"en" : "Outside"
}
},
"workspace.options.stroke.solid" : { "workspace.options.stroke.solid" : {
"used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:113" ], "used-in" : [ "src/uxbox/main/ui/workspace/sidebar/options/stroke.cljs:134" ],
"translations" : { "translations" : {
"en" : "Solid", "en" : "Solid",
"fr" : "Solide" "fr" : "Solide"
@ -1192,7 +1210,7 @@
} }
}, },
"workspace.viewport.click-to-close-path" : { "workspace.viewport.click-to-close-path" : {
"used-in" : [ "src/uxbox/main/ui/workspace/drawarea.cljs:329" ], "used-in" : [ "src/uxbox/main/ui/workspace/drawarea.cljs:335" ],
"translations" : { "translations" : {
"en" : "Click to close the path" "en" : "Click to close the path"
} }

View file

@ -14,7 +14,8 @@
[uxbox.util.interop :as itr] [uxbox.util.interop :as itr]
[uxbox.util.geom.matrix :as gmt] [uxbox.util.geom.matrix :as gmt]
[uxbox.util.geom.point :as gpt] [uxbox.util.geom.point :as gpt]
[uxbox.main.ui.shapes.bounding-box :refer [bounding-box]])) [uxbox.main.ui.shapes.bounding-box :refer [bounding-box]]
[uxbox.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]]))
;; --- Circle Wrapper ;; --- Circle Wrapper
@ -54,4 +55,6 @@
:ry ry :ry ry
:transform transform :transform transform
:id (str "shape-" id)}))] :id (str "shape-" id)}))]
[:> "ellipse" props])) [:& shape-custom-stroke {:shape shape
:base-props props
:elem-name "ellipse"}]))

View file

@ -0,0 +1,92 @@
;; 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) 2016-2019 Andrey Antukh <niwi@niwi.nz>
(ns uxbox.main.ui.shapes.custom-stroke
(:require
[rumext.alpha :as mf]
[uxbox.util.geom.shapes :as geom]
[uxbox.util.interop :as itr]))
; The SVG standard does not implement yet the 'stroke-alignment' attribute, to define the position
; of the stroke relative to the stroke axis (inner, center, outer). Here we implement a patch
; to be able to draw the stroke in the three cases. See discussion at:
; https://stackoverflow.com/questions/7241393/can-you-control-how-an-svgs-stroke-width-is-drawn
(mf/defc shape-custom-stroke
{::mf/wrap-props false}
[props]
(let [shape (unchecked-get props "shape")
base-props (unchecked-get props "base-props")
elem-name (unchecked-get props "elem-name")
{:keys [id x y width height]} (geom/shape->rect-shape shape)
stroke-style (:stroke-style shape :none)
stroke-position (:stroke-alignment shape :center)]
(cond
; Center alignment (or no stroke): the default in SVG
(or (= stroke-style :none) (= stroke-position :center))
[:> elem-name base-props]
; Inner alignment: display the shape with double width stroke, and clip the result
; with the original shape without stroke.
(= stroke-position :inner)
(let [clip-id (str "clip-" id)
clip-props (-> (itr/obj-assign! #js {} base-props)
(itr/obj-assign! #js {:stroke nil
:strokeWidth nil
:strokeOpacity nil
:strokeDasharray nil
:fill "white"
:fillOpacity 1}))
stroke-width (.-strokeWidth base-props)
shape-props (-> (itr/obj-assign! #js {} base-props)
(itr/obj-assign! #js {:strokeWidth (* stroke-width 2)
:clipPath (str "url('#" clip-id "')")}))]
[:*
[:> "clipPath" #js {:id clip-id}
[:> elem-name clip-props]]
[:> elem-name shape-props]])
; Outer alingmnent: display the shape in two layers. One without stroke (only fill),
; and another one only with stroke at double width (transparent fill) and passed
; through a mask that shows the whole shape, but hides the original shape without stroke
(= stroke-position :outer)
(let [mask-id (str "mask-" id)
stroke-width (.-strokeWidth base-props)
mask-props1 (-> (itr/obj-assign! #js {} base-props)
(itr/obj-assign! #js {:stroke "white"
:strokeWidth (* stroke-width 2)
:strokeOpacity 1
:strokeDasharray nil
:fill "white"
:fillOpacity 1}))
mask-props2 (-> (itr/obj-assign! #js {} base-props)
(itr/obj-assign! #js {:stroke nil
:strokeWidth nil
:strokeOpacity nil
:strokeDasharray nil
:fill "black"
:fillOpacity 1}))
shape-props1 (-> (itr/obj-assign! #js {} base-props)
(itr/obj-assign! #js {:stroke nil
:strokeWidth nil
:strokeOpacity nil
:strokeDasharray nil}))
shape-props2 (-> (itr/obj-assign! #js {} base-props)
(itr/obj-assign! #js {:strokeWidth (* stroke-width 2)
:fill "none"
:fillOpacity 0
:mask (str "url('#" mask-id "')")}))]
[:*
[:> "mask" #js {:id mask-id}
[:> elem-name mask-props1]
[:> elem-name mask-props2]]
[:> elem-name shape-props1]
[:> elem-name shape-props2]]))))

View file

@ -16,7 +16,8 @@
[uxbox.main.ui.shapes.common :as common] [uxbox.main.ui.shapes.common :as common]
[uxbox.util.interop :as itr] [uxbox.util.interop :as itr]
[uxbox.util.geom.matrix :as gmt] [uxbox.util.geom.matrix :as gmt]
[uxbox.main.ui.shapes.bounding-box :refer [bounding-box]])) [uxbox.main.ui.shapes.bounding-box :refer [bounding-box]]
[uxbox.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]]))
;; --- Path Wrapper ;; --- Path Wrapper
@ -83,5 +84,10 @@
:fill "transparent" :fill "transparent"
:stroke-width "20px" :stroke-width "20px"
:d pdata}] :d pdata}]
[:> "path" props]] [:& shape-custom-stroke {:shape shape
[:> "path" props]))) :base-props props
:elem-name "path"}]]
[:& shape-custom-stroke {:shape shape
:base-props props
:elem-name "path"}])))

View file

@ -15,7 +15,8 @@
[uxbox.main.ui.shapes.attrs :as attrs] [uxbox.main.ui.shapes.attrs :as attrs]
[uxbox.main.ui.shapes.common :as common] [uxbox.main.ui.shapes.common :as common]
[uxbox.util.interop :as itr] [uxbox.util.interop :as itr]
[uxbox.main.ui.shapes.bounding-box :refer [bounding-box]])) [uxbox.main.ui.shapes.bounding-box :refer [bounding-box]]
[uxbox.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]]))
;; --- Rect Wrapper ;; --- Rect Wrapper
@ -53,4 +54,8 @@
:id (str "shape-" id) :id (str "shape-" id)
:width width :width width
:height height}))] :height height}))]
[:> "rect" props]))
[:& shape-custom-stroke {:shape shape
:base-props props
:elem-name "rect"}]))

View file

@ -38,7 +38,8 @@
(def ^:private minimal-shapes (def ^:private minimal-shapes
[{:type :rect [{:type :rect
:name "Rect" :name "Rect"
:stroke-color "#000000"} :stroke-color "#000000"
:stroke-alignment :center}
{:type :image} {:type :image}
{:type :icon} {:type :icon}
{:type :circle {:type :circle
@ -48,17 +49,20 @@
:stroke-style :solid :stroke-style :solid
:stroke-color "#000000" :stroke-color "#000000"
:stroke-width 2 :stroke-width 2
:stroke-alignment :center
:fill-color "#000000" :fill-color "#000000"
:fill-opacity 0 :fill-opacity 0
:segments []} :segments []}
{:type :frame {:type :frame
:stroke-style :none :stroke-style :none
:stroke-alignment :center
:name "Artboard"} :name "Artboard"}
{:type :curve {:type :curve
:name "Path" :name "Path"
:stroke-style :solid :stroke-style :solid
:stroke-color "#000000" :stroke-color "#000000"
:stroke-width 2 :stroke-width 2
:stroke-alignment :center
:fill-color "#000000" :fill-color "#000000"
:fill-opacity 0 :fill-opacity 0
:segments []} :segments []}

View file

@ -32,6 +32,13 @@
(d/read-string))] (d/read-string))]
(st/emit! (udw/update-shape (:id shape) {:stroke-style value})))) (st/emit! (udw/update-shape (:id shape) {:stroke-style value}))))
on-stroke-alignment-change
(fn [event]
(let [value (-> (dom/get-target event)
(dom/get-value)
(d/read-string))]
(st/emit! (udw/update-shape (:id shape) {:stroke-alignment value}))))
on-stroke-width-change on-stroke-width-change
(fn [event] (fn [event]
(let [value (-> (dom/get-target event) (let [value (-> (dom/get-target event)
@ -120,22 +127,28 @@
:step "1" :step "1"
:on-change on-stroke-opacity-change}]] :on-change on-stroke-opacity-change}]]
;; Stroke Style & Width ;; Stroke Width, Alignment & Style
[:div.row-flex [:div.row-flex
[:select#style.input-select {:value (pr-str (:stroke-style shape))
:on-change on-stroke-style-change}
[:option {:value ":solid"} (t locale "workspace.options.stroke.solid")]
[:option {:value ":dotted"} (t locale "workspace.options.stroke.dotted")]
[:option {:value ":dashed"} (t locale "workspace.options.stroke.dashed")]
[:option {:value ":mixed"} (t locale "workspace.options.stroke.mixed")]]
[:div.input-element.pixels [:div.input-element.pixels
[:input.input-text {:type "number" [:input.input-text {:type "number"
:min "0" :min "0"
:value (str (-> (:stroke-width shape) :value (str (-> (:stroke-width shape)
(d/coalesce 1) (d/coalesce 1)
(math/round))) (math/round)))
:on-change on-stroke-width-change}]]]]] :on-change on-stroke-width-change}]]
[:select#style.input-select {:value (pr-str (:stroke-alignment shape))
:on-change on-stroke-alignment-change}
[:option {:value ":center"} (t locale "workspace.options.stroke.center")]
[:option {:value ":inner"} (t locale "workspace.options.stroke.inner")]
[:option {:value ":outer"} (t locale "workspace.options.stroke.outer")]]
[:select#style.input-select {:value (pr-str (:stroke-style shape))
:on-change on-stroke-style-change}
[:option {:value ":solid"} (t locale "workspace.options.stroke.solid")]
[:option {:value ":dotted"} (t locale "workspace.options.stroke.dotted")]
[:option {:value ":dashed"} (t locale "workspace.options.stroke.dashed")]
[:option {:value ":mixed"} (t locale "workspace.options.stroke.mixed")]]]]]
;; NO STROKE ;; NO STROKE
[:div.element-set [:div.element-set