0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-06 14:50:20 -05:00

🎉 Add new gradients UI

This commit is contained in:
alonso.torres 2024-11-04 09:16:48 +01:00 committed by Andrey Antukh
parent bc250c962d
commit 838c1324b9
21 changed files with 1490 additions and 281 deletions

View file

@ -10,6 +10,8 @@
### :sparkles: New features
- New gradients UI with multi-stop support.
### :bug: Bugs fixed

View file

@ -478,3 +478,63 @@
a (+ (* ah 100) (* av 10))
b (+ (* bh 100) (* bv 10))]
(compare a b)))
(defn interpolate-color
[c1 c2 offset]
(cond
(<= offset (:offset c1)) (assoc c1 :offset offset)
(>= offset (:offset c2)) (assoc c2 :offset offset)
:else
(let [tr-offset (/ (- offset (:offset c1)) (- (:offset c2) (:offset c1)))
[r1 g1 b1] (hex->rgb (:color c1))
[r2 g2 b2] (hex->rgb (:color c2))
a1 (:opacity c1)
a2 (:opacity c2)
r (+ r1 (* (- r2 r1) tr-offset))
g (+ g1 (* (- g2 g1) tr-offset))
b (+ b1 (* (- b2 b1) tr-offset))
a (+ a1 (* (- a2 a1) tr-offset))]
{:color (rgb->hex [r g b])
:opacity a
:r r
:g g
:b b
:alpha a
:offset offset})))
(defn- offset-spread
[from to num]
(->> (range 0 num)
(map #(mth/precision (+ from (* (/ (- to from) (dec num)) %)) 2))))
(defn uniform-spread?
"Checks if the gradient stops are spread uniformly"
[stops]
(let [cs (count stops)
from (first stops)
to (last stops)
expect-vals (offset-spread (:offset from) (:offset to) cs)
calculate-expected
(fn [expected-offset stop]
(and (mth/close? (:offset stop) expected-offset)
(let [ec (interpolate-color from to expected-offset)]
(and (= (:color ec) (:color stop))
(= (:opacity ec) (:opacity stop))))))]
(->> (map calculate-expected expect-vals stops)
(every? true?))))
(defn uniform-spread
"Assign an uniform spread to the offset values for the gradient"
[from to num-stops]
(->> (offset-spread (:offset from) (:offset to) num-stops)
(mapv (fn [offset]
(interpolate-color from to offset)))))
(defn interpolate-gradient
[stops offset]
(let [idx (d/index-of-pred stops #(<= offset (:offset %)))
start (if (= idx 0) (first stops) (get stops (dec idx)))
end (if (nil? idx) (last stops) (get stops idx))]
(interpolate-color start end offset)))

View file

@ -74,7 +74,7 @@
(-> p2 (gpt/add right-v) (gpt/add bottom-v))
(-> p3 (gpt/add left-v) (gpt/add bottom-v))])))
(defn- project-t
(defn project-t
"Given a point and a line returns the parametric t the cross point with the line going through the other axis projected"
[point [start end] other-axis-vec]

View file

@ -22,3 +22,143 @@ test("Bug 7549 - User clicks on color swatch to display the color picker next to
const distance = swatchBox.x - (pickerBox.x + pickerBox.width);
expect(distance).toBeLessThan(60);
});
test("Create a LINEAR gradient", async ({ page }) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
/get\-file\?/,
"workspace/get-file-not-empty.json",
);
await workspacePage.mockRPC(
"update-file?id=*",
"workspace/update-file-create-rect.json",
);
await workspacePage.goToWorkspace({
fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374",
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
});
await workspacePage.clickLeafLayer("Rectangle");
const swatch = workspacePage.page.getByRole("button", { name: "#B1B2B5" });
const swatchBox = await swatch.boundingBox();
await swatch.click();
const select = await workspacePage.page.getByText("Solid");
await select.click();
const gradOption = await workspacePage.page.getByText("Gradient");
await gradOption.click();
const addStopBtn = await workspacePage.page.getByRole("button", {
name: "Add stop",
});
await addStopBtn.click();
await addStopBtn.click();
await addStopBtn.click();
const removeBtn = await workspacePage.page
.getByTestId("colorpicker")
.getByRole("button", { name: "Remove color" })
.nth(2);
await removeBtn.click();
await removeBtn.click();
const inputColor1 = await workspacePage.page.getByPlaceholder("Mixed").nth(1);
await inputColor1.fill("fabada");
const inputOpacity1 = await workspacePage.page
.getByTestId("colorpicker")
.getByPlaceholder("--")
.nth(1);
await inputOpacity1.fill("100");
const inputColor2 = await workspacePage.page.getByPlaceholder("Mixed").nth(2);
await inputColor2.fill("red");
const inputOpacity2 = await workspacePage.page
.getByTestId("colorpicker")
.getByPlaceholder("--")
.nth(2);
await inputOpacity2.fill("100");
const inputOpacityGlobal = await workspacePage.page
.locator("div")
.filter({ hasText: /^FillLinear gradient%$/ })
.getByPlaceholder("--");
await inputOpacityGlobal.fill("100");
});
test("Create a RADIAL gradient", async ({ page }) => {
const workspacePage = new WorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
/get\-file\?/,
"workspace/get-file-not-empty.json",
);
await workspacePage.mockRPC(
"update-file?id=*",
"workspace/update-file-create-rect.json",
);
await workspacePage.goToWorkspace({
fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374",
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
});
await workspacePage.clickLeafLayer("Rectangle");
const swatch = workspacePage.page.getByRole("button", { name: "#B1B2B5" });
const swatchBox = await swatch.boundingBox();
await swatch.click();
const select = await workspacePage.page.getByText("Solid");
await select.click();
const gradOption = await workspacePage.page.getByText("Gradient");
await gradOption.click();
const gradTypeOptions = await workspacePage.page
.getByTestId("colorpicker")
.locator("div")
.filter({ hasText: "Linear" })
.nth(3);
await gradTypeOptions.click();
const gradRadialOption = await workspacePage.page
.locator("li")
.filter({ hasText: "Radial" });
await gradRadialOption.click();
const addStopBtn = await workspacePage.page.getByRole("button", {
name: "Add stop",
});
await addStopBtn.click();
await addStopBtn.click();
await addStopBtn.click();
const removeBtn = await workspacePage.page
.getByTestId("colorpicker")
.getByRole("button", { name: "Remove color" })
.nth(2);
await removeBtn.click();
await removeBtn.click();
const inputColor1 = await workspacePage.page.getByPlaceholder("Mixed").nth(1);
await inputColor1.fill("fabada");
const inputOpacity1 = await workspacePage.page
.getByTestId("colorpicker")
.getByPlaceholder("--")
.nth(1);
await inputOpacity1.fill("100");
const inputColor2 = await workspacePage.page.getByPlaceholder("Mixed").nth(2);
await inputColor2.fill("red");
const inputOpacity2 = await workspacePage.page
.getByTestId("colorpicker")
.getByPlaceholder("--")
.nth(2);
await inputOpacity2.fill("100");
});

View file

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" stroke-width="2" stroke-linecap="round">
<path d="M6.05 4h-.1m.1 4h-.1m.1 4h-.1m4.1-8h-.1m.1 4h-.1m.1 4h-.1"/>
</svg>

After

Width:  |  Height:  |  Size: 203 B

View file

@ -19,7 +19,7 @@
[potok.v2.core :as ptk]))
;; Change this to :info :debug or :trace to debug this module
(log/set-level! :debug)
(log/set-level! :info)
(def page-change?
#{:add-page :mod-page :del-page :mov-page})

View file

@ -541,16 +541,34 @@
(ctc/check-color! color))
(ptk/reify ::apply-color-from-colorpicker
ptk/WatchEvent
(watch [_ _ _]
;; FIXME: revisit this
ptk/UpdateEvent
(update [_ state]
(let [gradient-type (dm/get-in color [:gradient :type])]
(rx/of
(cond
(:image color) (activate-colorpicker-image)
(:color color) (activate-colorpicker-color)
(= :linear gradient-type) (activate-colorpicker-gradient :linear-gradient)
(= :radial gradient-type) (activate-colorpicker-gradient :radial-gradient)))))))
(update state :colorpicker
(fn [state]
(cond
(:image color)
(-> state
(assoc :type :image)
(dissoc :editing-stop :stops :gradient))
(:color color)
(-> state
(assoc :type :color)
(dissoc :editing-stop :stops :gradient))
(= :linear gradient-type)
(-> state
(assoc :type :linear-gradient)
(assoc :editing-stop 0)
(d/dissoc-in [:current-color :image]))
(= :radial gradient-type)
(-> state
(assoc :type :radial-gradient)
(assoc :editing-stop 0)
(d/dissoc-in [:current-color :image])))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -591,7 +609,7 @@
:width 1.0})
(defn get-color-from-colorpicker-state
[{:keys [type current-color stops gradient] :as state}]
[{:keys [type current-color stops gradient opacity] :as state}]
(cond
(= type :color)
(clear-color-components current-color)
@ -600,7 +618,8 @@
(clear-image-components current-color)
:else
{:gradient (-> gradient
{:opacity opacity
:gradient (-> gradient
(assoc :type (case type
:linear-gradient :linear
:radial-gradient :radial))
@ -625,13 +644,19 @@
(let [stopper (rx/merge
(rx/filter (ptk/type? ::finalize-colorpicker) stream)
(rx/filter (ptk/type? ::initialize-colorpicker) stream))]
(->> (rx/merge
(->> stream
(rx/filter (ptk/type? ::update-colorpicker-gradient))
(rx/debounce 200))
(rx/debounce 20))
(rx/filter (ptk/type? ::update-colorpicker-color) stream)
(rx/filter (ptk/type? ::activate-colorpicker-gradient) stream))
(->> (rx/filter (ptk/type? ::activate-colorpicker-gradient) stream)
(rx/debounce 20))
(rx/filter (ptk/type? ::update-colorpicker-stops) stream)
(rx/filter (ptk/type? ::update-colorpicker-gradient-opacity) stream)
(rx/filter (ptk/type? ::update-colorpicker-add-stop) stream)
(rx/filter (ptk/type? ::update-colorpicker-add-auto) stream)
(rx/filter (ptk/type? ::remove-gradient-stop) stream))
(rx/debounce 40)
(rx/map (constantly (colorpicker-onchange-runner on-change)))
(rx/take-until stopper))))
@ -660,14 +685,18 @@
(let [current-color (:current-color state)]
(if (some? gradient)
(let [stop (or (:editing-stop state) 0)
stops (mapv split-color-components (:stops gradient))]
(-> state
(assoc :current-color (nth stops stop))
(assoc :stops stops)
(assoc :gradient (-> gradient
(dissoc :stops)
(assoc :shape-id shape-id)))
(assoc :editing-stop stop)))
new-stops (mapv split-color-components (:stops gradient))
new-gradient (-> gradient
(dissoc :stops)
(assoc :shape-id shape-id))]
(if (and (= (:stops state) new-stops) (= (:gradient state) new-gradient))
state
(-> state
(assoc :opacity (:opacity data))
(assoc :current-color (get new-stops stop))
(assoc :stops new-stops)
(assoc :gradient new-gradient)
(assoc :editing-stop stop))))
(-> state
(cond-> (or (nil? current-color)
@ -678,6 +707,132 @@
(dissoc :gradient)
(dissoc :stops))))))))))
(defn update-colorpicker-gradient-opacity
[opacity]
(ptk/reify ::update-colorpicker-gradient-opacity
ptk/UpdateEvent
(update [_ state]
(update state :colorpicker
(fn [state]
(-> state
(assoc :opacity opacity)))))))
(defn update-colorpicker-add-auto
[]
(ptk/reify ::update-colorpicker-add-auto
ptk/UpdateEvent
(update [_ state]
(update state :colorpicker
(fn [{:keys [stops editing-stop] :as state}]
(if (cc/uniform-spread? stops)
;; Add to uniform
(let [stops (->> (cc/uniform-spread (first stops) (last stops) (inc (count stops)))
(mapv split-color-components))]
(-> state
(assoc :current-color (get stops editing-stop))
(assoc :stops stops)))
;; We add the stop to the middle point between the selected
;; and the next one.
;; If the last stop is selected then it's added between the
;; last two stops.
(let [index
(if (= editing-stop (dec (count stops)))
(dec editing-stop)
editing-stop)
{from-offset :offset} (get stops index)
{to-offset :offset} (get stops (inc index))
half-point-offset
(+ from-offset (/ (- to-offset from-offset) 2))
new-stop (-> (cc/interpolate-gradient stops half-point-offset)
(split-color-components))
stops (conj stops new-stop)
stops (into [] (sort-by :offset stops))
editing-stop (d/index-of-pred stops #(= new-stop %))]
(-> state
(assoc :editing-stop editing-stop)
(assoc :current-color (get stops editing-stop))
(assoc :stops stops)))))))))
(defn update-colorpicker-add-stop
[offset]
(ptk/reify ::update-colorpicker-add-stop
ptk/UpdateEvent
(update [_ state]
(update state :colorpicker
(fn [state]
(let [stops (:stops state)
new-stop (-> (cc/interpolate-gradient stops offset)
(split-color-components))
stops (conj stops new-stop)
stops (into [] (sort-by :offset stops))
editing-stop (d/index-of-pred stops #(= new-stop %))]
(-> state
(assoc :editing-stop editing-stop)
(assoc :current-color (get stops editing-stop))
(assoc :stops stops))))))))
(defn update-colorpicker-stops
[stops]
(ptk/reify ::update-colorpicker-stops
ptk/UpdateEvent
(update [_ state]
(update state :colorpicker
(fn [state]
(let [stop (or (:editing-stop state) 0)
stops (mapv split-color-components stops)]
(-> state
(assoc :current-color (get stops stop))
(assoc :stops stops))))))))
(defn sort-colorpicker-stops
[]
(ptk/reify ::sort-colorpicker-stops
ptk/UpdateEvent
(update [_ state]
(update state :colorpicker
(fn [state]
(let [stop (or (:editing-stop state) 0)
stops (mapv split-color-components (:stops state))
stop-val (get stops stop)
stops (into [] (sort-by :offset stops))
stop (d/index-of-pred stops #(= % stop-val))]
(-> state
(assoc :editing-stop stop)
(assoc :stops stops))))))))
(defn remove-gradient-stop
([]
(remove-gradient-stop nil))
([index]
(ptk/reify ::remove-gradient-stop
ptk/UpdateEvent
(update [_ state]
(update state :colorpicker
(fn [{:keys [editing-stop stops] :as state}]
(if (> (count stops) 2)
(let [delete-index (or index editing-stop 0)
delete-stop (get stops delete-index)
stops (into [] (remove #(= delete-stop %)) stops)
editing-stop
(cond
(< editing-stop delete-index) editing-stop
(> editing-stop delete-index) (dec editing-stop)
(>= (count stops) editing-stop) (dec (count stops))
:else editing-stop)]
(-> state
(assoc :editing-stop editing-stop)
(assoc :stops stops)))
;; Cannot delete
state)))))))
(defn update-colorpicker-color
[changes add-recent?]
(ptk/reify ::update-colorpicker-color
@ -723,16 +878,16 @@
(update-in state [:colorpicker :gradient] merge changes))))
(defn select-colorpicker-gradient-stop
[stop]
[index]
(ptk/reify ::select-colorpicket-gradient-stop
ptk/UpdateEvent
(update [_ state]
(update state :colorpicker
(fn [state]
(if-let [color (get-in state [:stops stop])]
(if-let [color (get-in state [:stops index])]
(assoc state
:current-color color
:editing-stop stop)
:editing-stop index)
state))))))
(defn activate-colorpicker-color

View file

@ -211,9 +211,12 @@
handle-focus
(mf/use-callback
(mf/deps on-focus select-on-focus?)
(fn [event]
(reset! last-value* (parse-value))
(let [target (dom/get-target event)]
(when on-focus
(mf/set-ref-val! dirty-ref true)
(on-focus event))
(when select-on-focus?

View file

@ -15,6 +15,7 @@
(def ^:private schema:icon-button
[:map
[:class {:optional true} :string]
[:icon-class {:optional true} :string]
[:icon
[:and :string [:fn #(contains? icon-list %)]]]
[:aria-label :string]
@ -24,7 +25,7 @@
(mf/defc icon-button*
{::mf/props :obj
::mf/schema schema:icon-button}
[{:keys [class icon variant aria-label children] :rest props}]
[{:keys [class icon icon-class variant aria-label children] :rest props}]
(let [variant (or variant "primary")
class (dm/str class " " (stl/css-case :icon-button true
:icon-button-primary (= variant "primary")
@ -33,4 +34,4 @@
:icon-button-action (= variant "action")
:icon-button-destructive (= variant "destructive")))
props (mf/spread-props props {:class class :title aria-label})]
[:> "button" props [:> icon* {:id icon :aria-label aria-label}] children]))
[:> "button" props [:> icon* {:id icon :aria-label aria-label :class icon-class}] children]))

View file

@ -50,10 +50,10 @@
(def ^:icon-id align-top "align-top")
(def ^:icon-id align-vertical-center "align-vertical-center")
(def ^:icon-id arrow "arrow")
(def ^:icon-id arrow-up "arrow-up")
(def ^:icon-id arrow-down "arrow-down")
(def ^:icon-id arrow-left "arrow-left")
(def ^:icon-id arrow-right "arrow-right")
(def ^:icon-id arrow-up "arrow-up")
(def ^:icon-id asc-sort "asc-sort")
(def ^:icon-id board "board")
(def ^:icon-id boards-thumbnail "boards-thumbnail")
@ -93,27 +93,27 @@
(def ^:icon-id clip-content "clip-content")
(def ^:icon-id clipboard "clipboard")
(def ^:icon-id clock "clock")
(def ^:icon-id close-small "close-small")
(def ^:icon-id close "close")
(def ^:icon-id close-small "close-small")
(def ^:icon-id code "code")
(def ^:icon-id column-reverse "column-reverse")
(def ^:icon-id column "column")
(def ^:icon-id column-reverse "column-reverse")
(def ^:icon-id comments "comments")
(def ^:icon-id component-copy "component-copy")
(def ^:icon-id component "component")
(def ^:icon-id component-copy "component-copy")
(def ^:icon-id constraint-horizontal "constraint-horizontal")
(def ^:icon-id constraint-vertical "constraint-vertical")
(def ^:icon-id corner-bottom "corner-bottom")
(def ^:icon-id corner-bottom-left "corner-bottom-left")
(def ^:icon-id corner-bottom-right "corner-bottom-right")
(def ^:icon-id corner-bottom "corner-bottom")
(def ^:icon-id corner-center "corner-center")
(def ^:icon-id corner-radius "corner-radius")
(def ^:icon-id corner-top "corner-top")
(def ^:icon-id corner-top-left "corner-top-left")
(def ^:icon-id corner-top-right "corner-top-right")
(def ^:icon-id curve "curve")
(def ^:icon-id delete-text "delete-text")
(def ^:icon-id delete "delete")
(def ^:icon-id delete-text "delete-text")
(def ^:icon-id desc-sort "desc-sort")
(def ^:icon-id detach "detach")
(def ^:icon-id detached "detached")
@ -122,33 +122,34 @@
(def ^:icon-id document "document")
(def ^:icon-id download "download")
(def ^:icon-id drop "drop")
(def ^:icon-id easing-ease-in-out "easing-ease-in-out")
(def ^:icon-id easing-ease-in "easing-ease-in")
(def ^:icon-id easing-ease-out "easing-ease-out")
(def ^:icon-id easing-ease "easing-ease")
(def ^:icon-id easing-ease-in "easing-ease-in")
(def ^:icon-id easing-ease-in-out "easing-ease-in-out")
(def ^:icon-id easing-ease-out "easing-ease-out")
(def ^:icon-id easing-linear "easing-linear")
(def ^:icon-id effects "effects")
(def ^:icon-id elipse "elipse")
(def ^:icon-id exit "exit")
(def ^:icon-id expand "expand")
(def ^:icon-id external-link "external-link")
(def ^:icon-id feedback "feedback")
(def ^:icon-id fill-content "fill-content")
(def ^:icon-id filter "filter")
(def ^:icon-id fixed-width "fixed-width")
(def ^:icon-id flex "flex")
(def ^:icon-id flex-grid "flex-grid")
(def ^:icon-id flex-horizontal "flex-horizontal")
(def ^:icon-id flex-vertical "flex-vertical")
(def ^:icon-id flex "flex")
(def ^:icon-id flip-horizontal "flip-horizontal")
(def ^:icon-id flip-vertical "flip-vertical")
(def ^:icon-id gap-horizontal "gap-horizontal")
(def ^:icon-id gap-vertical "gap-vertical")
(def ^:icon-id graphics "graphics")
(def ^:icon-id grid "grid")
(def ^:icon-id grid-column "grid-column")
(def ^:icon-id grid-columns "grid-columns")
(def ^:icon-id grid-gutter "grid-gutter")
(def ^:icon-id grid-margin "grid-margin")
(def ^:icon-id grid "grid")
(def ^:icon-id grid-row "grid-row")
(def ^:icon-id grid-rows "grid-rows")
(def ^:icon-id grid-square "grid-square")
@ -165,7 +166,6 @@
(def ^:icon-id info "info")
(def ^:icon-id interaction "interaction")
(def ^:icon-id join-nodes "join-nodes")
(def ^:icon-id external-link "external-link")
(def ^:icon-id justify-content-column-around "justify-content-column-around")
(def ^:icon-id justify-content-column-between "justify-content-column-between")
(def ^:icon-id justify-content-column-center "justify-content-column-center")
@ -216,6 +216,7 @@
(def ^:icon-id rectangle "rectangle")
(def ^:icon-id reload "reload")
(def ^:icon-id remove "remove")
(def ^:icon-id reorder "reorder")
(def ^:icon-id rgba "rgba")
(def ^:icon-id rgba-complementary "rgba-complementary")
(def ^:icon-id rotation "rotation")
@ -285,7 +286,7 @@
(def ^:private schema:icon
[:map
[:class {:optional true} :string]
[:class {:optional true} [:maybe :string]]
[:id [:and :string [:fn #(contains? icon-list %)]]]
[:size {:optional true}
[:maybe [:enum "s" "m"]]]])

View file

@ -37,7 +37,8 @@
(when (and wrapper
(not allow-click-outside)
(not (.contains wrapper current))
(not (= type (keyword (dom/get-data current "allow-click-modal")))))
(not (= type (keyword (dom/get-data current "allow-click-modal"))))
(= (.-button event) 0))
(dom/stop-propagation event)
(dom/prevent-default event)
(st/emit! (dm/hide)))))

View file

@ -47,8 +47,8 @@
(add-metadata! props gradient))
[:> :linearGradient props
(for [{:keys [offset color opacity]} (:stops gradient)]
[:stop {:key (dm/str id "-stop-" offset)
(for [[index {:keys [offset color opacity]}] (d/enumerate (sort-by :offset (:stops gradient)))]
[:stop {:key (dm/str id "-stop-" index)
:offset (d/nilv offset 0)
:stop-color color
:stop-opacity opacity}])]))
@ -109,8 +109,8 @@
(add-metadata! props gradient))
[:> :radialGradient props
(for [{:keys [offset color opacity]} (:stops gradient)]
[:stop {:key (dm/str id "-stop-" offset)
(for [[index {:keys [offset color opacity]}] (d/enumerate (:stops gradient))]
[:stop {:key (dm/str id "-stop-" index)
:offset (d/nilv offset 0)
:stop-color color
:stop-opacity opacity}])]))

View file

@ -10,9 +10,12 @@
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.config :as cfg]
[app.main.data.events :as-alias ev]
[app.main.data.modal :as modal]
[app.main.data.shortcuts :as dsc]
[app.main.data.workspace.colors :as dc]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.media :as dwm]
@ -20,6 +23,7 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.numeric-input :refer [numeric-input*]]
[app.main.ui.components.select :refer [select]]
[app.main.ui.ds.foundations.assets.icon :as ic]
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
@ -30,8 +34,10 @@
[app.main.ui.workspace.colorpicker.hsva :refer [hsva-selector]]
[app.main.ui.workspace.colorpicker.libraries :refer [libraries]]
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector]]
[app.main.ui.workspace.colorpicker.shortcuts :as sc]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.timers :as ts]
[cuerdas.core :as str]
[okulary.core :as l]
[potok.v2.core :as ptk]
@ -51,6 +57,14 @@
(def viewport
(l/derived :vport refs/workspace-local))
(defn opacity->string
[opacity]
(if (not= opacity :multiple)
(dm/str (-> opacity
(d/coalesce 1)
(* 100)))
:multiple))
;; --- Color Picker Modal
(defn use-color-picker-css-variables! [node-ref current-color]
@ -78,6 +92,8 @@
(let [state (mf/deref refs/colorpicker)
node-ref (mf/use-ref)
should-update? (mf/use-var true)
;; TODO: I think we need to put all this picking state under
;; the same object for avoid creating adhoc refs for each
;; value
@ -99,7 +115,12 @@
fill-image-ref (mf/use-ref nil)
selected-mode (get state :type :color)
color-type (get state :type :color)
selected-mode (case color-type
(:linear-gradient :radial-gradient)
:gradient
color-type)
disabled-color-accept? (and
(= selected-mode :image)
@ -147,9 +168,9 @@
(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)))))
:gradient (st/emit! (dc/activate-colorpicker-gradient :linear-gradient))
:image (st/emit! (dc/activate-colorpicker-image))
nil)))
handle-change-color
(mf/use-fn
@ -173,14 +194,6 @@
(do (modal/allow-click-outside!)
(st/emit! (dc/start-picker))))))
handle-change-stop
(mf/use-fn
(fn [event]
(let [offset (-> (dom/get-current-target event)
(dom/get-data "value")
(d/parse-integer))]
(st/emit! (dc/select-colorpicker-gradient-stop offset)))))
on-select-library-color
(mf/use-fn
(mf/deps data handle-change-color)
@ -203,6 +216,7 @@
(mf/use-fn
(mf/deps drag? node-ref)
(fn []
(reset! should-update? false)
(reset! drag? true)
(st/emit! (dwu/start-undo-transaction (mf/ref-val node-ref)))))
@ -210,6 +224,7 @@
(mf/use-fn
(mf/deps drag? node-ref)
(fn []
(reset! should-update? true)
(reset! drag? false)
(st/emit! (dwu/commit-undo-transaction (mf/ref-val node-ref)))))
@ -225,11 +240,104 @@
(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")}])
[{:value :gradient :label (tr "media.gradient")}])
(when (not disable-image)
[{:value :image :label (tr "media.image")}])))
handle-change-gradient-selected-stop
(mf/use-fn
(fn [index]
(st/emit! (dc/select-colorpicker-gradient-stop index))))
handle-change-gradient-type
(mf/use-fn
(fn [type]
(st/emit! (dc/activate-colorpicker-gradient type))))
handle-gradient-change-stop
(mf/use-fn
(mf/deps state)
(fn [prev-stop new-stop]
(let [stops (->> (:stops state)
(mapv #(if (= % prev-stop) new-stop %)))]
(st/emit! (dc/update-colorpicker-stops stops)))))
handle-gradient-add-stop-auto
(mf/use-fn
(fn []
(st/emit! (dc/update-colorpicker-add-auto))))
handle-gradient-add-stop-preview
(mf/use-fn
(fn [offset]
(st/emit! (dc/update-colorpicker-add-stop offset))))
handle-gradient-remove-stop
(mf/use-fn
(mf/deps state)
(fn [stop]
(when (> (count (:stops state)) 2)
(when-let [index (d/index-of-pred (:stops state) #(= % stop))]
(st/emit! (dc/remove-gradient-stop index))))))
handle-stop-edit-start
(mf/use-fn
(fn []
(reset! should-update? false)))
handle-stop-edit-finish
(mf/use-fn
(mf/deps state)
(fn []
(reset! should-update? true)
;; Update asynchronously so we can update with the new stops
(ts/schedule #(st/emit! (dc/sort-colorpicker-stops)))))
handle-rotate-stops
(mf/use-fn
(mf/deps state)
(fn []
(let [gradient (:gradient state)
mtx (gmt/rotate-matrix 90 (gpt/point 0.5 0.5))
start-p (-> (gpt/point (:start-x gradient) (:start-y gradient))
(gpt/transform mtx))
end-p (-> (gpt/point (:end-x gradient) (:end-y gradient))
(gpt/transform mtx))]
(st/emit! (dc/update-colorpicker-gradient {:start-x (:x start-p)
:start-y (:y start-p)
:end-x (:x end-p)
:end-y (:y end-p)})))))
handle-reverse-stops
(mf/use-fn
(mf/deps (:stops state))
(fn []
(let [stops (->> (:stops state)
(map (fn [it] (update it :offset #(+ 1 (* -1 %)))))
(sort-by :offset)
(into []))]
(st/emit! (dc/update-colorpicker-stops stops)))))
handle-reorder-stops
(mf/use-fn
(mf/deps (:stops state))
(fn [from-index to-index]
(let [stops (:stops state)
new-stops
(-> stops
(d/insert-at-index to-index [(get stops from-index)]))
stops
(mapv #(assoc %2 :offset (:offset %1)) stops new-stops)]
(st/emit! (dc/update-colorpicker-stops stops)))))
handle-change-gradient-opacity
(mf/use-fn
(fn [value]
(st/emit! (dc/update-colorpicker-gradient-opacity (/ value 100)))))
tabs
#js [#js {:aria-label (tr "workspace.libraries.colors.rgba")
:icon ic/rgba
@ -280,7 +388,8 @@
;; Update colorpicker with external color changes
(mf/with-effect [data]
(st/emit! (dc/update-colorpicker data)))
(when @should-update?
(st/emit! (dc/update-colorpicker data))))
;; Updates the CSS color variable when there is a change in the color
(use-color-picker-css-variables! node-ref current-color)
@ -300,24 +409,46 @@
:ref node-ref
:style {:touch-action "none"}}
[:div {:class (stl/css :top-actions)}
(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}]])
[:div {:class (stl/css :top-actions-right)}
(when (= :gradient selected-mode)
[:div {:class (stl/css :opacity-input-wrapper)}
[:span {:class (stl/css :icon-text)} "%"]
[:> numeric-input*
{:value (-> data :opacity opacity->string)
:on-change handle-change-gradient-opacity
:default 100
:min 0
:max 100}]])
(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])]
(when (or (= selected-mode :linear-gradient)
(= selected-mode :radial-gradient))
(when (= selected-mode :gradient)
[:& gradients
{:stops (:stops state)
{:type (:type state)
:stops (:stops state)
:editing-stop (:editing-stop state)
:on-select-stop handle-change-stop}])
:on-stop-edit-start handle-stop-edit-start
:on-stop-edit-finish handle-stop-edit-finish
:on-select-stop handle-change-gradient-selected-stop
:on-change-type handle-change-gradient-type
:on-change-stop handle-gradient-change-stop
:on-add-stop-auto handle-gradient-add-stop-auto
:on-add-stop-preview handle-gradient-add-stop-preview
:on-remove-stop handle-gradient-remove-stop
:on-rotate-stops handle-rotate-stops
:on-reverse-stops handle-reverse-stops
:on-reorder-stops handle-reorder-stops}])
(if (= selected-mode :image)
(let [uri (cfg/resolve-file-media (:image current-color))
@ -383,9 +514,9 @@
(defn calculate-position
"Calculates the style properties for the given coordinates and position"
[{vh :height} position x y]
[{vh :height} position x y gradient?]
(let [;; picker size in pixels
h 510
h (if gradient? 820 510)
w 284
;; Checks for overflow outside the viewport height
max-y (- vh h)
@ -433,22 +564,25 @@
dirty? (mf/use-var false)
last-change (mf/use-var nil)
position (d/nilv position :left)
style (calculate-position vport position x y)
style (calculate-position vport position x y (some? (:gradient data)))
on-change'
(mf/use-fn
(mf/deps on-change)
(fn [new-data]
(reset! dirty? (not= data new-data))
(reset! last-change new-data)
(if (fn? on-change)
(on-change new-data)
(st/emit! (dc/update-colorpicker new-data)))))]
(when (not= new-data @last-change)
(reset! last-change new-data)
(if (fn? on-change)
(on-change new-data)
(st/emit! (dc/update-colorpicker new-data))))))]
(mf/with-effect []
#(when (and @dirty? @last-change on-close)
(on-close @last-change)))
(st/emit! (st/emit! (dsc/push-shortcuts ::colorpicker sc/shortcuts)))
#(do
(st/emit! (dsc/pop-shortcuts ::colorpicker))
(when (and @dirty? @last-change on-close)
(on-close @last-change))))
[:div {:class (stl/css :colorpicker-tooltip)
:data-testid "colorpicker"
@ -460,4 +594,3 @@
:disable-image disable-image
:on-change on-change'
:on-accept on-accept}]]))

View file

@ -28,6 +28,17 @@
height: $s-40;
}
.top-actions-right {
display: flex;
gap: $s-8;
}
.opacity-input-wrapper {
@extend .input-element;
@include bodySmallTypography;
width: $s-68;
}
.picker-btn {
@include buttonStyle;
@include flexCenter;

View file

@ -7,35 +7,368 @@
(ns app.main.ui.workspace.colorpicker.gradients
(:require-macros [app.main.style :as stl])
(:require
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.math :as mth]
[app.main.ui.components.numeric-input :refer [numeric-input*]]
[app.main.ui.components.select :refer [select]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.formats :as fmt]
[app.main.ui.hooks :as h]
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]]
[app.util.dom :as dom]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn- format-rgba
[{:keys [r g b alpha offset]}]
(str/ffmt "rgba(%1, %2, %3, %4) %5%%" r g b alpha (* offset 100)))
(defn offset->string
[opacity]
(str (-> opacity
(d/coalesce 1)
(* 100)
(fmt/format-number))))
(defn- event->offset
[^js event]
(/ (.. event -nativeEvent -offsetX)
(-> event dom/get-current-target dom/get-bounding-rect :width)))
;; (defn- format-rgba
;; [{:keys [r g b alpha offset]}]
;; (str/ffmt "rgba(%1, %2, %3, %4) %5%%" r g b alpha (* offset 100)))
(defn- format-rgb
[{:keys [r g b offset]}]
(str/ffmt "rgb(%1, %2, %3) %4%%" r g b (* offset 100)))
(defn- gradient->string [stops]
(let [gradient-css (str/join ", " (map format-rgba stops))]
(str/ffmt "linear-gradient(90deg, %1)" gradient-css)))
(->> stops
(sort-by :offset)
(map (fn [{:keys [color opacity offset]}]
(let [[r g b] (cc/hex->rgb color)]
{:r r :g g :b b :alpha opacity :offset offset})))
(map format-rgb)
(str/join ", ")
(str/ffmt "linear-gradient(90deg, %1)")))
(mf/defc stop-input-row
[{:keys [stop
index
is-selected
on-select-stop
on-change-stop
on-remove-stop
on-reorder-stops
on-focus-stop-offset
on-blur-stop-offset
on-focus-stop-color
on-blur-stop-color]}]
(let [{:keys [color opacity offset]} stop
handle-change-stop-color
(mf/use-callback
(mf/deps on-change-stop stop)
(fn [value]
(on-change-stop stop (merge stop value))))
handle-change-offset
(mf/use-callback
(mf/deps on-change-stop stop)
(fn [value]
(on-change-stop stop (assoc stop :offset (mth/precision (/ value 100) 2)))))
handle-remove-stop
(mf/use-callback
(mf/deps on-remove-stop stop)
(fn []
(when on-remove-stop
(on-remove-stop stop))))
handle-focus-stop-offset
(mf/use-fn
(mf/deps on-select-stop on-focus-stop-offset index)
(fn []
(on-select-stop index)
(when on-focus-stop-offset
(on-focus-stop-offset))))
handle-blur-stop-offset
(mf/use-fn
(mf/deps on-select-stop on-blur-stop-offset index)
(fn []
(on-select-stop index)
(when on-blur-stop-offset
(on-blur-stop-offset))))
handle-focus-stop-color
(mf/use-fn
(mf/deps on-select-stop on-focus-stop-offset index)
(fn []
(on-select-stop index)
(when on-focus-stop-color
(on-focus-stop-offset))))
handle-blur-stop-color
(mf/use-fn
(mf/deps on-select-stop on-blur-stop-color index)
(fn []
(on-select-stop index)
(when on-blur-stop-color
(on-blur-stop-color))))
on-drop
(mf/use-fn
(mf/deps index on-reorder-stops)
(fn [position data]
(let [from-index (:index data)
to-index (if (= position :bot) (inc index) index)]
(when on-reorder-stops
(on-reorder-stops from-index to-index)))))
[dprops dref]
(h/use-sortable
:data-type "penpot/stops"
:on-drop on-drop
:data {:index index}
:draggable? true)]
[:div {:class (stl/css-case :gradient-stops-entry true
:is-selected is-selected
:dnd-over (= (:over dprops) :center)
:dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot))}
[:div {:ref dref
:class (stl/css :reorder)}
[:> icon*
{:id i/reorder
:class (stl/css :reorder-icon)
:aria-hidden true}]]
[:hr {:class (stl/css :reorder-separator-top)}]
[:hr {:class (stl/css :reorder-separator-bottom)}]
[:div {:class (stl/css :offset-input-wrapper)}
[:span {:class (stl/css :icon-text)} "%"]
[:> numeric-input*
{:value (-> offset offset->string)
:on-change handle-change-offset
:default 100
:min 0
:max 100
:on-focus handle-focus-stop-offset
:on-blur handle-blur-stop-offset}]]
[:& color-row
{:disable-gradient true
:disable-picker true
:color {:color color
:opacity opacity}
:index index
:on-change handle-change-stop-color
:on-remove handle-remove-stop
:on-focus handle-focus-stop-color
:on-blur handle-blur-stop-color}]]))
(mf/defc gradients
[{:keys [stops editing-stop on-select-stop]}]
[:div {:class (stl/css :gradient-stops)}
[:div {:class (stl/css :gradient-background-wrapper)}
[:div {:class (stl/css :gradient-background)
:style {:background (gradient->string stops)}}]]
[{:keys [type
stops
editing-stop
on-select-stop
on-change-type
on-change-stop
on-add-stop-preview
on-add-stop-auto
on-remove-stop
on-stop-edit-start
on-stop-edit-finish
on-reverse-stops
on-rotate-stops
on-reorder-stops]}]
[:div {:class (stl/css :gradient-stop-wrapper)}
(for [{:keys [offset hex r g b alpha] :as value} stops]
[:button {:class (stl/css-case :gradient-stop true
:selected (= editing-stop offset))
:data-value (str offset)
:on-click on-select-stop
:style {:left (dm/str (* offset 100) "%")}
:key (dm/str offset)}
(let [preview-state (mf/use-state {:hover? false :offset 0.5})
dragging-ref (mf/use-ref false)
start-ref (mf/use-ref nil)
start-offset (mf/use-ref nil)
background-ref (mf/use-ref nil)
[:div {:class (stl/css :gradient-stop-color)
:style {:background-color hex}}]
[:div {:class (stl/css :gradient-stop-alpha)
:style {:background-color (str/ffmt "rgba(%1, %2, %3, %4)" r g b alpha)}}]])]])
handle-select-stop
(mf/use-callback
(mf/deps on-select-stop)
(fn [event]
(when on-select-stop
(let [index (-> event dom/get-current-target (dom/get-data "index") d/read-string)]
(on-select-stop index)))))
handle-change-type
(mf/use-callback
(mf/deps on-change-type)
(fn [event]
(when on-change-type
(on-change-type event))))
handle-add-stop
(mf/use-callback
(mf/deps on-add-stop-auto)
(fn []
(when on-add-stop-auto
(on-add-stop-auto))))
handle-preview-enter
(mf/use-fn
(fn []
(swap! preview-state assoc :hover? true)))
handle-preview-leave
(mf/use-fn
(fn []
(swap! preview-state assoc :hover? false)))
handle-preview-move
(mf/use-fn
(fn [^js e]
(let [offset (-> (event->offset e)
(mth/precision 2))]
(swap! preview-state assoc :offset offset))))
handle-preview-down
(mf/use-fn
(mf/deps on-add-stop-preview)
(fn [^js e]
(let [offset (-> (event->offset e)
(mth/precision 2))]
(when on-add-stop-preview
(on-add-stop-preview offset)))))
handle-stop-marker-pointer-down
(mf/use-fn
(mf/deps on-stop-edit-start handle-select-stop stops)
(fn [event]
(let [index (-> event dom/get-current-target (dom/get-data "index") d/read-string)
stop (get stops index)]
(dom/capture-pointer event)
(handle-select-stop event)
(mf/set-ref-val! dragging-ref true)
(mf/set-ref-val! start-ref (dom/get-client-position event))
(mf/set-ref-val! start-offset (:offset stop))
(on-stop-edit-start))))
handle-stop-marker-pointer-move
(mf/use-fn
(mf/deps on-change-stop stops)
(fn [event]
(when-let [_ (mf/ref-val dragging-ref)]
(let [start-pt (mf/ref-val start-ref)
start-offset (mf/ref-val start-offset)
index (-> event dom/get-target (dom/get-data "index") d/read-string)
current-pt (dom/get-client-position event)
delta-x (- (:x current-pt) (:x start-pt))
background-node (mf/ref-val background-ref)
background-width (-> background-node dom/get-bounding-rect :width)
delta-offset (/ delta-x background-width)
stop (get stops index)
new-offset (mth/precision (mth/clamp (+ start-offset delta-offset) 0 1) 2)]
(on-change-stop stop (assoc stop :offset new-offset))))))
handle-stop-marker-lost-pointer-capture
(mf/use-fn
(mf/deps on-stop-edit-finish)
(fn [event]
(dom/release-pointer event)
(mf/set-ref-val! dragging-ref false)
(mf/set-ref-val! start-ref nil)
(on-stop-edit-finish)))
handle-rotate-gradient
(mf/use-fn
(mf/deps on-rotate-stops)
(fn []
(when on-rotate-stops
(on-rotate-stops))))
handle-reverse-gradient
(mf/use-fn
(mf/deps on-reverse-stops)
(fn []
(when on-reverse-stops
(on-reverse-stops))))]
[:div {:class (stl/css :gradient-panel)}
[:div {:class (stl/css :gradient-preview)}
[:div {:class (stl/css :gradient-background)
:ref background-ref
:style {:background (gradient->string stops)}
:on-pointer-enter handle-preview-enter
:on-pointer-leave handle-preview-leave
:on-pointer-move handle-preview-move
:on-pointer-down handle-preview-down}
[:div {:class (stl/css :gradient-preview-stop-preview)
:style {:display (if (:hover? @preview-state) "block" "none")
"--preview-position" (dm/str (* 100 (:offset @preview-state)) "%")}}]]
[:div {:class (stl/css :gradient-preview-stop-wrapper)}
(for [[index {:keys [color offset r g b alpha]}] (d/enumerate stops)]
[:* {:key (dm/str "preview-stop-" index)}
[:div
{:class (stl/css-case :gradient-preview-stop true
:is-selected (= editing-stop index))
:style {"--color-solid" color
"--color-alpha" (str/ffmt "rgba(%1, %2, %3, %4)" r g b alpha)
"--position" (dm/str (* offset 100) "%")}
:data-index index
:on-pointer-down handle-stop-marker-pointer-down
:on-pointer-move handle-stop-marker-pointer-move
:on-lost-pointer-capture handle-stop-marker-lost-pointer-capture}
[:div {:class (stl/css :gradient-preview-stop-color)
:style {:pointer-events "none"}}]
[:div {:class (stl/css :gradient-preview-stop-alpha)
:style {:pointer-events "none"}}]]
[:div {:class (stl/css :gradient-preview-stop-decoration)
:style {"--position" (dm/str (* offset 100) "%")}}]])]]
[:div {:class (stl/css :gradient-options)}
[:& select
{:default-value type
:options [{:value :linear-gradient :label "Linear"}
{:value :radial-gradient :label "Radial"}]
:on-change handle-change-type
:class (stl/css :gradient-options-select)}]
[:div {:class (stl/css :gradient-options-buttons)}
[:> icon-button* {:variant "ghost"
:aria-label "Rotate gradient"
:on-click handle-rotate-gradient
:icon-class (stl/css :rotate-icon)
:icon "reload"}]
[:> icon-button* {:variant "ghost"
:aria-label "Reverse gradient"
:on-click handle-reverse-gradient
:icon "switch"}]
[:> icon-button* {:variant "ghost"
:aria-label "Add stop"
:on-click handle-add-stop
:icon "add"}]]]
[:div {:class (stl/css :gradient-stops-list)}
[:& h/sortable-container {}
(for [[index stop] (d/enumerate stops)]
[:& stop-input-row
{:key index
:stop stop
:index index
:is-selected (= editing-stop index)
:on-select-stop on-select-stop
:on-change-stop on-change-stop
:on-remove-stop on-remove-stop
:on-focus-stop-offset on-stop-edit-start
:on-blur-stop-offset on-stop-edit-finish
:on-focus-stop-color on-stop-edit-start
:on-blur-stop-color on-stop-edit-finish
:on-reorder-stops on-reorder-stops}])]]
[:hr {:class (stl/css :gradient-separator)}]]))

View file

@ -6,57 +6,197 @@
@import "refactor/common-refactor.scss";
.gradient-stops {
display: flex;
height: $s-20;
width: 100%;
margin: $s-12 0;
background-color: var(--colorpicker-handlers-color);
border-radius: $br-6;
.gradient-panel {
margin-top: $s-12;
display: grid;
gap: $s-4;
grid-template-rows: $s-56 $s-32 1fr;
}
.gradient-background-wrapper {
height: 100%;
.gradient-preview {
width: 100%;
border-radius: $br-6;
background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAExJREFUSIljvHnz5n8GLEBNTQ2bMMOtW7ewiuNSz4RVlIpg1IKBt4Dx////WFMRqakFl/qhH0SjFhAELNRKLaNl0Qi2YLQsGrWAcgAA0gAgQPhT2rAAAAAASUVORK5CYII=")
left center;
height: 100%;
display: flex;
align-items: center;
padding: $s-12;
position: relative;
}
.gradient-background {
height: 100%;
width: 100%;
border-radius: $br-6;
border: $s-2 solid var(--colorpicker-details-color);
}
.gradient-stop-wrapper {
position: absolute;
width: calc(100% - $s-40);
left: $s-20;
}
.gradient-stop {
position: absolute;
display: grid;
grid-template-columns: 50% 50%;
padding: 0;
width: $s-16;
height: $s-24;
height: $s-20;
border-radius: $br-4;
margin-top: calc(-1 * $s-2);
margin-left: calc(-1 * $s-8);
border: $s-2 solid var(--colorpicker-handlers-color);
position: relative;
cursor: pointer;
}
.gradient-preview-stop-wrapper {
position: absolute;
width: calc(100% - $s-24 - $s-4);
height: 100%;
left: $s-2;
top: calc(-1 * $s-4);
pointer-events: none;
}
.gradient-preview-stop {
background-color: var(--color-foreground-primary);
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=");
background-position: left center;
background-size: 8px;
&.selected {
border: $s-2 solid var(--colorpicker-details-color-selected);
background-size: $s-8;
border-radius: $br-6;
border: $s-2 solid var(--color-foreground-primary);
box-shadow: 0px 0px $s-4 0px var(--menu-shadow-color);
height: calc($s-24 - $s-2);
left: var(--position);
overflow: hidden;
pointer-events: initial;
position: absolute;
width: calc($s-24 - $s-2);
&.is-selected,
&:hover {
outline: $s-2 solid var(--color-accent-primary);
}
}
.gradient-preview-stop-decoration {
background: var(--color-foreground-primary);
border-radius: 100%;
bottom: $s-32;
box-shadow: 0px 0px $s-4 0px var(--menu-shadow-color);
height: $s-4;
left: calc(var(--position) + $s-8);
position: absolute;
width: $s-4;
}
.gradient-preview-stop-color {
position: absolute;
left: 0;
width: 50%;
height: 100%;
background: var(--color-solid);
}
.gradient-preview-stop-alpha {
position: absolute;
left: 50%;
width: 50%;
height: 100%;
background: var(--color-alpha);
}
.gradient-options {
display: flex;
justify-content: space-between;
}
.gradient-options-buttons {
display: flex;
}
.gradient-options-select {
width: $s-140;
}
.rotate-icon {
transform: scaleX(-1);
}
.gradient-stops-list {
display: flex;
flex-direction: column;
gap: $s-4;
max-height: $s-180;
overflow-y: auto;
overflow-x: hidden;
padding: $s-1 0;
}
.gradient-stops-entry {
display: flex;
gap: $s-4;
padding: $s-2;
border-radius: $br-12;
border: $s-1 solid transparent;
&:hover .reorder-icon {
visibility: visible;
}
&.is-selected {
border-color: var(--color-accent-primary-muted);
}
&.dnd-over-top .reorder-separator-top {
display: block;
}
&.dnd-over-bot .reorder-separator-bottom {
display: block;
}
}
.gradient-stop-color,
.gradient-stop-alpha {
width: 100%;
height: 100%;
.reorder {
cursor: grab;
display: flex;
flex-direction: column;
height: $s-36;
justify-content: center;
left: calc(-1 * $s-2);
margin-top: calc(-1 * $s-2);
position: absolute;
width: $s-16;
}
.reorder-icon {
height: $s-16;
pointer-events: none;
stroke: var(--color-foreground-secondary);
visibility: hidden;
}
.reorder-separator-top {
border-color: var(--color-accent-primary);
display: none;
left: 0;
margin-left: $s-12;
margin-top: calc(-1 * $s-6);
position: absolute;
width: calc(100% - $s-24);
}
.reorder-separator-bottom {
border-color: var(--color-accent-primary);
display: none;
left: 0;
margin-left: $s-12;
margin-top: $s-36;
position: absolute;
width: calc(100% - $s-24);
}
.offset-input-wrapper {
@extend .input-element;
@include bodySmallTypography;
width: $s-92;
}
.gradient-separator {
border-color: var(--color-background-quaternary);
border-width: $s-3;
margin-left: -4%;
position: relative;
width: 108%;
}
.gradient-preview-stop-preview {
background: var(--color-foreground-primary);
border-radius: 50%;
height: $s-4;
left: calc(var(--preview-position, 0%) - $s-2);
pointer-events: none;
position: absolute;
top: calc(50% - $s-2);
width: $s-4;
}

View file

@ -0,0 +1,28 @@
;; 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) KALEIDOS INC
(ns app.main.ui.workspace.colorpicker.shortcuts
(:require
[app.main.data.shortcuts :as ds]
[app.main.data.workspace.colors :as dwc]
[app.main.data.workspace.shortcuts :as wsc]
[app.main.store :as st]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Shortcuts
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Shortcuts format https://github.com/ccampbell/mousetrap
(def shortcuts
(merge
wsc/shortcuts
{:delete-stop {:tooltip (ds/supr)
:command ["del" "backspace"]
:subsections [:edit]
:fn #(st/emit! (dwc/remove-gradient-stop))}}))

View file

@ -46,7 +46,7 @@
(if (= v :multiple) nil v))
(mf/defc color-row
[{:keys [index color disable-gradient disable-opacity disable-image on-change
[{:keys [index color disable-gradient disable-opacity disable-image disable-picker on-change
on-reorder on-detach on-open on-close on-remove
disable-drag on-focus on-blur select-only select-on-focus]}]
(let [current-file-id (mf/use-ctx ctx/current-file-id)
@ -72,8 +72,7 @@
editing-text? (deref editing-text*)
opacity?
(and (not gradient-color?)
(not multiple-colors?)
(and (not multiple-colors?)
(not library-color?)
(not disable-opacity))
@ -134,7 +133,7 @@
handle-click-color
(mf/use-fn
(mf/deps disable-gradient disable-opacity disable-image on-change on-close on-open)
(mf/deps disable-gradient disable-opacity disable-image disable-picker on-change on-close on-open)
(fn [color event]
(let [color (cond
multiple-colors?
@ -166,7 +165,8 @@
(when (fn? on-open)
(on-open color))
(modal/show! :colorpicker props))))
(when-not disable-picker
(modal/show! :colorpicker props)))))
prev-color (h/use-previous color)
@ -187,8 +187,8 @@
:name (str "Color row" index)})
[nil nil])]
(mf/with-effect [color prev-color]
(when (not= prev-color color)
(mf/with-effect [color prev-color disable-picker]
(when (and (not disable-picker) (not= prev-color color))
(modal/update-props! :colorpicker {:data (parse-color color)})))
[:div {:class (stl/css-case

View file

@ -153,14 +153,6 @@
}
}
.gradient-name-wrapper {
border-radius: 0 $br-8 $br-8 0;
.color-name {
@include flexRow;
border-radius: 0 $br-8 $br-8 0;
}
}
.library-name-wrapper {
border-radius: $br-8;
}

View file

@ -7,14 +7,18 @@
(ns app.main.ui.workspace.viewport.gradients
"Gradients handlers and renders"
(:require
[app.common.colors :as cc]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.points :as gsp]
[app.common.math :as mth]
[app.main.data.workspace.colors :as dc]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.workspace.viewport.viewport-ref :as uwvv]
[app.util.dom :as dom]
[app.util.mouse :as mse]
[beicon.v2.core :as rx]
@ -23,21 +27,27 @@
(def gradient-line-stroke-width 2)
(def gradient-line-stroke-color "var(--app-white)")
(def gradient-square-width 15)
(def gradient-square-radius 2)
(def gradient-square-width 20.5)
(def gradient-square-radius 4)
(def gradient-square-stroke-width 2)
(def gradient-width-handler-radius 5)
(def gradient-width-handler-radius 4)
(def gradient-width-handler-radius-selected 6)
(def gradient-width-handler-radius-handler 15)
(def gradient-width-handler-color "var(--app-white)")
(def gradient-square-stroke-color "var(--app-white)")
(def gradient-square-stroke-color-selected "var(--color-accent-tertiary)")
(mf/defc shadow [{:keys [id x y width height offset]}]
(def gradient-endpoint-radius 4)
(def gradient-endpoint-radius-selected 6)
(def gradient-endpoint-radius-handler 20)
(mf/defc shadow [{:keys [id offset]}]
[:filter {:id id
:x x
:y y
:width width
:height height
:filterUnits "userSpaceOnUse"
:x "-10%"
:y "-10%"
:width "120%"
:height "120%"
:filterUnits "objectBoundingBox"
:color-interpolation-filters "sRGB"}
[:feFlood {:flood-opacity "0" :result "BackgroundImageFix"}]
[:feColorMatrix {:in "SourceAlpha" :type "matrix" :values "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"}]
@ -47,53 +57,22 @@
[:feBlend {:mode "normal" :in2 "BackgroundImageFix" :result id}]
[:feBlend {:mode "normal" :in "SourceGraphic" :in2 id :result "shape"}]])
(mf/defc gradient-line-drop-shadow-filter [{:keys [id zoom from-p to-p]}]
[:& shadow
{:id id
:x (min (- (:x from-p) (/ 2 zoom))
(- (:x to-p) (/ 2 zoom)))
:y (min (- (:y from-p) (/ 2 zoom))
(- (:y to-p) (/ 2 zoom)))
:width (+ (mth/abs (- (:x to-p) (:x from-p))) (/ 4 zoom))
:height (+ (mth/abs (- (:y to-p) (:y from-p))) (/ 4 zoom))
:offset (/ 2 zoom)}])
(mf/defc gradient-square-drop-shadow-filter [{:keys [id zoom point]}]
[:& shadow
{:id id
:x (- (:x point) (/ gradient-square-width zoom 2) 2)
:y (- (:y point) (/ gradient-square-width zoom 2) 2)
:width (+ (/ gradient-square-width zoom) (/ 2 zoom) 4)
:height (+ (/ gradient-square-width zoom) (/ 2 zoom) 4)
:offset (/ 2 zoom)}])
(mf/defc gradient-width-handler-shadow-filter [{:keys [id zoom point]}]
[:& shadow
{:id id
:x (- (:x point) (/ gradient-width-handler-radius zoom) 2)
:y (- (:y point) (/ gradient-width-handler-radius zoom) 2)
:width (+ (/ (* 2 gradient-width-handler-radius) zoom) (/ 2 zoom) 4)
:height (+ (/ (* 2 gradient-width-handler-radius) zoom) (/ 2 zoom) 4)
:offset (/ 2 zoom)}])
(def checkerboard "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAIAAAC0tAIdAAACvUlEQVQoFQGyAk39AeLi4gAAAAAAAB0dHQAAAAAAAOPj4wAAAAAAAB0dHQAAAAAAAOPj4wAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAB////AAAAAAAA4+PjAAAAAAAAHR0dAAAAAAAA4+PjAAAAAAAAHR0dAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAATj4+MAAAAAAAAdHR0AAAAAAADj4+MAAAAAAAAdHR0AAAAAAADj4+MAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjScaa0cU7nIAAAAASUVORK5CYII=")
#_(def checkerboard "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAADFJREFUOE9jZGBgEAFifOANPknGUQMYhkkYEEgG+NMJKAwIAbwJbdQABnBCIgRoG4gAIF8IsXB/Rs4AAAAASUVORK5CYII=")
(mf/defc gradient-color-handler
[{:keys [filter-id zoom point color angle selected
on-click on-pointer-down on-pointer-up]}]
[:g {:filter (str/fmt "url(#%s)" filter-id)
[{:keys [zoom point color angle selected index
on-click on-pointer-down on-pointer-up on-pointer-move on-lost-pointer-capture]}]
[:g {:filter "url(#gradient-drop-shadow)"
:style {:cursor "pointer"}
:transform (gmt/rotate-matrix angle point)}
[:image {:href checkerboard
:x (- (:x point) (/ gradient-square-width 2 zoom))
:x (+ (- (:x point) (/ gradient-square-width 2 zoom)) (/ 12 zoom))
:y (- (:y point) (/ gradient-square-width 2 zoom))
:width (/ gradient-square-width zoom)
:height (/ gradient-square-width zoom)}]
[:rect {:x (- (:x point) (/ gradient-square-width 2 zoom))
[:rect {:x (+ (- (:x point) (/ gradient-square-width 2 zoom)) (/ 12 zoom))
:y (- (:y point) (/ gradient-square-width 2 zoom))
:rx (/ gradient-square-radius zoom)
:width (/ gradient-square-width zoom 2)
@ -103,40 +82,60 @@
:on-pointer-down (partial on-pointer-down :to-p)
:on-pointer-up (partial on-pointer-up :to-p)}]
(when selected
[:rect {:pointer-events "none"
:x (- (+ (- (:x point) (/ gradient-square-width 2 zoom)) (/ 12 zoom)) (/ 2 zoom))
:y (- (- (:y point) (/ gradient-square-width 2 zoom)) (/ 2 zoom))
:rx (/ (+ gradient-square-radius (/ 2 zoom)) zoom)
:width (+ (/ gradient-square-width zoom) (/ 4 zoom))
:height (+ (/ gradient-square-width zoom) (/ 4 zoom))
:stroke "var(--color-accent-tertiary)"
:stroke-width (/ gradient-square-stroke-width zoom)
:fill "transparent"}])
[:rect {:data-allow-click-modal "colorpicker"
:x (- (:x point) (/ gradient-square-width 2 zoom))
:data-index index
:pointer-events "all"
:x (+ (- (:x point) (/ gradient-square-width 2 zoom)) (/ 12 zoom))
:y (- (:y point) (/ gradient-square-width 2 zoom))
:rx (/ gradient-square-radius zoom)
:width (/ gradient-square-width zoom)
:height (/ gradient-square-width zoom)
:stroke (if selected "var(--color-accent-tertiary)" "var(--app-white)")
:stroke "var(--app-white)"
:stroke-width (/ gradient-square-stroke-width zoom)
:fill (:value color)
:fill-opacity (:opacity color)
:on-click on-click
:on-pointer-down on-pointer-down
:on-pointer-up on-pointer-up}]])
:on-pointer-up on-pointer-up
:on-pointer-move on-pointer-move
:on-lost-pointer-capture on-lost-pointer-capture}]
[:circle {:cx (:x point)
:cy (:y point)
:r (/ 2 zoom)
:fill "var(--app-white)"}]])
(mf/defc gradient-handler-transformed
[{:keys [from-p to-p width-p from-color to-color zoom editing
[{:keys [from-p
to-p
width-p
zoom
editing
stops
on-change-start on-change-finish on-change-width]}]
(let [moving-point (mf/use-var nil)
angle (+ 90 (gpt/angle from-p to-p))
angle (+ 90 (gpt/angle from-p to-p))
dragging-ref (mf/use-ref false)
start-offset (mf/use-ref nil)
on-click
(fn [position event]
(dom/stop-propagation event)
(dom/prevent-default event)
(when (#{:from-p :to-p} position)
(st/emit! (dc/select-colorpicker-gradient-stop
(case position
:from-p 0
:to-p 1)))))
on-pointer-down
handler-state (mf/use-state {:display? false :offset 0 :hover nil})
endpoint-on-pointer-down
(fn [position event]
(dom/stop-propagation event)
(dom/prevent-default event)
(dom/capture-pointer event)
(reset! moving-point position)
(when (#{:from-p :to-p} position)
(st/emit! (dc/select-colorpicker-gradient-stop
@ -144,11 +143,102 @@
:from-p 0
:to-p 1)))))
on-pointer-up
endpoint-on-pointer-up
(fn [_position event]
(dom/release-pointer event)
(dom/stop-propagation event)
(dom/prevent-default event)
(reset! moving-point nil))]
(reset! moving-point nil)
(swap! handler-state assoc :hover nil))
endpoint-on-pointer-enter
(mf/use-fn
(fn [position]
(swap! handler-state assoc :hover position)))
endpoint-on-pointer-leave
(mf/use-fn
(fn [_]
(swap! handler-state assoc :hover nil)))
points-on-pointer-enter
(mf/use-fn
(fn []
(swap! handler-state assoc :display? true)))
points-on-pointer-leave
(mf/use-fn
(fn []
(swap! handler-state assoc :display? false)))
points-on-pointer-down
(mf/use-fn
(mf/deps stops)
(fn [e]
(dom/prevent-default e)
(dom/stop-propagation e)
(let [raw-pt (dom/get-client-position e)
position (uwvv/point->viewport raw-pt)
lv (-> (gpt/to-vec from-p to-p) (gpt/unit))
nv (gpt/normal-left lv)
offset (-> (gsp/project-t position [from-p to-p] nv)
(mth/precision 2))
new-stop (cc/interpolate-gradient stops offset)
stops (conj stops new-stop)
stops (->> stops (sort-by :offset) (into []))]
(st/emit! (dc/update-colorpicker-stops stops)))))
points-on-pointer-move
(mf/use-fn
(mf/deps from-p to-p)
(fn [e]
(let [raw-pt (dom/get-client-position e)
position (uwvv/point->viewport raw-pt)
lv (-> (gpt/to-vec from-p to-p) (gpt/unit))
nv (gpt/normal-left lv)
offset (gsp/project-t position [from-p to-p] nv)]
(swap! handler-state assoc :offset offset))))
handle-marker-pointer-down
(mf/use-fn
(mf/deps stops)
(fn [event]
(let [index (-> event dom/get-current-target (dom/get-data "index") d/read-string)
stop (get stops index)]
(dom/capture-pointer event)
(st/emit! (dc/select-colorpicker-gradient-stop index))
(mf/set-ref-val! dragging-ref true)
(mf/set-ref-val! start-offset (:offset stop)))))
handle-marker-pointer-move
(mf/use-fn
(mf/deps stops)
(fn [event]
(when-let [_ (mf/ref-val dragging-ref)]
(let [index (-> event dom/get-target (dom/get-data "index") d/read-string)
raw-pt (dom/get-client-position event)
position (uwvv/point->viewport raw-pt)
lv (-> (gpt/to-vec from-p to-p) (gpt/unit))
nv (gpt/normal-left lv)
offset (gsp/project-t position [from-p to-p] nv)
offset (mth/precision (mth/clamp offset 0 1) 2)]
(st/emit! (dc/update-colorpicker-stops (assoc-in stops [index :offset] offset)))))))
handle-marker-lost-pointer-capture
(mf/use-fn
(mf/deps stops)
(fn [event]
(dom/release-pointer event)
(mf/set-ref-val! dragging-ref false)
(mf/set-ref-val! start-offset nil)
(let [stops (->> stops
(sort-by :offset)
(into []))]
(st/emit! (dc/update-colorpicker-stops stops)))))]
(mf/use-effect
(mf/deps @moving-point from-p to-p width-p)
@ -171,73 +261,193 @@
(on-change-width new-width-p)))
nil))))]
(fn [] (rx/dispose! subs)))))
[:g.gradient-handlers
[:g.gradient-handlers {:pointer-events "none"}
[:defs
[:& gradient-line-drop-shadow-filter {:id "gradient-line-drop-shadow" :from-p from-p :to-p to-p :zoom zoom}]
[:& gradient-line-drop-shadow-filter {:id "gradient-width-line-drop-shadow" :from-p from-p :to-p width-p :zoom zoom}]
[:& gradient-square-drop-shadow-filter {:id "gradient-square-from-drop-shadow" :point from-p :zoom zoom}]
[:& gradient-square-drop-shadow-filter {:id "gradient-square-to-drop-shadow" :point to-p :zoom zoom}]
[:& gradient-width-handler-shadow-filter {:id "gradient-width-handler-drop-shadow" :point width-p :zoom zoom}]]
[:& shadow {:id "gradient-drop-shadow" :offset (/ 2 zoom)}]]
[:g {:filter "url(#gradient-line-drop-shadow)"}
[:line {:x1 (:x from-p)
:y1 (:y from-p)
:x2 (:x to-p)
:y2 (:y to-p)
:stroke gradient-line-stroke-color
:stroke-width (/ gradient-line-stroke-width zoom)}]]
(let [lv (-> (gpt/to-vec from-p to-p)
(gpt/unit))
nv (gpt/normal-left lv)
width (/ 40 zoom)
points [(gpt/add from-p (gpt/scale nv (/ width -2)))
(gpt/add from-p (gpt/scale nv (/ width 2)))
(gpt/add to-p (gpt/scale nv (/ width 2)))
(gpt/add to-p (gpt/scale nv (/ width -2)))]
points-str
(->> points
(map #(dm/str (:x %) "," (:y %)))
(str/join ", "))]
[:polygon {:points points-str
:data-allow-click-modal "colorpicker"
:fill "transparent"
:pointer-events "all"
:on-pointer-enter points-on-pointer-enter
:on-pointer-leave points-on-pointer-leave
:on-pointer-down points-on-pointer-down
:on-pointer-move points-on-pointer-move}])
[:g {:filter "url(#gradient-drop-shadow)"}
(let [pu
(-> (gpt/to-vec from-p to-p)
(gpt/normal-right)
(gpt/unit))
sc (/ gradient-line-stroke-width zoom 2)
points
[(gpt/add from-p (gpt/scale pu (- sc)))
(gpt/add from-p (gpt/scale pu sc))
(gpt/add to-p (gpt/scale pu sc))
(gpt/add to-p (gpt/scale pu (- sc)))]]
;; Use the polygon shape instead of lines because horizontal/vertical lines won't work
;; with shadows
[:polygon
{:points
(->> points
(map #(dm/fmt "%, %" (:x %) (:y %)))
(str/join " "))
:fill gradient-line-stroke-color}])]
(when width-p
[:g {:filter "url(#gradient-width-line-drop-shadow)"}
[:line {:x1 (:x from-p)
:y1 (:y from-p)
:x2 (:x width-p)
:y2 (:y width-p)
:stroke gradient-line-stroke-color
:stroke-width (/ gradient-line-stroke-width zoom)}]])
[:g {:filter "url(#gradient-drop-shadow)"}
(let [pu
(-> (gpt/to-vec from-p width-p)
(gpt/normal-right)
(gpt/unit))
sc (/ gradient-line-stroke-width zoom 2)
points
[(gpt/add from-p (gpt/scale pu (- sc)))
(gpt/add from-p (gpt/scale pu sc))
(gpt/add width-p (gpt/scale pu sc))
(gpt/add width-p (gpt/scale pu (- sc)))]]
;; Use the polygon shape instead of lines because horizontal/vertical lines won't work
;; with shadows
[:polygon {:points
(->> points
(map #(dm/fmt "%, %" (:x %) (:y %)))
(str/join " "))
:fill gradient-line-stroke-color}])])
(when width-p
[:g {:filter "url(#gradient-width-handler-drop-shadow)"}
[:g {:filter "url(#gradient-drop-shadow)"}
(when (= :width-p (:hover @handler-state))
[:circle {:filter "url(#gradient-drop-shadow)"
:cx (:x width-p)
:cy (:y width-p)
:fill gradient-square-stroke-color-selected
:r (/ gradient-width-handler-radius-selected zoom)}])
[:circle {:data-allow-click-modal "colorpicker"
:cx (:x width-p)
:cy (:y width-p)
:r (/ gradient-width-handler-radius zoom)
:fill gradient-width-handler-color
:on-pointer-down (partial on-pointer-down :width-p)
:on-pointer-up (partial on-pointer-up :width-p)}]])
:fill gradient-width-handler-color}]
[:& gradient-color-handler
{:selected (or (not editing) (= editing 0))
:filter-id "gradient-square-from-drop-shadow"
:zoom zoom
:point from-p
:color from-color
:angle angle
:on-click (partial on-click :from-p)
:on-pointer-down (partial on-pointer-down :from-p)
:on-pointer-up (partial on-pointer-up :from-p)}]
[:circle {:data-allow-click-modal "colorpicker"
:pointer-events "all"
:cx (:x width-p)
:cy (:y width-p)
:r (/ gradient-width-handler-radius-handler zoom)
:fill "transpgarent"
:on-pointer-down (partial endpoint-on-pointer-down :width-p)
:on-pointer-enter (partial endpoint-on-pointer-enter :width-p)
:on-pointer-leave (partial endpoint-on-pointer-leave :width-p)
:on-pointer-up (partial endpoint-on-pointer-up :width-p)}]])
[:& gradient-color-handler
{:selected (= editing 1)
:filter-id "gradient-square-to-drop-shadow"
:zoom zoom
:point to-p
:color to-color
:angle angle
:on-click (partial on-click :to-p)
:on-pointer-down (partial on-pointer-down :to-p)
:on-pointer-up (partial on-pointer-up :to-p)}]]))
[:g
(when (= :from-p (:hover @handler-state))
[:circle {:filter "url(#gradient-drop-shadow)"
:cx (:x from-p)
:cy (:y from-p)
:fill gradient-square-stroke-color-selected
:r (/ gradient-endpoint-radius-selected zoom)}])
[:circle {:filter "url(#gradient-drop-shadow)"
:cx (:x from-p)
:cy (:y from-p)
:fill "var(--app-white)"
:r (/ gradient-endpoint-radius zoom)}]
[:circle {:data-allow-click-modal "colorpicker"
:pointer-events "all"
:cx (:x from-p)
:cy (:y from-p)
:fill "transparent"
:r (/ gradient-endpoint-radius-handler zoom)
:on-pointer-down (partial endpoint-on-pointer-down :from-p)
:on-pointer-up (partial endpoint-on-pointer-up :from-p)
:on-pointer-enter (partial endpoint-on-pointer-enter :from-p)
:on-pointer-leave (partial endpoint-on-pointer-leave :from-p)
:on-lost-pointer-capture (partial endpoint-on-pointer-up :from-p)}]]
[:g
(when (= :to-p (:hover @handler-state))
[:circle {:filter "url(#gradient-drop-shadow)"
:cx (:x to-p)
:cy (:y to-p)
:fill gradient-square-stroke-color-selected
:r (/ gradient-endpoint-radius-selected zoom)}])
[:circle {:filter "url(#gradient-drop-shadow)"
:cx (:x to-p)
:cy (:y to-p)
:fill "var(--app-white)"
:r (/ gradient-endpoint-radius zoom)}]
[:circle {:data-allow-click-modal "colorpicker"
:pointer-events "all"
:cx (:x to-p)
:cy (:y to-p)
:fill "transparent"
:r (/ gradient-endpoint-radius-handler zoom)
:on-pointer-down (partial endpoint-on-pointer-down :to-p)
:on-pointer-up (partial endpoint-on-pointer-up :to-p)
:on-pointer-enter (partial endpoint-on-pointer-enter :to-p)
:on-pointer-leave (partial endpoint-on-pointer-leave :to-p)
:on-lost-pointer-capture (partial endpoint-on-pointer-up :from-p)}]]
(for [[index stop] (d/enumerate stops)]
(let [stop-p
(gpt/add
from-p
(-> (gpt/to-vec from-p to-p)
(gpt/scale (:offset stop))))]
[:& gradient-color-handler
{:key index
:selected (= editing index)
:zoom zoom
:point stop-p
:color {:value (:color stop) :opacity (:opacity stop)}
:angle angle
:index index
:on-pointer-down handle-marker-pointer-down
:on-pointer-move handle-marker-pointer-move
:on-lost-pointer-capture handle-marker-lost-pointer-capture}]))
(when (:display? @handler-state)
(let [p (gpt/add from-p
(-> (gpt/to-vec from-p to-p)
(gpt/scale (:offset @handler-state))))]
[:circle {:filter "url(#gradient-drop-shadow)"
:cx (:x p)
:cy (:y p)
:r (/ 4 zoom)
:fill "var(--app-white)"}]))]))
(mf/defc gradient-handlers*
[{:keys [zoom stops gradient editing-stop shape]}]
[{:keys [zoom stops gradient editing shape] :as kk}]
(let [transform (gsh/transform-matrix shape)
transform-inverse (gsh/inverse-transform-matrix shape)
{:keys [x y width height] :as sr} (:selrect shape)
[{start-color :color start-opacity :opacity}
{end-color :color end-opacity :opacity}] stops
from-p (-> (gpt/point (+ x (* width (:start-x gradient)))
(+ y (* height (:start-y gradient))))
(gpt/transform transform))
@ -249,7 +459,7 @@
gradient-length (gpt/length gradient-vec)
width-v (-> gradient-vec
(gpt/normal-left)
(gpt/normal-right)
(gpt/multiply (gpt/point (* (:width gradient) (/ gradient-length (/ height 2)))))
(gpt/multiply (gpt/point (/ width 2))))
@ -289,12 +499,11 @@
(change! {:width norm-dist})))))]
[:& gradient-handler-transformed
{:editing editing-stop
{:editing editing
:from-p from-p
:to-p to-p
:width-p (when (= :radial (:type gradient)) width-p)
:from-color {:value start-color :opacity start-opacity}
:to-color {:value end-color :opacity end-opacity}
:stops stops
:zoom zoom
:on-change-start on-change-start
:on-change-finish on-change-finish
@ -305,17 +514,15 @@
[{:keys [id zoom]}]
(let [shape-ref (mf/use-memo (mf/deps id) #(refs/object-by-id id))
shape (mf/deref shape-ref)
state (mf/deref refs/colorpicker)
gradient (:gradient state)
stops (:stops state)
editing-stop (:editing-stop state)]
(when (and (some? gradient)
(= id (:shape-id gradient)))
(when (and (some? gradient) (= id (:shape-id gradient)))
[:& gradient-handlers*
{:zoom zoom
:gradient gradient
:stops stops
:editing-stop editing-stop
:editing editing-stop
:shape shape}])))

View file

@ -2146,7 +2146,6 @@ msgstr "YouTube"
msgid "media.choose-image"
msgstr "Choose image"
#, unused
msgid "media.gradient"
msgstr "Gradient"