mirror of
https://github.com/penpot/penpot.git
synced 2025-02-27 01:06:30 -05:00
Merge pull request #186 from uxbox/286/stroke-alignment
🎉 Add stroke-alignment option
This commit is contained in:
commit
c8c4bec316
8 changed files with 170 additions and 27 deletions
|
@ -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]))
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"}]))
|
||||||
|
|
92
frontend/src/uxbox/main/ui/shapes/custom_stroke.cljs
Normal file
92
frontend/src/uxbox/main/ui/shapes/custom_stroke.cljs
Normal 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]]))))
|
||||||
|
|
|
@ -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"}])))
|
||||||
|
|
||||||
|
|
|
@ -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"}]))
|
||||||
|
|
||||||
|
|
|
@ -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 []}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue