🐛 Fix text shadows and blur and refactor text rendering

This commit is contained in:
Elena Torro committed 2025-09-10 14:20:24 +02:00
1 parent 0e23c9f6ab
commit 4bd2eba573
27 files changed
+14417 -498

No files matched your search

+3
View File
@@ -23,6 +23,7 @@ export default defineConfig({
expect: {
timeout: process.env.CI ? 20000 : 5000,
},
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
@@ -62,7 +63,9 @@ export default defineConfig({
},
testDir: "./playwright/ui/render-wasm-specs",
snapshotPathTemplate: "{testDir}/{testFilePath}-snapshots/{arg}.png",
timeout: 2 * 60 * 1000,
expect: {
timeout: process.env.CI ? 20000 : 10000,
toHaveScreenshot: {
maxDiffPixelRatio: 0.001,
},
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,905 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/objects-map",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u6bd7c17d-4f59-815e-8006-5c1f6882469a",
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "New File 2",
"~:revn": 401,
"~:modified-at": "~m1757076417573",
"~:vern": 0,
"~:id": "~u15b74473-2908-8094-8006-bc90c3982c73",
"~:is-shared": false,
"~:migrations": {
"~#ordered-set": [
"legacy-2",
"legacy-3",
"legacy-5",
"legacy-6",
"legacy-7",
"legacy-8",
"legacy-9",
"legacy-10",
"legacy-11",
"legacy-12",
"legacy-13",
"legacy-14",
"legacy-16",
"legacy-17",
"legacy-18",
"legacy-19",
"legacy-25",
"legacy-26",
"legacy-27",
"legacy-28",
"legacy-29",
"legacy-31",
"legacy-32",
"legacy-33",
"legacy-34",
"legacy-36",
"legacy-37",
"legacy-38",
"legacy-39",
"legacy-40",
"legacy-41",
"legacy-42",
"legacy-43",
"legacy-44",
"legacy-45",
"legacy-46",
"legacy-47",
"legacy-48",
"legacy-49",
"legacy-50",
"legacy-51",
"legacy-52",
"legacy-53",
"legacy-54",
"legacy-55",
"legacy-56",
"legacy-57",
"legacy-59",
"legacy-62",
"legacy-65",
"legacy-66",
"legacy-67",
"0001-remove-tokens-from-groups",
"0002-normalize-bool-content-v2",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content-v2",
"0004-clean-shadow-color",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0007-clear-invalid-strokes-and-fills-v2",
"0008-fix-library-colors-v4",
"0009-clean-library-colors",
"0009-add-partial-text-touched-flags",
"0010-fix-swap-slots-pointing-non-existent-shapes"
]
},
"~:version": 67,
"~:project-id": "~u6bd7c17d-4f59-815e-8006-5c1f68846e43",
"~:created-at": "~m1756728830560",
"~:data": {
"~:pages": [
"~u15b74473-2908-8094-8006-bc90c3982c74"
],
"~:pages-index": {
"~u15b74473-2908-8094-8006-bc90c3982c74": {
"~:objects": {
"~u00000000-0000-0000-0000-000000000000": {
"~#shape": {
"~:y": 0,
"~:hide-fill-on-export": false,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:name": "Root Frame",
"~:width": 0.01,
"~:type": "~:frame",
"~:points": [
{
"~#point": {
"~:x": 0,
"~:y": 0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0.01
}
},
{
"~#point": {
"~:x": 0,
"~:y": 0.01
}
}
],
"~:r2": 0,
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 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,
"~: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": [
"~u88db2850-996a-804f-8006-c063323d68a4",
"~uf94516f3-2d43-80b3-8006-c1b5c96d7dae",
"~uf94516f3-2d43-80b3-8006-c1b60a252bcf"
]
}
},
"~u88db2850-996a-804f-8006-c063323d68a4": {
"~#shape": {
"~:y": -865.0000046417117,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:last-resize-direction": "~:horizontal",
"~:grow-type": "~:auto-height",
"~:content": {
"~:type": "root",
"~:key": "1ygxrlda8tl",
"~:children": [
{
"~:type": "paragraph-set",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:children": [
{
"~:line-height": "",
"~:font-style": "normal",
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:font-id": "sourcesanspro",
"~:key": "23svy7uenp6",
"~:font-size": "48",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [],
"~:font-family": "sourcesanspro",
"~:text": "shadows with multiple strokes and no fill"
}
],
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:text-align": "center",
"~:font-id": "sourcesanspro",
"~:key": "e92bgu67k4",
"~:font-size": "0",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:text-direction": "ltr",
"~:type": "paragraph",
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [],
"~:font-family": "sourcesanspro"
}
]
}
],
"~:vertical-align": "top"
},
"~:hide-in-viewer": false,
"~:name": "Text",
"~:width": 506.1299901710943,
"~:type": "~:text",
"~:points": [
{
"~#point": {
"~:x": -251.00000411998855,
"~:y": -865.0000046417117
}
},
{
"~#point": {
"~:x": 255.12998605110573,
"~:y": -865.0000046417117
}
},
{
"~#point": {
"~:x": 255.12998605110573,
"~:y": -749.0000040708983
}
},
{
"~#point": {
"~:x": -251.00000411998855,
"~:y": -749.0000040708983
}
}
],
"~:layout-item-h-sizing": "~:fix",
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:hidden": false,
"~:id": "~u88db2850-996a-804f-8006-c063323d68a4",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:position-data": [
{
"~#rect": {
"~:y": -804.7000015899539,
"~:font-style": "normal",
"~:text-transform": "none",
"~:font-size": "48px",
"~:font-weight": "400",
"~:y1": -2.6999969482421875,
"~:width": 464.2166748046875,
"~:text-decoration": "rgb(0, 0, 0)",
"~:letter-spacing": "normal",
"~:x": -230.05000717174636,
"~:x1": 20.949996948242188,
"~:y2": 60.30000305175781,
"~:fills": [],
"~:x2": 485.1666717529297,
"~:direction": "ltr",
"~:font-family": "\"sourcesanspro\"",
"~:height": 63,
"~:text": "shadows with multiple "
}
},
{
"~#rect": {
"~:y": -747.1000107452273,
"~:font-style": "normal",
"~:text-transform": "none",
"~:font-size": "48px",
"~:font-weight": "400",
"~:y1": 54.899993896484375,
"~:width": 354.3333435058594,
"~:text-decoration": "rgb(0, 0, 0)",
"~:letter-spacing": "normal",
"~:x": -175.10001022350417,
"~:x1": 75.89999389648438,
"~:y2": 117.89999389648438,
"~:fills": [],
"~:x2": 430.23333740234375,
"~:direction": "ltr",
"~:font-family": "\"sourcesanspro\"",
"~:height": 63,
"~:text": "strokes and no fill"
}
}
],
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [
{
"~:stroke-style": "~:solid",
"~:stroke-alignment": "~:inner",
"~:stroke-width": 1,
"~:stroke-color": "#00ff11",
"~:stroke-opacity": 1
},
{
"~:stroke-style": "~:solid",
"~:stroke-alignment": "~:outer",
"~:stroke-width": 1,
"~:stroke-color": "#ff00b1",
"~:stroke-opacity": 1
}
],
"~:x": -251.00000411998855,
"~:shadow": [
{
"~:color": {
"~:color": "#7750e1",
"~:opacity": 0.4722222222222222
},
"~:spread": 0,
"~:offset-y": 10,
"~:style": "~:drop-shadow",
"~:blur": 0,
"~:hidden": false,
"~:id": "~uf94516f3-2d43-80b3-8006-c1b5b7323de5",
"~:offset-x": 10
},
{
"~:color": {
"~:color": "#559fe1",
"~:opacity": 0.7333333333333333
},
"~:spread": 0,
"~:offset-y": -10,
"~:style": "~:drop-shadow",
"~:blur": 0,
"~:hidden": false,
"~:id": "~u427eca67-5b7f-80e6-8006-c0a7398ff4b4",
"~:offset-x": -10
}
],
"~:selrect": {
"~#rect": {
"~:x": -251.00000411998855,
"~:y": -865.0000046417117,
"~:width": 506.1299901710943,
"~:height": 116.00000057081343,
"~:x1": -251.00000411998855,
"~:y1": -865.0000046417117,
"~:x2": 255.12998605110573,
"~:y2": -749.0000040708983
}
},
"~:flip-x": null,
"~:height": 116.00000057081343,
"~:flip-y": null
}
},
"~uf94516f3-2d43-80b3-8006-c1b5c96d7dae": {
"~#shape": {
"~:y": -723.9999884292483,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:last-resize-direction": "~:horizontal",
"~:grow-type": "~:auto-height",
"~:content": {
"~:type": "root",
"~:key": "1ygxrlda8tl",
"~:children": [
{
"~:type": "paragraph-set",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:font-id": "sourcesanspro",
"~:key": "23svy7uenp6",
"~:font-size": "48",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#214ccd",
"~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro",
"~:text": "shadows with multiple strokes and solid fill"
}
],
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:text-align": "center",
"~:font-id": "sourcesanspro",
"~:key": "e92bgu67k4",
"~:font-size": "48",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:text-direction": "ltr",
"~:type": "paragraph",
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#214ccd",
"~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro"
}
]
}
],
"~:vertical-align": "top"
},
"~:hide-in-viewer": false,
"~:name": "Text",
"~:width": 609.1300277709961,
"~:type": "~:text",
"~:points": [
{
"~#point": {
"~:x": -303.0000086630885,
"~:y": -723.9999884292483
}
},
{
"~#point": {
"~:x": 306.1300191079076,
"~:y": -723.9999884292483
}
},
{
"~#point": {
"~:x": 306.1300191079076,
"~:y": -607.9999878584349
}
},
{
"~#point": {
"~:x": -303.0000086630885,
"~:y": -607.9999878584349
}
}
],
"~:layout-item-h-sizing": "~:fix",
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:hidden": false,
"~:id": "~uf94516f3-2d43-80b3-8006-c1b5c96d7dae",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:position-data": [
{
"~#rect": {
"~:y": -663.6999853774905,
"~:font-style": "normal",
"~:text-transform": "none",
"~:font-size": "48px",
"~:font-weight": "400",
"~:y1": -2.6999969482421875,
"~:width": 464.2166748046875,
"~:text-decoration": "rgb(33, 76, 205)",
"~:letter-spacing": "normal",
"~:x": -230.55001171484633,
"~:x1": 72.44999694824219,
"~:y2": 60.30000305175781,
"~:fills": [
{
"~:fill-color": "#214ccd",
"~:fill-opacity": 1
}
],
"~:x2": 536.6666717529297,
"~:direction": "ltr",
"~:font-family": "\"sourcesanspro\"",
"~:height": 63,
"~:text": "shadows with multiple "
}
},
{
"~#rect": {
"~:y": -606.099994532764,
"~:font-style": "normal",
"~:text-transform": "none",
"~:font-size": "48px",
"~:font-weight": "400",
"~:y1": 54.899993896484375,
"~:width": 398.8500061035156,
"~:text-decoration": "rgb(33, 76, 205)",
"~:letter-spacing": "normal",
"~:x": -197.8666773642604,
"~:x1": 105.13333129882812,
"~:y2": 117.89999389648438,
"~:fills": [
{
"~:fill-color": "#214ccd",
"~:fill-opacity": 1
}
],
"~:x2": 503.98333740234375,
"~:direction": "ltr",
"~:font-family": "\"sourcesanspro\"",
"~:height": 63,
"~:text": "strokes and solid fill"
}
}
],
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [
{
"~:stroke-style": "~:solid",
"~:stroke-alignment": "~:inner",
"~:stroke-width": 1,
"~:stroke-color": "#00ff11",
"~:stroke-opacity": 1
},
{
"~:stroke-style": "~:solid",
"~:stroke-alignment": "~:outer",
"~:stroke-width": 3,
"~:stroke-color": "#ff00b1",
"~:stroke-opacity": 1
}
],
"~:x": -303.0000086630885,
"~:shadow": [
{
"~:color": {
"~:color": "#7750e1",
"~:opacity": 0.4722222222222222
},
"~:spread": 0,
"~:offset-y": 4,
"~:style": "~:drop-shadow",
"~:blur": 0,
"~:hidden": false,
"~:id": "~uf94516f3-2d43-80b3-8006-c1b5b7323de5",
"~:offset-x": 4
},
{
"~:color": {
"~:color": "#559fe1",
"~:opacity": 0.7333333333333333
},
"~:spread": 0,
"~:offset-y": -4,
"~:style": "~:drop-shadow",
"~:blur": 0,
"~:hidden": false,
"~:id": "~u427eca67-5b7f-80e6-8006-c0a7398ff4b4",
"~:offset-x": -4
}
],
"~:selrect": {
"~#rect": {
"~:x": -303.0000086630885,
"~:y": -723.9999884292483,
"~:width": 609.1300277709961,
"~:height": 116.00000057081343,
"~:x1": -303.0000086630885,
"~:y1": -723.9999884292483,
"~:x2": 306.1300191079076,
"~:y2": -607.9999878584349
}
},
"~:flip-x": null,
"~:height": 116.00000057081343,
"~:flip-y": null
}
},
"~uf94516f3-2d43-80b3-8006-c1b60a252bcf": {
"~#shape": {
"~:y": -581.9999718032777,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:grow-type": "~:fixed",
"~:content": {
"~:type": "root",
"~:key": "1ygxrlda8tl",
"~:children": [
{
"~:type": "paragraph-set",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:children": [
{
"~:line-height": "",
"~:font-style": "normal",
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:font-id": "sourcesanspro",
"~:key": "23svy7uenp6",
"~:font-size": "48",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#ff7700",
"~:fill-opacity": 0.38333333333333336
}
],
"~:font-family": "sourcesanspro",
"~:text": "shadows with multiple strokes and transparent fill"
}
],
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:text-align": "center",
"~:font-id": "sourcesanspro",
"~:key": "e92bgu67k4",
"~:font-size": "0",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:text-direction": "ltr",
"~:type": "paragraph",
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#ff7700",
"~:fill-opacity": 0.38333333333333336
}
],
"~:font-family": "sourcesanspro"
}
]
}
],
"~:vertical-align": "top"
},
"~:hide-in-viewer": false,
"~:name": "Text",
"~:width": 753.1299834251404,
"~:type": "~:text",
"~:points": [
{
"~#point": {
"~:x": -374.99998553834683,
"~:y": -581.9999718032777
}
},
{
"~#point": {
"~:x": 378.1299978867936,
"~:y": -581.9999718032777
}
},
{
"~#point": {
"~:x": 378.1299978867936,
"~:y": -523.999971517871
}
},
{
"~#point": {
"~:x": -374.99998553834683,
"~:y": -523.999971517871
}
}
],
"~:layout-item-h-sizing": "~:fix",
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:hidden": false,
"~:id": "~uf94516f3-2d43-80b3-8006-c1b60a252bcf",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:position-data": [
{
"~#rect": {
"~:y": -521.6999687515199,
"~:font-style": "normal",
"~:text-transform": "none",
"~:font-size": "48px",
"~:font-weight": "400",
"~:y1": -2.6999969482421875,
"~:width": 706.3333740234375,
"~:text-decoration": "rgba(255, 119, 0, 0.383)",
"~:letter-spacing": "normal",
"~:x": -351.5999916418624,
"~:x1": 23.399993896484375,
"~:y2": 60.30000305175781,
"~:fills": [
{
"~:fill-color": "#ff7700",
"~:fill-opacity": 0.38333333333333336
}
],
"~:x2": 729.7333679199219,
"~:direction": "ltr",
"~:font-family": "\"sourcesanspro\"",
"~:height": 63,
"~:text": "shadows with multiple strokes and "
}
},
{
"~#rect": {
"~:y": -464.09997790679336,
"~:font-style": "normal",
"~:text-transform": "none",
"~:font-size": "48px",
"~:font-weight": "400",
"~:y1": 54.899993896484375,
"~:width": 295.48333740234375,
"~:text-decoration": "rgba(255, 119, 0, 0.383)",
"~:letter-spacing": "normal",
"~:x": -146.18331988893272,
"~:x1": 228.81666564941406,
"~:y2": 117.89999389648438,
"~:fills": [
{
"~:fill-color": "#ff7700",
"~:fill-opacity": 0.38333333333333336
}
],
"~:x2": 524.3000030517578,
"~:direction": "ltr",
"~:font-family": "\"sourcesanspro\"",
"~:height": 63,
"~:text": "transparent fill"
}
}
],
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [
{
"~:stroke-style": "~:solid",
"~:stroke-alignment": "~:outer",
"~:stroke-width": 3,
"~:stroke-color": "#ff00b0",
"~:stroke-opacity": 0.37222222222222223
}
],
"~:x": -374.9999855383468,
"~:shadow": [
{
"~:color": {
"~:color": "#00fb08",
"~:opacity": 1
},
"~:spread": 0,
"~:offset-y": 4,
"~:style": "~:drop-shadow",
"~:blur": 0,
"~:hidden": false,
"~:id": "~uf94516f3-2d43-80b3-8006-c1b5b7323de5",
"~:offset-x": 4
},
{
"~:color": {
"~:color": "#559fe1",
"~:opacity": 0.7333333333333333
},
"~:spread": 0,
"~:offset-y": -4,
"~:style": "~:drop-shadow",
"~:blur": 0,
"~:hidden": false,
"~:id": "~u427eca67-5b7f-80e6-8006-c0a7398ff4b4",
"~:offset-x": -4
}
],
"~:selrect": {
"~#rect": {
"~:x": -374.9999855383468,
"~:y": -581.9999718032777,
"~:width": 753.1299834251404,
"~:height": 58.00000028540671,
"~:x1": -374.9999855383468,
"~:y1": -581.9999718032777,
"~:x2": 378.1299978867936,
"~:y2": -523.999971517871
}
},
"~:flip-x": null,
"~:height": 58.00000028540671,
"~:flip-y": null
}
}
},
"~:id": "~u15b74473-2908-8094-8006-bc90c3982c74",
"~:name": "Page 1"
}
},
"~:id": "~u15b74473-2908-8094-8006-bc90c3982c73",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}
@@ -227,6 +227,71 @@ test("Renders a file with multiple emoji", async ({ page }) => {
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with multiple text shadows, strokes, and blur combinations", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile(
"render-wasm/get-file-text-shadows-and-blurs.json",
);
await workspace.goToWorkspace({
id: "15b74473-2908-8094-8006-bdb4fbd2c6a3",
pageId: "15b74473-2908-8094-8006-bdb4fbd2c6a4",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with different text leaves decoration", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile(
"render-wasm/get-file-text-leaves-decoration.json",
);
await workspace.goToWorkspace({
id: "15b74473-2908-8094-8006-bdb4fbd2c6a3",
pageId: "15b74473-2908-8094-8006-bdb4fbd2c6a4",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with different text shadows combinations", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile(
"render-wasm/get-file-text-shadows-combination.json",
);
await workspace.goToWorkspace({
id: "15b74473-2908-8094-8006-bdb4fbd2c6a3",
pageId: "15b74473-2908-8094-8006-bc90c3982c74",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with text in frames and different strokes, shadows, and blurs", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile(
"render-wasm/get-file-frame-clipping-shadows-and-texts.json",
);
await workspace.goToWorkspace({
id: "44471494-966a-8178-8006-c5bd93f0fe72",
pageId: "44471494-966a-8178-8006-c5bd93f0fe73",
});
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with texts with different alignments", async ({
page,
}) => {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 524 KiB

After

Width:  |  Height:  |  Size: 523 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 126 KiB

+244 -113
View File
@@ -436,6 +436,9 @@ impl RenderState {
let paint = skia::Paint::default();
self.surfaces
.draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint));
self.surfaces
.draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint));
@@ -457,8 +460,10 @@ impl RenderState {
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
}
let surface_ids =
SurfaceId::Strokes as u32 | SurfaceId::Fills as u32 | SurfaceId::InnerShadows as u32;
let surface_ids = SurfaceId::Strokes as u32
| SurfaceId::Fills as u32
| SurfaceId::InnerShadows as u32
| SurfaceId::TextDropShadows as u32;
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().clear(skia::Color::TRANSPARENT);
@@ -485,8 +490,10 @@ impl RenderState {
fills_surface_id: SurfaceId,
strokes_surface_id: SurfaceId,
innershadows_surface_id: SurfaceId,
text_drop_shadows_surface_id: SurfaceId,
apply_to_current_surface: bool,
offset: Option<(f32, f32)>,
parent_shadows: Option<Vec<skia_safe::Paint>>,
) {
let shape = if let Some(scale_content) = scale_content {
&shape.scale_content(*scale_content)
@@ -494,8 +501,10 @@ impl RenderState {
shape
};
let surface_ids =
fills_surface_id as u32 | strokes_surface_id as u32 | innershadows_surface_id as u32;
let surface_ids = fills_surface_id as u32
| strokes_surface_id as u32
| innershadows_surface_id as u32
| text_drop_shadows_surface_id as u32;
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().save();
});
@@ -604,72 +613,139 @@ impl RenderState {
});
let text_content = text_content.new_bounds(shape.selrect());
let drop_shadows = shape.drop_shadow_paints();
let inner_shadows = shape.inner_shadow_paints();
let blur_filter = shape.image_filter(1.);
let blur_mask = shape.mask_filter(1.);
let mut paragraphs = paragraph_builder_group_from_text(
&text_content,
blur_filter.as_ref(),
blur_mask.as_ref(),
None,
);
let count_inner_strokes = shape.count_visible_inner_strokes();
text::render(self, &shape, &mut paragraphs, Some(fills_surface_id));
for stroke in shape.visible_strokes().rev() {
let mut stroke_paragraphs = stroke_paragraph_builder_group_from_text(
&text_content,
stroke,
&shape.selrect(),
blur_filter.as_ref(),
blur_mask.as_ref(),
None,
count_inner_strokes,
);
let mut paragraphs = paragraph_builder_group_from_text(&text_content, None);
let mut paragraphs_with_shadows =
paragraph_builder_group_from_text(&text_content, Some(true));
let mut stroke_paragraphs_list = shape
.visible_strokes()
.map(|stroke| {
stroke_paragraph_builder_group_from_text(
&text_content,
stroke,
&shape.selrect(),
count_inner_strokes,
None,
)
})
.collect::<Vec<_>>();
strokes::render(
self,
&shape,
stroke,
Some(strokes_surface_id),
None,
Some(&mut stroke_paragraphs),
antialias,
);
let mut stroke_paragraphs_with_shadows_list = shape
.visible_strokes()
.map(|stroke| {
stroke_paragraph_builder_group_from_text(
&text_content,
stroke,
&shape.selrect(),
count_inner_strokes,
Some(true),
)
})
.collect::<Vec<_>>();
for inner_shadow in &inner_shadows {
let mut stroke_paragraphs_with_inner_shadows =
stroke_paragraph_builder_group_from_text(
&text_content,
stroke,
&shape.selrect(),
if let Some(parent_shadows) = parent_shadows {
if !shape.has_visible_strokes() {
for shadow in &parent_shadows {
text::render(
Some(self),
None,
&shape,
&mut paragraphs_with_shadows,
text_drop_shadows_surface_id.into(),
Some(shadow),
blur_filter.as_ref(),
blur_mask.as_ref(),
Some(inner_shadow),
count_inner_strokes,
);
shadows::render_text_inner_shadows(
}
} else {
shadows::render_text_shadows(
self,
&shape,
&mut stroke_paragraphs_with_inner_shadows,
innershadows_surface_id,
&mut paragraphs_with_shadows,
&mut stroke_paragraphs_with_shadows_list,
text_drop_shadows_surface_id.into(),
&parent_shadows,
&blur_filter,
);
}
}
} else {
// 1. Text drop shadows
if !shape.has_visible_strokes() {
for shadow in &drop_shadows {
text::render(
Some(self),
None,
&shape,
&mut paragraphs_with_shadows,
text_drop_shadows_surface_id.into(),
Some(shadow),
blur_filter.as_ref(),
);
}
}
for inner_shadow in &inner_shadows {
let mut paragraphs_with_inner_shadows = paragraph_builder_group_from_text(
&text_content,
// 2. Text fills
text::render(
Some(self),
None,
&shape,
&mut paragraphs,
Some(fills_surface_id),
None,
blur_filter.as_ref(),
blur_mask.as_ref(),
Some(inner_shadow),
);
shadows::render_text_inner_shadows(
// 3. Stroke drop shadows
shadows::render_text_shadows(
self,
&shape,
&mut paragraphs_with_inner_shadows,
innershadows_surface_id,
&mut paragraphs_with_shadows,
&mut stroke_paragraphs_with_shadows_list,
text_drop_shadows_surface_id.into(),
&drop_shadows,
&blur_filter,
);
// 4. Stroke fills
for stroke_paragraphs in stroke_paragraphs_list.iter_mut() {
text::render(
Some(self),
None,
&shape,
stroke_paragraphs,
Some(strokes_surface_id),
None,
blur_filter.as_ref(),
);
}
// 5. Stroke inner shadows
shadows::render_text_shadows(
self,
&shape,
&mut paragraphs_with_shadows,
&mut stroke_paragraphs_with_shadows_list,
Some(innershadows_surface_id),
&inner_shadows,
&blur_filter,
);
// 6. Fill Inner shadows
if !shape.has_visible_strokes() {
for shadow in &inner_shadows {
text::render(
Some(self),
None,
&shape,
&mut paragraphs_with_shadows,
Some(innershadows_surface_id),
Some(shadow),
blur_filter.as_ref(),
);
}
}
}
}
_ => {
@@ -718,7 +794,6 @@ impl RenderState {
stroke,
Some(strokes_surface_id),
None,
None,
antialias,
);
shadows::render_stroke_inner_shadows(
@@ -815,8 +890,10 @@ impl RenderState {
performance::begin_measure!("start_render_loop");
self.reset_canvas();
let surface_ids =
SurfaceId::Strokes as u32 | SurfaceId::Fills as u32 | SurfaceId::InnerShadows as u32;
let surface_ids = SurfaceId::Strokes as u32
| SurfaceId::Fills as u32
| SurfaceId::InnerShadows as u32
| SurfaceId::TextDropShadows as u32;
self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().scale((scale, scale));
});
@@ -997,8 +1074,10 @@ impl RenderState {
SurfaceId::Fills,
SurfaceId::Strokes,
SurfaceId::InnerShadows,
SurfaceId::TextDropShadows,
true,
None,
None,
);
}
@@ -1098,7 +1177,6 @@ impl RenderState {
self.surfaces
.canvas(SurfaceId::DropShadows)
.save_layer(&layer_rec);
self.surfaces
.canvas(SurfaceId::DropShadows)
.scale((scale, scale));
@@ -1116,8 +1194,10 @@ impl RenderState {
SurfaceId::DropShadows,
SurfaceId::DropShadows,
SurfaceId::DropShadows,
SurfaceId::DropShadows,
false,
Some((shadow.offset.0, shadow.offset.1)),
None,
);
self.surfaces.canvas(SurfaceId::DropShadows).restore();
@@ -1133,6 +1213,7 @@ impl RenderState {
) -> Result<(bool, bool), String> {
let mut iteration = 0;
let mut is_empty = true;
while let Some(node_render_state) = self.pending_nodes.pop() {
let NodeRenderState {
id: node_id,
@@ -1199,80 +1280,128 @@ impl RenderState {
}
self.render_shape_enter(element, mask);
if !node_render_state.is_root() && self.focus_mode.is_active() {
let scale = self.get_scale();
let scale: f32 = self.get_scale();
let translation = self
.surfaces
.get_render_context_translation(self.render_area, scale);
// Shadow rendering technique: Two-pass approach for proper opacity handling
//
// The shadow rendering uses a two-pass technique to ensure that overlapping
// shadow areas maintain correct opacity without unwanted darkening:
//
// 1. First pass: Render shadow shape in pure black (alpha channel preserved)
// - This creates the shadow silhouette with proper alpha gradients
// - The black color acts as a mask for the final shadow color
//
// 2. Second pass: Apply actual shadow color using SrcIn blend mode
// - SrcIn preserves the alpha channel from the black shadow
// - Only the color channels are replaced, maintaining transparency
// - This prevents overlapping shadows from accumulating opacity
//
// This approach is essential for complex shapes with transparency where
// multiple shadow areas might overlap, ensuring visual consistency.
for shadow in element.drop_shadows().rev().filter(|s| !s.hidden()) {
let paint = skia::Paint::default();
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
self.surfaces
.canvas(SurfaceId::DropShadows)
.save_layer(&layer_rec);
// For text shapes, render drop shadow using text rendering logic
if !matches!(element.shape_type, Type::Text(_)) {
// Shadow rendering technique: Two-pass approach for proper opacity handling
//
// The shadow rendering uses a two-pass technique to ensure that overlapping
// shadow areas maintain correct opacity without unwanted darkening:
//
// 1. First pass: Render shadow shape in pure black (alpha channel preserved)
// - This creates the shadow silhouette with proper alpha gradients
// - The black color acts as a mask for the final shadow color
//
// 2. Second pass: Apply actual shadow color using SrcIn blend mode
// - SrcIn preserves the alpha channel from the black shadow
// - Only the color channels are replaced, maintaining transparency
// - This prevents overlapping shadows from accumulating opacity
//
// This approach is essential for complex shapes with transparency where
// multiple shadow areas might overlap, ensuring visual consistency.
for shadow in element.drop_shadows_visible() {
let paint = skia::Paint::default();
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
// First pass: Render shadow in black to establish alpha mask
self.render_drop_black_shadow(
tree,
modifiers,
structure,
element,
shadow,
scale_content.get(&element.id),
clip_bounds,
scale,
translation,
);
self.surfaces
.canvas(SurfaceId::DropShadows)
.save_layer(&layer_rec);
// Nested shapes shadowing - apply black shadow to child shapes too
for shadow_shape_id in element.children.iter() {
let shadow_shape = tree.get(shadow_shape_id).unwrap();
let clip_bounds = node_render_state.get_shadow_clip_bounds(
element,
modifiers.get(&element.id),
shadow,
);
// First pass: Render shadow in black to establish alpha mask
self.render_drop_black_shadow(
tree,
modifiers,
structure,
shadow_shape,
element,
shadow,
scale_content.get(&element.id),
clip_bounds,
scale,
translation,
);
// Nested shapes shadowing - apply black shadow to child shapes too
for shadow_shape_id in element.children.iter() {
let shadow_shape = tree.get(shadow_shape_id).unwrap();
let clip_bounds = node_render_state.get_shadow_clip_bounds(
element,
modifiers.get(&element.id),
shadow,
);
if !matches!(shadow_shape.shape_type, Type::Text(_)) {
self.render_drop_black_shadow(
tree,
modifiers,
structure,
shadow_shape,
shadow,
scale_content.get(&element.id),
clip_bounds,
scale,
translation,
);
} else {
let paint = skia::Paint::default();
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
self.surfaces
.canvas(SurfaceId::DropShadows)
.save_layer(&layer_rec);
self.surfaces
.canvas(SurfaceId::DropShadows)
.scale((scale, scale));
self.surfaces
.canvas(SurfaceId::DropShadows)
.translate(translation);
let mut transformed_shadow: Cow<Shadow> = Cow::Borrowed(shadow);
// transformed_shadow.to_mut().offset = (0., 0.);
transformed_shadow.to_mut().color = skia::Color::BLACK;
transformed_shadow.to_mut().blur = transformed_shadow.blur * scale;
let mut new_shadow_paint = skia::Paint::default();
new_shadow_paint
.set_image_filter(transformed_shadow.get_drop_shadow_filter());
new_shadow_paint.set_blend_mode(skia::BlendMode::SrcOver);
self.render_shape(
tree,
modifiers,
structure,
shadow_shape,
scale_content.get(&element.id),
clip_bounds,
SurfaceId::DropShadows,
SurfaceId::DropShadows,
SurfaceId::DropShadows,
SurfaceId::DropShadows,
true,
None,
Some(vec![new_shadow_paint.clone()]),
);
self.surfaces.canvas(SurfaceId::DropShadows).restore();
}
}
// Second pass: Apply actual shadow color using SrcIn blend mode
// This preserves the alpha channel from the black shadow while
// replacing only the color channels, preventing opacity accumulation
let mut paint = skia::Paint::default();
paint.set_color(shadow.color);
paint.set_blend_mode(skia::BlendMode::SrcIn);
self.surfaces
.canvas(SurfaceId::DropShadows)
.draw_paint(&paint);
self.surfaces.canvas(SurfaceId::DropShadows).restore();
}
// Second pass: Apply actual shadow color using SrcIn blend mode
// This preserves the alpha channel from the black shadow while
// replacing only the color channels, preventing opacity accumulation
let mut paint = skia::Paint::default();
paint.set_color(shadow.color);
paint.set_blend_mode(skia::BlendMode::SrcIn);
self.surfaces
.canvas(SurfaceId::DropShadows)
.draw_paint(&paint);
self.surfaces.canvas(SurfaceId::DropShadows).restore();
}
self.surfaces
@@ -1292,8 +1421,10 @@ impl RenderState {
SurfaceId::Fills,
SurfaceId::Strokes,
SurfaceId::InnerShadows,
SurfaceId::TextDropShadows,
true,
None,
None,
);
self.surfaces
+51 -13
View File
@@ -1,9 +1,10 @@
use super::{RenderState, SurfaceId};
use crate::render::strokes;
use crate::render::text::{self};
use crate::shapes::{Shadow, Shape, Stroke, Type};
use skia_safe::textlayout::ParagraphBuilder;
use skia_safe::{Paint, Path};
use skia_safe::{canvas::SaveLayerRec, Paint, Path};
use crate::render::text;
use crate::textlayout::ParagraphBuilderGroup;
// Fill Shadows
pub fn render_fill_inner_shadows(
@@ -46,7 +47,6 @@ pub fn render_stroke_inner_shadows(
stroke,
Some(surface_id),
filter.as_ref(),
None,
antialias,
)
}
@@ -76,15 +76,6 @@ pub fn render_text_path_stroke_drop_shadows(
}
}
pub fn render_text_inner_shadows(
render_state: &mut RenderState,
shape: &Shape,
paragraphs: &mut [Vec<ParagraphBuilder>],
surface_id: SurfaceId,
) {
text::render(render_state, shape, paragraphs, Some(surface_id));
}
// Render text paths (unused)
#[allow(dead_code)]
pub fn render_text_path_stroke_inner_shadows(
@@ -129,3 +120,50 @@ fn render_shadow_paint(
_ => {}
}
}
pub fn render_text_shadows(
render_state: &mut RenderState,
shape: &Shape,
paragraphs: &mut [ParagraphBuilderGroup],
stroke_paragraphs_group: &mut [Vec<ParagraphBuilderGroup>],
surface_id: Option<SurfaceId>,
shadows: &[Paint],
blur_filter: &Option<skia_safe::ImageFilter>,
) {
if stroke_paragraphs_group.is_empty() {
return;
}
let canvas = render_state
.surfaces
.canvas(surface_id.unwrap_or(SurfaceId::TextDropShadows));
for shadow in shadows {
let shadow_layer = SaveLayerRec::default().paint(shadow);
canvas.save_layer(&shadow_layer);
text::render(
None,
Some(canvas),
shape,
paragraphs,
surface_id,
None,
blur_filter.as_ref(),
);
for stroke_paragraphs in stroke_paragraphs_group.iter_mut() {
text::render(
None,
Some(canvas),
shape,
stroke_paragraphs,
surface_id,
None,
blur_filter.as_ref(),
);
}
canvas.restore();
}
}
+2 -11
View File
@@ -3,11 +3,10 @@ use std::collections::HashMap;
use crate::math::{Matrix, Point, Rect};
use crate::shapes::{Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, Type};
use skia_safe::{self as skia, textlayout::ParagraphBuilder, ImageFilter, RRect};
use skia_safe::{self as skia, ImageFilter, RRect};
use super::{RenderState, SurfaceId};
use crate::render::filters::compose_filters;
use crate::render::text::{self};
use crate::render::{get_dest_rect, get_source_rect};
// FIXME: See if we can simplify these arguments
@@ -519,7 +518,6 @@ pub fn render(
stroke: &Stroke,
surface_id: Option<SurfaceId>,
shadow: Option<&ImageFilter>,
paragraphs: Option<&mut Vec<Vec<ParagraphBuilder>>>,
antialias: bool,
) {
let scale = render_state.get_scale();
@@ -564,14 +562,7 @@ pub fn render(
shape.image_filter(1.).as_ref(),
antialias,
),
Type::Text(_) => {
text::render(
render_state,
shape,
paragraphs.expect("Text shapes should have paragraphs"),
surface_id,
);
}
Type::Text(_) => {}
shape_type @ (Type::Path(_) | Type::Bool(_)) => {
if let Some(path) = shape_type.path() {
draw_stroke_on_path(
+30 -14
View File
@@ -17,15 +17,16 @@ const TILE_SIZE_MULTIPLIER: i32 = 2;
#[repr(u32)]
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum SurfaceId {
Target = 0b0_0000_0001,
Cache = 0b0_0000_0010,
Current = 0b0_0000_0100,
Fills = 0b0_0000_1000,
Strokes = 0b0_0001_0000,
DropShadows = 0b0_0010_0000,
InnerShadows = 0b0_0100_0000,
UI = 0b0_1000_0000,
Debug = 0b1_0000_0000,
Target = 0b00_0000_0001,
Cache = 0b00_0000_0010,
Current = 0b00_0000_0100,
Fills = 0b00_0000_1000,
Strokes = 0b00_0001_0000,
DropShadows = 0b00_0010_0000,
InnerShadows = 0b00_0100_0000,
TextDropShadows = 0b00_1000_0000,
UI = 0b01_0000_0000,
Debug = 0b10_0000_0001,
}
pub struct Surfaces {
@@ -42,6 +43,8 @@ pub struct Surfaces {
drop_shadows: skia::Surface,
// used for rendering over shadows.
inner_shadows: skia::Surface,
// used for rendering text drop shadows
text_drop_shadows: skia::Surface,
// used for displaying auxiliary workspace elements
ui: skia::Surface,
// for drawing debug info.
@@ -73,6 +76,8 @@ impl Surfaces {
gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims);
let inner_shadows =
gpu_state.create_surface_with_isize("inner_shadows".to_string(), extra_tile_dims);
let text_drop_shadows =
gpu_state.create_surface_with_isize("text_drop_shadows".to_string(), extra_tile_dims);
let shape_fills =
gpu_state.create_surface_with_isize("shape_fills".to_string(), extra_tile_dims);
let shape_strokes =
@@ -88,6 +93,7 @@ impl Surfaces {
current,
drop_shadows,
inner_shadows,
text_drop_shadows,
shape_fills,
shape_strokes,
ui,
@@ -166,6 +172,9 @@ impl Surfaces {
if ids & SurfaceId::InnerShadows as u32 != 0 {
f(self.get_mut(SurfaceId::InnerShadows));
}
if ids & SurfaceId::TextDropShadows as u32 != 0 {
f(self.get_mut(SurfaceId::TextDropShadows));
}
if ids & SurfaceId::DropShadows as u32 != 0 {
f(self.get_mut(SurfaceId::DropShadows));
}
@@ -189,11 +198,15 @@ impl Surfaces {
pub fn update_render_context(&mut self, render_area: skia::Rect, scale: f32) {
let translation = self.get_render_context_translation(render_area, scale);
self.apply_mut(
SurfaceId::Fills as u32 | SurfaceId::Strokes as u32 | SurfaceId::InnerShadows as u32,
SurfaceId::Fills as u32
| SurfaceId::Strokes as u32
| SurfaceId::InnerShadows as u32
| SurfaceId::TextDropShadows as u32,
|s| {
s.canvas().restore();
s.canvas().save();
s.canvas().translate(translation);
let canvas = s.canvas();
canvas.reset_matrix();
canvas.scale((scale, scale));
canvas.translate(translation);
},
);
}
@@ -206,6 +219,7 @@ impl Surfaces {
SurfaceId::Current => &mut self.current,
SurfaceId::DropShadows => &mut self.drop_shadows,
SurfaceId::InnerShadows => &mut self.inner_shadows,
SurfaceId::TextDropShadows => &mut self.text_drop_shadows,
SurfaceId::Fills => &mut self.shape_fills,
SurfaceId::Strokes => &mut self.shape_strokes,
SurfaceId::Debug => &mut self.debug,
@@ -257,13 +271,15 @@ impl Surfaces {
pub fn reset(&mut self, color: skia::Color) {
self.canvas(SurfaceId::Fills).restore_to_count(1);
self.canvas(SurfaceId::InnerShadows).restore_to_count(1);
self.canvas(SurfaceId::TextDropShadows).restore_to_count(1);
self.canvas(SurfaceId::Strokes).restore_to_count(1);
self.canvas(SurfaceId::Current).restore_to_count(1);
self.apply_mut(
SurfaceId::Fills as u32
| SurfaceId::Strokes as u32
| SurfaceId::Current as u32
| SurfaceId::InnerShadows as u32,
| SurfaceId::InnerShadows as u32
| SurfaceId::TextDropShadows as u32,
|s| {
s.canvas().clear(color).reset_matrix();
},
+43 -12
View File
@@ -1,22 +1,55 @@
use super::{RenderState, Shape, SurfaceId};
use crate::shapes::VerticalAlign;
use skia_safe::{
canvas::SaveLayerRec, textlayout::LineMetrics, textlayout::Paragraph,
textlayout::ParagraphBuilder, textlayout::RectHeightStyle, textlayout::RectWidthStyle,
textlayout::StyleMetrics, textlayout::TextDecoration, textlayout::TextStyle, Canvas, Paint,
Path,
canvas::SaveLayerRec,
textlayout::{
LineMetrics, Paragraph, ParagraphBuilder, RectHeightStyle, RectWidthStyle, StyleMetrics,
TextDecoration, TextStyle,
},
Canvas, ImageFilter, Paint, Path,
};
pub fn render(
render_state: &mut RenderState,
render_state: Option<&mut RenderState>,
canvas: Option<&Canvas>,
shape: &Shape,
paragraphs: &mut [Vec<ParagraphBuilder>],
surface_id: Option<SurfaceId>,
shadow: Option<&Paint>,
blur: Option<&ImageFilter>,
) {
let canvas = render_state
.surfaces
.canvas(surface_id.unwrap_or(SurfaceId::Fills));
let render_canvas = if let Some(rs) = render_state {
rs.surfaces.canvas(surface_id.unwrap_or(SurfaceId::Fills))
} else if let Some(c) = canvas {
c
} else {
return;
};
if let Some(blur_filter) = blur {
let mut blur_paint = Paint::default();
blur_paint.set_image_filter(blur_filter.clone());
let blur_layer = SaveLayerRec::default().paint(&blur_paint);
render_canvas.save_layer(&blur_layer);
}
if let Some(shadow_paint) = shadow {
let layer_rec = SaveLayerRec::default().paint(shadow_paint);
render_canvas.save_layer(&layer_rec);
draw_text(render_canvas, shape, paragraphs);
render_canvas.restore();
} else {
draw_text(render_canvas, shape, paragraphs);
}
if blur.is_some() {
render_canvas.restore();
}
render_canvas.restore();
}
fn draw_text(canvas: &Canvas, shape: &Shape, paragraphs: &mut [Vec<ParagraphBuilder>]) {
// Width
let paragraph_width = if let crate::shapes::Type::Text(text_content) = &shape.shape_type {
text_content.width()
@@ -63,8 +96,6 @@ pub fn render(
global_offset_y = group_offset_y;
}
}
canvas.restore();
}
fn draw_text_decorations(
@@ -286,11 +317,11 @@ pub fn render_as_path(
// let text_content = text_content.new_bounds(shape.selrect());
// let paths = text_content.get_paths(antialias);
// shadows::render_text_drop_shadows(self, &shape, &paths, antialias);
// shadows::render_text_shadows(self, &shape, &paths, antialias);
// text::render(self, &paths, None, None);
// for stroke in shape.visible_strokes().rev() {
// shadows::render_text_path_stroke_drop_shadows(
// shadows::render_text_path_stroke_shadows(
// self, &shape, &paths, stroke, antialias,
// );
// strokes::render_text_paths(self, &shape, stroke, &paths, None, None, antialias);
+27 -5
View File
@@ -573,6 +573,12 @@ impl Shape {
.filter(|stroke| stroke.width > MIN_STROKE_WIDTH)
}
pub fn has_visible_strokes(&self) -> bool {
self.strokes
.iter()
.any(|stroke| stroke.width > MIN_STROKE_WIDTH)
}
pub fn add_stroke(&mut self, s: Stroke) {
self.invalidate_extrect();
self.strokes.push(s)
@@ -984,6 +990,7 @@ impl Shape {
}
}
#[allow(dead_code)]
pub fn mask_filter(&self, scale: f32) -> Option<skia::MaskFilter> {
if !self.blur.hidden {
match self.blur.blur_type {
@@ -1022,6 +1029,12 @@ impl Shape {
.filter(|shadow| shadow.style() == ShadowStyle::Drop)
}
pub fn drop_shadows_visible(&self) -> impl DoubleEndedIterator<Item = &Shadow> {
self.shadows
.iter()
.filter(|shadow| shadow.style() == ShadowStyle::Drop && !shadow.hidden())
}
pub fn inner_shadows(&self) -> impl DoubleEndedIterator<Item = &Shadow> {
self.shadows
.iter()
@@ -1162,11 +1175,6 @@ impl Shape {
!self.fills.is_empty()
}
#[allow(dead_code)]
pub fn has_visible_inner_strokes(&self) -> bool {
self.visible_strokes().any(|s| s.kind == StrokeKind::Inner)
}
pub fn count_visible_inner_strokes(&self) -> usize {
self.visible_strokes()
.filter(|s| s.kind == StrokeKind::Inner)
@@ -1210,6 +1218,20 @@ impl Shape {
}
}
pub fn drop_shadow_paints(&self) -> Vec<skia_safe::Paint> {
let drop_shadows: Vec<&crate::shapes::shadows::Shadow> =
self.drop_shadows().filter(|s| !s.hidden()).collect();
drop_shadows
.into_iter()
.map(|shadow| {
let mut paint = skia_safe::Paint::default();
let filter = shadow.get_drop_shadow_filter();
paint.set_image_filter(filter);
paint
})
.collect()
}
pub fn inner_shadow_paints(&self) -> Vec<skia_safe::Paint> {
let inner_shadows: Vec<&crate::shapes::shadows::Shadow> =
self.inner_shadows().filter(|s| !s.hidden()).collect();
+6 -1
View File
@@ -255,7 +255,12 @@ pub fn merge_fills(fills: &[Fill], bounding_box: Rect) -> skia::Paint {
fills_paint
}
pub fn set_paint_fill(paint: &mut Paint, fill: &Fill, bounding_box: &Rect) {
pub fn set_paint_fill(paint: &mut Paint, fill: &Fill, bounding_box: &Rect, remove_alpha: bool) {
if remove_alpha {
paint.set_color(skia::Color::BLACK);
paint.set_alpha(255);
return;
}
let shader = get_fill_shader(fill, bounding_box);
if let Some(shader) = shader {
paint.set_shader(shader);
+2 -2
View File
@@ -200,7 +200,7 @@ fn propagate_transform(
match content.grow_type() {
GrowType::AutoHeight => {
let paragraph_width = shape_bounds_after.width();
let mut paragraphs = paragraph_builder_group_from_text(content, None, None, None);
let mut paragraphs = paragraph_builder_group_from_text(content, None);
let height = auto_height(&mut paragraphs, paragraph_width);
let resize_transform = math::resize_matrix(
&shape_bounds_after,
@@ -213,7 +213,7 @@ fn propagate_transform(
}
GrowType::AutoWidth => {
let paragraph_width = content.width();
let mut paragraphs = paragraph_builder_group_from_text(content, None, None, None);
let mut paragraphs = paragraph_builder_group_from_text(content, None);
let height = auto_height(&mut paragraphs, paragraph_width);
let resize_transform = math::resize_matrix(
&shape_bounds_after,
+7
View File
@@ -264,4 +264,11 @@ impl Stroke {
paint
}
pub fn is_transparent(&self) -> bool {
match &self.fill {
Fill::Solid(SolidColor(color)) => color.a() == 0,
_ => false,
}
}
}
+28 -25
View File
@@ -4,7 +4,11 @@ use crate::{
textlayout::paragraph_builder_group_from_text,
};
use skia_safe::{self as skia, paint::Paint, textlayout::ParagraphStyle, ImageFilter, MaskFilter};
use skia_safe::{
self as skia,
paint::{self, Paint},
textlayout::ParagraphStyle,
};
use std::collections::HashSet;
use super::FontFamily;
@@ -86,7 +90,7 @@ impl TextContent {
pub fn width(&self) -> f32 {
if self.grow_type() == GrowType::AutoWidth {
let temp_paragraphs = paragraph_builder_group_from_text(self, None, None, None);
let temp_paragraphs = paragraph_builder_group_from_text(self, None);
let mut temp_paragraphs = temp_paragraphs;
auto_width(&mut temp_paragraphs, f32::MAX).ceil()
} else {
@@ -104,7 +108,7 @@ impl TextContent {
pub fn visual_bounds(&self) -> (f32, f32) {
let paragraph_width = self.width();
let mut paragraphs = paragraph_builder_group_from_text(self, None, None, None);
let mut paragraphs = paragraph_builder_group_from_text(self, None);
let paragraph_height = auto_height(&mut paragraphs, paragraph_width);
(paragraph_width, paragraph_height)
}
@@ -308,26 +312,20 @@ impl TextLeaf {
&self,
content_bounds: &Rect,
fallback_fonts: &HashSet<String>,
_blur: Option<&ImageFilter>,
blur_mask: Option<&MaskFilter>,
shadow: Option<&Paint>,
remove_alpha: bool,
) -> skia::textlayout::TextStyle {
let mut style = skia::textlayout::TextStyle::default();
if shadow.is_some() {
let paint = shadow.unwrap().clone();
style.set_foreground_paint(&paint);
let mut paint = paint::Paint::default();
if remove_alpha {
paint.set_color(skia::Color::BLACK);
paint.set_alpha(255);
} else {
let paint = merge_fills(&self.fills, *content_bounds);
style.set_foreground_paint(&paint);
}
if let Some(blur_mask) = blur_mask {
let mut paint = skia::Paint::default();
paint.set_mask_filter(blur_mask.clone());
style.set_foreground_paint(&paint);
paint = merge_fills(&self.fills, *content_bounds);
}
style.set_foreground_paint(&paint);
style.set_font_size(self.font_size);
style.set_letter_spacing(self.letter_spacing);
style.set_half_leading(false);
@@ -359,12 +357,20 @@ impl TextLeaf {
&self,
stroke_paint: &Paint,
fallback_fonts: &HashSet<String>,
blur: Option<&ImageFilter>,
blur_mask: Option<&MaskFilter>,
shadow: Option<&Paint>,
remove_alpha: bool,
) -> skia::textlayout::TextStyle {
let mut style = self.to_style(&Rect::default(), fallback_fonts, blur, blur_mask, shadow);
style.set_foreground_paint(stroke_paint);
let mut style = self.to_style(&Rect::default(), fallback_fonts, remove_alpha);
if remove_alpha {
let mut paint = skia::Paint::default();
paint.set_style(stroke_paint.style());
paint.set_stroke_width(stroke_paint.stroke_width());
paint.set_color(skia::Color::BLACK);
paint.set_alpha(255);
style.set_foreground_paint(&paint);
} else {
style.set_foreground_paint(stroke_paint);
}
style.set_font_size(self.font_size);
style.set_letter_spacing(self.letter_spacing);
style.set_decoration_type(match self.text_decoration {
@@ -406,9 +412,6 @@ impl TextLeaf {
}
pub fn is_transparent(&self) -> bool {
if self.fills.is_empty() {
return true;
}
self.fills.iter().all(|fill| match fill {
shapes::Fill::Solid(shapes::SolidColor(color)) => color.a() == 0,
_ => false,
+1 -1
View File
@@ -20,7 +20,7 @@ impl TextPaths {
let mut paths = Vec::new();
let mut offset_y = self.bounds.y();
let mut paragraphs = paragraph_builder_group_from_text(&self.0, None, None, None);
let mut paragraphs = paragraph_builder_group_from_text(&self.0, None);
for paragraphs in paragraphs.iter_mut() {
for paragraph_builder in paragraphs.iter_mut() {
+34 -151
View File
@@ -1,7 +1,6 @@
use skia_safe::{self as skia, textlayout::ParagraphBuilder, ImageFilter, MaskFilter, Paint, Rect};
use skia_safe::{self as skia, textlayout::ParagraphBuilder, Paint, Rect};
use crate::{
render::filters::compose_filters,
shapes::{merge_fills, set_paint_fill, Stroke, StrokeKind, TextContent},
utils::{get_fallback_fonts, get_font_collection},
};
@@ -51,13 +50,11 @@ pub fn build_paragraphs_with_width(
.collect()
}
type ParagraphBuilderGroup = Vec<ParagraphBuilder>;
pub type ParagraphBuilderGroup = Vec<ParagraphBuilder>;
pub fn paragraph_builder_group_from_text(
text_content: &TextContent,
blur: Option<&ImageFilter>,
blur_mask: Option<&MaskFilter>,
shadow: Option<&Paint>,
use_shadow: Option<bool>,
) -> Vec<ParagraphBuilderGroup> {
let fonts = get_font_collection();
let fallback_fonts = get_fallback_fonts();
@@ -67,13 +64,8 @@ pub fn paragraph_builder_group_from_text(
let paragraph_style = paragraph.paragraph_to_style();
let mut builder = ParagraphBuilder::new(&paragraph_style, fonts);
for leaf in paragraph.children() {
let text_style = leaf.to_style(
&text_content.bounds(),
fallback_fonts,
blur,
blur_mask,
shadow,
);
let remove_alpha = use_shadow.unwrap_or(false) && !leaf.is_transparent();
let text_style = leaf.to_style(&text_content.bounds(), fallback_fonts, remove_alpha);
let text = leaf.apply_text_transform();
builder.push_style(&text_style);
builder.add_text(&text);
@@ -88,43 +80,27 @@ pub fn stroke_paragraph_builder_group_from_text(
text_content: &TextContent,
stroke: &Stroke,
bounds: &Rect,
blur: Option<&ImageFilter>,
blur_mask: Option<&MaskFilter>,
shadow: Option<&Paint>,
count_inner_strokes: usize,
use_shadow: Option<bool>,
) -> Vec<ParagraphBuilderGroup> {
let fallback_fonts = get_fallback_fonts();
let fonts = get_font_collection();
let mut paragraph_group = Vec::new();
let remove_stroke_alpha = use_shadow.unwrap_or(false) && !stroke.is_transparent();
for paragraph in text_content.paragraphs() {
let mut stroke_paragraphs_map: std::collections::HashMap<usize, ParagraphBuilder> =
std::collections::HashMap::new();
for leaf in paragraph.children().iter() {
let mut text_paint = merge_fills(leaf.fills(), *bounds);
if let Some(blur_mask) = blur_mask {
text_paint.set_mask_filter(blur_mask.clone());
}
let stroke_paints = if shadow.is_some() {
get_text_stroke_paints_with_shadows(
stroke,
blur,
blur_mask,
shadow,
leaf.is_transparent(),
)
} else {
get_text_stroke_paints(
stroke,
bounds,
&text_paint,
blur,
blur_mask,
count_inner_strokes,
)
};
let text_paint: skia_safe::Handle<_> = merge_fills(leaf.fills(), *bounds);
let stroke_paints = get_text_stroke_paints(
stroke,
bounds,
&text_paint,
count_inner_strokes,
remove_stroke_alpha,
);
let text: String = leaf.apply_text_transform();
@@ -134,8 +110,9 @@ pub fn stroke_paragraph_builder_group_from_text(
ParagraphBuilder::new(&paragraph_style, fonts)
});
let stroke_paint = stroke_paint.clone();
let remove_alpha = use_shadow.unwrap_or(false) && !leaf.is_transparent();
let stroke_style =
leaf.to_stroke_style(&stroke_paint, fallback_fonts, blur, blur_mask, None);
leaf.to_stroke_style(&stroke_paint, fallback_fonts, remove_alpha);
builder.push_style(&stroke_style);
builder.add_text(&text);
}
@@ -158,101 +135,12 @@ fn get_built_paragraphs(
build_paragraphs_with_width(paragraphs, width)
}
fn get_text_stroke_paints_with_shadows(
stroke: &Stroke,
blur: Option<&ImageFilter>,
blur_mask: Option<&MaskFilter>,
shadow: Option<&Paint>,
is_transparent: bool,
) -> Vec<Paint> {
let mut paints = Vec::new();
match stroke.kind {
StrokeKind::Inner => {
let mut paint = Paint::default();
paint.set_style(skia::PaintStyle::Fill);
paint.set_anti_alias(true);
if let Some(blur) = blur {
paint.set_image_filter(blur.clone());
}
if let Some(shadow) = shadow {
paint.set_image_filter(shadow.image_filter());
}
paints.push(paint.clone());
if is_transparent {
let image_filter = skia_safe::image_filters::erode(
(stroke.width, stroke.width),
paint.image_filter(),
None,
);
paint.set_image_filter(image_filter);
paint.set_blend_mode(skia::BlendMode::DstOut);
paints.push(paint.clone());
}
}
StrokeKind::Center => {
let mut paint = skia_safe::Paint::default();
paint.set_anti_alias(true);
paint.set_stroke_width(stroke.width);
if let Some(blur) = blur {
paint.set_image_filter(blur.clone());
}
if let Some(shadow) = shadow {
paint.set_image_filter(shadow.image_filter());
}
if is_transparent {
paint.set_style(skia::PaintStyle::Stroke);
} else {
paint.set_style(skia::PaintStyle::StrokeAndFill);
}
paints.push(paint);
}
StrokeKind::Outer => {
let mut paint = skia_safe::Paint::default();
paint.set_style(skia::PaintStyle::StrokeAndFill);
paint.set_anti_alias(true);
paint.set_stroke_width(stroke.width * 2.0);
if let Some(blur_mask) = blur_mask {
paint.set_mask_filter(blur_mask.clone());
}
if let Some(shadow) = shadow {
paint.set_image_filter(shadow.image_filter());
}
paints.push(paint.clone());
if is_transparent {
let image_filter = skia_safe::image_filters::erode(
(stroke.width, stroke.width),
paint.image_filter(),
None,
);
paint.set_image_filter(image_filter);
paint.set_blend_mode(skia::BlendMode::DstOut);
paints.push(paint.clone());
}
}
}
paints
}
fn get_text_stroke_paints(
stroke: &Stroke,
bounds: &Rect,
text_paint: &Paint,
blur: Option<&ImageFilter>,
blur_mask: Option<&MaskFilter>,
count_inner_strokes: usize,
remove_stroke_alpha: bool,
) -> Vec<Paint> {
let mut paints = Vec::new();
@@ -269,34 +157,37 @@ fn get_text_stroke_paints(
let mut paint = text_paint.clone();
paint.set_style(skia::PaintStyle::Fill);
paint.set_anti_alias(true);
if let Some(blur) = blur {
paint.set_image_filter(blur.clone());
}
paints.push(paint);
let mut paint = skia::Paint::default();
paint.set_style(skia::PaintStyle::Stroke);
paint.set_blend_mode(skia::BlendMode::SrcIn);
paint.set_anti_alias(true);
paint.set_stroke_width(stroke.width * 2.0);
set_paint_fill(&mut paint, &stroke.fill, bounds);
if let Some(blur) = blur {
paint.set_image_filter(blur.clone());
}
set_paint_fill(&mut paint, &stroke.fill, bounds, remove_stroke_alpha);
paints.push(paint);
} else {
let mut paint = text_paint.clone();
let mut paint = skia::Paint::default();
if remove_stroke_alpha {
paint.set_color(skia::Color::BLACK);
paint.set_alpha(255);
} else {
paint = text_paint.clone();
set_paint_fill(&mut paint, &stroke.fill, bounds, false);
}
paint.set_style(skia::PaintStyle::Fill);
paint.set_anti_alias(false);
set_paint_fill(&mut paint, &stroke.fill, bounds);
paints.push(paint);
let mut paint = skia::Paint::default();
let image_filter =
skia_safe::image_filters::erode((stroke.width, stroke.width), None, None);
let filter = compose_filters(blur, image_filter.as_ref());
paint.set_image_filter(filter);
paint.set_image_filter(image_filter);
paint.set_anti_alias(false);
paint.set_color(skia::Color::BLACK);
paint.set_alpha(255);
paint.set_blend_mode(skia::BlendMode::DstOut);
paints.push(paint);
}
@@ -306,12 +197,7 @@ fn get_text_stroke_paints(
paint.set_style(skia::PaintStyle::Stroke);
paint.set_anti_alias(true);
paint.set_stroke_width(stroke.width);
set_paint_fill(&mut paint, &stroke.fill, bounds);
if let Some(blur) = blur {
paint.set_image_filter(blur.clone());
}
set_paint_fill(&mut paint, &stroke.fill, bounds, remove_stroke_alpha);
paints.push(paint);
}
StrokeKind::Outer => {
@@ -320,10 +206,7 @@ fn get_text_stroke_paints(
paint.set_blend_mode(skia::BlendMode::DstOver);
paint.set_anti_alias(true);
paint.set_stroke_width(stroke.width * 2.0);
set_paint_fill(&mut paint, &stroke.fill, bounds);
if let Some(blur_mask) = blur_mask {
paint.set_mask_filter(blur_mask.clone());
}
set_paint_fill(&mut paint, &stroke.fill, bounds, remove_stroke_alpha);
paints.push(paint);
let mut paint = skia::Paint::default();
+3 -5
View File
@@ -46,7 +46,7 @@ pub extern "C" fn get_text_dimensions() -> *mut u8 {
if let Type::Text(content) = &shape.shape_type {
// 1. Reset Paragraphs
let paragraph_width = content.width();
let mut paragraphs = paragraph_builder_group_from_text(content, None, None, None);
let mut paragraphs = paragraph_builder_group_from_text(content, None);
let built_paragraphs = build_paragraphs_with_width(&mut paragraphs, paragraph_width);
// 2. Max Width Calculation
@@ -58,14 +58,12 @@ pub extern "C" fn get_text_dimensions() -> *mut u8 {
// 3. Width and Height Calculation
match content.grow_type() {
GrowType::AutoHeight => {
let mut paragraph_height =
paragraph_builder_group_from_text(content, None, None, None);
let mut paragraph_height = paragraph_builder_group_from_text(content, None);
height = auto_height(&mut paragraph_height, paragraph_width).ceil();
}
GrowType::AutoWidth => {
width = paragraph_width;
let mut paragraph_height =
paragraph_builder_group_from_text(content, None, None, None);
let mut paragraph_height = paragraph_builder_group_from_text(content, None);
height = auto_height(&mut paragraph_height, paragraph_width).ceil();
}
GrowType::Fixed => {}