Ability to add multiple fills to a shape

This commit is contained in:
Alejandro Alonso 2022-02-04 09:53:42 +01:00 committed by Alonso Torres
parent aecb8a1464
commit 23a9c74297
25 changed files with 475 additions and 243 deletions

View file

@ -15,6 +15,7 @@
### :sparkles: New features ### :sparkles: New features
- Ability to add multiple fills to a shape [Taiga #1394](https://tree.taiga.io/project/penpot/us/1394)
- Team members redesign [Taiga #2283](https://tree.taiga.io/project/penpot/us/2283) - Team members redesign [Taiga #2283](https://tree.taiga.io/project/penpot/us/2283)
- Rotation to snap to 15º intervals with shift [Taiga #2437](https://tree.taiga.io/project/penpot/issue/2437) - Rotation to snap to 15º intervals with shift [Taiga #2437](https://tree.taiga.io/project/penpot/issue/2437)
- Support border radius and stroke properties for images [Taiga #497](https://tree.taiga.io/project/penpot/us/497) - Support border radius and stroke properties for images [Taiga #497](https://tree.taiga.io/project/penpot/us/497)

View file

@ -36,6 +36,7 @@
;; :rx nil ;; :rx nil
;; :ry nil} ;; :ry nil}
;; ;;
(defn get-attrs-multi (defn get-attrs-multi
([objs attrs] ([objs attrs]
(get-attrs-multi objs attrs = identity)) (get-attrs-multi objs attrs = identity))

View file

@ -9,12 +9,13 @@
[app.common.colors :as clr] [app.common.colors :as clr]
[app.common.uuid :as uuid])) [app.common.uuid :as uuid]))
(def file-version 13) (def file-version 14)
(def default-color clr/gray-20) (def default-color clr/gray-20)
(def root uuid/zero) (def root uuid/zero)
(def component-sync-attrs (def component-sync-attrs
{:name :name-group {:name :name-group
:fills :fill-group
:fill-color :fill-group :fill-color :fill-group
:fill-opacity :fill-group :fill-opacity :fill-group
:fill-color-gradient :fill-group :fill-color-gradient :fill-group

View file

@ -33,16 +33,16 @@
(def default-frame-attrs (def default-frame-attrs
{:frame-id uuid/zero {:frame-id uuid/zero
:fill-color clr/white :fills [{:fill-color clr/white
:fill-opacity 1 :fill-opacity 1}]
:shapes [] :shapes []
:hide-fill-on-export false}) :hide-fill-on-export false})
(def ^:private minimal-shapes (def ^:private minimal-shapes
[{:type :rect [{:type :rect
:name "Rect-1" :name "Rect-1"
:fill-color default-color :fills [{:fill-color default-color
:fill-opacity 1 :fill-opacity 1}]
:stroke-style :none :stroke-style :none
:stroke-alignment :center :stroke-alignment :center
:stroke-width 0 :stroke-width 0
@ -53,12 +53,13 @@
{:type :image {:type :image
:rx 0 :rx 0
:ry 0} :ry 0
:fills []}
{:type :circle {:type :circle
:name "Circle-1" :name "Circle-1"
:fill-color default-color :fills [{:fill-color default-color
:fill-opacity 1 :fill-opacity 1}]
:stroke-style :none :stroke-style :none
:stroke-alignment :center :stroke-alignment :center
:stroke-width 0 :stroke-width 0
@ -67,6 +68,7 @@
{:type :path {:type :path
:name "Path-1" :name "Path-1"
:fills []
:stroke-style :solid :stroke-style :solid
:stroke-alignment :center :stroke-alignment :center
:stroke-width 2 :stroke-width 2
@ -75,8 +77,8 @@
{:type :frame {:type :frame
:name "Artboard-1" :name "Artboard-1"
:fill-color clr/white :fills [{:fill-color clr/white
:fill-opacity 1 :fill-opacity 1}]
:stroke-style :none :stroke-style :none
:stroke-alignment :center :stroke-alignment :center
:stroke-width 0 :stroke-width 0

View file

@ -300,3 +300,33 @@
(update page :objects #(d/mapm update-object %)))] (update page :objects #(d/mapm update-object %)))]
(update data :pages-index #(d/mapm update-page %)))) (update data :pages-index #(d/mapm update-page %))))
(defn set-fills
[shape]
(let [attrs {:fill-color (:fill-color shape)
:fill-color-gradient (:fill-color-gradient shape)
:fill-color-ref-file (:fill-color-ref-file shape)
:fill-color-ref-id (:fill-color-ref-id shape)
:fill-opacity (:fill-opacity shape)}
clean-attrs (d/without-nils attrs)]
(-> shape
(assoc :fills [clean-attrs])
(dissoc :fill-color)
(dissoc :fill-color-gradient)
(dissoc :fill-color-ref-file)
(dissoc :fill-color-ref-id)
(dissoc :fill-opacity))))
;; Add fills to shapes
(defmethod migrate 14
[data]
(letfn [(update-object [_ object]
(cond-> object
(and (not (= :text (:type object))) (nil? (:fills object)))
(set-fills)))
(update-page [_ page]
(update page :objects #(d/mapm update-object %)))]
(update data :pages-index #(d/mapm update-page %))))

View file

@ -499,6 +499,10 @@
margin-bottom: $size-2; margin-bottom: $size-2;
position: relative; position: relative;
&[draggable="true"] {
cursor: pointer;
}
.color-name { .color-name {
font-size: $fs12; font-size: $fs12;
margin: 5px 6px 0px 6px; margin: 5px 6px 0px 6px;

View file

@ -954,6 +954,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;
svg { svg {
width: 12px; width: 12px;
height: 12px; height: 12px;
@ -961,6 +962,11 @@
stroke: $color-gray-20; stroke: $color-gray-20;
} }
&.remove {
min-width: 20px;
min-height: 20px;
}
&:hover svg, &:hover svg,
&.active svg { &.active svg {
fill: $color-primary; fill: $color-primary;

View file

@ -111,42 +111,105 @@
(assoc-in [:workspace-local :picked-color-select] value) (assoc-in [:workspace-local :picked-color-select] value)
(assoc-in [:workspace-local :picked-shift?] shift?))))) (assoc-in [:workspace-local :picked-shift?] shift?)))))
(defn transform-fill
[state ids color transform]
(let [page-id (:current-page-id state)
objects (wsh/lookup-page-objects state page-id)
is-text? #(= :text (:type (get objects %)))
text-ids (filter is-text? ids)
shape-ids (filter (comp not is-text?) ids)
attrs (cond-> {:fill-color nil
:fill-color-gradient nil
:fill-color-ref-file nil
:fill-color-ref-id nil
:fill-opacity nil}
(contains? color :color)
(assoc :fill-color (:color color))
(contains? color :id)
(assoc :fill-color-ref-id (:id color))
(contains? color :file-id)
(assoc :fill-color-ref-file (:file-id color))
(contains? color :gradient)
(assoc :fill-color-gradient (:gradient color))
(contains? color :opacity)
(assoc :fill-opacity (:opacity color)))
;; Not nil attrs
clean-attrs (d/without-nils attrs)]
(rx/concat
(rx/from (map #(dwt/update-text-attrs {:id % :attrs attrs}) text-ids))
(rx/of (dch/update-shapes
shape-ids
#(transform % clean-attrs))))))
(defn swap-fills [shape index new-index]
(let [first (get-in shape [:fills index])
second (get-in shape [:fills new-index])]
(-> shape
(assoc-in [:fills index] second)
(assoc-in [:fills new-index] first))
))
(defn reorder-fills
[ids index new-index]
(ptk/reify ::reorder-fills
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dch/update-shapes
ids
#(swap-fills % index new-index))))))
(defn change-fill (defn change-fill
[ids color] [ids color position]
(ptk/reify ::change-fill (ptk/reify ::change-fill
ptk/WatchEvent ptk/WatchEvent
(watch [_ state _] (watch [_ state _]
(let [page-id (:current-page-id state) (let [change (fn [shape attrs] (assoc-in shape [:fills position] (into {} attrs)))]
objects (wsh/lookup-page-objects state page-id) (transform-fill state ids color change)))))
is-text? #(= :text (:type (get objects %))) (defn change-fill-and-clear
text-ids (filter is-text? ids) [ids color]
shape-ids (filter (comp not is-text?) ids) (ptk/reify ::change-fill-and-clear
ptk/WatchEvent
(watch [_ state _]
(let [set (fn [shape attrs] (assoc shape :fills [attrs]))]
(transform-fill state ids color set)))))
attrs (cond-> {:fill-color nil (defn add-fill
:fill-color-gradient nil [ids color]
::fill-color-ref-file nil (ptk/reify ::add-fill
:fill-color-ref-id nil ptk/WatchEvent
:fill-opacity nil} (watch [_ state _]
(let [add (fn [shape attrs] (assoc shape :fills (into [attrs] (:fills shape))))]
(transform-fill state ids color add)))))
(contains? color :color) (defn remove-fill
(assoc :fill-color (:color color)) [ids color position]
(ptk/reify ::remove-fill
ptk/WatchEvent
(watch [_ state _]
(let [remove-fill-by-index (fn [values index] (->> (d/enumerate values)
(filterv (fn [[idx _]] (not= idx index)))
(mapv second)))
(contains? color :id) remove (fn [shape _] (update shape :fills remove-fill-by-index position))]
(assoc :fill-color-ref-id (:id color)) (transform-fill state ids color remove)))))
(contains? color :file-id) (defn remove-all-fills
(assoc :fill-color-ref-file (:file-id color)) [ids color]
(ptk/reify ::remove-all-fills
ptk/WatchEvent
(watch [_ state _]
(let [remove-all (fn [shape _] (assoc shape :fills []))]
(transform-fill state ids color remove-all)))))
(contains? color :gradient)
(assoc :fill-color-gradient (:gradient color))
(contains? color :opacity)
(assoc :fill-opacity (:opacity color)))]
(rx/concat
(rx/from (map #(dwt/update-text-attrs {:id % :attrs attrs}) text-ids))
(rx/of (dch/update-shapes shape-ids (fn [shape] (d/merge shape attrs)))))))))
(defn change-hide-fill-on-export (defn change-hide-fill-on-export
[ids hide-fill-on-export] [ids hide-fill-on-export]
@ -207,7 +270,7 @@
update-events update-events
(fn [color] (fn [color]
(rx/of (change-fill ids color)))] (rx/of (change-fill ids color 0)))]
(rx/merge (rx/merge
;; Stream that updates the stroke/width and stops if `esc` pressed ;; Stream that updates the stroke/width and stops if `esc` pressed

View file

@ -73,26 +73,28 @@
(defn setup-fill [shape] (defn setup-fill [shape]
(cond-> shape (cond-> shape
;; Color present as attribute ;; Color present as attribute
(uc/color? (get-in shape [:svg-attrs :fill])) (uc/color? (str/trim (get-in shape [:svg-attrs :fill])))
(-> (update :svg-attrs dissoc :fill) (-> (update :svg-attrs dissoc :fill)
(assoc :fill-color (-> (get-in shape [:svg-attrs :fill]) (assoc-in [:fills 0 :fill-color] (-> (get-in shape [:svg-attrs :fill])
(uc/parse-color)))) (str/trim)
(uc/parse-color))))
;; Color present as style ;; Color present as style
(uc/color? (get-in shape [:svg-attrs :style :fill])) (uc/color? (str/trim (get-in shape [:svg-attrs :style :fill])))
(-> (update-in [:svg-attrs :style] dissoc :fill) (-> (update-in [:svg-attrs :style] dissoc :fill)
(assoc :fill-color (-> (get-in shape [:svg-attrs :style :fill]) (assoc-in [:fills 0 :fill-color] (-> (get-in shape [:svg-attrs :style :fill])
(uc/parse-color)))) (str/trim)
(uc/parse-color))))
(get-in shape [:svg-attrs :fill-opacity]) (get-in shape [:svg-attrs :fill-opacity])
(-> (update :svg-attrs dissoc :fill-opacity) (-> (update :svg-attrs dissoc :fill-opacity)
(assoc :fill-opacity (-> (get-in shape [:svg-attrs :fill-opacity]) (assoc-in [:fills 0 :fill-opacity] (-> (get-in shape [:svg-attrs :fill-opacity])
(d/parse-double)))) (d/parse-double))))
(get-in shape [:svg-attrs :style :fill-opacity]) (get-in shape [:svg-attrs :style :fill-opacity])
(-> (update-in [:svg-attrs :style] dissoc :fill-opacity) (-> (update-in [:svg-attrs :style] dissoc :fill-opacity)
(assoc :fill-opacity (-> (get-in shape [:svg-attrs :style :fill-opacity]) (assoc-in [:fills 0 :fill-opacity] (-> (get-in shape [:svg-attrs :style :fill-opacity])
(d/parse-double)))))) (d/parse-double))))))
(defn setup-stroke [shape] (defn setup-stroke [shape]
(let [stroke-linecap (-> (or (get-in shape [:svg-attrs :stroke-linecap]) (let [stroke-linecap (-> (or (get-in shape [:svg-attrs :stroke-linecap])
@ -378,9 +380,10 @@
:polygon (create-path-shape name frame-id svg-data (-> element-data usvg/polygon->path)) :polygon (create-path-shape name frame-id svg-data (-> element-data usvg/polygon->path))
:line (create-path-shape name frame-id svg-data (-> element-data usvg/line->path)) :line (create-path-shape name frame-id svg-data (-> element-data usvg/line->path))
:image (create-image-shape name frame-id svg-data element-data) :image (create-image-shape name frame-id svg-data element-data)
#_other (create-raw-svg name frame-id svg-data element-data)) #_other (create-raw-svg name frame-id svg-data element-data)))
shape (assoc shape :fills [])
)
shape (when (some? shape) shape (when (some? shape)
(-> shape (-> shape
(assoc :svg-defs (select-keys (:defs svg-data) references)) (assoc :svg-defs (select-keys (:defs svg-data) references))

View file

@ -29,6 +29,7 @@
[props external-ref] [props external-ref]
(let [value (obj/get props "value") (let [value (obj/get props "value")
on-change (obj/get props "onChange") on-change (obj/get props "onChange")
on-blur (obj/get props "onBlur")
;; We need a ref pointing to the input dom element, but the user ;; We need a ref pointing to the input dom element, but the user
;; of this component may provide one (that is forwarded here). ;; of this component may provide one (that is forwarded here).
@ -92,6 +93,8 @@
(mf/deps parse-value apply-value update-input) (mf/deps parse-value apply-value update-input)
(fn [_] (fn [_]
(let [new-value (parse-value)] (let [new-value (parse-value)]
(when on-blur
(on-blur))
(if new-value (if new-value
(apply-value new-value) (apply-value new-value)
(update-input value))))) (update-input value)))))

View file

@ -70,7 +70,7 @@
object (cond-> object object (cond-> object
(:hide-fill-on-export object) (:hide-fill-on-export object)
(assoc :fill-color nil :fill-opacity 0)) (assoc :fills []))
{:keys [x y width height] :as bs} (calc-bounds object objects) {:keys [x y width height] :as bs} (calc-bounds object objects)
[_ _ width height :as coords] (->> [x y width height] (map #(* % zoom))) [_ _ width height :as coords] (->> [x y width height] (map #(* % zoom)))

View file

@ -79,14 +79,15 @@
"z")})) "z")}))
attrs)) attrs))
(defn add-fill [attrs shape render-id] (defn add-fill [attrs shape render-id index]
(let [fill-attrs (cond (let [
fill-attrs (cond
(contains? shape :fill-image) (contains? shape :fill-image)
(let [fill-image-id (str "fill-image-" render-id)] (let [fill-image-id (str "fill-image-" render-id)]
{:fill (str/format "url(#%s)" fill-image-id)}) {:fill (str/format "url(#%s)" fill-image-id)})
(contains? shape :fill-color-gradient) (contains? shape :fill-color-gradient)
(let [fill-color-gradient-id (str "fill-color-gradient_" render-id)] (let [fill-color-gradient-id (str "fill-color-gradient_" render-id "_" index)]
{:fill (str/format "url(#%s)" fill-color-gradient-id)}) {:fill (str/format "url(#%s)" fill-color-gradient-id)})
(contains? shape :fill-color) (contains? shape :fill-color)
@ -193,9 +194,23 @@
styles (-> (obj/get props "style" (obj/new)) styles (-> (obj/get props "style" (obj/new))
(obj/merge! svg-styles) (obj/merge! svg-styles)
(add-fill shape render-id)
(add-stroke shape render-id) (add-stroke shape render-id)
(add-layer-props shape))] (add-layer-props shape))
styles (cond (or (some? (:fill-image shape))
(= :image (:type shape))
(> (count (:fills shape)) 1)
(some #(some? (:fill-color-gradient %)) (:fills shape)))
(obj/set! styles "fill" (str "url(#fill-" render-id ")"))
;; imported svgs can have fill and fill-opacity attributes
(obj/contains? svg-styles "fill")
(-> styles
(obj/set! "fill" (obj/get svg-styles "fill"))
(obj/set! "fillOpacity" (obj/get svg-styles "fillOpacity")))
:else
(add-fill styles (get-in shape [:fills 0]) render-id 0))]
(-> props (-> props
(obj/merge! svg-attrs) (obj/merge! svg-attrs)
@ -208,10 +223,10 @@
(add-style-attrs shape))) (add-style-attrs shape)))
(defn extract-fill-attrs (defn extract-fill-attrs
[shape] [shape index]
(let [render-id (mf/use-ctx muc/render-ctx) (let [render-id (mf/use-ctx muc/render-ctx)
fill-styles (-> (obj/get shape "style" (obj/new)) fill-styles (-> (obj/get shape "style" (obj/new))
(add-fill shape render-id))] (add-fill shape render-id index))]
(-> (obj/new) (-> (obj/new)
(obj/set! "style" fill-styles)))) (obj/set! "style" fill-styles))))

View file

@ -103,11 +103,6 @@
(-> (add! :rx) (-> (add! :rx)
(add! :ry))) (add! :ry)))
(cond-> image?
(-> (add! :fill-color)
(add! :fill-opacity)
(add! :fill-color-gradient)))
(cond-> path? (cond-> path?
(-> (add! :stroke-cap-start) (-> (add! :stroke-cap-start)
(add! :stroke-cap-end))) (add! :stroke-cap-end)))
@ -293,6 +288,7 @@
:penpot:background-overlay ((d/nilf str) (:background-overlay interaction)) :penpot:background-overlay ((d/nilf str) (:background-overlay interaction))
:penpot:preserve-scroll ((d/nilf str) (:preserve-scroll interaction))}])]))) :penpot:preserve-scroll ((d/nilf str) (:preserve-scroll interaction))}])])))
(mf/defc export-data (mf/defc export-data
[{:keys [shape]}] [{:keys [shape]}]
(let [props (-> (obj/new) (add-data shape) (add-library-refs shape))] (let [props (-> (obj/new) (add-data shape) (add-library-refs shape))]

View file

@ -1,46 +0,0 @@
;; 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) UXBOX Labs SL
(ns app.main.ui.shapes.fill-image
(:require
[app.common.geom.shapes :as gsh]
[app.config :as cfg]
[app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.embed :as embed]
[app.util.object :as obj]
[rumext.alpha :as mf]))
(mf/defc fill-image-pattern
{::mf/wrap-props false}
[props]
(let [shape (obj/get props "shape")
render-id (obj/get props "render-id")]
(when (contains? shape :fill-image)
(let [{:keys [x y width height]} (:selrect shape)
fill-image-id (str "fill-image-" render-id)
uri (cfg/resolve-file-media (:fill-image shape))
embed (embed/use-data-uris [uri])
transform (gsh/transform-matrix shape)
shape-without-image (dissoc shape :fill-image)
fill-attrs (-> (attrs/extract-fill-attrs shape-without-image)
(obj/set! "width" width)
(obj/set! "height" height))]
[:pattern {:id fill-image-id
:patternUnits "userSpaceOnUse"
:x x
:y y
:height height
:width width
:patternTransform transform
:data-loading (str (not (contains? embed uri)))}
[:g
[:> :rect fill-attrs]
[:image {:xlinkHref (get embed uri uri)
:width width
:height height}]
]]))))

View file

@ -0,0 +1,64 @@
;; 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) UXBOX Labs SL
(ns app.main.ui.shapes.fills
(:require
[app.common.data :as d]
[app.common.geom.shapes :as gsh]
[app.config :as cfg]
[app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.embed :as embed]
[app.main.ui.shapes.gradients :as grad]
[app.util.object :as obj]
[rumext.alpha :as mf]))
(mf/defc fills
{::mf/wrap-props false}
[props]
(let [shape (obj/get props "shape")
render-id (obj/get props "render-id")
{:keys [x y width height]} (:selrect shape)
{:keys [metadata]} shape
fill-id (str "fill-" render-id)
has-image (or metadata (:fill-image shape))
uri (if metadata
(cfg/resolve-file-media metadata)
(cfg/resolve-file-media (:fill-image shape)))
embed (embed/use-data-uris [uri])
transform (gsh/transform-matrix shape)
pattern-attrs (cond-> #js {:id fill-id
:patternUnits "userSpaceOnUse"
:x x
:y y
:height height
:width width
:data-loading (str (not (contains? embed uri)))}
(= :path (:type shape))
(obj/set! "patternTransform" transform))]
[:*
(for [[index value] (-> (d/enumerate (:fills shape [])) reverse)]
(cond (some? (:fill-color-gradient value))
(case (:type (:fill-color-gradient value))
:linear [:> grad/linear-gradient #js {:id (str (name :fill-color-gradient) "_" render-id "_" index)
:gradient (:fill-color-gradient value)
:shape shape}]
:radial [:> grad/radial-gradient #js {:id (str (name :fill-color-gradient) "_" render-id "_" index)
:gradient (:fill-color-gradient value)
:shape shape}])))
[:> :pattern pattern-attrs
[:g
(for [[index value] (-> (d/enumerate (:fills shape [])) reverse)]
[:> :rect (-> (attrs/extract-fill-attrs value index)
(obj/set! "width" width)
(obj/set! "height" height))])
(when has-image
[:image {:xlinkHref (get embed uri uri)
:width width
:height height}])]]]))

View file

@ -51,10 +51,6 @@
shape (unchecked-get props "shape") shape (unchecked-get props "shape")
{:keys [x y width height]} shape {:keys [x y width height]} shape
has-background? (or (some? (:fill-color shape))
(some? (:fill-color-gradient shape)))
has-stroke? (not= :none (:stroke-style shape))
props (-> (attrs/extract-style-attrs shape) props (-> (attrs/extract-style-attrs shape)
(obj/merge! (obj/merge!
#js {:x x #js {:x x
@ -63,8 +59,8 @@
:height height :height height
:className "frame-background"}))] :className "frame-background"}))]
[:* [:*
(when (or has-background? has-stroke?) [:> :rect props]
[:> :rect props])
(for [item childs] (for [item childs]
[:& shape-wrapper {:shape item [:& shape-wrapper {:shape item
:key (:id item)}])]))) :key (:id item)}])])))

View file

@ -7,11 +7,8 @@
(ns app.main.ui.shapes.image (ns app.main.ui.shapes.image
(:require (:require
[app.common.geom.shapes :as gsh] [app.common.geom.shapes :as gsh]
[app.config :as cfg]
[app.main.ui.context :as muc]
[app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]] [app.main.ui.shapes.custom-stroke :refer [shape-custom-stroke]]
[app.main.ui.shapes.embed :as embed]
[app.util.object :as obj] [app.util.object :as obj]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
@ -20,19 +17,8 @@
[props] [props]
(let [shape (unchecked-get props "shape") (let [shape (unchecked-get props "shape")
{:keys [x y width height metadata]} shape {:keys [x y width height]} shape
uri (cfg/resolve-file-media metadata)
embed (embed/use-data-uris [uri])
transform (gsh/transform-matrix shape) transform (gsh/transform-matrix shape)
fill-attrs (-> (attrs/extract-fill-attrs shape)
(obj/set! "width" width)
(obj/set! "height" height))
render-id (mf/use-ctx muc/render-ctx)
fill-image-id (str "fill-image-" render-id)
shape (assoc shape :fill-image fill-image-id)
props (-> (attrs/extract-style-attrs shape) props (-> (attrs/extract-style-attrs shape)
(obj/merge! (attrs/extract-border-radius-attrs shape)) (obj/merge! (attrs/extract-border-radius-attrs shape))
(obj/merge! (obj/merge!
@ -44,19 +30,6 @@
path? (some? (.-d props))] path? (some? (.-d props))]
[:g [:g
[:defs
[:pattern {:id fill-image-id
:patternUnits "userSpaceOnUse"
:x x
:y y
:height height
:width width
:data-loading (str (not (contains? embed uri)))}
[:g
[:> :rect fill-attrs]
[:image {:xlinkHref (get embed uri uri)
:width width
:height height}]]]]
[:& shape-custom-stroke {:shape shape} [:& shape-custom-stroke {:shape shape}
(if path? (if path?
[:> :path props] [:> :path props]

View file

@ -13,8 +13,6 @@
[app.util.path.format :as upf] [app.util.path.format :as upf]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
;; --- Path Shape
(mf/defc path-shape (mf/defc path-shape
{::mf/wrap-props false} {::mf/wrap-props false}
[props] [props]

View file

@ -12,7 +12,7 @@
[app.main.ui.shapes.attrs :as attrs] [app.main.ui.shapes.attrs :as attrs]
[app.main.ui.shapes.custom-stroke :as cs] [app.main.ui.shapes.custom-stroke :as cs]
[app.main.ui.shapes.export :as ed] [app.main.ui.shapes.export :as ed]
[app.main.ui.shapes.fill-image :as fim] [app.main.ui.shapes.fills :as fills]
[app.main.ui.shapes.filters :as filters] [app.main.ui.shapes.filters :as filters]
[app.main.ui.shapes.frame :as frame] [app.main.ui.shapes.frame :as frame]
[app.main.ui.shapes.gradients :as grad] [app.main.ui.shapes.gradients :as grad]
@ -63,9 +63,12 @@
[:defs [:defs
[:& defs/svg-defs {:shape shape :render-id render-id}] [:& defs/svg-defs {:shape shape :render-id render-id}]
[:& filters/filters {:shape shape :filter-id filter-id}] [:& filters/filters {:shape shape :filter-id filter-id}]
[:& grad/gradient {:shape shape :attr :fill-color-gradient}]
[:& grad/gradient {:shape shape :attr :stroke-color-gradient}] [:& grad/gradient {:shape shape :attr :stroke-color-gradient}]
[:& fim/fill-image-pattern {:shape shape :render-id render-id}] (when (or (some? (:fill-image shape))
(= :image (:type shape))
(> (count (:fills shape)) 1)
(some :fill-color-gradient (:fills shape)))
[:& fills/fills {:shape shape :render-id render-id}])
[:& cs/stroke-defs {:shape shape :render-id render-id}] [:& cs/stroke-defs {:shape shape :render-id render-id}]
[:& frame/frame-clip-def {:shape shape :render-id render-id}]] [:& frame/frame-clip-def {:shape shape :render-id render-id}]]
children]])) children]]))

View file

@ -26,7 +26,8 @@
(and (and
(not (contains? #{:text :group} (:type shape))) (not (contains? #{:text :group} (:type shape)))
(or (:fill-color shape) (or (:fill-color shape)
(:fill-color-gradient shape)))) (:fill-color-gradient shape)
(seq (:fills shape)))))
(defn copy-data [shape] (defn copy-data [shape]
(cg/generate-css-props (cg/generate-css-props
@ -55,5 +56,9 @@
[:& copy-button {:data (copy-data (first shapes))}])] [:& copy-button {:data (copy-data (first shapes))}])]
(for [shape shapes] (for [shape shapes]
[:& fill-block {:key (str "fill-block-" (:id shape)) (if (seq (:fills shape))
:shape shape}])]))) (for [value (:fills shape [])]
[:& fill-block {:key (str "fill-block-" (:id shape))
:shape value}])
[:& fill-block {:key (str "fill-block-" (:id shape))
:shape shape}]))])))

View file

@ -46,7 +46,7 @@
(fn [event] (fn [event]
(if (kbd/alt? event) (if (kbd/alt? event)
(st/emit! (mdc/change-stroke ids-with-children (merge uc/empty-color color))) (st/emit! (mdc/change-stroke ids-with-children (merge uc/empty-color color)))
(st/emit! (mdc/change-fill ids-with-children (merge uc/empty-color color)))))] (st/emit! (mdc/change-fill ids-with-children (merge uc/empty-color color) 0))))]
[:div.color-cell {:on-click select-color} [:div.color-cell {:on-click select-color}
[:& cb/color-bullet {:color color}] [:& cb/color-bullet {:color color}]

View file

@ -799,7 +799,7 @@
(let [ids (wsh/lookup-selected @st/state)] (let [ids (wsh/lookup-selected @st/state)]
(if (kbd/alt? event) (if (kbd/alt? event)
(st/emit! (dc/change-stroke ids color)) (st/emit! (dc/change-stroke ids color))
(st/emit! (dc/change-fill ids color))))) (st/emit! (dc/change-fill ids color 0)))))
rename-color rename-color
(fn [name] (fn [name]

View file

@ -6,18 +6,23 @@
(ns app.main.ui.workspace.sidebar.options.menus.fill (ns app.main.ui.workspace.sidebar.options.menus.fill
(:require (:require
[app.common.attrs :as attrs]
[app.common.colors :as clr]
[app.common.data :as d]
[app.common.pages :as cp] [app.common.pages :as cp]
[app.main.data.workspace.colors :as dc] [app.main.data.workspace.colors :as dc]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.hooks :as h]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]]
[app.util.color :as uc]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[cuerdas.core :as str]
[rumext.alpha :as mf])) [rumext.alpha :as mf]))
(def fill-attrs (def fill-attrs
[:fill-color [:fills
:fill-color
:fill-opacity :fill-opacity
:fill-color-ref-id :fill-color-ref-id
:fill-color-ref-file :fill-color-ref-file
@ -27,24 +32,60 @@
(def fill-attrs-shape (def fill-attrs-shape
(conj fill-attrs :hide-fill-on-export)) (conj fill-attrs :hide-fill-on-export))
(defn color-values
[color]
{:color (:fill-color color)
:opacity (:fill-opacity color)
:id (:fill-color-ref-id color)
:file-id (:fill-color-ref-file color)
:gradient (:fill-color-gradient color)})
(mf/defc fill-menu (mf/defc fill-menu
{::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values"]))]} {::mf/wrap [#(mf/memo' % (mf/check-props ["ids" "values"]))]}
[{:keys [ids type values disable-remove?] :as props}] [{:keys [ids type values disable-remove?] :as props}]
(let [show? (or (not (nil? (:fill-color values))) (let [label (case type
(not (nil? (:fill-color-gradient values))))
label (case type
:multiple (tr "workspace.options.selection-fill") :multiple (tr "workspace.options.selection-fill")
:group (tr "workspace.options.group-fill") :group (tr "workspace.options.group-fill")
(tr "workspace.options.fill")) (tr "workspace.options.fill"))
color {:color (:fill-color values) ;; Excluding nil values
:opacity (:fill-opacity values) values (d/without-nils values)
:id (:fill-color-ref-id values)
:file-id (:fill-color-ref-file values)
:gradient (:fill-color-gradient values)}
hide-fill-on-export? (:hide-fill-on-export values) only-shapes? (and (contains? values :fills)
;; texts have :fill-* attributes, the rest of the shapes have :fills
(= (count (filter #(str/starts-with? (d/name %) "fill-") (keys values))) 0))
shapes-and-texts? (and (contains? values :fills)
;; texts have :fill-* attributes, the rest of the shapes have :fills
(> (count (filter #(str/starts-with? (d/name %) "fill-") (keys values))) 0))
;; Texts still have :fill-* attributes and the rest of the shapes just :fills so we need some extra calculation when multiple selection happens to detect them
plain-values (if (vector? (:fills values))
(concat (:fills values) [(dissoc values :fills)])
values)
plain-values (attrs/get-attrs-multi plain-values [:fill-color :fill-opacity :fill-color-ref-id :fill-color-ref-file :fill-color-gradient])
plain-values (if (empty? plain-values)
values
plain-values)
;; We must control some rare situations like
;; - Selecting texts and shapes with different fills
;; - Selecting a text and a shape with empty fills
plain-values (if (and shapes-and-texts?
(or
(= (:fills values) :multiple)
(= 0 (count (:fills values)))))
{:fills :multiple
:fill-color :multiple
:fill-opacity :multiple
:fill-color-ref-id :multiple
:fill-color-ref-file :multiple
:fill-color-gradient :multiple}
plain-values)
hide-fill-on-export? (:hide-fill-on-export values false)
checkbox-ref (mf/use-ref) checkbox-ref (mf/use-ref)
@ -52,31 +93,47 @@
(mf/use-callback (mf/use-callback
(mf/deps ids) (mf/deps ids)
(fn [_] (fn [_]
(st/emit! (dc/change-fill ids {:color cp/default-color (st/emit! (dc/add-fill ids {:color cp/default-color
:opacity 1})))) :opacity 1}))))
on-delete
(mf/use-callback
(mf/deps ids)
(fn [_]
(st/emit! (dc/change-fill ids (into {} uc/empty-color)))))
on-change on-change
(mf/use-callback
(mf/deps ids)
(fn [index]
(fn [color]
(st/emit! (dc/change-fill ids color index)))))
on-reorder
(mf/use-callback
(mf/deps ids)
(fn [new-index]
(fn [index]
(st/emit! (dc/reorder-fills ids index new-index)))))
on-change-mixed-shapes
(mf/use-callback (mf/use-callback
(mf/deps ids) (mf/deps ids)
(fn [color] (fn [color]
(let [remove-multiple (fn [[_ value]] (not= value :multiple)) (st/emit! (dc/change-fill-and-clear ids color))))
color (into {} (filter remove-multiple) color)]
(st/emit! (dc/change-fill ids color))))) on-remove
(fn [index]
(fn []
(st/emit! (dc/remove-fill ids {:color cp/default-color
:opacity 1} index))))
on-remove-all
(fn [_]
(st/emit! (dc/remove-all-fills ids {:color clr/black
:opacity 1})))
on-detach on-detach
(mf/use-callback (mf/use-callback
(mf/deps ids) (mf/deps ids)
(fn [] (fn [index]
(let [remove-multiple (fn [[_ value]] (not= value :multiple)) (fn [color]
color (-> (into {} (filter remove-multiple) color) (let [color (-> color
(assoc :id nil :file-id nil))] (assoc :id nil :file-id nil))]
(st/emit! (dc/change-fill ids color))))) (st/emit! (dc/change-fill ids color index))))))
on-change-show-fill-on-export on-change-show-fill-on-export
(mf/use-callback (mf/use-callback
@ -95,18 +152,46 @@
(dom/set-attribute checkbox "indeterminate" true) (dom/set-attribute checkbox "indeterminate" true)
(dom/remove-attribute checkbox "indeterminate"))))) (dom/remove-attribute checkbox "indeterminate")))))
(if show?
[:div.element-set [:div.element-set
[:div.element-set-title [:div.element-set-title
[:span label] [:span label]
(when (not disable-remove?) (when (and (not disable-remove?) (not (= :multiple (:fills values))) only-shapes?)
[:div.add-page {:on-click on-delete} i/minus])] [:div.add-page {:on-click on-add} i/close])]
[:div.element-set-content [:div.element-set-content
[:& color-row {:color color
:title (tr "workspace.options.fill") (if only-shapes?
:on-change on-change (cond
:on-detach on-detach}] (= :multiple (:fills values))
[:div.element-set-options-group
[:div.element-set-label (tr "settings.multiple")]
[:div.element-set-actions
[:div.element-set-actions-button {:on-click on-remove-all}
i/minus]]]
(seq (:fills values))
[:& h/sortable-container {}
(for [[index value] (d/enumerate (:fills values []))]
[:& color-row {:color {:color (:fill-color value)
:opacity (:fill-opacity value)
:id (:fill-color-ref-id value)
:file-id (:fill-color-ref-file value)
:gradient (:fill-color-gradient value)}
:index index
:title (tr "workspace.options.fill")
:on-change (on-change index)
:on-reorder (on-reorder index)
:on-detach (on-detach index)
:on-remove (on-remove index)}])])
[:& color-row {:color {:color (:fill-color plain-values)
:opacity (:fill-opacity plain-values)
:id (:fill-color-ref-id plain-values)
:file-id (:fill-color-ref-file plain-values)
:gradient (:fill-color-gradient plain-values)}
:title (tr "workspace.options.fill")
:on-change on-change-mixed-shapes
:on-detach (on-detach 0)}])
(when (or (= type :frame) (when (or (= type :frame)
(and (= type :multiple) (some? hide-fill-on-export?))) (and (= type :multiple) (some? hide-fill-on-export?)))
@ -118,9 +203,4 @@
:on-change on-change-show-fill-on-export}] :on-change on-change-show-fill-on-export}]
[:label {:for "show-fill-on-export"} [:label {:for "show-fill-on-export"}
(tr "workspace.options.show-fill-on-export")]])]] (tr "workspace.options.show-fill-on-export")]])]]))
[:div.element-set
[:div.element-set-title
[:span label]
[:div.add-page {:on-click on-add} i/close]]])))

View file

@ -61,11 +61,12 @@
(if (= v :multiple) nil v)) (if (= v :multiple) nil v))
(mf/defc color-row (mf/defc color-row
[{:keys [color disable-gradient disable-opacity on-change on-detach on-open on-close title]}] [{:keys [index color disable-gradient disable-opacity on-change on-reorder on-detach on-open on-close title on-remove]}]
(let [current-file-id (mf/use-ctx ctx/current-file-id) (let [current-file-id (mf/use-ctx ctx/current-file-id)
file-colors (mf/deref refs/workspace-file-colors) file-colors (mf/deref refs/workspace-file-colors)
shared-libs (mf/deref refs/workspace-libraries) shared-libs (mf/deref refs/workspace-libraries)
hover-detach (mf/use-state false) hover-detach (mf/use-state false)
disable-drag (mf/use-state false)
get-color-name (fn [{:keys [id file-id]}] get-color-name (fn [{:keys [id file-id]}]
(let [src-colors (if (= file-id current-file-id) (let [src-colors (if (= file-id current-file-id)
@ -77,6 +78,9 @@
(-> color (-> color
(update :color #(or % (:value color))))) (update :color #(or % (:value color)))))
detach-value (fn []
(when on-detach (on-detach color)))
change-value (fn [new-value] change-value (fn [new-value]
(when on-change (on-change (-> color (when on-change (on-change (-> color
(assoc :color new-value) (assoc :color new-value)
@ -104,7 +108,12 @@
(change-opacity (/ value 100))) (change-opacity (/ value 100)))
select-all (fn [event] select-all (fn [event]
(dom/select-text! (dom/get-target event))) (when (not @disable-drag)
(dom/select-text! (dom/get-target event)))
(reset! disable-drag true))
on-blur (fn [_]
(reset! disable-drag false))
handle-click-color (mf/use-callback handle-click-color (mf/use-callback
(mf/deps color) (mf/deps color)
@ -115,7 +124,22 @@
handle-open handle-open
handle-close)) handle-close))
prev-color (h/use-previous color)] prev-color (h/use-previous color)
on-drop
(fn [_ data]
(on-reorder (:index data)))
[dprops dref] (if (some? on-reorder)
(h/use-sortable
:data-type "penpot/color-row"
:on-drop on-drop
:disabled @disable-drag
:detect-center? false
:data {:id (str "color-row-" index)
:index index
:name (str "Color row" index)})
[nil nil])]
(mf/use-effect (mf/use-effect
(mf/deps color prev-color) (mf/deps color prev-color)
@ -123,7 +147,11 @@
(when (not= prev-color color) (when (not= prev-color color)
(modal/update-props! :colorpicker {:data (parse-color color)})))) (modal/update-props! :colorpicker {:data (parse-color color)}))))
[:div.row-flex.color-data {:title title} [:div.row-flex.color-data {:title title
:class (dom/classnames
:dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot))
:ref dref}
[:& cb/color-bullet {:color color [:& cb/color-bullet {:color color
:on-click handle-click-color}] :on-click handle-click-color}]
@ -137,7 +165,7 @@
[:div.element-set-actions-button [:div.element-set-actions-button
{:on-mouse-enter #(reset! hover-detach true) {:on-mouse-enter #(reset! hover-detach true)
:on-mouse-leave #(reset! hover-detach false) :on-mouse-leave #(reset! hover-detach false)
:on-click on-detach} :on-click detach-value}
(if @hover-detach i/unchain i/chain)])] (if @hover-detach i/unchain i/chain)])]
;; Rendering a gradient ;; Rendering a gradient
@ -156,6 +184,7 @@
(-> color :color uc/remove-hash)) (-> color :color uc/remove-hash))
:placeholder (tr "settings.multiple") :placeholder (tr "settings.multiple")
:on-click select-all :on-click select-all
:on-blur on-blur
:on-change handle-value-change}]] :on-change handle-value-change}]]
(when (and (not disable-opacity) (when (and (not disable-opacity)
@ -167,5 +196,7 @@
:on-click select-all :on-click select-all
:on-change handle-opacity-change :on-change handle-opacity-change
:min 0 :min 0
:max 100}]])])])) :max 100}]])])
(when (some? on-remove)
[:div.element-set-actions-button.remove {:on-click on-remove} i/minus])]))

View file

@ -358,16 +358,40 @@
(= type :path) (= type :path)
(parse-path center svg-data)))) (parse-path center svg-data))))
(defn add-library-refs
[props node]
(let [stroke-color-ref-id (get-meta node :stroke-color-ref-id uuid/uuid)
stroke-color-ref-file (get-meta node :stroke-color-ref-file uuid/uuid)
component-id (get-meta node :component-id uuid/uuid)
component-file (get-meta node :component-file uuid/uuid)
shape-ref (get-meta node :shape-ref uuid/uuid)
component-root? (get-meta node :component-root str->bool)]
(cond-> props
(some? stroke-color-ref-id)
(assoc :stroke-color-ref-id stroke-color-ref-id
:stroke-color-ref-file stroke-color-ref-file)
(some? component-id)
(assoc :component-id component-id
:component-file component-file)
component-root?
(assoc :component-root? component-root?)
(some? shape-ref)
(assoc :shape-ref shape-ref))))
(defn add-fill (defn add-fill
[props node svg-data] [props node svg-data]
(let [fill (:fill svg-data) (let [fill (:fill svg-data)
hide-fill-on-export (get-meta node :hide-fill-on-export str->bool) hide-fill-on-export (get-meta node :hide-fill-on-export str->bool)
fill-color-ref-id (get-meta node :fill-color-ref-id uuid/uuid)
fill-color-ref-file (get-meta node :fill-color-ref-file uuid/uuid)
gradient (when (str/starts-with? fill "url") gradient (when (str/starts-with? fill "url")
(parse-gradient node fill)) (parse-gradient node fill))]
meta-fill-color (get-meta node :fill-color)
meta-fill-opacity (get-meta node :fill-opacity)
meta-fill-color-gradient (get-meta node :fill-color-gradient)]
(cond-> props (cond-> props
:always :always
@ -386,14 +410,9 @@
(some? hide-fill-on-export) (some? hide-fill-on-export)
(assoc :hide-fill-on-export hide-fill-on-export) (assoc :hide-fill-on-export hide-fill-on-export)
(some? meta-fill-color) (some? fill-color-ref-id)
(assoc :fill-color meta-fill-color (assoc :fill-color-ref-id fill-color-ref-id
:fill-opacity (d/parse-double meta-fill-opacity)) :fill-color-ref-file fill-color-ref-file))))
(some? meta-fill-color-gradient)
(assoc :fill-color-gradient meta-fill-color-gradient
:fill-color nil
:fill-opacity nil))))
(defn add-stroke (defn add-stroke
[props node svg-data] [props node svg-data]
@ -658,6 +677,21 @@
props))) props)))
(defn add-fills
[props node svg-data]
(let [fills (-> node
(find-node :defs)
(find-node :pattern)
(find-node :g)
(find-all-nodes :rect)
(reverse))
fills (if (= 0 (count fills))
[(add-fill {} node svg-data)]
(map #(add-fill {} node (get-svg-data :rect %)) fills))]
(-> props
(assoc :fills fills))))
(defn add-svg-content (defn add-svg-content
[props node] [props node]
(let [svg-content (get-data node :penpot:svg-content) (let [svg-content (get-data node :penpot:svg-content)
@ -715,37 +749,6 @@
svg-data (or image-data pattern-data)] svg-data (or image-data pattern-data)]
(:xlink:href svg-data))) (:xlink:href svg-data)))
(defn add-library-refs
[props node]
(let [fill-color-ref-id (get-meta node :fill-color-ref-id uuid/uuid)
fill-color-ref-file (get-meta node :fill-color-ref-file uuid/uuid)
stroke-color-ref-id (get-meta node :stroke-color-ref-id uuid/uuid)
stroke-color-ref-file (get-meta node :stroke-color-ref-file uuid/uuid)
component-id (get-meta node :component-id uuid/uuid)
component-file (get-meta node :component-file uuid/uuid)
shape-ref (get-meta node :shape-ref uuid/uuid)
component-root? (get-meta node :component-root str->bool)]
(cond-> props
(some? fill-color-ref-id)
(assoc :fill-color-ref-id fill-color-ref-id
:fill-color-ref-file fill-color-ref-file)
(some? stroke-color-ref-id)
(assoc :stroke-color-ref-id stroke-color-ref-id
:stroke-color-ref-file stroke-color-ref-file)
(some? component-id)
(assoc :component-id component-id
:component-file component-file)
component-root?
(assoc :component-root? component-root?)
(some? shape-ref)
(assoc :shape-ref shape-ref))))
(defn parse-data (defn parse-data
[type node] [type node]
@ -754,7 +757,6 @@
(-> {} (-> {}
(add-common-data node) (add-common-data node)
(add-position type node svg-data) (add-position type node svg-data)
(add-fill node svg-data)
(add-stroke node svg-data) (add-stroke node svg-data)
(add-layer-options svg-data) (add-layer-options svg-data)
(add-shadows node) (add-shadows node)
@ -762,6 +764,7 @@
(add-exports node) (add-exports node)
(add-svg-attrs node svg-data) (add-svg-attrs node svg-data)
(add-library-refs node) (add-library-refs node)
(add-fills node svg-data)
(cond-> (= :svg-raw type) (cond-> (= :svg-raw type)
(add-svg-content node)) (add-svg-content node))