0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-28 15:41:25 -05:00

Merge pull request #3611 from penpot/superalex-support-for-images-as-fills

🎉 Support for images as fills
This commit is contained in:
Aitor Moreno 2023-11-14 14:15:20 +01:00 committed by GitHub
commit 099f9c074d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
46 changed files with 1278 additions and 587 deletions

View file

@ -112,18 +112,21 @@
(let [xform (comp
(map :objects)
(mapcat vals)
(keep (fn [{:keys [type] :as obj}]
(case type
:path (get-in obj [:fill-image :id])
:bool (get-in obj [:fill-image :id])
(mapcat (fn [obj]
;; NOTE: because of some bug, we ended with
;; many shape types having the ability to
;; have fill-image attribute (which initially
;; designed for :path shapes).
:group (get-in obj [:fill-image :id])
:image (get-in obj [:metadata :id])
nil))))
(sequence
(keep :id)
(concat [(:fill-image obj)
(:metadata obj)]
(map :fill-image (:fills obj))
(map :stroke-image (:strokes obj))
(->> (:content obj)
(tree-seq map? :children)
(mapcat :fills)
(map :fill-image)))))))
pages (concat
(vals (:pages-index data))
(vals (:components data)))]

View file

@ -364,6 +364,161 @@
(t/is (nil? (sto/get-object storage (:thumbnail-id fmo1))))
)))
(t/deftest file-gc-image-fills-and-strokes
(letfn [(add-file-media-object [& {:keys [profile-id file-id]}]
(let [mfile {:filename "sample.jpg"
:path (th/tempfile "backend_tests/test_files/sample.jpg")
:mtype "image/jpeg"
:size 312043}
params {::th/type :upload-file-media-object
::rpc/profile-id profile-id
:file-id file-id
:is-local true
:name "testfile"
:content mfile}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(:result out)))
(update-file! [& {:keys [profile-id file-id changes revn] :or {revn 0}}]
(let [params {::th/type :update-file
::rpc/profile-id profile-id
:id file-id
:session-id (uuid/random)
:revn revn
:components-v2 true
:changes changes}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(:result out)))]
(let [storage (:app.storage/storage th/*system*)
profile (th/create-profile* 1)
file (th/create-file* 1 {:profile-id (:id profile)
:project-id (:default-project-id profile)
:is-shared false})
fmo1 (add-file-media-object :profile-id (:id profile) :file-id (:id file))
fmo2 (add-file-media-object :profile-id (:id profile) :file-id (:id file))
fmo3 (add-file-media-object :profile-id (:id profile) :file-id (:id file))
fmo4 (add-file-media-object :profile-id (:id profile) :file-id (:id file))
fmo5 (add-file-media-object :profile-id (:id profile) :file-id (:id file))
s-shid (uuid/random)
t-shid (uuid/random)
page-id (first (get-in file [:data :pages]))]
;; Update file inserting a new image object
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:changes
[{:type :add-obj
:page-id page-id
:id s-shid
:parent-id uuid/zero
:frame-id uuid/zero
:components-v2 true
:obj (cts/setup-shape
{:id s-shid
:name "image"
:frame-id uuid/zero
:parent-id uuid/zero
:type :image
:metadata {:id (:id fmo1) :width 100 :height 100 :mtype "image/jpeg"}
:fills [{:opacity 1 :fill-image {:id (:id fmo2) :width 100 :height 100 :mtype "image/jpeg"}}]
:strokes [{:opacity 1 :stroke-image {:id (:id fmo3) :width 100 :height 100 :mtype "image/jpeg"}}]})}
{:type :add-obj
:page-id page-id
:id t-shid
:parent-id uuid/zero
:frame-id uuid/zero
:components-v2 true
:obj (cts/setup-shape
{:id t-shid
:name "text"
:frame-id uuid/zero
:parent-id uuid/zero
:type :text
:content {:type "root"
:children [{:type "paragraph-set"
:children [{:type "paragraph"
:children [{:fills [{:fill-opacity 1
:fill-image {:id (:id fmo4)
:width 417
:height 354
:mtype "image/png"
:name "text fill image"}}]
:text "hi"}
{:fills [{:fill-opacity 1
:fill-color "#000000"}]
:text "bye"}]}]}]}
:strokes [{:opacity 1 :stroke-image {:id (:id fmo5) :width 100 :height 100 :mtype "image/jpeg"}}]})}])
;; run the file-gc task immediately without forced min-age
(let [res (th/run-task! "file-gc")]
(t/is (= 0 (:processed res))))
;; run the task again
(let [res (th/run-task! "file-gc" {:min-age 0})]
(t/is (= 1 (:processed res))))
;; retrieve file and check trimmed attribute
(let [row (th/db-get :file {:id (:id file)})]
(t/is (true? (:has-media-trimmed row))))
;; check file media objects
(let [rows (th/db-exec! ["select * from file_media_object where file_id = ?" (:id file)])]
(t/is (= 5 (count rows))))
;; The underlying storage objects are still available.
(t/is (some? (sto/get-object storage (:media-id fmo5))))
(t/is (some? (sto/get-object storage (:media-id fmo4))))
(t/is (some? (sto/get-object storage (:media-id fmo3))))
(t/is (some? (sto/get-object storage (:media-id fmo2))))
(t/is (some? (sto/get-object storage (:media-id fmo1))))
;; proceed to remove usage of the file
(update-file!
:file-id (:id file)
:profile-id (:id profile)
:revn 0
:changes [{:type :del-obj
:page-id (first (get-in file [:data :pages]))
:id s-shid}
{:type :del-obj
:page-id (first (get-in file [:data :pages]))
:id t-shid}])
;; Now, we have deleted the usage of pointers to the
;; file-media-objects, if we paste file-gc, they should be marked
;; as deleted.
(let [task (:app.tasks.file-gc/handler th/*system*)
res (task {:min-age (dt/duration 0)})]
(t/is (= 1 (:processed res))))
;; Now that file-gc have deleted the file-media-object usage,
;; lets execute the touched-gc task, we should see that two of
;; them are marked to be deleted.
(let [task (:app.storage/gc-touched-task th/*system*)
res (task {:min-age (dt/duration 0)})]
(t/is (= 0 (:freeze res)))
(t/is (= 2 (:delete res))))
;; Finally, check that some of the objects that are marked as
;; deleted we are unable to retrieve them using standard storage
;; public api.
(t/is (nil? (sto/get-object storage (:media-id fmo5))))
(t/is (nil? (sto/get-object storage (:media-id fmo4))))
(t/is (nil? (sto/get-object storage (:media-id fmo3))))
(t/is (nil? (sto/get-object storage (:media-id fmo2))))
(t/is (nil? (sto/get-object storage (:media-id fmo1)))))))
(t/deftest permissions-checks-creating-file
(let [profile1 (th/create-profile* 1)
profile2 (th/create-profile* 2)

View file

@ -329,7 +329,8 @@
image-url (or (:href attrs) (:xlink:href attrs))
image-data (dm/get-in svg-data [:image-data image-url])
metadata {:width (:width image-data)
metadata {:name name
:width (:width image-data)
:height (:height image-data)
:mtype (:mtype image-data)
:id (:id image-data)}
@ -344,9 +345,11 @@
(when (some? image-data)
(cts/setup-shape
(-> (calculate-rect-metadata rect transform)
(assoc :type :image)
(assoc :type :rect)
(assoc :name name)
(assoc :frame-id frame-id)
(assoc :fills [{:fill-opacity 1
:fill-image metadata}])
(assoc :metadata metadata)
(assoc :svg-viewbox rect)
(assoc :svg-attrs props))))))

View file

@ -46,6 +46,14 @@
::oapi/type "integer"
::oapi/format "int64"}})
(sm/def! ::image-color
[:map {:title "ImageColor"}
[:name {:optional true} :string]
[:width :int]
[:height :int]
[:mtype {:optional true} [:maybe :string]]
[:id ::sm/uuid]])
(sm/def! ::gradient
[:map {:title "Gradient"}
[:type [::sm/one-of #{:linear :radial "linear" "radial"}]]
@ -72,17 +80,19 @@
[:modified-at {:optional true} ::sm/inst]
[:ref-id {:optional true} ::sm/uuid]
[:ref-file {:optional true} ::sm/uuid]
[:gradient {:optional true} [:maybe ::gradient]]])
[:gradient {:optional true} [:maybe ::gradient]]
[:image {:optional true} [:maybe ::image-color]]])
;; FIXME: incomplete schema
(sm/def! ::recent-color
[:and
[:map {:title "RecentColot"}
[:map {:title "RecentColor"}
[:opacity {:optional true} [:maybe ::sm/safe-number]]
[:color {:optional true} [:maybe ::rgb-color]]
[:gradient {:optional true} [:maybe ::gradient]]]
[::sm/contains-any {:strict true} [:color :gradient]]])
[:gradient {:optional true} [:maybe ::gradient]]
[:image {:optional true} [:maybe ::image-color]]]
[::sm/contains-any {:strict true} [:color :gradient :image]]])
(def color?
(sm/pred-fn ::color))
@ -102,17 +112,19 @@
{:color (:fill-color fill)
:opacity (:fill-opacity fill)
:gradient (:fill-color-gradient fill)
:image (:fill-image fill)
:ref-id (:fill-color-ref-id fill)
:ref-file (:fill-color-ref-file fill)}))
(defn set-fill-color
[shape position color opacity gradient]
[shape position color opacity gradient image]
(update-in shape [:fills position]
(fn [fill]
(d/without-nils (assoc fill
:fill-color color
:fill-opacity opacity
:fill-color-gradient gradient)))))
:fill-color-gradient gradient
:fill-image image)))))
(defn attach-fill-color
[shape position ref-id ref-file]
@ -133,17 +145,19 @@
(d/without-nils {:color (:stroke-color stroke)
:opacity (:stroke-opacity stroke)
:gradient (:stroke-color-gradient stroke)
:image (:stroke-image stroke)
:ref-id (:stroke-color-ref-id stroke)
:ref-file (:stroke-color-ref-file stroke)}))
(defn set-stroke-color
[shape position color opacity gradient]
[shape position color opacity gradient image]
(update-in shape [:strokes position]
(fn [stroke]
(d/without-nils (assoc stroke
:stroke-color color
:stroke-opacity opacity
:stroke-color-gradient gradient)))))
:stroke-color-gradient gradient
:stroke-image image)))))
(defn attach-stroke-color
[shape position ref-id ref-file]
@ -336,7 +350,8 @@
position
(:color library-color)
(:opacity library-color)
(:gradient library-color))
(:gradient library-color)
(:image library-color))
(detach-fn shape position)))
shape))]

View file

@ -105,7 +105,8 @@
[:fill-opacity {:optional true} ::sm/safe-number]
[:fill-color-gradient {:optional true} [:maybe ::ctc/gradient]]
[:fill-color-ref-file {:optional true} [:maybe ::sm/uuid]]
[:fill-color-ref-id {:optional true} [:maybe ::sm/uuid]]])
[:fill-color-ref-id {:optional true} [:maybe ::sm/uuid]]
[:fill-image {:optional true} ::ctc/image-color]])
(sm/def! ::stroke
[:map {:title "Stroke"}
@ -122,7 +123,8 @@
[::sm/one-of stroke-caps]]
[:stroke-cap-end {:optional true}
[::sm/one-of stroke-caps]]
[:stroke-color-gradient {:optional true} ::ctc/gradient]])
[:stroke-color-gradient {:optional true} ::ctc/gradient]
[:stroke-image {:optional true} ::ctc/image-color]])
(sm/def! ::minimal-shape-attrs
[:map {:title "ShapeMinimalRecord"}
@ -346,6 +348,13 @@
(def valid-shape?
(sm/pred-fn ::shape))
(defn has-images?
[{:keys [fills strokes]}]
(or
(some :fill-image fills)
(some :stroke-image strokes)))
;; --- Initialization
(def ^:private minimal-rect-attrs

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View file

@ -21,6 +21,7 @@
.top-actions {
display: flex;
margin-bottom: $size-1;
flex-direction: row-reverse;
justify-content: space-between;
.picker-btn {
@ -38,8 +39,44 @@
height: 14px;
}
}
.element-set-content {
width: auto;
padding: 0.25rem 0;
.custom-select {
border: none;
&:hover {
border: none;
}
.custom-select-dropdown {
left: auto;
right: 0;
}
}
}
}
.select-image {
.content {
display: flex;
justify-content: center;
background-image: url("/images/colorpicker-no-image.png");
background-position: center;
background-size: auto 6.75rem;
height: 6.75rem;
img {
height: fit-content;
width: fit-content;
max-height: 100%;
max-width: 100%;
margin: auto;
}
}
button {
width: 100%;
margin-top: 10px;
}
}
.gradients-buttons {
.gradient {
cursor: pointer;

View file

@ -143,6 +143,8 @@
.color-text {
width: 3rem;
text-transform: uppercase;
text-overflow: ellipsis;
overflow: hidden;
}
.attributes-color-display {

View file

@ -1530,27 +1530,45 @@
;; for retrieve the image data and convert it to the
;; data-url.
(prepare-object [objects parent-frame-id {:keys [type] :as obj}]
(let [obj (maybe-translate obj objects parent-frame-id)]
(if (= type :image)
(let [url (cf/resolve-file-media (:metadata obj))]
(->> (http/send! {:method :get
:uri url
:response-type :blob})
(rx/map :body)
(rx/mapcat wapi/read-file-as-data-url)
(rx/map #(assoc obj ::data %))
(rx/take 1)))
(let [obj (maybe-translate obj objects parent-frame-id)
;; Texts can have different fills for pieces of the text
fill-images-data (->> (or (:position-data obj) [obj])
(map :fills)
(reduce into [])
(filter :fill-image)
(map :fill-image))
stroke-images-data (->> (:strokes obj)
(filter :stroke-image)
(map :stroke-image))
images-data (concat
fill-images-data
stroke-images-data
(when (= type :image)
[(:metadata obj)]))]
(if (> (count images-data) 0)
(->> (rx/from images-data)
(rx/mapcat (fn [image-data]
(let [url (cf/resolve-file-media image-data)]
(->> (http/send! {:method :get
:uri url
:response-type :blob})
(rx/map :body)
(rx/mapcat wapi/read-file-as-data-url)
(rx/map #(assoc image-data :data %))))))
(rx/reduce conj [])
(rx/map
(fn [images]
(assoc obj ::data images))))
(rx/of obj))))
;; Collects all the items together and split images into a
;; separated data structure for a more easy paste process.
(collect-data [res {:keys [id metadata] :as item}]
(collect-data [res {:keys [id] :as item}]
(let [res (update res :objects assoc id (dissoc item ::data))]
(if (= :image (:type item))
(let [img-part {:id (:id metadata)
:name (:name item)
:file-data (::data item)}]
(update res :images conj img-part))
(if (::data item)
(update res :images into (::data item))
res)))
(maybe-translate [shape objects parent-frame-id]
@ -1713,7 +1731,7 @@
(letfn [;; Given a file-id and img (part generated by the
;; copy-selected event), uploads the new media.
(upload-media [file-id imgpart]
(->> (http/send! {:uri (:file-data imgpart)
(->> (http/send! {:uri (:data imgpart)
:response-type :blob
:method :get})
(rx/map :body)
@ -1727,19 +1745,43 @@
(rx/map (fn [media]
(assoc media :prev-id (:id imgpart))))))
(translate-staled-media [mdata attribute media-idx]
(let [id (get-in mdata [attribute :id])
mobj (get media-idx id)]
(if mobj
(update mdata attribute #(assoc %
:id (:id mobj)
:path (:path mobj)))
mdata)))
;; Analyze the rchange and replace staled media and
;; references to the new uploaded media-objects.
(process-rchange [media-idx item]
(if (and (= (:type item) :add-obj)
(= :image (get-in item [:obj :type])))
(update-in item [:obj :metadata]
(fn [{:keys [id] :as mdata}]
(if-let [mobj (get media-idx id)]
(assoc mdata
:id (:id mobj)
:path (:path mobj))
mdata)))
item))
(let [;; Texts can have different fills for pieces of the text
obj (:obj item)
fills (mapv #(translate-staled-media % :fill-image media-idx) (:fills obj))
strokes (mapv #(translate-staled-media % :stroke-image media-idx) (:strokes obj))
position-data (->> (:position-data obj)
(mapv (fn [p-data]
(let [fills (mapv #(translate-staled-media % :fill-image media-idx) (:fills p-data))]
(assoc p-data :fills fills)))))
content (txt/transform-nodes #(translate-staled-media % :fill-image media-idx) (:content obj))]
(if (= (:type item) :add-obj)
(-> item
(update-in [:obj :metadata]
(fn [{:keys [id] :as mdata}]
(if-let [mobj (get media-idx id)]
(assoc mdata
:id (:id mobj)
:path (:path mobj))
mdata)))
(assoc-in [:obj :fills] fills)
(assoc-in [:obj :strokes] strokes)
(assoc-in [:obj :content] content)
(cond->
(> (count position-data) 0) (assoc-in [:obj :position-data] position-data)))
item)))
(calculate-paste-position [state mouse-pos in-viewport?]
(let [page-objects (wsh/lookup-page-objects state)

View file

@ -105,6 +105,9 @@
(contains? color :opacity)
(assoc :fill-opacity (:opacity color))
(contains? color :image)
(assoc :fill-image (:image color))
:always
(d/without-nils))
@ -223,9 +226,15 @@
(assoc :stroke-color-gradient (:gradient attrs))
(contains? attrs :opacity)
(assoc :stroke-opacity (:opacity attrs)))
(assoc :stroke-opacity (:opacity attrs))
attrs (merge attrs color-attrs)]
(contains? attrs :image)
(assoc :stroke-image (:image attrs)))
attrs (->
(merge attrs color-attrs)
(dissoc :image)
(dissoc :gradient))]
(rx/of (dch/update-shapes
ids
@ -455,7 +464,11 @@
(defn clear-color-components
[data]
(dissoc data :hex :alpha :r :g :b :h :s :v))
(dissoc data :hex :alpha :r :g :b :h :s :v :image))
(defn clear-image-components
[data]
(dissoc data :hex :alpha :r :g :b :h :s :v :color))
(defn- create-gradient
[type]
@ -467,8 +480,14 @@
(defn get-color-from-colorpicker-state
[{:keys [type current-color stops gradient] :as state}]
(if (= type :color)
(cond
(= type :color)
(clear-color-components current-color)
(= type :image)
(clear-image-components current-color)
:else
{:gradient (-> gradient
(assoc :type (case type
:linear-gradient :linear
@ -487,7 +506,7 @@
(on-change color)))))
(defn initialize-colorpicker
[on-change]
[on-change tab]
(ptk/reify ::initialize-colorpicker
ptk/WatchEvent
(watch [_ _ stream]
@ -502,7 +521,14 @@
(rx/filter (ptk/type? ::update-colorpicker-color) stream)
(rx/filter (ptk/type? ::activate-colorpicker-gradient) stream))
(rx/map (constantly (colorpicker-onchange-runner on-change)))
(rx/take-until stoper))))))
(rx/take-until stoper))))
ptk/UpdateEvent
(update [_ state]
(update state :colorpicker
(fn [state]
(-> state
(assoc :type tab)))))))
(defn finalize-colorpicker
[]
@ -522,13 +548,8 @@
(let [current-color (:current-color state)]
(if (some? gradient)
(let [stop (or (:editing-stop state) 0)
stops (mapv split-color-components (:stops gradient))
type (case (:type gradient)
:linear :linear-gradient
:radial :radial-gradient
(:type state))]
stops (mapv split-color-components (:stops gradient))]
(-> state
(assoc :type type)
(assoc :current-color (nth stops stop))
(assoc :stops stops)
(assoc :gradient (-> gradient
@ -537,7 +558,6 @@
(assoc :editing-stop stop)))
(-> state
(assoc :type :color)
(cond-> (or (nil? current-color)
(not= (:color data) (:color current-color))
(not= (:opacity data) (:opacity current-color)))
@ -553,9 +573,11 @@
(update [_ state]
(update state :colorpicker
(fn [state]
(let [state (-> state
(let [type (:type state)
state (-> state
(update :current-color merge changes)
(update :current-color materialize-color-components)
(update :current-color #(if (not= type :image) (dissoc % :image) %))
;; current color can be a library one I'm changing via colorpicker
(d/dissoc-in [:current-color :id])
(d/dissoc-in [:current-color :file-id]))]
@ -564,7 +586,6 @@
(merge data)
(materialize-color-components))))
(-> state
(assoc :type :color)
(dissoc :gradient :stops :editing-stop)))))))
ptk/WatchEvent
(watch [_ state _]
@ -592,6 +613,17 @@
:editing-stop stop)
state))))))
(defn activate-colorpicker-color
[]
(ptk/reify ::activate-colorpicker-color
ptk/UpdateEvent
(update [_ state]
(update state :colorpicker
(fn [state]
(-> state
(assoc :type :color)
(dissoc :editing-stop :stops :gradient)))))))
(defn activate-colorpicker-gradient
[type]
(ptk/reify ::activate-colorpicker-gradient
@ -599,23 +631,32 @@
(update [_ state]
(update state :colorpicker
(fn [state]
(if (= type (:type state))
(do
(-> state
(assoc :type :color)
(dissoc :editing-stop :stops :gradient)))
(let [gradient (create-gradient type)
color (:current-color state)]
(-> state
(assoc :type type)
(assoc :gradient gradient)
(cond-> (not (:stops state))
(assoc :editing-stop 0
:stops [(assoc color :offset 0)
(-> color
(assoc :alpha 0)
(assoc :offset 1)
(materialize-color-components))]))))))))))
(let [gradient (create-gradient type)
color (:current-color state)]
(-> state
(assoc :type type)
(assoc :gradient gradient)
(d/dissoc-in [:current-color :image])
(cond-> (not (:stops state))
(assoc :editing-stop 0
:stops [(-> color
(assoc :offset 0)
(materialize-color-components))
(-> color
(assoc :alpha 0)
(assoc :offset 1)
(materialize-color-components))])))))))))
(defn activate-colorpicker-image
[]
(ptk/reify ::activate-colorpicker-image
ptk/UpdateEvent
(update [_ state]
(update state :colorpicker
(fn [state]
(-> state
(assoc :type :image)
(dissoc :editing-stop :stops :gradient)))))))
(defn select-color
[position add-color]

View file

@ -97,7 +97,8 @@
(let [id (uuid/next)
color (-> color
(assoc :id id)
(assoc :name (or (:color color)
(assoc :name (or (get-in color [:image :name])
(:color color)
(uc/gradient-type->string (get-in color [:gradient :type])))))]
(dm/assert! ::ctc/color color)
(ptk/reify ::add-color

View file

@ -78,11 +78,13 @@
:height height
:x (mth/round (- x (/ width 2)))
:y (mth/round (- y (/ height 2)))
:metadata {:width width
:height height
:mtype mtype
:id id}}]
(rx/of (dwsh/create-and-add-shape :image x y shape))))))
:fills [{:fill-opacity 1
:fill-image {:name name
:width width
:height height
:mtype mtype
:id id}}]}]
(rx/of (dwsh/create-and-add-shape :rect x y shape))))))
(defn svg-uploaded
[svg-data file-id position]
@ -171,64 +173,64 @@
[:uris {:optional true} [:sequential :string]]
[:mtype {:optional true} :string]])
(defn handle-media-error [error on-error]
(if (ex/ex-info? error)
(handle-media-error (ex-data error) on-error)
(cond
(= (:code error) :invalid-svg-file)
(rx/of (msg/error (tr "errors.media-type-not-allowed")))
(= (:code error) :media-type-not-allowed)
(rx/of (msg/error (tr "errors.media-type-not-allowed")))
(= (:code error) :unable-to-access-to-url)
(rx/of (msg/error (tr "errors.media-type-not-allowed")))
(= (:code error) :invalid-image)
(rx/of (msg/error (tr "errors.media-type-not-allowed")))
(= (:code error) :media-max-file-size-reached)
(rx/of (msg/error (tr "errors.media-too-large")))
(= (:code error) :media-type-mismatch)
(rx/of (msg/error (tr "errors.media-type-mismatch")))
(= (:code error) :unable-to-optimize)
(rx/of (msg/error (:hint error)))
(fn? on-error)
(on-error error)
:else
(do
(.error js/console "ERROR" error)
(rx/of (msg/error (tr "errors.cannot-upload")))))))
(defn- process-media-objects
[{:keys [uris on-error] :as params}]
(dm/assert!
(and (sm/valid? schema:process-media-objects params)
(or (contains? params :blobs)
(contains? params :uris))))
(and (sm/valid? schema:process-media-objects params)
(or (contains? params :blobs)
(contains? params :uris))))
(letfn [(handle-error [error]
(if (ex/ex-info? error)
(handle-error (ex-data error))
(cond
(= (:code error) :invalid-svg-file)
(rx/of (msg/error (tr "errors.media-type-not-allowed")))
(= (:code error) :media-type-not-allowed)
(rx/of (msg/error (tr "errors.media-type-not-allowed")))
(= (:code error) :unable-to-access-to-url)
(rx/of (msg/error (tr "errors.media-type-not-allowed")))
(= (:code error) :invalid-image)
(rx/of (msg/error (tr "errors.media-type-not-allowed")))
(= (:code error) :media-max-file-size-reached)
(rx/of (msg/error (tr "errors.media-too-large")))
(= (:code error) :media-type-mismatch)
(rx/of (msg/error (tr "errors.media-type-mismatch")))
(= (:code error) :unable-to-optimize)
(rx/of (msg/error (:hint error)))
(fn? on-error)
(on-error error)
:else
(do
(.error js/console "ERROR" error)
(rx/of (msg/error (tr "errors.cannot-upload")))))))]
(ptk/reify ::process-media-objects
ptk/WatchEvent
(watch [_ _ _]
(rx/concat
(rx/of (msg/show {:content (tr "media.loading")
:type :info
:timeout nil
:tag :media-loading}))
(->> (if (seq uris)
(ptk/reify ::process-media-objects
ptk/WatchEvent
(watch [_ _ _]
(rx/concat
(rx/of (msg/show {:content (tr "media.loading")
:type :info
:timeout nil
:tag :media-loading}))
(->> (if (seq uris)
;; Media objects is a list of URL's pointing to the path
(process-uris params)
(process-uris params)
;; Media objects are blob of data to be upload
(process-blobs params))
(process-blobs params))
;; Every stream has its own sideeffect. We need to ignore the result
(rx/ignore)
(rx/catch handle-error)
(rx/finalize #(st/emit! (msg/hide-tag :media-loading)))))))))
(rx/ignore)
(rx/catch #(handle-media-error % on-error))
(rx/finalize #(st/emit! (msg/hide-tag :media-loading))))))))
;; Deprecated in components-v2
(defn upload-media-asset
@ -248,6 +250,35 @@
(process-media-objects params)))
(defn upload-fill-image
[file on-success]
(dm/assert!
"expected a valid blob for `file` param"
(dmm/blob? file))
(ptk/reify ::upload-fill-image
ptk/WatchEvent
(watch [_ state _]
(let [on-upload-success
(fn [image]
(on-success image)
(dmm/notify-finished-loading))
prepare
(fn [content]
{:file-id (get-in state [:workspace-file :id])
:name (if (dmm/file? content) (.-name content) (tr "media.image"))
:is-local false
:content content})]
(dmm/notify-start-loading)
(->> (rx/of file)
(rx/map dmm/validate-file)
(rx/map prepare)
(rx/mapcat #(rp/cmd! :upload-file-media-object %))
(rx/do on-upload-success)
(rx/catch handle-media-error))))))
;; --- Upload File Media objects
(defn load-and-parse-svg
@ -283,7 +314,7 @@
(defn create-shapes-img
"Convert a media object that contains a bitmap image into shapes,
one shape of type :image and one group that contains it."
one shape of type :rect containing an image fill and one group that contains it."
[pos {:keys [name width height id mtype] :as media-obj} & {:keys [wrapper-type] :or {wrapper-type :group}}]
(let [group-shape (cts/setup-shape
{:type wrapper-type
@ -296,15 +327,17 @@
:parent-id uuid/zero})
img-shape (cts/setup-shape
{:type :image
{:type :rect
:x (:x pos)
:y (:y pos)
:width width
:height height
:metadata {:id id
:width width
:height height
:mtype mtype}
:fills [{:fill-opacity 1
:fill-image {:name name
:id id
:width width
:height height
:mtype mtype}}]
:name name
:frame-id uuid/zero
:parent-id (:id group-shape)})]

View file

@ -6,8 +6,11 @@
(ns app.main.ui.components.color-bullet
(:require
[app.config :as cfg]
[app.util.color :as uc]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(mf/defc color-bullet
@ -31,8 +34,15 @@
:is-gradient (some? (:gradient color)))
:on-click on-click
:title (uc/get-color-name color)}
(if (:gradient color)
(cond
(:gradient color)
[:div.color-bullet-wrapper {:style {:background (uc/color->background color)}}]
(:image color)
(let [uri (cfg/resolve-file-media (:image color))]
[:div.color-bullet-wrapper {:style {:background-size "contain" :background-image (str/ffmt "url(%)" uri)}}])
:else
[:div.color-bullet-wrapper
[:div.color-bullet-left {:style {:background (uc/color->background (assoc color :opacity 1))}}]
[:div.color-bullet-right {:style {:background (uc/color->background color)}}]])]))))
@ -40,10 +50,12 @@
(mf/defc color-name
{::mf/wrap-props false}
[{:keys [color size on-click on-double-click]}]
(let [{:keys [name color gradient]} (if (string? color) {:color color :opacity 1} color)]
(let [{:keys [name color gradient image]} (if (string? color) {:color color :opacity 1} color)]
(when (or (not size) (= size :big))
[:span.color-text
{:on-click on-click
:on-double-click on-double-click
:title name}
(or name color (uc/gradient-type->string (:type gradient)))])))
(if (some? image)
(tr "media.image")
(or name color (uc/gradient-type->string (:type gradient))))])))

View file

@ -7,7 +7,10 @@
(ns app.main.ui.components.color-bullet-new
(:require-macros [app.main.style :as stl])
(:require
[app.config :as cfg]
[app.util.color :as uc]
[app.util.i18n :as i18n :refer [tr]]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(mf/defc color-bullet
@ -26,7 +29,8 @@
(let [color (if (string? color) {:color color :opacity 1} color)
id (:id color)
gradient (:gradient color)
opacity (:opacity color)]
opacity (:opacity color)
image (:image color)]
[:div
{:class (stl/css-case
:color-bullet true
@ -38,21 +42,27 @@
:grid-area area)
:on-click on-click}
(if (some? gradient)
(cond
(some? gradient)
[:div {:class (stl/css :color-bullet-wrapper)
:style {:background (uc/color->background color)}}]
(some? image)
(let [uri (cfg/resolve-file-media image)]
[:div {:class (stl/css :color-bullet-wrapper)
:style {:background-image (str/ffmt "url(%)" uri)}}])
:else
[:div {:class (stl/css :color-bullet-wrapper)}
[:div {:class (stl/css :color-bullet-left)
:style {:background (uc/color->background (assoc color :opacity 1))}}]
[:div {:class (stl/css :color-bullet-right)
:style {:background (uc/color->background color)}}]])]))))
(mf/defc color-name
{::mf/wrap-props false}
[{:keys [color size on-click on-double-click]}]
(let [{:keys [name color gradient]} (if (string? color) {:color color :opacity 1} color)]
(let [{:keys [name color gradient image]} (if (string? color) {:color color :opacity 1} color)]
(when (or (not size) (> size 64))
[:span {:class (stl/css-case
:color-text (< size 72)
@ -60,4 +70,6 @@
:big-text (>= size 72))
:on-click on-click
:on-double-click on-double-click}
(or name color (uc/gradient-type->string (:type gradient)))])))
(if (some? image)
(tr "media.image")
(or name color (uc/gradient-type->string (:type gradient))))])))

View file

@ -55,6 +55,9 @@
height: 100%;
width: 100%;
clip-path: circle(50%);
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.color-bullet-wrapper > * {
width: 100%;

View file

@ -7,6 +7,7 @@
(ns app.main.ui.components.shape-icon
(:require
[app.common.types.component :as ctk]
[app.common.types.shape :as cts]
[app.common.types.shape.layout :as ctl]
[app.main.ui.icons :as i]
[rumext.v2 :as mf]))
@ -32,10 +33,10 @@
:else
i/artboard)
:image i/image
:line i/line
:circle i/circle
:path i/curve
:rect i/box
:line (if (cts/has-images? shape) i/image i/line)
:circle (if (cts/has-images? shape) i/image i/circle)
:path (if (cts/has-images? shape) i/image i/curve)
:rect (if (cts/has-images? shape) i/image i/box)
:text i/text
:group (if (:masked-group shape)
i/mask
@ -47,39 +48,3 @@
#_:default i/bool-union)
:svg-raw i/file-svg
nil)))
(mf/defc element-icon-refactor
[{:keys [shape main-instance?] :as props}]
(if (ctk/instance-head? shape)
(if main-instance?
i/component-refactor
i/copy-refactor)
(case (:type shape)
:frame (cond
(and (ctl/flex-layout? shape) (ctl/col? shape))
i/flex-vertical-refactor
(and (ctl/flex-layout? shape) (ctl/row? shape))
i/flex-horizontal-refactor
(ctl/grid-layout? shape)
i/grid-refactor
:else
i/board-refactor)
:image i/img-refactor
:line i/path-refactor
:circle i/elipse-refactor
:path i/curve-refactor
:rect i/rectangle-refactor
:text i/text-refactor
:group (if (:masked-group shape)
i/mask-refactor
i/group-refactor)
:bool (case (:bool-type shape)
:difference i/boolean-difference-refactor
:exclude i/boolean-exclude-refactor
:intersection i/boolean-intersection-refactor
#_:default i/boolean-union-refactor)
:svg-raw i/svg-refactor
nil)))

View file

@ -7,11 +7,11 @@
(ns app.main.ui.components.shape-icon-refactor
(:require
[app.common.types.component :as ctk]
[app.common.types.shape :as cts]
[app.common.types.shape.layout :as ctl]
[app.main.ui.icons :as i]
[rumext.v2 :as mf]))
(mf/defc element-icon-refactor
{::mf/wrap-props false}
[{:keys [shape main-instance?]}]
@ -33,10 +33,10 @@
i/board-refactor)
;; TODO -> THUMBNAIL ICON
:image i/img-refactor
:line i/path-refactor
:circle i/elipse-refactor
:path i/path-refactor
:rect i/rectangle-refactor
:line (if (cts/has-images? shape) i/img-refactor i/path-refactor)
:circle (if (cts/has-images? shape) i/img-refactor i/elipse-refactor)
:path (if (cts/has-images? shape) i/img-refactor i/curve-refactor)
:rect (if (cts/has-images? shape) i/img-refactor i/rectangle-refactor)
:text i/text-refactor
:group (if (:masked-group shape)
i/mask-refactor

View file

@ -62,14 +62,14 @@
(defn add-fill!
[attrs fill-data render-id index type]
(let [index (if (some? index) (dm/str "_" index) "")]
(let [index (if (some? index) (dm/str "-" index) "")]
(cond
(contains? fill-data :fill-image)
(let [id (dm/str "fill-image-" render-id)]
(obj/set! attrs "fill" (dm/str "url(#" id ")")))
(some? (:fill-color-gradient fill-data))
(let [id (dm/str "fill-color-gradient_" render-id index)]
(let [id (dm/str "fill-color-gradient-" render-id index)]
(obj/set! attrs "fill" (dm/str "url(#" id ")")))
(contains? fill-data :fill-color)
@ -100,7 +100,7 @@
(obj/set! attrs "strokeWidth" width)
(when (some? gradient)
(let [gradient-id (dm/str "stroke-color-gradient_" render-id "_" index)]
(let [gradient-id (dm/str "stroke-color-gradient-" render-id "-" index)]
(obj/set! attrs "stroke" (str/ffmt "url(#%)" gradient-id))))
(when-not (some? gradient)

View file

@ -13,8 +13,10 @@
[app.common.geom.shapes.bounds :as gsb]
[app.common.geom.shapes.text :as gst]
[app.common.pages.helpers :as cph]
[app.config :as cfg]
[app.main.ui.context :as muc]
[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]
[cuerdas.core :as str]
@ -83,11 +85,18 @@
(let [id-prefix (dm/str "marker-" render-id)
gradient (:stroke-color-gradient stroke)
image (:stroke-image stroke)
cap-start (:stroke-cap-start stroke)
cap-end (:stroke-cap-end stroke)
color (if (some? gradient)
color (cond
(some? gradient)
(str/ffmt "url(#stroke-color-gradient-%s-%s)" render-id index)
(some? image)
(str/ffmt "url(#stroke-fill-%-%)" render-id index)
:else
(:stroke-color stroke))
opacity (when-not (some? gradient)
@ -192,21 +201,57 @@
(mf/defc stroke-defs
{::mf/wrap-props false}
[{:keys [shape stroke render-id index]}]
(let [open-path? (and ^boolean (cph/path-shape? shape)
^boolean (gsh/open-path? shape))
gradient (:stroke-color-gradient stroke)
alignment (:stroke-alignment stroke :center)
width (:stroke-width stroke 0)
(let [open-path? (and ^boolean (cph/path-shape? shape)
^boolean (gsh/open-path? shape))
gradient (:stroke-color-gradient stroke)
alignment (:stroke-alignment stroke :center)
width (:stroke-width stroke 0)
props #js {:id (dm/str "stroke-color-gradient-" render-id "-" index)
:gradient gradient
:shape shape}]
props #js {:id (dm/str "stroke-color-gradient-" render-id "-" index)
:gradient gradient
:shape shape}
stroke-image (:stroke-image stroke)
uri (when stroke-image (cfg/resolve-file-media stroke-image))
embed (embed/use-data-uris [uri])
stroke-width (case (:stroke-alignment stroke :center)
:center (/ (:stroke-width stroke 0) 2)
:outer (:stroke-width stroke 0)
0)
margin (gsb/shape-stroke-margin stroke stroke-width)
selrect (mf/with-memo [shape]
(if (cph/text-shape? shape)
(gst/shape->rect shape)
(grc/points->rect (:points shape))))
stroke-margin (+ stroke-width margin)
w (+ (dm/get-prop selrect :width) (* 2 stroke-margin))
h (+ (dm/get-prop selrect :height) (* 2 stroke-margin))
image-props #js {:href (get embed uri uri)
:preserveAspectRatio "xMidYMid slice"
:width 1
:height 1
:id (dm/str "stroke-image-" render-id "-" index)}]
[:*
(when (some? gradient)
(case (:type gradient)
:linear [:> grad/linear-gradient props]
:radial [:> grad/radial-gradient props]))
(when (:stroke-image stroke)
;; We need to make the pattern size and the image fit so it's not repeated
[:pattern {:id (dm/str "stroke-fill-" render-id "-" index)
:patternContentUnits "objectBoundingBox"
:x (- (/ stroke-margin (dm/get-prop selrect :width)))
:y (- (/ stroke-margin (dm/get-prop selrect :height)))
:width (/ w (dm/get-prop selrect :width))
:height (/ h (dm/get-prop selrect :height))
:viewBox "0 0 1 1"
:preserveAspectRatio "xMidYMid slice"}
[:> :image image-props]])
(cond
(and (not open-path?)
(= :inner alignment)
@ -345,6 +390,7 @@
index (unchecked-get props "index")
render-id (mf/use-ctx muc/render-id)
render-id (d/nilv (unchecked-get props "render-id") render-id)
stroke-width (:stroke-width stroke 0)
stroke-style (:stroke-style stroke :none)
@ -385,7 +431,8 @@
url-fill? (or ^boolean (some? (:fill-image shape))
^boolean (cph/image-shape? shape)
^boolean (> (count shape-fills) 1)
^boolean (some? (some :fill-color-gradient shape-fills)))
^boolean (some? (some :fill-color-gradient shape-fills))
^boolean (some? (some :fill-image shape-fills)))
props (if (cph/frame-shape? shape)
props
@ -447,6 +494,10 @@
(obj/set! "fillOpacity" "none")
(obj/merge! (attrs/get-stroke-style value position render-id)))
style (if (:stroke-image value)
(obj/set! style "stroke" (dm/fmt "url(#stroke-fill-%-%)" render-id position))
style)
props (-> (obj/clone props)
(obj/unset! "fill")
(obj/unset! "fillOpacity")

View file

@ -10,6 +10,7 @@
importation."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.shapes :as gsh]
[app.common.svg :as csvg]
[app.main.ui.context :as muc]
@ -282,36 +283,47 @@
(defn- export-fills-data [{:keys [fills]}]
(when-let [fills (seq fills)]
(mf/html
[:> "penpot:fills" #js {}
(for [[index fill] (d/enumerate fills)]
[:> "penpot:fill"
#js {:penpot:fill-color (if (some? (:fill-color-gradient fill))
(str/format "url(#%s)" (str "fill-color-gradient_" (mf/use-ctx muc/render-id) "_" index))
(d/name (:fill-color fill)))
:penpot:fill-color-ref-file (d/name (:fill-color-ref-file fill))
:penpot:fill-color-ref-id (d/name (:fill-color-ref-id fill))
:penpot:fill-opacity (d/name (:fill-opacity fill))}])])))
(when-let [fills (seq fills)]
(let [render-id (mf/use-ctx muc/render-id)]
(mf/html
[:> "penpot:fills" #js {}
(for [[index fill] (d/enumerate fills)]
(let [fill-image-id (dm/str "fill-image-" render-id "-" index)]
[:> "penpot:fill"
#js {:penpot:fill-color (cond
(some? (:fill-color-gradient fill))
(str/format "url(#%s)" (str "fill-color-gradient-" render-id "-" index))
:else
(d/name (:fill-color fill)))
:penpot:fill-image-id (when (:fill-image fill) fill-image-id)
:penpot:fill-color-ref-file (d/name (:fill-color-ref-file fill))
:penpot:fill-color-ref-id (d/name (:fill-color-ref-id fill))
:penpot:fill-opacity (d/name (:fill-opacity fill))}]))]))))
(defn- export-strokes-data [{:keys [strokes]}]
(when-let [strokes (seq strokes)]
(mf/html
[:> "penpot:strokes" #js {}
(for [[index stroke] (d/enumerate strokes)]
[:> "penpot:stroke"
#js {:penpot:stroke-color (if (some? (:stroke-color-gradient stroke))
(str/format "url(#%s)" (str "stroke-color-gradient_" (mf/use-ctx muc/render-id) "_" index))
(d/name (:stroke-color stroke)))
:penpot:stroke-color-ref-file (d/name (:stroke-color-ref-file stroke))
:penpot:stroke-color-ref-id (d/name (:stroke-color-ref-id stroke))
:penpot:stroke-opacity (d/name (:stroke-opacity stroke))
:penpot:stroke-style (d/name (:stroke-style stroke))
:penpot:stroke-width (d/name (:stroke-width stroke))
:penpot:stroke-alignment (d/name (:stroke-alignment stroke))
:penpot:stroke-cap-start (d/name (:stroke-cap-start stroke))
:penpot:stroke-cap-end (d/name (:stroke-cap-end stroke))}])])))
(let [render-id (mf/use-ctx muc/render-id)]
(mf/html
[:> "penpot:strokes" #js {}
(for [[index stroke] (d/enumerate strokes)]
(let [stroke-image-id (dm/str "stroke-image-" render-id "-" index)]
[:> "penpot:stroke"
#js {:penpot:stroke-color (cond
(some? (:stroke-color-gradient stroke))
(str/format "url(#%s)" (str "stroke-color-gradient-" render-id "-" index))
:else
(d/name (:stroke-color stroke)))
:penpot:stroke-image-id (when (:stroke-image stroke) stroke-image-id)
:penpot:stroke-color-ref-file (d/name (:stroke-color-ref-file stroke))
:penpot:stroke-color-ref-id (d/name (:stroke-color-ref-id stroke))
:penpot:stroke-opacity (d/name (:stroke-opacity stroke))
:penpot:stroke-style (d/name (:stroke-style stroke))
:penpot:stroke-width (d/name (:stroke-width stroke))
:penpot:stroke-alignment (d/name (:stroke-alignment stroke))
:penpot:stroke-cap-start (d/name (:stroke-cap-start stroke))
:penpot:stroke-cap-end (d/name (:stroke-cap-end stroke))}]))]))))
(defn- export-interactions-data [{:keys [interactions]}]
(when-let [interactions (seq interactions)]
@ -461,5 +473,5 @@
(export-strokes-data shape)
(export-grid-data shape)
(export-layout-container-data shape)
(export-layout-item-data shape)]))
(export-layout-item-data shape)]))

View file

@ -36,21 +36,26 @@
height (dm/get-prop selrect :height)
has-image? (or (some? metadata)
(some? image))
(some? image))
uri (cond
(some? metadata)
(cfg/resolve-file-media metadata)
uri (cond
(some? metadata)
(cfg/resolve-file-media metadata)
(some? image)
(cfg/resolve-file-media image))
(some? image)
(cfg/resolve-file-media image))
embed (embed/use-data-uris [uri])
uris (into [uri]
(comp
(keep :fill-image)
(map cfg/resolve-file-media))
fills)
embed (embed/use-data-uris uris)
transform (gsh/transform-str shape)
;; When tru e the image has not loaded yet
loading? (and (some? uri)
(not (contains? embed uri)))
;; When true the image has not loaded yet
loading? (not-any? (partial contains? embed) uris)
pat-props #js {:patternUnits "userSpaceOnUse"
:x x
@ -65,10 +70,10 @@
(for [[shape-index shape] (d/enumerate (or (:position-data shape) [shape]))]
[:* {:key (dm/str shape-index)}
(for [[fill-index value] (reverse (d/enumerate fills))]
(for [[fill-index value] (reverse (d/enumerate (get shape :fills [])))]
(when (some? (:fill-color-gradient value))
(let [gradient (:fill-color-gradient value)
props #js {:id (dm/str "fill-color-gradient_" render-id "_" fill-index)
props #js {:id (dm/str "fill-color-gradient-" render-id "-" fill-index)
:key (dm/str fill-index)
:gradient gradient
:shape shape}]
@ -84,13 +89,23 @@
(-> (obj/set! "width" (* width no-repeat-padding))
(obj/set! "height" (* height no-repeat-padding)))))
[:g
(for [[fill-index value] (reverse (d/enumerate fills))]
(for [[fill-index value] (reverse (d/enumerate (get shape :fills [])))]
(let [style (attrs/get-fill-style value fill-index render-id type)
props #js {:key (dm/str fill-index)
:width width
:height height
:style style}]
[:> :rect props]))
(if (:fill-image value)
(let [uri (cfg/resolve-file-media (:fill-image value))
image-props #js {:id (dm/str "fill-image-" render-id "-" fill-index)
:href (get embed uri uri)
:preserveAspectRatio "xMidYMid slice"
:width width
:height height
:key (dm/str fill-index)
:opacity (:fill-opacity value)}]
[:> :image image-props])
[:> :rect props])))
(when ^boolean has-image?
[:g
@ -121,5 +136,6 @@
(or (= type :image)
(= type :text))
(> (count fills) 1)
(some :fill-color-gradient fills))
(some :fill-color-gradient fills)
(some :fill-image fills))
[:> fills* props])))

View file

@ -125,7 +125,7 @@
id (if (some? id)
id
(dm/str (name attr) "_" rid))
(dm/str (name attr) "-" rid))
gradient (get shape attr)
props #js {:id id

View file

@ -54,7 +54,7 @@
(attrs/add-border-props! shape))
get-gradient-id
(fn [index]
(str render-id "_" (:id shape) "_" index))]
(str render-id "-" (:id shape) "-" index))]
[:*
;; Definition of gradients for partial elements
@ -62,7 +62,7 @@
[:defs
(for [[index data] (d/enumerate position-data)]
(when (some? (:fill-color-gradient data))
(let [id (dm/str "fill-color-gradient_" (get-gradient-id index))]
(let [id (dm/str "fill-color-gradient-" (get-gradient-id index))]
[:& grad/gradient {:id id
:key id
:attr :fill-color-gradient
@ -99,6 +99,6 @@
(obj/merge! browser-props)))
shape (assoc shape :fills (:fills data))]
[:& (mf/provider muc/render-id) {:key index :value (str render-id "_" (:id shape) "_" index)}
[:& (mf/provider muc/render-id) {:key index :value render-id}
[:& shape-custom-strokes {:shape shape :position index :render-id render-id}
[:> :text props (:text data)]]]))]]))

View file

@ -8,6 +8,8 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.colors :as cc]
[app.common.media :as cm]
[app.config :as cf]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.color-bullet :refer [color-bullet color-name]]
@ -49,36 +51,64 @@
colors-library (get-colors-library color)
file-colors (get-file-colors)
color-library-name (get-in (or colors-library file-colors) [(:id color) :name])
color (assoc color :color-library-name color-library-name)]
color (assoc color :color-library-name color-library-name)
image (:image color)]
(if new-css-system
[:div {:class (stl/css :attributes-color-row)}
[:div {:class (stl/css :bullet-wrapper)
:style #js {"--bullet-size" "16px"}}
[:& cbn/color-bullet {:color color
:mini? true}]]
[:*
[:div {:class (stl/css :attributes-color-row)}
[:div {:class (stl/css :bullet-wrapper)
:style #js {"--bullet-size" "16px"}}
[:& cbn/color-bullet {:color color
:mini? true}]]
[:div {:class (stl/css :format-wrapper)}
(when-not (and on-change-format (:gradient color))
(when-not image
[:div {:class (stl/css :format-wrapper)}
(when-not (and on-change-format (or (:gradient color) image))
[:div {:class (stl/css :select-format-wrapper)}
[:& select
{:default-value format
:options [{:value :hex :label (tr "inspect.attributes.color.hex")}
{:value :rgba :label (tr "inspect.attributes.color.rgba")}
{:value :hsla :label (tr "inspect.attributes.color.hsla")}]
:on-change on-change-format}]])
(when (:gradient color)
[:div {:class (stl/css :format-info)} "rgba"])])
[:div {:class (stl/css :select-format-wrapper)}
[:& select
{:default-value format
:options [{:value :hex :label (tr "inspect.attributes.color.hex")}
{:value :rgba :label (tr "inspect.attributes.color.rgba")}
{:value :hsla :label (tr "inspect.attributes.color.hsla")}]
:on-change on-change-format}]])
(when (:gradient color)
[:div {:class (stl/css :format-info)} "rgba"])]
(if (and copy-data (not image))
[:& copy-button {:data copy-data
:class (stl/css :color-row-copy-btn)}
[:*
[:div {:class (stl/css :first-row)}
[:div {:class (stl/css :name-opacity)}
[:span {:class (stl/css-case :color-value-wrapper true
:gradient-name (:gradient color))}
(if (:gradient color)
[:& cbn/color-name {:color color
:size 80}]
(case format
:hex [:& cbn/color-name {:color color
:size 80}]
:rgba (let [[r g b a] (cc/hex->rgba (:color color) (:opacity color))]
[:* (str/fmt "%s, %s, %s, %s" r g b a)])
:hsla (let [[h s l a] (cc/hex->hsla (:color color) (:opacity color))
result (cc/format-hsla [h s l a])]
[:* result])))]
(if copy-data
[:& copy-button {:data copy-data
:class (stl/css :color-row-copy-btn)}
[:*
(when-not (:gradient color)
[:span {:class (stl/css :opacity-info)}
(str (* 100 (:opacity color)) "%")])]]
(when color-library-name
[:div {:class (stl/css :second-row)}
[:div {:class (stl/css :color-name-library)}
color-library-name]])]]
[:div {:class (stl/css :color-info)}
[:div {:class (stl/css :first-row)}
[:div {:class (stl/css :name-opacity)}
[:span {:class (stl/css-case :color-value-wrapper true
:gradient-name (:gradient color))}
:gradient-name (:gradient color))}
(if (:gradient color)
[:& cbn/color-name {:color color
:size 80}]
@ -98,90 +128,66 @@
(when color-library-name
[:div {:class (stl/css :second-row)}
[:div {:class (stl/css :color-name-library)}
color-library-name]])]]
color-library-name]])])]
[:div {:class (stl/css :color-info)}
[:div {:class (stl/css :first-row)}
[:div {:class (stl/css :name-opacity)}
[:span {:class (stl/css-case :color-value-wrapper true
:gradient-name (:gradient color))}
(if (:gradient color)
[:& cbn/color-name {:color color
:size 80}]
(case format
:hex [:& cbn/color-name {:color color
:size 80}]
:rgba (let [[r g b a] (cc/hex->rgba (:color color) (:opacity color))]
[:* (str/fmt "%s, %s, %s, %s" r g b a)])
:hsla (let [[h s l a] (cc/hex->hsla (:color color) (:opacity color))
result (cc/format-hsla [h s l a])]
[:* result])))]
(when image
(let [mtype (-> image :mtype)
name (or (:name image) (tr "media.image"))
extension (cm/mtype->extension mtype)]
[:a {:class (stl/css :download-button)
:target "_blank"
:download (cond-> name extension (str/concat extension))
:href (cf/resolve-file-media image)}
(tr "inspect.attributes.image.download")]))]
(when-not (:gradient color)
[:span {:class (stl/css :opacity-info)}
(str (* 100 (:opacity color)) "%")])]]
[:*
[:div.attributes-color-row
(when color-library-name
[:div.attributes-color-id
[:& color-bullet {:color color}]
[:div color-library-name]])
(when color-library-name
[:div {:class (stl/css :second-row)}
[:div {:class (stl/css :color-name-library)}
color-library-name]])
;; [:span {:class (stl/css-case :color-name-wrapper true
;; :gradient-color (:gradient color))}
[:div.attributes-color-value {:class (when color-library-name "hide-color")}
[:& color-bullet {:color color}]
;; [:div {:class (stl/css :color-value-wrapper)}
;; (if (:gradient color)
;; [:& cbn/color-name {:color color
;; :size 80}]
;; (case format
;; :hex [:& cbn/color-name {:color color
;; :size 80}]
;; :rgba (let [[r g b a] (cc/hex->rgba (:color color) (:opacity color))]
;; [:* (str/fmt "%s, %s, %s, %s" r g b a)])
;; :hsla (let [[h s l a] (cc/hex->hsla (:color color) (:opacity color))
;; result (cc/format-hsla [h s l a])]
;; [:* result])))]
(cond
(:gradient color)
[:& color-name {:color color}]
;; (when color-library-name
;; [:div {:class (stl/css :color-name-library)}
;; color-library-name])]
(= format :rgba)
(let [[r g b a] (cc/hex->rgba (:color color) (:opacity color))]
[:div (str/fmt "%s, %s, %s, %s" r g b a)])
;; (when-not (:gradient color)
;; [:div {:class (stl/css :opacity-info)}
;; (str (* 100 (:opacity color)) "%")])
])]
(= format :hsla)
(let [[h s l a] (cc/hex->hsla (:color color) (:opacity color))
result (cc/format-hsla [h s l a])]
[:div result])
:else
[:*
[:& color-name {:color color}]
(when-not (:gradient color) [:div (str (* 100 (:opacity color)) "%")])])
[:div.attributes-color-row
(when color-library-name
[:div.attributes-color-id
[:& color-bullet {:color color}]
[:div color-library-name]])
(when-not (and on-change-format (or (:gradient color) image))
[:select.color-format-select {:on-change #(-> (dom/get-target-val %) keyword on-change-format)}
[:option {:value "hex"}
(tr "inspect.attributes.color.hex")]
[:div.attributes-color-value {:class (when color-library-name "hide-color")}
[:& color-bullet {:color color}]
[:option {:value "rgba"}
(tr "inspect.attributes.color.rgba")]
(if (:gradient color)
[:& color-name {:color color}]
(case format
:rgba (let [[r g b a] (cc/hex->rgba (:color color) (:opacity color))]
[:div (str/fmt "%s, %s, %s, %s" r g b a)])
:hsla (let [[h s l a] (cc/hex->hsla (:color color) (:opacity color))
result (cc/format-hsla [h s l a])]
[:div result])
[:*
[:& color-name {:color color}]
(when-not (:gradient color) [:div (str (* 100 (:opacity color)) "%")])]))
[:option {:value "hsla"}
(tr "inspect.attributes.color.hsla")]])]
(when-not (and on-change-format (:gradient color))
[:select.color-format-select {:on-change #(-> (dom/get-target-val %) keyword on-change-format)}
[:option {:value "hex"}
(tr "inspect.attributes.color.hex")]
(when (and copy-data (not image))
[:& copy-button {:data copy-data}])]
[:option {:value "rgba"}
(tr "inspect.attributes.color.rgba")]
[:option {:value "hsla"}
(tr "inspect.attributes.color.hsla")]])]
(when copy-data
[:& copy-button {:data copy-data}])])))
(when image
(let [mtype (-> image :mtype)
name (or (:name image) (tr "media.image"))
extension (cm/mtype->extension mtype)]
[:a.download-button {:target "_blank"
:download (cond-> name extension (str/concat extension))
:href (cf/resolve-file-media image)}
(tr "inspect.attributes.image.download")]))])))

View file

@ -72,7 +72,6 @@
@include titleTipography;
color: var(--menu-foreground-color);
padding: $s-8 0;
height: $s-32;
}
button {
@ -129,3 +128,10 @@
}
}
}
.download-button {
@extend .button-secondary;
@include tabTitleTipography;
height: $s-32;
margin-top: $s-4;
}

View file

@ -21,7 +21,8 @@
:opacity (:fill-opacity shape)
:gradient (:fill-color-gradient shape)
:id (:fill-color-ref-id shape)
:file-id (:fill-color-ref-file shape)})
:file-id (:fill-color-ref-file shape)
:image (:fill-image shape)})
(defn has-fill? [shape]
(and

View file

@ -25,7 +25,8 @@
:opacity (:stroke-opacity shape)
:gradient (:stroke-color-gradient shape)
:id (:stroke-color-ref-id shape)
:file-id (:stroke-color-ref-file shape)})
:file-id (:stroke-color-ref-file shape)
:image (:stroke-image shape)})
(defn has-stroke? [shape]
(seq (:strokes shape)))

View file

@ -35,12 +35,13 @@
(get-in state [:viewer-libraries file-id :data :typographies]))]
#(l/derived get-library st/state)))
(defn fill->color [{:keys [fill-color fill-opacity fill-color-gradient fill-color-ref-id fill-color-ref-file]}]
(defn fill->color [{:keys [fill-color fill-opacity fill-color-gradient fill-color-ref-id fill-color-ref-file fill-image]}]
{:color fill-color
:opacity fill-opacity
:gradient fill-color-gradient
:id fill-color-ref-id
:file-id fill-color-ref-file})
:file-id fill-color-ref-file
:image fill-image})
(defn copy-style-data
[style & properties]

View file

@ -11,6 +11,7 @@
[app.common.types.component :as ctk]
[app.main.refs :as refs]
[app.main.ui.components.shape-icon :as si]
[app.main.ui.components.shape-icon-refactor :as sir]
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
[app.main.ui.components.tabs-container :refer [tabs-container tabs-element]]
[app.main.ui.context :as ctx]
@ -97,7 +98,7 @@
[:span {:class (stl/css :layer-title)} (tr "inspect.tabs.code.selected.multiple" (count shapes))]]
[:*
[:span {:class (stl/css :shape-icon)}
[:& si/element-icon-refactor {:shape first-shape :main-instance? main-instance?}]]
[:& sir/element-icon-refactor {:shape first-shape :main-instance? main-instance?}]]
;; Execution time translation strings:
;; inspect.tabs.code.selected.circle
;; inspect.tabs.code.selected.component

View file

@ -38,7 +38,7 @@
(mf/defc palette
[{:keys [current-colors size width]}]
(let [;; We had to do this due to a bug that leave some bugged colors
current-colors (h/use-equal-memo (filter #(or (:gradient %) (:color %)) current-colors))
current-colors (h/use-equal-memo (filter #(or (:gradient %) (:color %) (:image %)) current-colors))
state (mf/use-state {:show-menu false})
offset-step (cond
(<= size 64) 40

View file

@ -37,7 +37,7 @@
(mf/defc palette
[{:keys [current-colors recent-colors file-colors shared-libs selected on-select]}]
(let [;; We had to do this due to a bug that leave some bugged colors
current-colors (h/use-equal-memo (filter #(or (:gradient %) (:color %)) current-colors))
current-colors (h/use-equal-memo (filter #(or (:gradient %) (:color %) (:image %)) current-colors))
state (mf/use-state {:show-menu false})
width (:width @state 0)

View file

@ -8,12 +8,17 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.colors :as cc]
[app.common.data :as d]
[app.config :as cfg]
[app.main.data.modal :as modal]
[app.main.data.workspace.colors :as dc]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.media :as dwm]
[app.main.data.workspace.undo :as dwu]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.select :refer [select]]
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
[app.main.ui.context :as ctx]
[app.main.ui.icons :as i]
@ -46,22 +51,50 @@
;; --- Color Picker Modal
(mf/defc colorpicker
[{:keys [data disable-gradient disable-opacity on-change on-accept]}]
(let [new-css-system (mf/use-ctx ctx/new-css-system)
state (mf/deref refs/colorpicker)
node-ref (mf/use-ref)
[{:keys [data disable-gradient disable-opacity disable-image on-change on-accept]}]
(let [new-css-system (mf/use-ctx ctx/new-css-system)
state (mf/deref refs/colorpicker)
node-ref (mf/use-ref)
;; TODO: I think we need to put all this picking state under
;; the same object for avoid creating adhoc refs for each
;; value
picking-color? (mf/deref picking-color?)
picked-color (mf/deref picked-color)
picked-color-select (mf/deref picked-color-select)
picking-color? (mf/deref picking-color?)
picked-color (mf/deref picked-color)
picked-color-select (mf/deref picked-color-select)
current-color (:current-color state)
current-color (:current-color state)
active-tab (mf/use-state (dc/get-active-color-tab))
drag? (mf/use-state false)
active-fill-tab (if (:image data)
:image
(if-let [gradient (:gradient data)]
(case (:type gradient)
:linear :linear-gradient
:radial :radial-gradient)
:color))
active-color-tab (mf/use-state (dc/get-active-color-tab))
drag? (mf/use-state false)
fill-image-ref (mf/use-ref nil)
selected-mode (get state :type :color)
disabled-color-accept? (and
(= selected-mode :image)
(not (:image current-color)))
on-fill-image-success
(mf/use-fn
(fn [image]
(st/emit! (dc/update-colorpicker-color {:image (select-keys image [:id :width :height :mtype :name])} (not @drag?)))))
on-fill-image-click
(mf/use-callback #(dom/click (mf/ref-val fill-image-ref)))
on-fill-image-selected
(mf/use-fn
(fn [file]
(st/emit! (dwm/upload-fill-image file on-fill-image-success))))
set-tab!
(mf/use-fn
@ -69,9 +102,18 @@
(let [tab (-> (dom/get-current-target event)
(dom/get-data "tab")
(keyword))]
(reset! active-tab tab)
(reset! active-color-tab tab)
(dc/set-active-color-tab! tab))))
handle-change-mode
(mf/use-fn
(fn [value]
(case value
:color (st/emit! (dc/activate-colorpicker-color))
:linear-gradient (st/emit! (dc/activate-colorpicker-gradient :linear-gradient))
:radial-gradient (st/emit! (dc/activate-colorpicker-gradient :radial-gradient))
:image (st/emit! (dc/activate-colorpicker-image)))))
handle-change-color
(mf/use-fn
(mf/deps current-color @drag?)
@ -105,7 +147,7 @@
on-select-library-color
(mf/use-fn
(fn [state color]
(let [type-origin (:type state)
(let [type-origin selected-mode
editig-stop-origin (:editing-stop state)
is-gradient? (some? (:gradient color))
change-to (fn [new-color]
@ -149,12 +191,6 @@
(fn [_]
(st/emit! (dwl/add-color (dc/get-color-from-colorpicker-state state)))))
on-activate-linear-gradient
(mf/use-fn #(st/emit! (dc/activate-colorpicker-gradient :linear-gradient)))
on-activate-radial-gradient
(mf/use-fn #(st/emit! (dc/activate-colorpicker-gradient :radial-gradient)))
on-start-drag
(mf/use-fn
(mf/deps drag? node-ref)
@ -174,11 +210,21 @@
(mf/deps state)
(fn []
(on-accept (dc/get-color-from-colorpicker-state state))
(modal/hide!)))]
(modal/hide!)))
options
(mf/with-memo [selected-mode disable-gradient disable-image]
(d/concat-vec
[{:value :color :label (tr "media.solid")}]
(when (not disable-gradient)
[{:value :linear-gradient :label (tr "media.linear")}
{:value :radial-gradient :label (tr "media.radial")}])
(when (not disable-image)
[{:value :image :label (tr "media.image")}])))]
;; Initialize colorpicker state
(mf/with-effect []
(st/emit! (dc/initialize-colorpicker on-change))
(st/emit! (dc/initialize-colorpicker on-change active-fill-tab))
(partial st/emit! (dc/finalize-colorpicker)))
;; Update colorpicker with external color changes
@ -220,181 +266,219 @@
:ref node-ref
:style {:touch-action "none"}}
[:div {:class (stl/css :top-actions)}
[:button {:class (stl/css-case :picker-btn true
:selected picking-color?)
:on-click handle-click-picker}
i/picker-refactor]
(when (not disable-gradient)
[:div {:class (stl/css :gradient-buttons)}
[:button
{:on-click on-activate-linear-gradient
:class (stl/css-case :gradient-btn true
:linear-gradient-btn true
:selected (= :linear-gradient (:type state)))}]
(when (or (not disable-gradient) (not disable-image))
[:div {:class (stl/css :select)}
[:& select
{:default-value selected-mode
:options options
:on-change handle-change-mode}]])
(when (not= selected-mode :image)
[:button {:class (stl/css-case :picker-btn true
:selected picking-color?)
:on-click handle-click-picker}
i/picker-refactor])]
[:button
{:on-click on-activate-radial-gradient
:class (stl/css-case :gradient-btn true
:radial-gradient-btn true
:selected (= :radial-gradient (:type state)))}]])]
(when (or (= (:type state) :linear-gradient)
(= (:type state) :radial-gradient))
(when (or (= selected-mode :linear-gradient)
(= selected-mode :radial-gradient))
[:& gradients
{:stops (:stops state)
:editing-stop (:editing-stop state)
:on-select-stop handle-change-stop}])
[:div {:class (stl/css :colorpicker-tabs)}
[:& tab-container
{:on-change-tab set-tab!
:selected @active-tab
:collapsable? false}
(if (= selected-mode :image)
(let [uri (cfg/resolve-file-media (:image current-color))]
[:div {:class (stl/css :select-image)}
[:div {:class (stl/css :content)}
(when (:image current-color)
[:img {:src uri}])]
[:button
{:class (stl/css :choose-image)
:title (tr "media.choose-image")
:aria-label (tr "media.choose-image")
:on-click on-fill-image-click}
(tr "media.choose-image")
[:& file-uploader
{:input-id "fill-image-upload"
:accept "image/jpeg,image/png"
:multi false
:ref fill-image-ref
:on-selected on-fill-image-selected}]]])
[:*
[:div {:class (stl/css :colorpicker-tabs)}
[:& tab-container
{:on-change-tab set-tab!
:selected @active-color-tab
:collapsable? false}
[:& tab-element {:id :ramp :title i/rgba-refactor}
(if picking-color?
[:div {:class (stl/css :picker-detail-wrapper)}
[:div {:class (stl/css :center-circle)}]
[:canvas#picker-detail {:width 256 :height 140}]]
[:& ramp-selector
{:color current-color
:disable-opacity disable-opacity
:on-change handle-change-color
:on-start-drag on-start-drag
:on-finish-drag on-finish-drag}])]
[:& tab-element {:id :ramp :title i/rgba-refactor}
(if picking-color?
[:div {:class (stl/css :picker-detail-wrapper)}
[:div {:class (stl/css :center-circle)}]
[:canvas#picker-detail {:width 256 :height 140}]]
[:& ramp-selector
{:color current-color
:disable-opacity disable-opacity
:on-change handle-change-color
:on-start-drag on-start-drag
:on-finish-drag on-finish-drag}])]
[:& tab-element {:id :harmony :title i/rgba-complementary-refactor}
(if picking-color?
[:div {:class (stl/css :picker-detail-wrapper)}
[:div {:class (stl/css :center-circle)}]
[:canvas#picker-detail {:width 256 :height 140}]]
[:& harmony-selector
{:color current-color
:disable-opacity disable-opacity
:on-change handle-change-color
:on-start-drag on-start-drag
:on-finish-drag on-finish-drag}])]
[:& tab-element {:id :harmony :title i/rgba-complementary-refactor}
(if picking-color?
[:div {:class (stl/css :picker-detail-wrapper)}
[:div {:class (stl/css :center-circle)}]
[:canvas#picker-detail {:width 256 :height 140}]]
[:& harmony-selector
{:color current-color
:disable-opacity disable-opacity
:on-change handle-change-color
:on-start-drag on-start-drag
:on-finish-drag on-finish-drag}])]
[:& tab-element {:id :hsva :title i/hsva-refactor}
(if picking-color?
[:div {:class (stl/css :picker-detail-wrapper)}
[:div {:class (stl/css :center-circle)}]
[:canvas#picker-detail {:width 256 :height 140}]]
[:& hsva-selector
{:color current-color
:disable-opacity disable-opacity
:on-change handle-change-color
:on-start-drag on-start-drag
:on-finish-drag on-finish-drag}])]]]
[:& tab-element {:id :hsva :title i/hsva-refactor}
(if picking-color?
[:div {:class (stl/css :picker-detail-wrapper)}
[:div {:class (stl/css :center-circle)}]
[:canvas#picker-detail {:width 256 :height 140}]]
[:& hsva-selector
{:color current-color
:disable-opacity disable-opacity
:on-change handle-change-color
:on-start-drag on-start-drag
:on-finish-drag on-finish-drag}])]]]
[:& color-inputs
{:type (if (= @active-tab :hsva) :hsv :rgb)
:disable-opacity disable-opacity
:color current-color
:on-change handle-change-color}]
[:& color-inputs
{:type (if (= @active-color-tab :hsva) :hsv :rgb)
:disable-opacity disable-opacity
:color current-color
:on-change handle-change-color}]
[:& libraries
{:state state
:current-color current-color
:on-select-color on-select-library-color
:on-add-library-color on-add-library-color}]
[:& libraries
{:state state
:current-color current-color
:disable-gradient disable-gradient
:disable-opacity disable-opacity
:disable-image disable-image
:on-select-color on-select-library-color
:on-add-library-color on-add-library-color}]])
(when on-accept
[:div {:class (stl/css :actions)}
[:button {:class (stl/css :accept-color)
:on-click on-color-accept}
[:button {:class (stl/css-case
:accept-color true
:btn-disabled disabled-color-accept?)
:on-click on-color-accept
:disabled disabled-color-accept?}
(tr "workspace.libraries.colors.save-color")]])]
[:div.colorpicker {:ref node-ref
:style {:touch-action "none"}}
[:div.colorpicker-content
[:div.top-actions
[:button.picker-btn
{:class (when picking-color? "active")
:on-click handle-click-picker}
i/picker]
(when (not disable-gradient)
[:div.gradients-buttons
[:button.gradient.linear-gradient
{:on-click on-activate-linear-gradient
:class (when (= :linear-gradient (:type state)) "active")}]
[:button.gradient.radial-gradient
{:on-click on-activate-radial-gradient
:class (when (= :radial-gradient (:type state)) "active")}]])]
(when (or (not disable-gradient) (not disable-image))
[:div.element-set-content
[:& select
{:default-value selected-mode
:options options
:on-change handle-change-mode}]])
(when (not= selected-mode :image)
[:button.picker-btn
{:class (when picking-color? "active")
:on-click handle-click-picker}
i/picker])]
(when (or (= (:type state) :linear-gradient)
(= (:type state) :radial-gradient))
(= (:type state) :radial-gradient))
[:& gradients
{:stops (:stops state)
:editing-stop (:editing-stop state)
:on-select-stop handle-change-stop}])
[:div.colorpicker-tabs
[:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand
{:class (when (= @active-tab :ramp) "active")
:alt (tr "workspace.libraries.colors.rgba")
:on-click set-tab!
:data-tab "ramp"} i/picker-ramp]
[:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand
{:class (when (= @active-tab :harmony) "active")
:alt (tr "workspace.libraries.colors.rgb-complementary")
:on-click set-tab!
:data-tab "harmony"} i/picker-harmony]
[:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand
{:class (when (= @active-tab :hsva) "active")
:alt (tr "workspace.libraries.colors.hsv")
:on-click set-tab!
:data-tab "hsva"} i/picker-hsv]]
(if (= selected-mode :image)
(let [uri (cfg/resolve-file-media (:image current-color))]
[:div.select-image
[:div.content
(when (:image current-color)
[:img {:src uri}])]
[:button.btn-secondary
{:title (tr "media.choose-image")
:aria-label (tr "media.choose-image")
:on-click on-fill-image-click}
(tr "media.choose-image")
[:& file-uploader
{:input-id "fill-image-upload"
:accept "image/jpeg,image/png"
:multi false
:ref fill-image-ref
:on-selected on-fill-image-selected}]]])
[:*
[:div.colorpicker-tabs
[:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand
{:class (when (= @active-color-tab :ramp) "active")
:alt (tr "workspace.libraries.colors.rgba")
:on-click set-tab!
:data-tab "ramp"} i/picker-ramp]
[:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand
{:class (when (= @active-color-tab :harmony) "active")
:alt (tr "workspace.libraries.colors.rgb-complementary")
:on-click set-tab!
:data-tab "harmony"} i/picker-harmony]
[:div.colorpicker-tab.tooltip.tooltip-bottom.tooltip-expand
{:class (when (= @active-color-tab :hsva) "active")
:alt (tr "workspace.libraries.colors.hsv")
:on-click set-tab!
:data-tab "hsva"} i/picker-hsv]]
(if picking-color?
[:div.picker-detail-wrapper
[:div.center-circle]
[:canvas#picker-detail {:width 200 :height 160}]]
(case @active-tab
:ramp
[:& ramp-selector
{:color current-color
:disable-opacity disable-opacity
:on-change handle-change-color
:on-start-drag on-start-drag
:on-finish-drag on-finish-drag}]
:harmony
[:& harmony-selector
{:color current-color
:disable-opacity disable-opacity
:on-change handle-change-color
:on-start-drag on-start-drag
:on-finish-drag on-finish-drag}]
:hsva
[:& hsva-selector
{:color current-color
:disable-opacity disable-opacity
:on-change handle-change-color
:on-start-drag on-start-drag
:on-finish-drag on-finish-drag}]
nil))
(if picking-color?
[:div.picker-detail-wrapper
[:div.center-circle]
[:canvas#picker-detail {:width 200 :height 160}]]
(case @active-color-tab
:ramp
[:& ramp-selector
{:color current-color
:disable-opacity disable-opacity
:on-change handle-change-color
:on-start-drag on-start-drag
:on-finish-drag on-finish-drag}]
:harmony
[:& harmony-selector
{:color current-color
:disable-opacity disable-opacity
:on-change handle-change-color
:on-start-drag on-start-drag
:on-finish-drag on-finish-drag}]
:hsva
[:& hsva-selector
{:color current-color
:disable-opacity disable-opacity
:on-change handle-change-color
:on-start-drag on-start-drag
:on-finish-drag on-finish-drag}]
nil))
[:& color-inputs
{:type (if (= @active-tab :hsva) :hsv :rgb)
:disable-opacity disable-opacity
:color current-color
:on-change handle-change-color}]
[:& color-inputs
{:type (if (= @active-color-tab :hsva) :hsv :rgb)
:disable-opacity disable-opacity
:color current-color
:on-change handle-change-color}]
[:& libraries
{:state state
:current-color current-color
:disable-gradient disable-gradient
:disable-opacity disable-opacity
:on-select-color on-select-library-color
:on-add-library-color on-add-library-color}]
[:& libraries
{:state state
:current-color current-color
:disable-gradient disable-gradient
:disable-opacity disable-opacity
:disable-image disable-image
:on-select-color on-select-library-color
:on-add-library-color on-add-library-color}]])
(when on-accept
[:div.actions
[:button.btn-primary.btn-large
{:on-click on-color-accept}
{:on-click on-color-accept
:disabled disabled-color-accept?
:class (dom/classnames
:btn-disabled disabled-color-accept?)}
(tr "workspace.libraries.colors.save-color")]])]])))
(defn calculate-position
@ -420,6 +504,7 @@
[{:keys [x y data position
disable-gradient
disable-opacity
disable-image
on-change on-close on-accept] :as props}]
(let [new-css-system (mf/use-ctx ctx/new-css-system)
vport (mf/deref viewport)
@ -445,6 +530,7 @@
[:& colorpicker {:data data
:disable-gradient disable-gradient
:disable-opacity disable-opacity
:disable-image disable-image
:on-change handle-change
:on-accept on-accept}]]))

View file

@ -20,8 +20,9 @@
.top-actions {
display: flex;
align-items: flex-start;
flex-direction: row-reverse;
justify-content: space-between;
height: $s-28;
height: $s-40;
.picker-btn {
@include buttonStyle;
@include flexCenter;
@ -32,6 +33,7 @@
width: $s-20;
border-radius: $br-4;
padding: 0;
margin-top: $s-4;
svg {
@extend .button-icon;
stroke: var(--button-tertiary-foreground-color-rest);
@ -96,7 +98,7 @@
display: flex;
gap: $s-4;
.accept-color {
@include titleTipography;
@include tabTitleTipography;
@extend .button-secondary;
width: 100%;
height: $s-32;
@ -122,3 +124,36 @@
}
}
}
.select {
width: $s-116;
}
.select-image {
margin-top: $s-4;
.content {
border-radius: $br-8;
display: flex;
justify-content: center;
background-image: url("/images/colorpicker-no-image.png");
background-position: center;
background-size: auto $s-140;
height: $s-140;
margin-bottom: $s-6;
margin-right: $s-1;
img {
height: fit-content;
width: fit-content;
max-height: 100%;
max-width: 100%;
margin: auto;
}
}
.choose-image {
@extend .button-secondary;
@include tabTitleTipography;
width: 100%;
margin-top: $s-12;
height: $s-32;
}
}

View file

@ -27,7 +27,7 @@
[rumext.v2 :as mf]))
(mf/defc libraries
[{:keys [state on-select-color on-add-library-color disable-gradient disable-opacity]}]
[{:keys [state on-select-color on-add-library-color disable-gradient disable-opacity disable-image]}]
(let [new-css-system (mf/use-ctx ctx/new-css-system)
selected (h/use-shared-state mdc/colorpicker-selected-broadcast-key :recent)
current-colors (mf/use-state [])
@ -35,7 +35,7 @@
shared-libs (mf/deref refs/workspace-libraries)
file-colors (mf/deref refs/workspace-file-colors)
recent-colors (mf/deref refs/workspace-recent-colors)
recent-colors (h/use-equal-memo (filter #(or (:gradient %) (:color %)) recent-colors))
recent-colors (h/use-equal-memo (filter #(or (:gradient %) (:color %) (:image %)) recent-colors))
on-library-change
(mf/use-fn
@ -52,7 +52,8 @@
check-valid-color?
(fn [color]
(and (or (not disable-gradient) (not (:gradient color)))
(or (not disable-opacity) (= 1 (:opacity color)))))
(or (not disable-opacity) (= 1 (:opacity color)))
(or (not disable-image) (not (:image color)))))
toggle-palette
(mf/use-fn

View file

@ -32,10 +32,12 @@
:opacity (:fill-opacity fill)
:id color-id
:file-id color-file-id
:gradient (:fill-color-gradient fill)})
:gradient (:fill-color-gradient fill)
:image (:fill-image fill)})
(d/without-nils {:color (str/lower (:fill-color fill))
:opacity (:fill-opacity fill)
:gradient (:fill-color-gradient fill)}))]
:gradient (:fill-color-gradient fill)
:image (:fill-image fill)}))]
{:attrs attrs
:prop :fill
:shape-id (:shape-id fill)
@ -47,16 +49,18 @@
color-id (:stroke-color-ref-id stroke)
shared-libs-colors (dm/get-in shared-libs [color-file-id :data :colors])
is-shared? (contains? shared-libs-colors color-id)
has-color? (not (nil? (:stroke-color stroke)))
has-color? (or (not (nil? (:stroke-color stroke))) (not (nil? (:stroke-image stroke))) )
attrs (if (or is-shared? (= color-file-id file-id))
(d/without-nils {:color (str/lower (:stroke-color stroke))
:opacity (:stroke-opacity stroke)
:id color-id
:file-id color-file-id
:gradient (:stroke-color-gradient stroke)})
:gradient (:stroke-color-gradient stroke)
:image (:stroke-image stroke)})
(d/without-nils {:color (str/lower (:stroke-color stroke))
:opacity (:stroke-opacity stroke)
:gradient (:stroke-color-gradient stroke)}))]
:gradient (:stroke-color-gradient stroke)
:image (:stroke-image stroke)}))]
(when has-color?
{:attrs attrs
:prop :stroke

View file

@ -163,7 +163,8 @@
:opacity (:fill-opacity value)
:id (:fill-color-ref-id value)
:file-id (:fill-color-ref-file value)
:gradient (:fill-color-gradient value)}
:gradient (:fill-color-gradient value)
:image (:fill-image value)}
:key index
:index index
:title (tr "workspace.options.fill")
@ -215,7 +216,8 @@
:opacity (:fill-opacity value)
:id (:fill-color-ref-id value)
:file-id (:fill-color-ref-file value)
:gradient (:fill-color-gradient value)}
:gradient (:fill-color-gradient value)
:image (:fill-image value)}
:key index
:index index
:title (tr "workspace.options.fill")

View file

@ -105,9 +105,10 @@
(mf/use-fn
(mf/deps grid)
(fn [color]
(-> grid
(update :params assoc :color color)
(on-change))))
(let [color (dissoc color :id :file-id)]
(-> grid
(update :params assoc :color color)
(on-change)))))
handle-detach-color
(mf/use-fn
@ -189,6 +190,7 @@
[:& color-row {:color (:color params)
:title (tr "workspace.options.grid.params.color")
:disable-gradient true
:disable-image true
:on-change handle-change-color
:on-detach handle-detach-color}]
[:button {:class (stl/css :show-more-options)
@ -228,6 +230,7 @@
[:& color-row {:color (:color params)
:title (tr "workspace.options.grid.params.color")
:disable-gradient true
:disable-image true
:on-change handle-change-color
:on-detach handle-detach-color}]]]
@ -384,6 +387,7 @@
[:& color-row {:color (:color params)
:title (tr "workspace.options.grid.params.color")
:disable-gradient true
:disable-image true
:on-change handle-change-color
:on-detach handle-detach-color}]
[:div.row-flex

View file

@ -115,7 +115,8 @@
(fn [color]
(st/emit! (dch/update-shapes
ids
#(assoc-in % [:shadow index :color] color)))))
#(assoc-in % [:shadow index :color]
(dissoc color :id :file-id))))))
detach-color
(mf/use-fn
@ -242,6 +243,7 @@
(:color value))
:title (tr "workspace.options.shadow-options.color")
:disable-gradient true
:disable-image true
:on-change update-color
:on-detach detach-color
:on-open manage-on-open
@ -335,6 +337,7 @@
(:color value))
:title (tr "workspace.options.shadow-options.color")
:disable-gradient true
:disable-image true
:on-change update-color
:on-detach detach-color
:on-open manage-on-open

View file

@ -38,6 +38,7 @@
[:& color-row
{:disable-gradient true
:disable-opacity true
:disable-image true
:title (tr "workspace.options.canvas-background")
:color {:color (get options :background clr/canvas)
:opacity 1}
@ -51,6 +52,7 @@
[:& color-row
{:disable-gradient true
:disable-opacity true
:disable-image true
:title (tr "workspace.options.canvas-background")
:color {:color (get options :background clr/canvas)
:opacity 1}

View file

@ -62,6 +62,8 @@
gradient-color? (and (not multiple-colors?)
(:gradient color)
(get-in color [:gradient :type]))
image-color? (and (not multiple-colors?)
(:image color))
editing-text* (mf/use-state false)
editing-text? (deref editing-text*)
@ -218,6 +220,12 @@
[:div {:class (stl/css :color-name)}
(uc/gradient-type->string (get-in color [:gradient :type]))]]
;; Rendering an image
image-color?
[:*
[:div {:class (stl/css :color-name)}
(tr "media.image")]]
;; Rendering a plain color
:else
[:span {:class (stl/css :color-input-wrapper)}

View file

@ -188,12 +188,13 @@
:dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot))
:ref dref}
;; Stroke Color
;; Stroke Color
[:& color-row {:color {:color (:stroke-color stroke)
:opacity (:stroke-opacity stroke)
:id (:stroke-color-ref-id stroke)
:file-id (:stroke-color-ref-file stroke)
:gradient (:stroke-color-gradient stroke)}
:gradient (:stroke-color-gradient stroke)
:image (:stroke-image stroke)}
:index index
:title title
:on-change on-color-change-refactor
@ -263,7 +264,8 @@
:opacity (:stroke-opacity stroke)
:id (:stroke-color-ref-id stroke)
:file-id (:stroke-color-ref-file stroke)
:gradient (:stroke-color-gradient stroke)}
:gradient (:stroke-color-gradient stroke)
:image (:stroke-image stroke)}
:index index
:title title
:on-change (on-color-change index)

View file

@ -74,7 +74,7 @@
(= file-id :multiple)))
(def empty-color
(into {} (map #(vector % nil)) [:color :id :file-id :gradient :opacity]))
(into {} (map #(vector % nil)) [:color :id :file-id :gradient :opacity :image]))
(defn get-color-name
[color]

View file

@ -101,6 +101,10 @@
(get-in (get-data m) [:attrs ns-att]))]
(when val (val-fn val)))))
(defn find-node-by-metadata-value
[meta value coll]
(->> coll (d/seek #(= value (get-meta % meta)))))
(defn get-children
[node]
(cond-> (:content node)
@ -429,7 +433,7 @@
fill-color-ref-file (get-meta node :fill-color-ref-file uuid/uuid)
meta-fill-color (get-meta node :fill-color)
meta-fill-opacity (get-meta node :fill-opacity)
meta-fill-color-gradient (if (str/starts-with? meta-fill-color "url")
meta-fill-color-gradient (if (str/starts-with? meta-fill-color "url#fill-color-gradient")
(parse-gradient node meta-fill-color)
(get-meta node :fill-color-gradient))
gradient (when (str/starts-with? fill "url")
@ -465,13 +469,14 @@
(defn add-stroke
[props node svg-data]
(let [stroke-style (get-meta node :stroke-style keyword)
(let [stroke-style (get-meta node :stroke-style keyword)
stroke-alignment (get-meta node :stroke-alignment keyword)
stroke (:stroke svg-data)
gradient (when (str/starts-with? stroke "url")
(parse-gradient node stroke))
stroke (:stroke svg-data)
gradient (when (str/starts-with? stroke "url(#stroke-color-gradient")
(parse-gradient node stroke))
stroke-cap-start (get-meta node :stroke-cap-start keyword)
stroke-cap-end (get-meta node :stroke-cap-end keyword)]
stroke-cap-end (get-meta node :stroke-cap-end keyword)]
(cond-> props
:always
@ -728,17 +733,22 @@
(defn parse-fills
[node svg-data]
(let [fills-node (get-data node :penpot:fills)
images (:images node)
fills (->> (find-all-nodes fills-node :penpot:fill)
(mapv (fn [fill-node]
{:fill-color (when (not (str/starts-with? (get-meta fill-node :fill-color) "url"))
(get-meta fill-node :fill-color))
:fill-color-gradient (when (str/starts-with? (get-meta fill-node :fill-color) "url")
(parse-gradient node (get-meta fill-node :fill-color)))
:fill-color-ref-file (get-meta fill-node :fill-color-ref-file uuid/uuid)
:fill-color-ref-id (get-meta fill-node :fill-color-ref-id uuid/uuid)
:fill-opacity (get-meta fill-node :fill-opacity d/parse-double)}))
(let [fill-image-id (get-meta fill-node :fill-image-id)]
{:fill-color (when (not (str/starts-with? (get-meta fill-node :fill-color) "url"))
(get-meta fill-node :fill-color))
:fill-color-gradient (when (str/starts-with? (get-meta fill-node :fill-color) "url(#fill-color-gradient")
(parse-gradient node (get-meta fill-node :fill-color)))
:fill-image (when fill-image-id
(get images fill-image-id))
:fill-color-ref-file (get-meta fill-node :fill-color-ref-file uuid/uuid)
:fill-color-ref-id (get-meta fill-node :fill-color-ref-id uuid/uuid)
:fill-opacity (get-meta fill-node :fill-opacity d/parse-double)})))
(mapv d/without-nils)
(filterv #(not= (:fill-color %) "none")))]
(if (seq fills)
fills
(->> [(-> (add-fill {} node svg-data)
@ -748,22 +758,27 @@
(defn parse-strokes
[node svg-data]
(let [strokes-node (get-data node :penpot:strokes)
images (:images node)
strokes (->> (find-all-nodes strokes-node :penpot:stroke)
(mapv (fn [stroke-node]
{:stroke-color (when (not (str/starts-with? (get-meta stroke-node :stroke-color) "url"))
(get-meta stroke-node :stroke-color))
:stroke-color-gradient (when (str/starts-with? (get-meta stroke-node :stroke-color) "url")
(parse-gradient node (get-meta stroke-node :stroke-color)))
:stroke-color-ref-file (get-meta stroke-node :stroke-color-ref-file uuid/uuid)
:stroke-color-ref-id (get-meta stroke-node :stroke-color-ref-id uuid/uuid)
:stroke-opacity (get-meta stroke-node :stroke-opacity d/parse-double)
:stroke-style (get-meta stroke-node :stroke-style keyword)
:stroke-width (get-meta stroke-node :stroke-width d/parse-double)
:stroke-alignment (get-meta stroke-node :stroke-alignment keyword)
:stroke-cap-start (get-meta stroke-node :stroke-cap-start keyword)
:stroke-cap-end (get-meta stroke-node :stroke-cap-end keyword)}))
(let [stroke-image-id (get-meta stroke-node :stroke-image-id)]
{:stroke-color (when (not (str/starts-with? (get-meta stroke-node :stroke-color) "url"))
(get-meta stroke-node :stroke-color))
:stroke-color-gradient (when (str/starts-with? (get-meta stroke-node :stroke-color) "url(#stroke-color-gradient")
(parse-gradient node (get-meta stroke-node :stroke-color)))
:stroke-image (when stroke-image-id
(get images stroke-image-id))
:stroke-color-ref-file (get-meta stroke-node :stroke-color-ref-file uuid/uuid)
:stroke-color-ref-id (get-meta stroke-node :stroke-color-ref-id uuid/uuid)
:stroke-opacity (get-meta stroke-node :stroke-opacity d/parse-double)
:stroke-style (get-meta stroke-node :stroke-style keyword)
:stroke-width (get-meta stroke-node :stroke-width d/parse-double)
:stroke-alignment (get-meta stroke-node :stroke-alignment keyword)
:stroke-cap-start (get-meta stroke-node :stroke-cap-start keyword)
:stroke-cap-end (get-meta stroke-node :stroke-cap-end keyword)})))
(mapv d/without-nils)
(filterv #(not= (:stroke-color %) "none")))]
(if (seq strokes)
strokes
(->> [(-> (add-stroke {} node svg-data)
@ -804,6 +819,25 @@
(cond-> (d/not-empty? grids)
(assoc :grids grids)))))
(defn get-stroke-images-data
[node]
(let [strokes
(-> node
(find-node :penpot:shape)
(find-node :penpot:strokes))]
(->> (find-all-nodes strokes :penpot:stroke)
(mapv (fn [stroke-node]
(let [id (get-in stroke-node [:attrs :penpot:stroke-image-id])
image-node (->> node (node-seq) (find-node-by-id id))]
{:id id
:href (get-in image-node [:attrs :href])})))
(filterv #(some? (:id %))))))
(defn has-stroke-images?
[node]
(let [stroke-images (get-stroke-images-data node)]
(> (count stroke-images) 0)))
(defn has-image?
[node]
(let [type (get-type node)
@ -812,7 +846,9 @@
(find-node :defs)
(find-node :pattern)
(find-node :g)
(find-node :g)
(find-node :image))]
(or (= type :image)
(some? pattern-image))))
@ -827,12 +863,32 @@
(find-node :defs)
(find-node :pattern)
(find-node :g)
(find-node :g)
(find-node :image)
:attrs)
image-data (get-svg-data :image node)
svg-data (or image-data pattern-data)]
svg-data (or pattern-data image-data)]
(or (:href svg-data) (:xlink:href svg-data))))
(defn get-fill-images-data
[node]
(let [fills
(-> node
(find-node :penpot:shape)
(find-node :penpot:fills))]
(->> (find-all-nodes fills :penpot:fill)
(mapv (fn [fill-node]
(let [id (get-in fill-node [:attrs :penpot:fill-image-id])
image-node (->> node (node-seq) (find-node-by-id id))]
{:id id
:href (get-in image-node [:attrs :href])})))
(filterv #(some? (:id %))))))
(defn has-fill-images?
[node]
(let [fill-images (get-fill-images-data node)]
(> (count fill-images) 0)))
(defn get-image-fill
[node]
(let [linear-gradient-node (-> node

View file

@ -317,36 +317,59 @@
(defn resolve-media
[context file-id node]
(if (and (not (cip/close? node))
(cip/has-image? node))
(let [name (cip/get-image-name node)
image-data (cip/get-image-data node)
image-fill (cip/get-image-fill node)]
(->> (upload-media-files context file-id name image-data)
(rx/catch #(do (.error js/console "Error uploading media: " name)
(rx/of node)))
(if (or (and (not (cip/close? node))
(cip/has-image? node))
(cip/has-stroke-images? node)
(cip/has-fill-images? node))
(let [name (cip/get-image-name node)
has-image (cip/has-image? node)
image-data (cip/get-image-data node)
image-fill (cip/get-image-fill node)
fill-images-data (->> (cip/get-fill-images-data node)
(map #(assoc % :type :fill)))
stroke-images-data (->> (cip/get-stroke-images-data node)
(map #(assoc % :type :stroke)))
images-data (concat
fill-images-data
stroke-images-data
(when has-image
[{:href image-data}]))]
(->> (rx/from images-data)
(rx/mapcat (fn [image-data]
(->> (upload-media-files context file-id name (:href image-data))
(rx/catch #(do (.error js/console "Error uploading media: " name)
(rx/of node)))
(rx/map #(vector (:id image-data) %)))))
(rx/reduce (fn [acc [id data]] (assoc acc id data)) {})
(rx/map
(fn [media]
(-> node
(assoc-in [:attrs :penpot:media-id] (:id media))
(assoc-in [:attrs :penpot:media-width] (:width media))
(assoc-in [:attrs :penpot:media-height] (:height media))
(assoc-in [:attrs :penpot:media-mtype] (:mtype media))
(fn [images]
(let [media (get images nil)]
(-> node
(assoc :images images)
(cond-> (some? media)
(->
(assoc-in [:attrs :penpot:media-id] (:id media))
(assoc-in [:attrs :penpot:media-width] (:width media))
(assoc-in [:attrs :penpot:media-height] (:height media))
(assoc-in [:attrs :penpot:media-mtype] (:mtype media))
(assoc-in [:attrs :penpot:fill-color] (:fill image-fill))
(assoc-in [:attrs :penpot:fill-color-ref-file] (:fill-color-ref-file image-fill))
(assoc-in [:attrs :penpot:fill-color-ref-id] (:fill-color-ref-id image-fill))
(assoc-in [:attrs :penpot:fill-opacity] (:fill-opacity image-fill))
(assoc-in [:attrs :penpot:fill-color-gradient] (:fill-color-gradient image-fill)))))))
(assoc-in [:attrs :penpot:fill-color] (:fill image-fill))
(assoc-in [:attrs :penpot:fill-color-ref-file] (:fill-color-ref-file image-fill))
(assoc-in [:attrs :penpot:fill-color-ref-id] (:fill-color-ref-id image-fill))
(assoc-in [:attrs :penpot:fill-opacity] (:fill-opacity image-fill))
(assoc-in [:attrs :penpot:fill-color-gradient] (:fill-color-gradient image-fill))))))))))
;; If the node is not an image just return the node
(->> (rx/of node)
(rx/observe-on :async))))
(defn media-node? [node]
(and (cip/shape? node)
(cip/has-image? node)
(not (cip/close? node))))
(or (and (cip/shape? node)
(cip/has-image? node)
(not (cip/close? node)))
(cip/has-stroke-images? node)
(cip/has-fill-images? node)))
(defn import-page
[context file [page-id page-name content]]
@ -379,7 +402,8 @@
(rx/mapcat
(fn [node]
(->> (resolve-media context file-id node)
(rx/map (fn [result] [node result])))))
(rx/map (fn [result]
[node result])))))
(rx/reduce conj {}))]
(->> pre-process-images

View file

@ -4993,3 +4993,21 @@ msgstr "Click to close the path"
#, markdown
msgid "workspace.top-bar.read-only"
msgstr "**Inspect mode** (View Only)"
msgid "media.image"
msgstr "Image"
msgid "media.solid"
msgstr "Solid"
msgid "media.linear"
msgstr "Linear"
msgid "media.radial"
msgstr "Radial"
msgid "media.gradient"
msgstr "Gradient"
msgid "media.choose-image"
msgstr "Choose image"

View file

@ -5094,3 +5094,21 @@ msgstr "Pulsar para cerrar la ruta"
#, markdown
msgid "workspace.top-bar.read-only"
msgstr "**Modo inspección** (View only)"
msgid "media.image"
msgstr "Imagen"
msgid "media.solid"
msgstr "Sólido"
msgid "media.linear"
msgstr "Linear"
msgid "media.radial"
msgstr "Radial"
msgid "media.gradient"
msgstr "Gradiente"
msgid "media.choose-image"
msgstr "Elegir imagen"