0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-27 23:21:47 -05:00

🎉 Allow a different radius for each rect corner

This commit is contained in:
Andrés Moya 2021-02-18 16:54:45 +01:00 committed by Andrey Antukh
parent d56b758490
commit c38117d116
15 changed files with 287 additions and 33 deletions

View file

@ -11,6 +11,7 @@
- Bounce & Complaint handling [#635](https://github.com/penpot/penpot/pull/635)
- Disable groups interactions when holding "Ctrl" key (deep selection)
- New action in context menu to "edit" some shapes (binded to key "Enter")
- Allow to set border radius of each rect corner individually
### :bug: Bugs fixed

View file

@ -42,6 +42,10 @@
:stroke-alignment :stroke-group
:rx :radius-group
:ry :radius-group
:r1 :radius-group
:r2 :radius-group
:r3 :radius-group
:r4 :radius-group
:selrect :geometry-group
:points :geometry-group
:locked :geometry-group

View file

@ -220,6 +220,10 @@
(s/def :internal.shape/proportion-lock boolean?)
(s/def :internal.shape/rx ::safe-number)
(s/def :internal.shape/ry ::safe-number)
(s/def :internal.shape/r1 ::safe-number)
(s/def :internal.shape/r2 ::safe-number)
(s/def :internal.shape/r3 ::safe-number)
(s/def :internal.shape/r4 ::safe-number)
(s/def :internal.shape/stroke-color string?)
(s/def :internal.shape/stroke-color-gradient (s/nilable ::gradient))
(s/def :internal.shape/stroke-color-ref-file (s/nilable uuid?))
@ -296,6 +300,10 @@
:internal.shape/proportion-lock
:internal.shape/rx
:internal.shape/ry
:internal.shape/r1
:internal.shape/r2
:internal.shape/r3
:internal.shape/r4
:internal.shape/x
:internal.shape/y
:internal.shape/exports

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
<path fill-rule="evenodd" clip-rule="evenodd" d="M349.998 50H150C94.77 50 50 94.772 50 150v200c0 55.228 44.771 100 100 100h199.998C405.228 450 450 405.228 450 350V150c0-55.228-44.771-100-100.002-100zM150 0C67.157 0 0 67.157 0 150v200c0 82.844 67.157 150 150 150h199.998C432.84 500 500 432.844 500 350V150C500 67.157 432.841 0 349.998 0z"/>
</svg>

After

Width:  |  Height:  |  Size: 415 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="500" height="500">
<path d="M312.498 50h37.5C405.228 50 450 94.772 450 150v37.5h50V150C500 67.157 432.841 0 349.998 0h-37.5zM187.5 50V0H150C67.157 0 0 67.157 0 150v37.5h50V150C50 94.772 94.771 50 150 50zM50 312.5H0V350c0 82.844 67.157 150 150 150h37.5v-50H150C94.77 450 50 405.228 50 350zM312.498 450v50h37.5C432.84 500 500 432.844 500 350v-37.5h-50V350c0 55.228-44.771 100-100.002 100z"/>
</svg>

After

Width:  |  Height:  |  Size: 446 B

View file

@ -3640,6 +3640,20 @@
"es" : "Radio"
}
},
"workspace.options.radius.all-corners" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/measures.cljs" ],
"translations" : {
"en" : "All corners",
"es" : "Todas las esquinas"
}
},
"workspace.options.radius.single-corners" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/measures.cljs" ],
"translations" : {
"en" : "Single corners",
"es" : "Esquinas individuales"
}
},
"workspace.options.rotation" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/measures.cljs" ],
"translations" : {

View file

@ -385,6 +385,10 @@ ul.slider-dots {
right: 6px;
}
&.mini {
width: 43px;
}
// Input amounts
&.pixels {

View file

@ -90,7 +90,7 @@
position: relative;
display: flex;
flex-direction: row;
padding: 1rem 0.5rem;
padding: 1rem 1.6rem 1rem 0.5rem;
.attributes-label,
.attributes-value {

View file

@ -595,6 +595,35 @@
}
.radius-options {
align-items: center;
border: 1px solid $color-gray-60;
border-radius: 4px;
display: flex;
justify-content: space-between;
padding: 8px;
width: 64px;
.radius-icon {
display: flex;
align-items: center;
svg {
cursor: pointer;
height: 16px;
fill: $color-gray-30;
width: 16px;
}
&:hover,
&.selected {
svg {
fill: $color-primary;
}
}
}
}
.orientation-icon {
margin-left: $small;
display: flex;

View file

@ -17,13 +17,17 @@
[app.util.code-gen :as cg]
[app.main.ui.components.copy-button :refer [copy-button]]))
(def properties [:width :height :x :y :radius :rx])
(def properties [:width :height :x :y :radius :rx :r1])
(def params
{:to-prop {:x "left"
:y "top"
:rotation "transform"
:rx "border-radius"}
:format {:rotation #(str/fmt "rotate(%sdeg)" %)}})
:rx "border-radius"
:r1 "border-radius"}
:format {:rotation #(str/fmt "rotate(%sdeg)" %)
:r1 #(apply str/fmt "%spx, %spx, %spx, %spx" %)}
:multi {:r1 [:r1 :r2 :r3 :r4]}})
(defn copy-data
([shape]
@ -62,6 +66,19 @@
[:div.attributes-value (mth/precision (:rx shape) 2) "px"]
[:& copy-button {:data (copy-data shape :rx)}]])
(when (and (:r1 shape)
(or (not= (:r1 shape) 0)
(not= (:r2 shape) 0)
(not= (:r3 shape) 0)
(not= (:r4 shape) 0)))
[:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.radius")]
[:div.attributes-value (mth/precision (:r1 shape) 2) ", "
(mth/precision (:r2 shape) 2) ", "
(mth/precision (:r3 shape) 2) ", "
(mth/precision (:r4 shape) 2) "px"]
[:& copy-button {:data (copy-data shape :r1)}]])
(when (not= (:rotation shape 0) 0)
[:div.attributes-unit-row
[:div.attributes-label (t locale "handoff.attributes.layout.rotation")]

View file

@ -87,6 +87,8 @@
(def play (icon-xref :play))
(def plus (icon-xref :plus))
(def radius (icon-xref :radius))
(def radius-1 (icon-xref :radius-1))
(def radius-4 (icon-xref :radius-4))
(def recent (icon-xref :recent))
(def redo (icon-xref :redo))
(def rotate (icon-xref :rotate))

View file

@ -22,11 +22,56 @@
:dashed "10,10"
nil))
(defn- truncate-side
[shape ra-attr rb-attr dimension-attr]
(let [ra (ra-attr shape)
rb (rb-attr shape)
dimension (dimension-attr shape)]
(if (<= (+ ra rb) dimension)
[ra rb]
[(/ (* ra dimension) (+ ra rb))
(/ (* rb dimension) (+ ra rb))])))
(defn- truncate-radius
[shape]
(let [[r-top-left r-top-right]
(truncate-side shape :r1 :r2 :width)
[r-right-top r-right-bottom]
(truncate-side shape :r2 :r3 :height)
[r-bottom-right r-bottom-left]
(truncate-side shape :r3 :r4 :width)
[r-left-bottom r-left-top]
(truncate-side shape :r4 :r1 :height)]
[(min r-top-left r-left-top)
(min r-top-right r-right-top)
(min r-right-bottom r-bottom-right)
(min r-bottom-left r-left-bottom)]))
(defn add-border-radius [attrs shape]
(if (or (:rx shape) (:ry shape))
(obj/merge! attrs #js {:rx (:rx shape)
:ry (:ry shape)})
attrs))
(if (or (:r1 shape) (:r2 shape) (:r3 shape) (:r4 shape))
(let [[r1 r2 r3 r4] (truncate-radius shape)
top (- (:width shape) r1 r2)
right (- (:height shape) r2 r3)
bottom (- (:width shape) r3 r4)
left (- (:height shape) r4 r1)]
(obj/merge! attrs #js {:d (str "M" (+ (:x shape) r1) "," (:y shape) " "
"h" top " "
"a" r2 "," r2 " 0 0 1 " r2 "," r2 " "
"v" right " "
"a" r3 "," r3 " 0 0 1 " (- r3) "," r3 " "
"h" (- bottom) " "
"a" r4 "," r4 " 0 0 1 " (- r4) "," (- r4) " "
"v" (- left) " "
"a" r1 "," r1 " 0 0 1 " r1 "," (- r1) " "
"z")}))
(if (or (:rx shape) (:ry shape))
(obj/merge! attrs #js {:rx (:rx shape)
:ry (:ry shape)})
attrs)))
(defn add-fill [attrs shape render-id]
(let [fill-color-gradient-id (str "fill-color-gradient_" render-id)]

View file

@ -37,4 +37,7 @@
[:& shape-custom-stroke {:shape shape
:base-props props
:elem-name "rect"}]))
:elem-name
(if (.-d props)
"path"
"rect")}]))

View file

@ -24,7 +24,13 @@
[app.common.math :as math]
[app.util.i18n :refer [t] :as i18n]))
(def measure-attrs [:proportion-lock :width :height :x :y :rotation :rx :ry :selrect])
(def measure-attrs [:proportion-lock
:width :height
:x :y
:rotation
:rx :ry
:r1 :r2 :r3 :r4
:selrect])
(defn- attr->string [attr values]
(let [value (attr values)]
@ -93,20 +99,70 @@
(fn [value]
(st/emit! (udw/increase-rotation ids value))))
on-radius-change
on-switch-to-radius-1
(mf/use-callback
(mf/deps ids)
(fn [value]
(let [radius-update
(fn [shape]
(cond-> shape
(:rx shape) (assoc :rx value :ry value)))]
(:r1 shape)
(-> (assoc :rx 0 :ry 0)
(dissoc :r1 :r2 :r3 :r4))))]
(st/emit! (dwc/update-shapes ids-with-children radius-update)))))
on-switch-to-radius-4
(mf/use-callback
(mf/deps ids)
(fn [value]
(let [radius-update
(fn [shape]
(cond-> shape
(:rx shape)
(-> (assoc :r1 0 :r2 0 :r3 0 :r4 0)
(dissoc :rx :ry))))]
(st/emit! (dwc/update-shapes ids-with-children radius-update)))))
on-radius-1-change
(mf/use-callback
(mf/deps ids)
(fn [value]
(let [radius-update
(fn [shape]
(cond-> shape
(:r1 shape)
(-> (dissoc :r1 :r2 :r3 :r4)
(assoc :rx 0 :ry 0))
(or (:rx shape) (:r1 shape))
(assoc :rx value :ry value)))]
(st/emit! (dwc/update-shapes ids-with-children radius-update)))))
on-radius-4-change
(mf/use-callback
(mf/deps ids)
(fn [value attr]
(let [radius-update
(fn [shape]
(cond-> shape
(:rx shape)
(-> (dissoc :rx :rx)
(assoc :r1 0 :r2 0 :r3 0 :r4 0))
(attr shape)
(assoc attr value)))]
(st/emit! (dwc/update-shapes ids-with-children radius-update)))))
on-width-change #(on-size-change % :width)
on-height-change #(on-size-change % :height)
on-pos-x-change #(on-position-change % :x)
on-pos-y-change #(on-position-change % :y)
on-radius-r1-change #(on-radius-4-change % :r1)
on-radius-r2-change #(on-radius-4-change % :r2)
on-radius-r3-change #(on-radius-4-change % :r3)
on-radius-r4-change #(on-radius-4-change % :r4)
select-all #(-> % (dom/get-target) (.select))]
[:div.element-set
@ -181,14 +237,61 @@
:value (attr->string :rotation values)}]])
;; RADIUS
(when (and (options :radius) (not (nil? (:rx values))))
[:div.row-flex
[:span.element-set-subtitle (t locale "workspace.options.radius")]
[:div.input-element.pixels
[:> numeric-input
{:placeholder "--"
:min 0
:on-click select-all
:on-change on-radius-change
:value (attr->string :rx values)}]]
[:div.input-element]])]]))
(let [radius-1? (some? (:rx values))
radius-4? (some? (:r1 values))]
(when (and (options :radius) (or radius-1? radius-4?))
[:div.row-flex
[:div.radius-options
[:div.radius-icon.tooltip.tooltip-bottom
{:class (classnames
:selected
(and radius-1? (not radius-4?)))
:alt (t locale "workspace.options.radius.all-corners")
:on-click on-switch-to-radius-1}
i/radius-1]
[:div.radius-icon.tooltip.tooltip-bottom
{:class (classnames
:selected
(and radius-4? (not radius-1?)))
:alt (t locale "workspace.options.radius.single-corners")
:on-click on-switch-to-radius-4}
i/radius-4]]
(if radius-1?
[:div.input-element.mini
[:> numeric-input
{:placeholder "--"
:min 0
:on-click select-all
:on-change on-radius-1-change
:value (attr->string :rx values)}]]
[:*
[:div.input-element.mini
[:> numeric-input
{:placeholder "--"
:min 0
:on-click select-all
:on-change on-radius-r1-change
:value (attr->string :r1 values)}]]
[:div.input-element.mini
[:> numeric-input
{:placeholder "--"
:min 0
:on-click select-all
:on-change on-radius-r2-change
:value (attr->string :r2 values)}]]
[:div.input-element.mini
[:> numeric-input
{:placeholder "--"
:min 0
:on-click select-all
:on-change on-radius-r3-change
:value (attr->string :r3 values)}]]
[:div.input-element.mini
[:> numeric-input
{:placeholder "--"
:min 0
:on-click select-all
:on-change on-radius-r4-change
:value (attr->string :r4 values)}]]])
]))]]))

View file

@ -39,9 +39,15 @@
(str/format "%spx %s %s" width style (uc/color->background color)))))
(def styles-data
{:layout {:props [:width :height :x :y :radius :rx]
:to-prop {:x "left" :y "top" :rotation "transform" :rx "border-radius"}
:format {:rotation #(str/fmt "rotate(%sdeg)" %)}}
{:layout {:props [:width :height :x :y :radius :rx :r1]
:to-prop {:x "left"
:y "top"
:rotation "transform"
:rx "border-radius"
:r1 "border-radius"}
:format {:rotation #(str/fmt "rotate(%sdeg)" %)
:r1 #(apply str/fmt "%spx, %spx, %spx, %spx" %)}
:multi {:r1 [:r1 :r2 :r3 :r4]}}
:fill {:props [:fill-color :fill-color-gradient]
:to-prop {:fill-color "background" :fill-color-gradient "background"}
:format {:fill-color format-fill-color :fill-color-gradient format-fill-color}}
@ -74,13 +80,14 @@
:text-transform name
:fill-color format-fill-color}})
(defn generate-css-props
([values properties]
(generate-css-props values properties nil))
([values properties params]
(let [{:keys [to-prop format tab-size] :or {to-prop {} tab-size 0}} params
(let [{:keys [to-prop format tab-size multi]
:or {to-prop {} tab-size 0 multi {}}} params
;; We allow the :format and :to-prop to be a map for different properties
;; or just a value for a single property. This code transform a single
;; property to a uniform one
@ -94,19 +101,28 @@
(into {} (map #(vector % to-prop) properties))
to-prop)
get-value (fn [prop]
(if-let [props (prop multi)]
(map #(get values %) props)
(get values prop)))
null? (fn [value]
(if (coll? value)
(every? #(or (nil? %) (= % 0)) value)
(or (nil? value) (= value 0))))
default-format (fn [value] (str (mth/precision value 2) "px"))
format-property (fn [prop]
(let [css-prop (or (prop to-prop) (name prop))
format-fn (or (prop format) default-format)
css-val (format-fn (prop values) values)]
css-val (format-fn (get-value prop) values)]
(when css-val
(str
(str/repeat " " tab-size)
(str/fmt "%s: %s;" css-prop css-val)))))]
(->> properties
(remove #(let [value (get values %)]
(or (nil? value) (= value 0))))
(remove #(null? (get-value %)))
(map format-property)
(filter (comp not nil?))
(str/join "\n")))))
@ -114,9 +130,11 @@
(defn shape->properties [shape]
(let [props (->> styles-data vals (mapcat :props))
to-prop (->> styles-data vals (map :to-prop) (reduce merge))
format (->> styles-data vals (map :format) (reduce merge))]
format (->> styles-data vals (map :format) (reduce merge))
multi (->> styles-data vals (map :multi) (reduce merge))]
(generate-css-props shape props {:to-prop to-prop
:format format
:multi multi
:tab-size 2})))
(defn text->properties [shape]
(let [text-shape-style (select-keys styles-data [:layout :shadow :blur])
@ -149,7 +167,7 @@
properties (if (= :text (:type shape))
(text->properties shape)
(shape->properties shape))
selector (str/css-selector name)
selector (if (str/starts-with? selector "-") (subs selector 1) selector)]
(str/join "\n" [(str/fmt "/* %s */" name)