Merge pull request #7264 from penpot/elenatorro-12002-draw-shadows-and-blurs-on-texts-on-surfaces

🐛 Fix text shadows and blur and refactor text rendering
This commit is contained in:
Alejandro Alonso
2025-09-10 15:50:33 +02:00
committed by GitHub
27 changed files with 14417 additions and 498 deletions

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

View File

@@ -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"
}
}
}

View File

@@ -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

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,
);
strokes::render(
self,
&shape,
stroke,
Some(strokes_surface_id),
None,
Some(&mut stroke_paragraphs),
antialias,
);
for inner_shadow in &inner_shadows {
let mut stroke_paragraphs_with_inner_shadows =
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(),
blur_filter.as_ref(),
blur_mask.as_ref(),
Some(inner_shadow),
count_inner_strokes,
None,
)
})
.collect::<Vec<_>>();
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<_>>();
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(),
);
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,12 +1280,15 @@ 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);
// 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
@@ -1221,9 +1305,10 @@ impl RenderState {
//
// 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()) {
for shadow in element.drop_shadows_visible() {
let paint = skia::Paint::default();
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
self.surfaces
.canvas(SurfaceId::DropShadows)
.save_layer(&layer_rec);
@@ -1249,6 +1334,8 @@ impl RenderState {
modifiers.get(&element.id),
shadow,
);
if !matches!(shadow_shape.shape_type, Type::Text(_)) {
self.render_drop_black_shadow(
tree,
modifiers,
@@ -1260,6 +1347,47 @@ impl RenderState {
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
@@ -1274,6 +1402,7 @@ impl RenderState {
self.surfaces.canvas(SurfaceId::DropShadows).restore();
}
}
self.surfaces
.draw_into(SurfaceId::DropShadows, SurfaceId::Current, None);
@@ -1292,8 +1421,10 @@ impl RenderState {
SurfaceId::Fills,
SurfaceId::Strokes,
SurfaceId::InnerShadows,
SurfaceId::TextDropShadows,
true,
None,
None,
);
self.surfaces

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();
}
}

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(

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();
},

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);

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();

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);

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,

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,
}
}
}

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);
paint = merge_fills(&self.fills, *content_bounds);
}
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);
}
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);
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,

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() {

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(
let text_paint: skia_safe::Handle<_> = merge_fills(leaf.fills(), *bounds);
let stroke_paints = get_text_stroke_paints(
stroke,
bounds,
&text_paint,
blur,
blur_mask,
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();

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 => {}