mirror of
https://github.com/penpot/penpot.git
synced 2025-05-21 15:36:11 +02:00
🎉 Cap stop amount in UI for wasm (#6438)
* 🎉 Cap in the colorpicker the amount of stops a gradient can have * 🎉 Cap the stops amount in gradient handlers * 🎉 Disable add stop in gradient handlers (viewport + colorpicker) * ✨ Add integration test for gradient limits * 💄 Address PR suggestion
This commit is contained in:
parent
69cc4fb4c2
commit
91fbe8f8ef
10 changed files with 438 additions and 102 deletions
|
@ -761,3 +761,5 @@
|
||||||
(d/patch-object (select-keys props basic-extract-props))
|
(d/patch-object (select-keys props basic-extract-props))
|
||||||
(cond-> (cfh/text-shape? shape) (patch-text-props props))
|
(cond-> (cfh/text-shape? shape) (patch-text-props props))
|
||||||
(cond-> (cfh/frame-shape? shape) (patch-layout-props props)))))
|
(cond-> (cfh/frame-shape? shape) (patch-layout-props props)))))
|
||||||
|
|
||||||
|
(def MAX-GRADIENT-STOPS 16)
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
|
@ -36,6 +36,17 @@ export class BasePage {
|
||||||
async mockRPC(path, jsonFilename, options) {
|
async mockRPC(path, jsonFilename, options) {
|
||||||
return BasePage.mockRPC(this.page, 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;
|
export default BasePage;
|
||||||
|
|
|
@ -49,6 +49,12 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||||
"get-profiles-for-file-comments?file-id=*",
|
"get-profiles-for-file-comments?file-id=*",
|
||||||
"workspace/get-profile-for-file-comments.json",
|
"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";
|
static anyTeamId = "c7ce0794-0992-8105-8004-38e630f7920a";
|
||||||
|
|
|
@ -43,48 +43,44 @@ test("Create a LINEAR gradient", async ({ page }) => {
|
||||||
await workspacePage.clickLeafLayer("Rectangle");
|
await workspacePage.clickLeafLayer("Rectangle");
|
||||||
|
|
||||||
const swatch = workspacePage.page.getByRole("button", { name: "#B1B2B5" });
|
const swatch = workspacePage.page.getByRole("button", { name: "#B1B2B5" });
|
||||||
const swatchBox = await swatch.boundingBox();
|
|
||||||
await swatch.click();
|
await swatch.click();
|
||||||
|
|
||||||
const select = await workspacePage.page.getByText("Solid");
|
const select = workspacePage.page.getByText("Solid");
|
||||||
await select.click();
|
await select.click();
|
||||||
|
|
||||||
const gradOption = await workspacePage.page.getByText("Gradient");
|
const gradOption = workspacePage.page.getByText("Gradient");
|
||||||
await gradOption.click();
|
await gradOption.click();
|
||||||
|
|
||||||
const addStopBtn = await workspacePage.page.getByRole("button", {
|
const addStopBtn = workspacePage.page.getByRole("button", {
|
||||||
name: "Add stop",
|
name: "Add stop",
|
||||||
});
|
});
|
||||||
await addStopBtn.click();
|
await addStopBtn.click();
|
||||||
await addStopBtn.click();
|
await addStopBtn.click();
|
||||||
await addStopBtn.click();
|
await addStopBtn.click();
|
||||||
|
|
||||||
const removeBtn = await workspacePage.page
|
const removeBtn = workspacePage.colorpicker
|
||||||
.getByTestId("colorpicker")
|
|
||||||
.getByRole("button", { name: "Remove color" })
|
.getByRole("button", { name: "Remove color" })
|
||||||
.nth(2);
|
.nth(2);
|
||||||
await removeBtn.click();
|
await removeBtn.click();
|
||||||
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");
|
await inputColor1.fill("fabada");
|
||||||
|
|
||||||
const inputOpacity1 = await workspacePage.page
|
const inputOpacity1 = workspacePage.colorpicker.getByPlaceholder("--").nth(1);
|
||||||
.getByTestId("colorpicker")
|
|
||||||
.getByPlaceholder("--")
|
|
||||||
.nth(1);
|
|
||||||
await inputOpacity1.fill("100");
|
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");
|
await inputColor2.fill("red");
|
||||||
|
|
||||||
const inputOpacity2 = await workspacePage.page
|
const inputOpacity2 = workspacePage.colorpicker.getByPlaceholder("--").nth(1);
|
||||||
.getByTestId("colorpicker")
|
|
||||||
.getByPlaceholder("--")
|
|
||||||
.nth(2);
|
|
||||||
await inputOpacity2.fill("100");
|
await inputOpacity2.fill("100");
|
||||||
|
|
||||||
const inputOpacityGlobal = await workspacePage.page
|
const inputOpacityGlobal = workspacePage.page
|
||||||
.locator("div")
|
.locator("div")
|
||||||
.filter({ hasText: /^FillLinear gradient%$/ })
|
.filter({ hasText: /^FillLinear gradient%$/ })
|
||||||
.getByPlaceholder("--");
|
.getByPlaceholder("--");
|
||||||
|
@ -110,60 +106,75 @@ test("Create a RADIAL gradient", async ({ page }) => {
|
||||||
await workspacePage.clickLeafLayer("Rectangle");
|
await workspacePage.clickLeafLayer("Rectangle");
|
||||||
|
|
||||||
const swatch = workspacePage.page.getByRole("button", { name: "#B1B2B5" });
|
const swatch = workspacePage.page.getByRole("button", { name: "#B1B2B5" });
|
||||||
const swatchBox = await swatch.boundingBox();
|
|
||||||
await swatch.click();
|
await swatch.click();
|
||||||
|
|
||||||
const select = await workspacePage.page.getByText("Solid");
|
const select = workspacePage.page.getByText("Solid");
|
||||||
await select.click();
|
await select.click();
|
||||||
|
|
||||||
const gradOption = await workspacePage.page.getByText("Gradient");
|
const gradOption = workspacePage.page.getByText("Gradient");
|
||||||
await gradOption.click();
|
await gradOption.click();
|
||||||
|
|
||||||
const gradTypeOptions = await workspacePage.page
|
const gradTypeOptions = workspacePage.colorpicker
|
||||||
.getByTestId("colorpicker")
|
|
||||||
.locator("div")
|
.locator("div")
|
||||||
.filter({ hasText: "Linear" })
|
.filter({ hasText: "Linear" })
|
||||||
.nth(3);
|
.nth(3);
|
||||||
await gradTypeOptions.click();
|
await gradTypeOptions.click();
|
||||||
|
|
||||||
const gradRadialOption = await workspacePage.page
|
const gradRadialOption = workspacePage.page
|
||||||
.locator("li")
|
.locator("li")
|
||||||
.filter({ hasText: "Radial" });
|
.filter({ hasText: "Radial" });
|
||||||
await gradRadialOption.click();
|
await gradRadialOption.click();
|
||||||
|
|
||||||
const addStopBtn = await workspacePage.page.getByRole("button", {
|
const addStopBtn = workspacePage.page.getByRole("button", {
|
||||||
name: "Add stop",
|
name: "Add stop",
|
||||||
});
|
});
|
||||||
await addStopBtn.click();
|
await addStopBtn.click();
|
||||||
await addStopBtn.click();
|
await addStopBtn.click();
|
||||||
await addStopBtn.click();
|
await addStopBtn.click();
|
||||||
|
|
||||||
const removeBtn = await workspacePage.page
|
const removeBtn = workspacePage.colorpicker
|
||||||
.getByTestId("colorpicker")
|
|
||||||
.getByRole("button", { name: "Remove color" })
|
.getByRole("button", { name: "Remove color" })
|
||||||
.nth(2);
|
.nth(2);
|
||||||
await removeBtn.click();
|
await removeBtn.click();
|
||||||
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");
|
await inputColor1.fill("fabada");
|
||||||
|
|
||||||
const inputOpacity1 = await workspacePage.page
|
const inputOpacity1 = workspacePage.colorpicker.getByPlaceholder("--").nth(1);
|
||||||
.getByTestId("colorpicker")
|
|
||||||
.getByPlaceholder("--")
|
|
||||||
.nth(1);
|
|
||||||
await inputOpacity1.fill("100");
|
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");
|
await inputColor2.fill("red");
|
||||||
|
|
||||||
const inputOpacity2 = await workspacePage.page
|
const inputOpacity2 = workspacePage.colorpicker.getByPlaceholder("--").nth(1);
|
||||||
.getByTestId("colorpicker")
|
|
||||||
.getByPlaceholder("--")
|
|
||||||
.nth(2);
|
|
||||||
await inputOpacity2.fill("100");
|
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
|
// Fix for https://tree.taiga.io/project/penpot/issue/9900
|
||||||
test("Bug 9900 - Color picker has no inputs for HSV values", async ({
|
test("Bug 9900 - Color picker has no inputs for HSV values", async ({
|
||||||
page,
|
page,
|
||||||
|
@ -203,7 +214,6 @@ test("Bug 10089 - Cannot change alpha", async ({ page }) => {
|
||||||
await workspacePage.clickLeafLayer("Rectangle");
|
await workspacePage.clickLeafLayer("Rectangle");
|
||||||
|
|
||||||
const swatch = workspacePage.page.getByRole("button", { name: "#B1B2B5" });
|
const swatch = workspacePage.page.getByRole("button", { name: "#B1B2B5" });
|
||||||
const swatchBox = await swatch.boundingBox();
|
|
||||||
await swatch.click();
|
await swatch.click();
|
||||||
|
|
||||||
const alpha = workspacePage.page.getByLabel("A", { exact: true });
|
const alpha = workspacePage.page.getByLabel("A", { exact: true });
|
||||||
|
|
|
@ -13,8 +13,9 @@
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.text :as txt]
|
[app.common.text :as txt]
|
||||||
[app.common.types.color :as ctc]
|
[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.common.types.shape.shadow :refer [check-shadow]]
|
||||||
|
[app.config :as cfg]
|
||||||
[app.main.broadcast :as mbc]
|
[app.main.broadcast :as mbc]
|
||||||
[app.main.data.event :as ev]
|
[app.main.data.event :as ev]
|
||||||
[app.main.data.helpers :as dsh]
|
[app.main.data.helpers :as dsh]
|
||||||
|
@ -24,6 +25,7 @@
|
||||||
[app.main.data.workspace.shapes :as dwsh]
|
[app.main.data.workspace.shapes :as dwsh]
|
||||||
[app.main.data.workspace.texts :as dwt]
|
[app.main.data.workspace.texts :as dwt]
|
||||||
[app.main.data.workspace.undo :as dwu]
|
[app.main.data.workspace.undo :as dwu]
|
||||||
|
[app.main.features :as features]
|
||||||
[app.util.storage :as storage]
|
[app.util.storage :as storage]
|
||||||
[beicon.v2.core :as rx]
|
[beicon.v2.core :as rx]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
|
@ -421,7 +423,7 @@
|
||||||
[ids stroke]
|
[ids stroke]
|
||||||
|
|
||||||
(assert
|
(assert
|
||||||
(check-stroke stroke)
|
(shp/check-stroke stroke)
|
||||||
"expected a valid stroke struct")
|
"expected a valid stroke struct")
|
||||||
|
|
||||||
(assert
|
(assert
|
||||||
|
@ -821,39 +823,43 @@
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(update state :colorpicker
|
(update state :colorpicker
|
||||||
(fn [{:keys [stops editing-stop] :as state}]
|
(fn [{:keys [stops editing-stop] :as state}]
|
||||||
(if (cc/uniform-spread? stops)
|
(let [cap-stops? (or (features/active-feature? state "render-wasm/v1") (contains? cfg/flags :binary-fills))
|
||||||
;; Add to uniform
|
can-add-stop? (or (not cap-stops?) (< (count stops) shp/MAX-GRADIENT-STOPS))]
|
||||||
(let [stops (->> (cc/uniform-spread (first stops) (last stops) (inc (count stops)))
|
(if can-add-stop?
|
||||||
(mapv split-color-components))]
|
(if (cc/uniform-spread? stops)
|
||||||
(-> state
|
;; Add to uniform
|
||||||
(assoc :current-color (get stops editing-stop))
|
(let [stops (->> (cc/uniform-spread (first stops) (last stops) (inc (count stops)))
|
||||||
(assoc :stops 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
|
;; We add the stop to the middle point between the selected
|
||||||
;; and the next one.
|
;; and the next one.
|
||||||
;; If the last stop is selected then it's added between the
|
;; If the last stop is selected then it's added between the
|
||||||
;; last two stops.
|
;; last two stops.
|
||||||
(let [index
|
(let [index
|
||||||
(if (= editing-stop (dec (count stops)))
|
(if (= editing-stop (dec (count stops)))
|
||||||
(dec editing-stop)
|
(dec editing-stop)
|
||||||
editing-stop)
|
editing-stop)
|
||||||
|
|
||||||
{from-offset :offset} (get stops index)
|
{from-offset :offset} (get stops index)
|
||||||
{to-offset :offset} (get stops (inc index))
|
{to-offset :offset} (get stops (inc index))
|
||||||
|
|
||||||
half-point-offset
|
half-point-offset
|
||||||
(+ from-offset (/ (- to-offset from-offset) 2))
|
(+ from-offset (/ (- to-offset from-offset) 2))
|
||||||
|
|
||||||
new-stop (-> (cc/interpolate-gradient stops half-point-offset)
|
new-stop (-> (cc/interpolate-gradient stops half-point-offset)
|
||||||
(split-color-components))
|
(split-color-components))
|
||||||
|
|
||||||
stops (conj stops new-stop)
|
stops (conj stops new-stop)
|
||||||
stops (into [] (sort-by :offset stops))
|
stops (into [] (sort-by :offset stops))
|
||||||
editing-stop (d/index-of-pred stops #(= new-stop %))]
|
editing-stop (d/index-of-pred stops #(= new-stop %))]
|
||||||
(-> state
|
(-> state
|
||||||
(assoc :editing-stop editing-stop)
|
(assoc :editing-stop editing-stop)
|
||||||
(assoc :current-color (get stops editing-stop))
|
(assoc :current-color (get stops editing-stop))
|
||||||
(assoc :stops stops)))))))))
|
(assoc :stops stops))))
|
||||||
|
state)))))))
|
||||||
|
|
||||||
(defn update-colorpicker-add-stop
|
(defn update-colorpicker-add-stop
|
||||||
[offset]
|
[offset]
|
||||||
|
@ -863,15 +869,18 @@
|
||||||
(update state :colorpicker
|
(update state :colorpicker
|
||||||
(fn [state]
|
(fn [state]
|
||||||
(let [stops (:stops state)
|
(let [stops (:stops state)
|
||||||
new-stop (-> (cc/interpolate-gradient stops offset)
|
cap-stops? (or (features/active-feature? state "render-wasm/v1") (contains? cfg/flags :binary-fills))
|
||||||
(split-color-components))
|
can-add-stop? (or (not cap-stops?) (< (count stops) shp/MAX-GRADIENT-STOPS))]
|
||||||
stops (conj stops new-stop)
|
(if can-add-stop? (let [new-stop (-> (cc/interpolate-gradient stops offset)
|
||||||
stops (into [] (sort-by :offset stops))
|
(split-color-components))
|
||||||
editing-stop (d/index-of-pred stops #(= new-stop %))]
|
stops (conj stops new-stop)
|
||||||
(-> state
|
stops (into [] (sort-by :offset stops))
|
||||||
(assoc :editing-stop editing-stop)
|
editing-stop (d/index-of-pred stops #(= new-stop %))]
|
||||||
(assoc :current-color (get stops editing-stop))
|
(-> state
|
||||||
(assoc :stops stops))))))))
|
(assoc :editing-stop editing-stop)
|
||||||
|
(assoc :current-color (get stops editing-stop))
|
||||||
|
(assoc :stops stops)))
|
||||||
|
state)))))))
|
||||||
|
|
||||||
(defn update-colorpicker-stops
|
(defn update-colorpicker-stops
|
||||||
[stops]
|
[stops]
|
||||||
|
@ -881,7 +890,8 @@
|
||||||
(update state :colorpicker
|
(update state :colorpicker
|
||||||
(fn [state]
|
(fn [state]
|
||||||
(let [stop (or (:editing-stop state) 0)
|
(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
|
(-> state
|
||||||
(assoc :current-color (get stops stop))
|
(assoc :current-color (get stops stop))
|
||||||
(assoc :stops stops))))))))
|
(assoc :stops stops))))))))
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.geom.matrix :as gmt]
|
[app.common.geom.matrix :as gmt]
|
||||||
[app.common.geom.point :as gpt]
|
[app.common.geom.point :as gpt]
|
||||||
|
[app.common.types.shape :as shp]
|
||||||
[app.config :as cfg]
|
[app.config :as cfg]
|
||||||
[app.main.data.event :as-alias ev]
|
[app.main.data.event :as-alias ev]
|
||||||
[app.main.data.modal :as modal]
|
[app.main.data.modal :as modal]
|
||||||
|
@ -20,6 +21,7 @@
|
||||||
[app.main.data.workspace.libraries :as dwl]
|
[app.main.data.workspace.libraries :as dwl]
|
||||||
[app.main.data.workspace.media :as dwm]
|
[app.main.data.workspace.media :as dwm]
|
||||||
[app.main.data.workspace.undo :as dwu]
|
[app.main.data.workspace.undo :as dwu]
|
||||||
|
[app.main.features :as features]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.components.file-uploader :refer [file-uploader]]
|
[app.main.ui.components.file-uploader :refer [file-uploader]]
|
||||||
|
@ -336,6 +338,8 @@
|
||||||
(fn [value]
|
(fn [value]
|
||||||
(st/emit! (dc/update-colorpicker-gradient-opacity (/ value 100)))))
|
(st/emit! (dc/update-colorpicker-gradient-opacity (/ value 100)))))
|
||||||
|
|
||||||
|
cap-stops? (or (features/use-feature "render-wasm/v1") (contains? cfg/flags :binary-fills))
|
||||||
|
|
||||||
tabs
|
tabs
|
||||||
#js [#js {:aria-label (tr "workspace.libraries.colors.rgba")
|
#js [#js {:aria-label (tr "workspace.libraries.colors.rgba")
|
||||||
:icon ic/rgba
|
:icon ic/rgba
|
||||||
|
@ -435,7 +439,7 @@
|
||||||
(when (= selected-mode :gradient)
|
(when (= selected-mode :gradient)
|
||||||
[:> gradients*
|
[:> gradients*
|
||||||
{:type (:type state)
|
{: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)
|
:editing-stop (:editing-stop state)
|
||||||
:on-stop-edit-start handle-stop-edit-start
|
:on-stop-edit-start handle-stop-edit-start
|
||||||
:on-stop-edit-finish handle-stop-edit-finish
|
:on-stop-edit-finish handle-stop-edit-finish
|
||||||
|
|
|
@ -11,6 +11,9 @@
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
[app.common.math :as mth]
|
[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.numeric-input :refer [numeric-input*]]
|
||||||
[app.main.ui.components.reorder-handler :refer [reorder-handler]]
|
[app.main.ui.components.reorder-handler :refer [reorder-handler]]
|
||||||
[app.main.ui.components.select :refer [select]]
|
[app.main.ui.components.select :refer [select]]
|
||||||
|
@ -283,7 +286,9 @@
|
||||||
(mf/deps on-reverse-stops)
|
(mf/deps on-reverse-stops)
|
||||||
(fn []
|
(fn []
|
||||||
(when on-reverse-stops
|
(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-panel)}
|
||||||
[:div {:class (stl/css :gradient-preview)}
|
[:div {:class (stl/css :gradient-preview)}
|
||||||
|
@ -294,9 +299,10 @@
|
||||||
:on-pointer-leave handle-preview-leave
|
:on-pointer-leave handle-preview-leave
|
||||||
:on-pointer-move handle-preview-move
|
:on-pointer-move handle-preview-move
|
||||||
:on-pointer-down handle-preview-down}
|
:on-pointer-down handle-preview-down}
|
||||||
[:div {:class (stl/css :gradient-preview-stop-preview)
|
(when (not add-stop-disabled?)
|
||||||
:style {:display (if (:hover? @preview-state) "block" "none")
|
[:div {:class (stl/css :gradient-preview-stop-preview)
|
||||||
"--preview-position" (dm/str (* 100 (:offset @preview-state)) "%")}}]]
|
:style {:display (if (:hover? @preview-state) "block" "none")
|
||||||
|
"--preview-position" (dm/str (* 100 (:offset @preview-state)) "%")}}])]
|
||||||
|
|
||||||
[:div {:class (stl/css :gradient-preview-stop-wrapper)}
|
[:div {:class (stl/css :gradient-preview-stop-wrapper)}
|
||||||
(for [[index {:keys [color offset r g b alpha]}] (d/enumerate stops)]
|
(for [[index {:keys [color offset r g b alpha]}] (d/enumerate stops)]
|
||||||
|
@ -339,6 +345,7 @@
|
||||||
:icon "switch"}]
|
:icon "switch"}]
|
||||||
[:> icon-button* {:variant "ghost"
|
[:> icon-button* {:variant "ghost"
|
||||||
:aria-label "Add stop"
|
:aria-label "Add stop"
|
||||||
|
:disabled add-stop-disabled?
|
||||||
:on-click handle-add-stop
|
:on-click handle-add-stop
|
||||||
:icon "add"}]]]
|
:icon "add"}]]]
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,10 @@
|
||||||
[app.common.geom.shapes :as gsh]
|
[app.common.geom.shapes :as gsh]
|
||||||
[app.common.geom.shapes.points :as gsp]
|
[app.common.geom.shapes.points :as gsp]
|
||||||
[app.common.math :as mth]
|
[app.common.math :as mth]
|
||||||
|
[app.common.types.shape :as shp]
|
||||||
|
[app.config :as cfg]
|
||||||
[app.main.data.workspace.colors :as dc]
|
[app.main.data.workspace.colors :as dc]
|
||||||
|
[app.main.features :as features]
|
||||||
[app.main.refs :as refs]
|
[app.main.refs :as refs]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.workspace.viewport.viewport-ref :as uwvv]
|
[app.main.ui.workspace.viewport.viewport-ref :as uwvv]
|
||||||
|
@ -131,6 +134,9 @@
|
||||||
|
|
||||||
handler-state (mf/use-state {:display? false :offset 0 :hover nil})
|
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
|
endpoint-on-pointer-down
|
||||||
(fn [position event]
|
(fn [position event]
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
|
@ -164,7 +170,8 @@
|
||||||
points-on-pointer-enter
|
points-on-pointer-enter
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(fn []
|
(fn []
|
||||||
(swap! handler-state assoc :display? true)))
|
(when can-add-stop?
|
||||||
|
(swap! handler-state assoc :display? true))))
|
||||||
|
|
||||||
points-on-pointer-leave
|
points-on-pointer-leave
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
@ -177,17 +184,17 @@
|
||||||
(fn [e]
|
(fn [e]
|
||||||
(dom/prevent-default e)
|
(dom/prevent-default e)
|
||||||
(dom/stop-propagation e)
|
(dom/stop-propagation e)
|
||||||
|
(when can-add-stop?
|
||||||
(let [raw-pt (dom/get-client-position e)
|
(let [raw-pt (dom/get-client-position e)
|
||||||
position (uwvv/point->viewport raw-pt)
|
position (uwvv/point->viewport raw-pt)
|
||||||
lv (-> (gpt/to-vec from-p to-p) (gpt/unit))
|
lv (-> (gpt/to-vec from-p to-p) (gpt/unit))
|
||||||
nv (gpt/normal-left lv)
|
nv (gpt/normal-left lv)
|
||||||
offset (-> (gsp/project-t position [from-p to-p] nv)
|
offset (-> (gsp/project-t position [from-p to-p] nv)
|
||||||
(mth/precision 2))
|
(mth/precision 2))
|
||||||
new-stop (cc/interpolate-gradient stops offset)
|
new-stop (cc/interpolate-gradient stops offset)
|
||||||
stops (conj stops new-stop)
|
stops (conj stops new-stop)
|
||||||
stops (->> stops (sort-by :offset) (into []))]
|
stops (->> stops (sort-by :offset) (into []))]
|
||||||
(st/emit! (dc/update-colorpicker-stops stops)))))
|
(st/emit! (dc/update-colorpicker-stops stops))))))
|
||||||
|
|
||||||
points-on-pointer-move
|
points-on-pointer-move
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
|
@ -354,7 +361,7 @@
|
||||||
:cx (:x width-p)
|
:cx (:x width-p)
|
||||||
:cy (:y width-p)
|
:cy (:y width-p)
|
||||||
:r (/ gradient-width-handler-radius-handler zoom)
|
:r (/ gradient-width-handler-radius-handler zoom)
|
||||||
:fill "transpgarent"
|
:fill "transparent"
|
||||||
:on-pointer-down (partial endpoint-on-pointer-down :width-p)
|
:on-pointer-down (partial endpoint-on-pointer-down :width-p)
|
||||||
:on-pointer-enter (partial endpoint-on-pointer-enter :width-p)
|
:on-pointer-enter (partial endpoint-on-pointer-enter :width-p)
|
||||||
:on-pointer-leave (partial endpoint-on-pointer-leave :width-p)
|
:on-pointer-leave (partial endpoint-on-pointer-leave :width-p)
|
||||||
|
@ -518,7 +525,10 @@
|
||||||
shape (mf/deref shape-ref)
|
shape (mf/deref shape-ref)
|
||||||
state (mf/deref refs/colorpicker)
|
state (mf/deref refs/colorpicker)
|
||||||
gradient (:gradient state)
|
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)]
|
editing-stop (:editing-stop state)]
|
||||||
|
|
||||||
(when (and (some? gradient) (= id (:shape-id gradient)))
|
(when (and (some? gradient) (= id (:shape-id gradient)))
|
||||||
|
|
|
@ -1,16 +1,13 @@
|
||||||
(ns app.render-wasm.serializers.fills
|
(ns app.render-wasm.serializers.fills
|
||||||
(:require
|
(:require
|
||||||
|
[app.common.types.shape :as shp]
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
[app.render-wasm.serializers.color :as clr]))
|
[app.render-wasm.serializers.color :as clr]))
|
||||||
|
|
||||||
(def ^:private GRADIENT-STOP-SIZE 8)
|
(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 SOLID-BYTE-SIZE 4)
|
||||||
(def IMAGE-BYTE-SIZE 28)
|
(def IMAGE-BYTE-SIZE 28)
|
||||||
|
|
||||||
|
@ -48,7 +45,7 @@
|
||||||
end-x (:end-x gradient)
|
end-x (:end-x gradient)
|
||||||
end-y (:end-y gradient)
|
end-y (:end-y gradient)
|
||||||
width (or (:width gradient) 0)
|
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)]
|
type (if (= (:type gradient) :linear) 0x01 0x02)]
|
||||||
(.setUint8 dview offset type true)
|
(.setUint8 dview offset type true)
|
||||||
(.setFloat32 dview (+ offset 4) start-x true)
|
(.setFloat32 dview (+ offset 4) start-x true)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue