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:
parent
d56b758490
commit
c38117d116
15 changed files with 287 additions and 33 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
3
frontend/resources/images/icons/radius-1.svg
Normal file
3
frontend/resources/images/icons/radius-1.svg
Normal 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 |
3
frontend/resources/images/icons/radius-4.svg
Normal file
3
frontend/resources/images/icons/radius-4.svg
Normal 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 |
|
@ -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" : {
|
||||
|
|
|
@ -385,6 +385,10 @@ ul.slider-dots {
|
|||
right: 6px;
|
||||
}
|
||||
|
||||
&.mini {
|
||||
width: 43px;
|
||||
}
|
||||
|
||||
// Input amounts
|
||||
|
||||
&.pixels {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -37,4 +37,7 @@
|
|||
|
||||
[:& shape-custom-stroke {:shape shape
|
||||
:base-props props
|
||||
:elem-name "rect"}]))
|
||||
:elem-name
|
||||
(if (.-d props)
|
||||
"path"
|
||||
"rect")}]))
|
||||
|
|
|
@ -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)}]]])
|
||||
]))]]))
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue