From 91fbe8f8efd059dc3286ab9ac371e3b783df2522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bel=C3=A9n=20Albeza?= Date: Tue, 13 May 2025 10:37:05 +0200 Subject: [PATCH] :tada: Cap stop amount in UI for wasm (#6438) * :tada: Cap in the colorpicker the amount of stops a gradient can have * :tada: Cap the stops amount in gradient handlers * :tada: Disable add stop in gradient handlers (viewport + colorpicker) * :sparkles: Add integration test for gradient limits * :lipstick: Address PR suggestion --- common/src/app/common/types/shape.cljc | 2 + .../get-file-fragment-gradient-limits.json | 279 ++++++++++++++++++ frontend/playwright/ui/pages/BasePage.js | 11 + frontend/playwright/ui/pages/WorkspacePage.js | 6 + .../playwright/ui/specs/colorpicker.spec.js | 84 +++--- .../src/app/main/data/workspace/colors.cljs | 90 +++--- .../app/main/ui/workspace/colorpicker.cljs | 6 +- .../ui/workspace/colorpicker/gradients.cljs | 15 +- .../main/ui/workspace/viewport/gradients.cljs | 38 ++- .../app/render_wasm/serializers/fills.cljs | 9 +- 10 files changed, 438 insertions(+), 102 deletions(-) create mode 100644 frontend/playwright/data/workspace/get-file-fragment-gradient-limits.json diff --git a/common/src/app/common/types/shape.cljc b/common/src/app/common/types/shape.cljc index 63caf358f2..4f8f1f313b 100644 --- a/common/src/app/common/types/shape.cljc +++ b/common/src/app/common/types/shape.cljc @@ -761,3 +761,5 @@ (d/patch-object (select-keys props basic-extract-props)) (cond-> (cfh/text-shape? shape) (patch-text-props props)) (cond-> (cfh/frame-shape? shape) (patch-layout-props props))))) + +(def MAX-GRADIENT-STOPS 16) \ No newline at end of file diff --git a/frontend/playwright/data/workspace/get-file-fragment-gradient-limits.json b/frontend/playwright/data/workspace/get-file-fragment-gradient-limits.json new file mode 100644 index 0000000000..f1c89c7922 --- /dev/null +++ b/frontend/playwright/data/workspace/get-file-fragment-gradient-limits.json @@ -0,0 +1,279 @@ +{ + "~:id": "~u66697432-c33d-8055-8006-2c62de27d653", + "~:file-id": "~u66697432-c33d-8055-8006-2c62cc084cac", + "~:created-at": "~m1747053122715", + "~:data": { + "~:options": {}, + "~:objects": { + "~u00000000-0000-0000-0000-000000000000": { + "~#shape": { + "~:y": 0, + "~:hide-fill-on-export": false, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:name": "Root Frame", + "~:width": 0.01, + "~:type": "~:frame", + "~:points": [ + { + "~#point": { + "~:x": 0.0, + "~:y": 0.0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.0 + } + }, + { + "~#point": { + "~:x": 0.01, + "~:y": 0.01 + } + }, + { + "~#point": { + "~:x": 0.0, + "~:y": 0.01 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~u00000000-0000-0000-0000-000000000000", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 0, + "~:proportion": 1.0, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 0, + "~:y": 0, + "~:width": 0.01, + "~:height": 0.01, + "~:x1": 0, + "~:y1": 0, + "~:x2": 0.01, + "~:y2": 0.01 + } + }, + "~:fills": [ + { + "~:fill-color": "#FFFFFF", + "~:fill-opacity": 1 + } + ], + "~:flip-x": null, + "~:height": 0.01, + "~:flip-y": null, + "~:shapes": [ + "~u4a1d24c0-c794-80ff-8006-2c62cd7f23e0" + ] + } + }, + "~u4a1d24c0-c794-80ff-8006-2c62cd7f23e0": { + "~#shape": { + "~:y": 303, + "~:transform": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:rotation": 0, + "~:hide-in-viewer": false, + "~:name": "Rectangle", + "~:width": 100, + "~:type": "~:rect", + "~:points": [ + { + "~#point": { + "~:x": 205, + "~:y": 303 + } + }, + { + "~#point": { + "~:x": 305, + "~:y": 303 + } + }, + { + "~#point": { + "~:x": 305, + "~:y": 403 + } + }, + { + "~#point": { + "~:x": 205, + "~:y": 403 + } + } + ], + "~:r2": 0, + "~:proportion-lock": false, + "~:transform-inverse": { + "~#matrix": { + "~:a": 1.0, + "~:b": 0.0, + "~:c": 0.0, + "~:d": 1.0, + "~:e": 0.0, + "~:f": 0.0 + } + }, + "~:r3": 0, + "~:r1": 0, + "~:id": "~u4a1d24c0-c794-80ff-8006-2c62cd7f23e0", + "~:parent-id": "~u00000000-0000-0000-0000-000000000000", + "~:frame-id": "~u00000000-0000-0000-0000-000000000000", + "~:strokes": [], + "~:x": 205, + "~:proportion": 1, + "~:r4": 0, + "~:selrect": { + "~#rect": { + "~:x": 205, + "~:y": 303, + "~:width": 100, + "~:height": 100, + "~:x1": 205, + "~:y1": 303, + "~:x2": 305, + "~:y2": 403 + } + }, + "~:fills": [ + { + "~:fill-color-gradient": { + "~:start-x": 0.5, + "~:start-y": 0, + "~:end-x": 0.5, + "~:end-y": 1, + "~:width": 1, + "~:type": "~:linear", + "~:stops": [ + { + "~:color": "#003fff", + "~:offset": 0, + "~:opacity": 1 + }, + { + "~:color": "#003fff", + "~:offset": 0.07, + "~:opacity": 0.9299999999999999 + }, + { + "~:color": "#003fff", + "~:offset": 0.13, + "~:opacity": 0.87 + }, + { + "~:color": "#003fff", + "~:offset": 0.2, + "~:opacity": 0.8 + }, + { + "~:color": "#003fff", + "~:offset": 0.27, + "~:opacity": 0.73 + }, + { + "~:color": "#003fff", + "~:offset": 0.33, + "~:opacity": 0.6699999999999999 + }, + { + "~:color": "#003fff", + "~:offset": 0.4, + "~:opacity": 0.6 + }, + { + "~:color": "#003fff", + "~:offset": 0.47, + "~:opacity": 0.53 + }, + { + "~:color": "#003fff", + "~:offset": 0.53, + "~:opacity": 0.47 + }, + { + "~:color": "#003fff", + "~:offset": 0.6, + "~:opacity": 0.4 + }, + { + "~:color": "#003fff", + "~:offset": 0.67, + "~:opacity": 0.32999999999999996 + }, + { + "~:color": "#003fff", + "~:offset": 0.73, + "~:opacity": 0.27 + }, + { + "~:color": "#003fff", + "~:offset": 0.8, + "~:opacity": 0.19999999999999996 + }, + { + "~:color": "#003fff", + "~:offset": 0.87, + "~:opacity": 0.13 + }, + { + "~:color": "#003fff", + "~:offset": 0.93, + "~:opacity": 0.06999999999999995 + }, + { + "~:color": "#003fff", + "~:offset": 1, + "~:opacity": 0 + } + ] + } + } + ], + "~:flip-x": null, + "~:height": 100, + "~:flip-y": null + } + } + }, + "~:id": "~u66697432-c33d-8055-8006-2c62cc084cad", + "~:name": "Page 1" + } +} \ No newline at end of file diff --git a/frontend/playwright/ui/pages/BasePage.js b/frontend/playwright/ui/pages/BasePage.js index b233be0604..155d7150b6 100644 --- a/frontend/playwright/ui/pages/BasePage.js +++ b/frontend/playwright/ui/pages/BasePage.js @@ -36,6 +36,17 @@ export class BasePage { async mockRPC(path, jsonFilename, options) { return BasePage.mockRPC(this.page, path, jsonFilename, options); } + + async mockConfigFlags(flags) { + const url = "**/js/config.js?ts=*"; + return await this.page.route(url, (route) => + route.fulfill({ + status: 200, + contentType: "application/javascript", + body: `var penpotFlags = "${flags.join(" ")}";`, + }), + ); + } } export default BasePage; diff --git a/frontend/playwright/ui/pages/WorkspacePage.js b/frontend/playwright/ui/pages/WorkspacePage.js index 6e79fa686a..9f28446928 100644 --- a/frontend/playwright/ui/pages/WorkspacePage.js +++ b/frontend/playwright/ui/pages/WorkspacePage.js @@ -49,6 +49,12 @@ export class WorkspacePage extends BaseWebSocketPage { "get-profiles-for-file-comments?file-id=*", "workspace/get-profile-for-file-comments.json", ); + + await BaseWebSocketPage.mockRPC( + page, + "update-profile-props", + "workspace/update-profile-empty.json", + ); } static anyTeamId = "c7ce0794-0992-8105-8004-38e630f7920a"; diff --git a/frontend/playwright/ui/specs/colorpicker.spec.js b/frontend/playwright/ui/specs/colorpicker.spec.js index d0356995d0..231c4f9d73 100644 --- a/frontend/playwright/ui/specs/colorpicker.spec.js +++ b/frontend/playwright/ui/specs/colorpicker.spec.js @@ -43,48 +43,44 @@ test("Create a LINEAR gradient", async ({ page }) => { 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"); + const select = workspacePage.page.getByText("Solid"); await select.click(); - const gradOption = await workspacePage.page.getByText("Gradient"); + const gradOption = workspacePage.page.getByText("Gradient"); await gradOption.click(); - const addStopBtn = await workspacePage.page.getByRole("button", { + const addStopBtn = workspacePage.page.getByRole("button", { name: "Add stop", }); await addStopBtn.click(); await addStopBtn.click(); await addStopBtn.click(); - const removeBtn = await workspacePage.page - .getByTestId("colorpicker") + const removeBtn = workspacePage.colorpicker .getByRole("button", { name: "Remove color" }) .nth(2); await removeBtn.click(); await removeBtn.click(); - const inputColor1 = await workspacePage.page.getByPlaceholder("Mixed").nth(1); + const inputColor1 = workspacePage.colorpicker + .getByPlaceholder("Mixed") + .nth(1); await inputColor1.fill("fabada"); - const inputOpacity1 = await workspacePage.page - .getByTestId("colorpicker") - .getByPlaceholder("--") - .nth(1); + const inputOpacity1 = workspacePage.colorpicker.getByPlaceholder("--").nth(1); await inputOpacity1.fill("100"); - const inputColor2 = await workspacePage.page.getByPlaceholder("Mixed").nth(2); + const inputColor2 = workspacePage.colorpicker + .getByPlaceholder("Mixed") + .nth(1); await inputColor2.fill("red"); - const inputOpacity2 = await workspacePage.page - .getByTestId("colorpicker") - .getByPlaceholder("--") - .nth(2); + const inputOpacity2 = workspacePage.colorpicker.getByPlaceholder("--").nth(1); await inputOpacity2.fill("100"); - const inputOpacityGlobal = await workspacePage.page + const inputOpacityGlobal = workspacePage.page .locator("div") .filter({ hasText: /^FillLinear gradient%$/ }) .getByPlaceholder("--"); @@ -110,60 +106,75 @@ test("Create a RADIAL gradient", async ({ page }) => { 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"); + const select = workspacePage.page.getByText("Solid"); await select.click(); - const gradOption = await workspacePage.page.getByText("Gradient"); + const gradOption = workspacePage.page.getByText("Gradient"); await gradOption.click(); - const gradTypeOptions = await workspacePage.page - .getByTestId("colorpicker") + const gradTypeOptions = workspacePage.colorpicker .locator("div") .filter({ hasText: "Linear" }) .nth(3); await gradTypeOptions.click(); - const gradRadialOption = await workspacePage.page + const gradRadialOption = workspacePage.page .locator("li") .filter({ hasText: "Radial" }); await gradRadialOption.click(); - const addStopBtn = await workspacePage.page.getByRole("button", { + const addStopBtn = workspacePage.page.getByRole("button", { name: "Add stop", }); await addStopBtn.click(); await addStopBtn.click(); await addStopBtn.click(); - const removeBtn = await workspacePage.page - .getByTestId("colorpicker") + const removeBtn = workspacePage.colorpicker .getByRole("button", { name: "Remove color" }) .nth(2); await removeBtn.click(); await removeBtn.click(); - const inputColor1 = await workspacePage.page.getByPlaceholder("Mixed").nth(1); + const inputColor1 = workspacePage.page.getByPlaceholder("Mixed").nth(1); await inputColor1.fill("fabada"); - const inputOpacity1 = await workspacePage.page - .getByTestId("colorpicker") - .getByPlaceholder("--") - .nth(1); + const inputOpacity1 = workspacePage.colorpicker.getByPlaceholder("--").nth(1); await inputOpacity1.fill("100"); - const inputColor2 = await workspacePage.page.getByPlaceholder("Mixed").nth(2); + const inputColor2 = workspacePage.page.getByPlaceholder("Mixed").nth(2); await inputColor2.fill("red"); - const inputOpacity2 = await workspacePage.page - .getByTestId("colorpicker") - .getByPlaceholder("--") - .nth(2); + const inputOpacity2 = workspacePage.colorpicker.getByPlaceholder("--").nth(1); await inputOpacity2.fill("100"); }); +test("Gradient stops limit", async ({ page }) => { + const workspacePage = new WorkspacePage(page); + await workspacePage.mockConfigFlags(["enable-binary-fills"]); + await workspacePage.setupEmptyFile(page); + await workspacePage.mockRPC( + "get-file-fragment?file-id=*&fragment-id=*", + "workspace/get-file-fragment-gradient-limits.json", + ); + + await workspacePage.goToWorkspace(); + await workspacePage.clickLeafLayer("Rectangle"); + + const swatch = workspacePage.page.getByRole("button", { + name: "Linear gradient", + }); + await swatch.click(); + + await expect(workspacePage.colorpicker).toBeVisible(); + + await expect( + workspacePage.colorpicker.getByRole("button", { name: "Add stop" }), + ).toBeDisabled(); +}); + // Fix for https://tree.taiga.io/project/penpot/issue/9900 test("Bug 9900 - Color picker has no inputs for HSV values", async ({ page, @@ -203,7 +214,6 @@ test("Bug 10089 - Cannot change alpha", async ({ page }) => { await workspacePage.clickLeafLayer("Rectangle"); const swatch = workspacePage.page.getByRole("button", { name: "#B1B2B5" }); - const swatchBox = await swatch.boundingBox(); await swatch.click(); const alpha = workspacePage.page.getByLabel("A", { exact: true }); diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index d417a111bc..5b7a0ea0f5 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -13,8 +13,9 @@ [app.common.schema :as sm] [app.common.text :as txt] [app.common.types.color :as ctc] - [app.common.types.shape :refer [check-stroke]] + [app.common.types.shape :as shp] [app.common.types.shape.shadow :refer [check-shadow]] + [app.config :as cfg] [app.main.broadcast :as mbc] [app.main.data.event :as ev] [app.main.data.helpers :as dsh] @@ -24,6 +25,7 @@ [app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.texts :as dwt] [app.main.data.workspace.undo :as dwu] + [app.main.features :as features] [app.util.storage :as storage] [beicon.v2.core :as rx] [cuerdas.core :as str] @@ -421,7 +423,7 @@ [ids stroke] (assert - (check-stroke stroke) + (shp/check-stroke stroke) "expected a valid stroke struct") (assert @@ -821,39 +823,43 @@ (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))) + (let [cap-stops? (or (features/active-feature? state "render-wasm/v1") (contains? cfg/flags :binary-fills)) + can-add-stop? (or (not cap-stops?) (< (count stops) shp/MAX-GRADIENT-STOPS))] + (if can-add-stop? + (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) + ;; 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)) + {from-offset :offset} (get stops index) + {to-offset :offset} (get stops (inc index)) - half-point-offset - (+ from-offset (/ (- to-offset from-offset) 2)) + half-point-offset + (+ from-offset (/ (- to-offset from-offset) 2)) - new-stop (-> (cc/interpolate-gradient stops half-point-offset) - (split-color-components)) + 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))))))))) + 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)))) + state))))))) (defn update-colorpicker-add-stop [offset] @@ -863,15 +869,18 @@ (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)))))))) + cap-stops? (or (features/active-feature? state "render-wasm/v1") (contains? cfg/flags :binary-fills)) + can-add-stop? (or (not cap-stops?) (< (count stops) shp/MAX-GRADIENT-STOPS))] + (if can-add-stop? (let [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))) + state))))))) (defn update-colorpicker-stops [stops] @@ -881,7 +890,8 @@ (update state :colorpicker (fn [state] (let [stop (or (:editing-stop state) 0) - stops (mapv split-color-components stops)] + cap-stops? (or (features/active-feature? state "render-wasm/v1") (contains? cfg/flags :binary-fills)) + stops (mapv split-color-components (if cap-stops? (take shp/MAX-GRADIENT-STOPS stops) stops))] (-> state (assoc :current-color (get stops stop)) (assoc :stops stops)))))))) diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index a6fb5ba9af..f7717138c4 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -12,6 +12,7 @@ [app.common.data.macros :as dm] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] + [app.common.types.shape :as shp] [app.config :as cfg] [app.main.data.event :as-alias ev] [app.main.data.modal :as modal] @@ -20,6 +21,7 @@ [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.media :as dwm] [app.main.data.workspace.undo :as dwu] + [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.file-uploader :refer [file-uploader]] @@ -336,6 +338,8 @@ (fn [value] (st/emit! (dc/update-colorpicker-gradient-opacity (/ value 100))))) + cap-stops? (or (features/use-feature "render-wasm/v1") (contains? cfg/flags :binary-fills)) + tabs #js [#js {:aria-label (tr "workspace.libraries.colors.rgba") :icon ic/rgba @@ -435,7 +439,7 @@ (when (= selected-mode :gradient) [:> gradients* {:type (:type state) - :stops (:stops state) + :stops (if cap-stops? (vec (take shp/MAX-GRADIENT-STOPS (:stops state))) (:stops state)) :editing-stop (:editing-stop state) :on-stop-edit-start handle-stop-edit-start :on-stop-edit-finish handle-stop-edit-finish diff --git a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs index e3721e4a1c..05cc9b3719 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker/gradients.cljs @@ -11,6 +11,9 @@ [app.common.data :as d] [app.common.data.macros :as dm] [app.common.math :as mth] + [app.common.types.shape :as shp] + [app.config :as cfg] + [app.main.features :as features] [app.main.ui.components.numeric-input :refer [numeric-input*]] [app.main.ui.components.reorder-handler :refer [reorder-handler]] [app.main.ui.components.select :refer [select]] @@ -283,7 +286,9 @@ (mf/deps on-reverse-stops) (fn [] (when on-reverse-stops - (on-reverse-stops))))] + (on-reverse-stops)))) + cap-stops? (or (features/use-feature "render-wasm/v1") (contains? cfg/flags :binary-fills)) + add-stop-disabled? (when cap-stops? (>= (count stops) shp/MAX-GRADIENT-STOPS))] [:div {:class (stl/css :gradient-panel)} [:div {:class (stl/css :gradient-preview)} @@ -294,9 +299,10 @@ :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)) "%")}}]] + (when (not add-stop-disabled?) + [: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)] @@ -339,6 +345,7 @@ :icon "switch"}] [:> icon-button* {:variant "ghost" :aria-label "Add stop" + :disabled add-stop-disabled? :on-click handle-add-stop :icon "add"}]]] diff --git a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs index 9f5e2fbe00..20e8fb5d32 100644 --- a/frontend/src/app/main/ui/workspace/viewport/gradients.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/gradients.cljs @@ -15,7 +15,10 @@ [app.common.geom.shapes :as gsh] [app.common.geom.shapes.points :as gsp] [app.common.math :as mth] + [app.common.types.shape :as shp] + [app.config :as cfg] [app.main.data.workspace.colors :as dc] + [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.workspace.viewport.viewport-ref :as uwvv] @@ -131,6 +134,9 @@ handler-state (mf/use-state {:display? false :offset 0 :hover nil}) + cap-stops? (or (features/use-feature "render-wasm/v1") (contains? cfg/flags :binary-fills)) + can-add-stop? (if cap-stops? (< (count stops) shp/MAX-GRADIENT-STOPS) true) + endpoint-on-pointer-down (fn [position event] (dom/stop-propagation event) @@ -164,7 +170,8 @@ points-on-pointer-enter (mf/use-fn (fn [] - (swap! handler-state assoc :display? true))) + (when can-add-stop? + (swap! handler-state assoc :display? true)))) points-on-pointer-leave (mf/use-fn @@ -177,17 +184,17 @@ (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))))) + (when can-add-stop? + (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 @@ -354,7 +361,7 @@ :cx (:x width-p) :cy (:y width-p) :r (/ gradient-width-handler-radius-handler zoom) - :fill "transpgarent" + :fill "transparent" :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) @@ -518,7 +525,10 @@ shape (mf/deref shape-ref) state (mf/deref refs/colorpicker) gradient (:gradient state) - stops (:stops state) + cap-stops? (or (features/use-feature "render-wasm/v1") (contains? cfg/flags :binary-fills)) + stops (if cap-stops? + (vec (take shp/MAX-GRADIENT-STOPS (:stops state))) + (:stops state)) editing-stop (:editing-stop state)] (when (and (some? gradient) (= id (:shape-id gradient))) diff --git a/frontend/src/app/render_wasm/serializers/fills.cljs b/frontend/src/app/render_wasm/serializers/fills.cljs index 4b21af4f4b..1dcf88711f 100644 --- a/frontend/src/app/render_wasm/serializers/fills.cljs +++ b/frontend/src/app/render_wasm/serializers/fills.cljs @@ -1,16 +1,13 @@ (ns app.render-wasm.serializers.fills (:require + [app.common.types.shape :as shp] [app.common.uuid :as uuid] [app.render-wasm.serializers.color :as clr])) (def ^:private GRADIENT-STOP-SIZE 8) -(def ^:private GRADIENT-BASE-SIZE 28) -;; TODO: Define in shape model -(def ^:private MAX-GRADIENT-STOPS 16) -(def GRADIENT-BYTE-SIZE - (+ GRADIENT-BASE-SIZE (* MAX-GRADIENT-STOPS GRADIENT-STOP-SIZE))) +(def GRADIENT-BYTE-SIZE 156) (def SOLID-BYTE-SIZE 4) (def IMAGE-BYTE-SIZE 28) @@ -48,7 +45,7 @@ end-x (:end-x gradient) end-y (:end-y gradient) width (or (:width gradient) 0) - stops (take MAX-GRADIENT-STOPS (:stops gradient)) + stops (take shp/MAX-GRADIENT-STOPS (:stops gradient)) type (if (= (:type gradient) :linear) 0x01 0x02)] (.setUint8 dview offset type true) (.setFloat32 dview (+ offset 4) start-x true)