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: { expect: {
timeout: process.env.CI ? 20000 : 5000, timeout: process.env.CI ? 20000 : 5000,
}, },
/* Reporter to use. See https://playwright.dev/docs/test-reporters */ /* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: "html", reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ /* 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", testDir: "./playwright/ui/render-wasm-specs",
snapshotPathTemplate: "{testDir}/{testFilePath}-snapshots/{arg}.png", snapshotPathTemplate: "{testDir}/{testFilePath}-snapshots/{arg}.png",
timeout: 2 * 60 * 1000,
expect: { expect: {
timeout: process.env.CI ? 20000 : 10000,
toHaveScreenshot: { toHaveScreenshot: {
maxDiffPixelRatio: 0.001, 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(); 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 ({ test("Renders a file with texts with different alignments", async ({
page, 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(); let paint = skia::Paint::default();
self.surfaces
.draw_into(SurfaceId::TextDropShadows, SurfaceId::Current, Some(&paint));
self.surfaces self.surfaces
.draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint)); .draw_into(SurfaceId::Fills, SurfaceId::Current, Some(&paint));
@@ -457,8 +460,10 @@ impl RenderState {
.draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint)); .draw_into(SurfaceId::InnerShadows, SurfaceId::Current, Some(&paint));
} }
let surface_ids = let surface_ids = SurfaceId::Strokes as u32
SurfaceId::Strokes as u32 | SurfaceId::Fills as u32 | SurfaceId::InnerShadows as u32; | SurfaceId::Fills as u32
| SurfaceId::InnerShadows as u32
| SurfaceId::TextDropShadows as u32;
self.surfaces.apply_mut(surface_ids, |s| { self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().clear(skia::Color::TRANSPARENT); s.canvas().clear(skia::Color::TRANSPARENT);
@@ -485,8 +490,10 @@ impl RenderState {
fills_surface_id: SurfaceId, fills_surface_id: SurfaceId,
strokes_surface_id: SurfaceId, strokes_surface_id: SurfaceId,
innershadows_surface_id: SurfaceId, innershadows_surface_id: SurfaceId,
text_drop_shadows_surface_id: SurfaceId,
apply_to_current_surface: bool, apply_to_current_surface: bool,
offset: Option<(f32, f32)>, offset: Option<(f32, f32)>,
parent_shadows: Option<Vec<skia_safe::Paint>>,
) { ) {
let shape = if let Some(scale_content) = scale_content { let shape = if let Some(scale_content) = scale_content {
&shape.scale_content(*scale_content) &shape.scale_content(*scale_content)
@@ -494,8 +501,10 @@ impl RenderState {
shape shape
}; };
let surface_ids = let surface_ids = fills_surface_id as u32
fills_surface_id as u32 | strokes_surface_id as u32 | innershadows_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| { self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().save(); s.canvas().save();
}); });
@@ -604,72 +613,139 @@ impl RenderState {
}); });
let text_content = text_content.new_bounds(shape.selrect()); let text_content = text_content.new_bounds(shape.selrect());
let drop_shadows = shape.drop_shadow_paints();
let inner_shadows = shape.inner_shadow_paints(); let inner_shadows = shape.inner_shadow_paints();
let blur_filter = shape.image_filter(1.); 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(); let count_inner_strokes = shape.count_visible_inner_strokes();
text::render(self, &shape, &mut paragraphs, Some(fills_surface_id)); let mut paragraphs = paragraph_builder_group_from_text(&text_content, None);
for stroke in shape.visible_strokes().rev() { let mut paragraphs_with_shadows =
let mut stroke_paragraphs = stroke_paragraph_builder_group_from_text( paragraph_builder_group_from_text(&text_content, Some(true));
&text_content, let mut stroke_paragraphs_list = shape
stroke, .visible_strokes()
&shape.selrect(), .map(|stroke| {
blur_filter.as_ref(), stroke_paragraph_builder_group_from_text(
blur_mask.as_ref(), &text_content,
None, stroke,
count_inner_strokes, &shape.selrect(),
); count_inner_strokes,
None,
)
})
.collect::<Vec<_>>();
strokes::render( let mut stroke_paragraphs_with_shadows_list = shape
self, .visible_strokes()
&shape, .map(|stroke| {
stroke, stroke_paragraph_builder_group_from_text(
Some(strokes_surface_id), &text_content,
None, stroke,
Some(&mut stroke_paragraphs), &shape.selrect(),
antialias, count_inner_strokes,
); Some(true),
)
})
.collect::<Vec<_>>();
for inner_shadow in &inner_shadows { if let Some(parent_shadows) = parent_shadows {
let mut stroke_paragraphs_with_inner_shadows = if !shape.has_visible_strokes() {
stroke_paragraph_builder_group_from_text( for shadow in &parent_shadows {
&text_content, text::render(
stroke, Some(self),
&shape.selrect(), None,
&shape,
&mut paragraphs_with_shadows,
text_drop_shadows_surface_id.into(),
Some(shadow),
blur_filter.as_ref(), 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, self,
&shape, &shape,
&mut stroke_paragraphs_with_inner_shadows, &mut paragraphs_with_shadows,
innershadows_surface_id, &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 { // 2. Text fills
let mut paragraphs_with_inner_shadows = paragraph_builder_group_from_text( text::render(
&text_content, Some(self),
None,
&shape,
&mut paragraphs,
Some(fills_surface_id),
None,
blur_filter.as_ref(), 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, self,
&shape, &shape,
&mut paragraphs_with_inner_shadows, &mut paragraphs_with_shadows,
innershadows_surface_id, &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, stroke,
Some(strokes_surface_id), Some(strokes_surface_id),
None, None,
None,
antialias, antialias,
); );
shadows::render_stroke_inner_shadows( shadows::render_stroke_inner_shadows(
@@ -815,8 +890,10 @@ impl RenderState {
performance::begin_measure!("start_render_loop"); performance::begin_measure!("start_render_loop");
self.reset_canvas(); self.reset_canvas();
let surface_ids = let surface_ids = SurfaceId::Strokes as u32
SurfaceId::Strokes as u32 | SurfaceId::Fills as u32 | SurfaceId::InnerShadows as u32; | SurfaceId::Fills as u32
| SurfaceId::InnerShadows as u32
| SurfaceId::TextDropShadows as u32;
self.surfaces.apply_mut(surface_ids, |s| { self.surfaces.apply_mut(surface_ids, |s| {
s.canvas().scale((scale, scale)); s.canvas().scale((scale, scale));
}); });
@@ -997,8 +1074,10 @@ impl RenderState {
SurfaceId::Fills, SurfaceId::Fills,
SurfaceId::Strokes, SurfaceId::Strokes,
SurfaceId::InnerShadows, SurfaceId::InnerShadows,
SurfaceId::TextDropShadows,
true, true,
None, None,
None,
); );
} }
@@ -1098,7 +1177,6 @@ impl RenderState {
self.surfaces self.surfaces
.canvas(SurfaceId::DropShadows) .canvas(SurfaceId::DropShadows)
.save_layer(&layer_rec); .save_layer(&layer_rec);
self.surfaces self.surfaces
.canvas(SurfaceId::DropShadows) .canvas(SurfaceId::DropShadows)
.scale((scale, scale)); .scale((scale, scale));
@@ -1116,8 +1194,10 @@ impl RenderState {
SurfaceId::DropShadows, SurfaceId::DropShadows,
SurfaceId::DropShadows, SurfaceId::DropShadows,
SurfaceId::DropShadows, SurfaceId::DropShadows,
SurfaceId::DropShadows,
false, false,
Some((shadow.offset.0, shadow.offset.1)), Some((shadow.offset.0, shadow.offset.1)),
None,
); );
self.surfaces.canvas(SurfaceId::DropShadows).restore(); self.surfaces.canvas(SurfaceId::DropShadows).restore();
@@ -1133,6 +1213,7 @@ impl RenderState {
) -> Result<(bool, bool), String> { ) -> Result<(bool, bool), String> {
let mut iteration = 0; let mut iteration = 0;
let mut is_empty = true; let mut is_empty = true;
while let Some(node_render_state) = self.pending_nodes.pop() { while let Some(node_render_state) = self.pending_nodes.pop() {
let NodeRenderState { let NodeRenderState {
id: node_id, id: node_id,
@@ -1199,80 +1280,128 @@ impl RenderState {
} }
self.render_shape_enter(element, mask); self.render_shape_enter(element, mask);
if !node_render_state.is_root() && self.focus_mode.is_active() { 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 let translation = self
.surfaces .surfaces
.get_render_context_translation(self.render_area, scale); .get_render_context_translation(self.render_area, scale);
// Shadow rendering technique: Two-pass approach for proper opacity handling // For text shapes, render drop shadow using text rendering logic
// if !matches!(element.shape_type, Type::Text(_)) {
// The shadow rendering uses a two-pass technique to ensure that overlapping // Shadow rendering technique: Two-pass approach for proper opacity handling
// shadow areas maintain correct opacity without unwanted darkening: //
// // The shadow rendering uses a two-pass technique to ensure that overlapping
// 1. First pass: Render shadow shape in pure black (alpha channel preserved) // shadow areas maintain correct opacity without unwanted darkening:
// - This creates the shadow silhouette with proper alpha gradients //
// - The black color acts as a mask for the final shadow color // 1. First pass: Render shadow shape in pure black (alpha channel preserved)
// // - This creates the shadow silhouette with proper alpha gradients
// 2. Second pass: Apply actual shadow color using SrcIn blend mode // - The black color acts as a mask for the final shadow color
// - SrcIn preserves the alpha channel from the black shadow //
// - Only the color channels are replaced, maintaining transparency // 2. Second pass: Apply actual shadow color using SrcIn blend mode
// - This prevents overlapping shadows from accumulating opacity // - SrcIn preserves the alpha channel from the black shadow
// // - Only the color channels are replaced, maintaining transparency
// This approach is essential for complex shapes with transparency where // - This prevents overlapping shadows from accumulating opacity
// multiple shadow areas might overlap, ensuring visual consistency. //
for shadow in element.drop_shadows().rev().filter(|s| !s.hidden()) { // This approach is essential for complex shapes with transparency where
let paint = skia::Paint::default(); // multiple shadow areas might overlap, ensuring visual consistency.
let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint); for shadow in element.drop_shadows_visible() {
self.surfaces let paint = skia::Paint::default();
.canvas(SurfaceId::DropShadows) let layer_rec = skia::canvas::SaveLayerRec::default().paint(&paint);
.save_layer(&layer_rec);
// First pass: Render shadow in black to establish alpha mask self.surfaces
self.render_drop_black_shadow( .canvas(SurfaceId::DropShadows)
tree, .save_layer(&layer_rec);
modifiers,
structure,
element,
shadow,
scale_content.get(&element.id),
clip_bounds,
scale,
translation,
);
// Nested shapes shadowing - apply black shadow to child shapes too // First pass: Render shadow in black to establish alpha mask
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,
);
self.render_drop_black_shadow( self.render_drop_black_shadow(
tree, tree,
modifiers, modifiers,
structure, structure,
shadow_shape, element,
shadow, shadow,
scale_content.get(&element.id), scale_content.get(&element.id),
clip_bounds, clip_bounds,
scale, scale,
translation, 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 self.surfaces
@@ -1292,8 +1421,10 @@ impl RenderState {
SurfaceId::Fills, SurfaceId::Fills,
SurfaceId::Strokes, SurfaceId::Strokes,
SurfaceId::InnerShadows, SurfaceId::InnerShadows,
SurfaceId::TextDropShadows,
true, true,
None, None,
None,
); );
self.surfaces self.surfaces

View File

@@ -1,9 +1,10 @@
use super::{RenderState, SurfaceId}; use super::{RenderState, SurfaceId};
use crate::render::strokes; use crate::render::strokes;
use crate::render::text::{self};
use crate::shapes::{Shadow, Shape, Stroke, Type}; use crate::shapes::{Shadow, Shape, Stroke, Type};
use skia_safe::textlayout::ParagraphBuilder; use skia_safe::{canvas::SaveLayerRec, Paint, Path};
use skia_safe::{Paint, Path};
use crate::render::text;
use crate::textlayout::ParagraphBuilderGroup;
// Fill Shadows // Fill Shadows
pub fn render_fill_inner_shadows( pub fn render_fill_inner_shadows(
@@ -46,7 +47,6 @@ pub fn render_stroke_inner_shadows(
stroke, stroke,
Some(surface_id), Some(surface_id),
filter.as_ref(), filter.as_ref(),
None,
antialias, 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) // Render text paths (unused)
#[allow(dead_code)] #[allow(dead_code)]
pub fn render_text_path_stroke_inner_shadows( 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::math::{Matrix, Point, Rect};
use crate::shapes::{Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, Type}; 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 super::{RenderState, SurfaceId};
use crate::render::filters::compose_filters; use crate::render::filters::compose_filters;
use crate::render::text::{self};
use crate::render::{get_dest_rect, get_source_rect}; use crate::render::{get_dest_rect, get_source_rect};
// FIXME: See if we can simplify these arguments // FIXME: See if we can simplify these arguments
@@ -519,7 +518,6 @@ pub fn render(
stroke: &Stroke, stroke: &Stroke,
surface_id: Option<SurfaceId>, surface_id: Option<SurfaceId>,
shadow: Option<&ImageFilter>, shadow: Option<&ImageFilter>,
paragraphs: Option<&mut Vec<Vec<ParagraphBuilder>>>,
antialias: bool, antialias: bool,
) { ) {
let scale = render_state.get_scale(); let scale = render_state.get_scale();
@@ -564,14 +562,7 @@ pub fn render(
shape.image_filter(1.).as_ref(), shape.image_filter(1.).as_ref(),
antialias, antialias,
), ),
Type::Text(_) => { Type::Text(_) => {}
text::render(
render_state,
shape,
paragraphs.expect("Text shapes should have paragraphs"),
surface_id,
);
}
shape_type @ (Type::Path(_) | Type::Bool(_)) => { shape_type @ (Type::Path(_) | Type::Bool(_)) => {
if let Some(path) = shape_type.path() { if let Some(path) = shape_type.path() {
draw_stroke_on_path( draw_stroke_on_path(

View File

@@ -17,15 +17,16 @@ const TILE_SIZE_MULTIPLIER: i32 = 2;
#[repr(u32)] #[repr(u32)]
#[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, PartialEq, Clone, Copy)]
pub enum SurfaceId { pub enum SurfaceId {
Target = 0b0_0000_0001, Target = 0b00_0000_0001,
Cache = 0b0_0000_0010, Cache = 0b00_0000_0010,
Current = 0b0_0000_0100, Current = 0b00_0000_0100,
Fills = 0b0_0000_1000, Fills = 0b00_0000_1000,
Strokes = 0b0_0001_0000, Strokes = 0b00_0001_0000,
DropShadows = 0b0_0010_0000, DropShadows = 0b00_0010_0000,
InnerShadows = 0b0_0100_0000, InnerShadows = 0b00_0100_0000,
UI = 0b0_1000_0000, TextDropShadows = 0b00_1000_0000,
Debug = 0b1_0000_0000, UI = 0b01_0000_0000,
Debug = 0b10_0000_0001,
} }
pub struct Surfaces { pub struct Surfaces {
@@ -42,6 +43,8 @@ pub struct Surfaces {
drop_shadows: skia::Surface, drop_shadows: skia::Surface,
// used for rendering over shadows. // used for rendering over shadows.
inner_shadows: skia::Surface, inner_shadows: skia::Surface,
// used for rendering text drop shadows
text_drop_shadows: skia::Surface,
// used for displaying auxiliary workspace elements // used for displaying auxiliary workspace elements
ui: skia::Surface, ui: skia::Surface,
// for drawing debug info. // for drawing debug info.
@@ -73,6 +76,8 @@ impl Surfaces {
gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims); gpu_state.create_surface_with_isize("drop_shadows".to_string(), extra_tile_dims);
let inner_shadows = let inner_shadows =
gpu_state.create_surface_with_isize("inner_shadows".to_string(), extra_tile_dims); 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 = let shape_fills =
gpu_state.create_surface_with_isize("shape_fills".to_string(), extra_tile_dims); gpu_state.create_surface_with_isize("shape_fills".to_string(), extra_tile_dims);
let shape_strokes = let shape_strokes =
@@ -88,6 +93,7 @@ impl Surfaces {
current, current,
drop_shadows, drop_shadows,
inner_shadows, inner_shadows,
text_drop_shadows,
shape_fills, shape_fills,
shape_strokes, shape_strokes,
ui, ui,
@@ -166,6 +172,9 @@ impl Surfaces {
if ids & SurfaceId::InnerShadows as u32 != 0 { if ids & SurfaceId::InnerShadows as u32 != 0 {
f(self.get_mut(SurfaceId::InnerShadows)); 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 { if ids & SurfaceId::DropShadows as u32 != 0 {
f(self.get_mut(SurfaceId::DropShadows)); 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) { pub fn update_render_context(&mut self, render_area: skia::Rect, scale: f32) {
let translation = self.get_render_context_translation(render_area, scale); let translation = self.get_render_context_translation(render_area, scale);
self.apply_mut( 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| {
s.canvas().restore(); let canvas = s.canvas();
s.canvas().save(); canvas.reset_matrix();
s.canvas().translate(translation); canvas.scale((scale, scale));
canvas.translate(translation);
}, },
); );
} }
@@ -206,6 +219,7 @@ impl Surfaces {
SurfaceId::Current => &mut self.current, SurfaceId::Current => &mut self.current,
SurfaceId::DropShadows => &mut self.drop_shadows, SurfaceId::DropShadows => &mut self.drop_shadows,
SurfaceId::InnerShadows => &mut self.inner_shadows, SurfaceId::InnerShadows => &mut self.inner_shadows,
SurfaceId::TextDropShadows => &mut self.text_drop_shadows,
SurfaceId::Fills => &mut self.shape_fills, SurfaceId::Fills => &mut self.shape_fills,
SurfaceId::Strokes => &mut self.shape_strokes, SurfaceId::Strokes => &mut self.shape_strokes,
SurfaceId::Debug => &mut self.debug, SurfaceId::Debug => &mut self.debug,
@@ -257,13 +271,15 @@ impl Surfaces {
pub fn reset(&mut self, color: skia::Color) { pub fn reset(&mut self, color: skia::Color) {
self.canvas(SurfaceId::Fills).restore_to_count(1); self.canvas(SurfaceId::Fills).restore_to_count(1);
self.canvas(SurfaceId::InnerShadows).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::Strokes).restore_to_count(1);
self.canvas(SurfaceId::Current).restore_to_count(1); self.canvas(SurfaceId::Current).restore_to_count(1);
self.apply_mut( self.apply_mut(
SurfaceId::Fills as u32 SurfaceId::Fills as u32
| SurfaceId::Strokes as u32 | SurfaceId::Strokes as u32
| SurfaceId::Current as u32 | SurfaceId::Current as u32
| SurfaceId::InnerShadows as u32, | SurfaceId::InnerShadows as u32
| SurfaceId::TextDropShadows as u32,
|s| { |s| {
s.canvas().clear(color).reset_matrix(); s.canvas().clear(color).reset_matrix();
}, },

View File

@@ -1,22 +1,55 @@
use super::{RenderState, Shape, SurfaceId}; use super::{RenderState, Shape, SurfaceId};
use crate::shapes::VerticalAlign; use crate::shapes::VerticalAlign;
use skia_safe::{ use skia_safe::{
canvas::SaveLayerRec, textlayout::LineMetrics, textlayout::Paragraph, canvas::SaveLayerRec,
textlayout::ParagraphBuilder, textlayout::RectHeightStyle, textlayout::RectWidthStyle, textlayout::{
textlayout::StyleMetrics, textlayout::TextDecoration, textlayout::TextStyle, Canvas, Paint, LineMetrics, Paragraph, ParagraphBuilder, RectHeightStyle, RectWidthStyle, StyleMetrics,
Path, TextDecoration, TextStyle,
},
Canvas, ImageFilter, Paint, Path,
}; };
pub fn render( pub fn render(
render_state: &mut RenderState, render_state: Option<&mut RenderState>,
canvas: Option<&Canvas>,
shape: &Shape, shape: &Shape,
paragraphs: &mut [Vec<ParagraphBuilder>], paragraphs: &mut [Vec<ParagraphBuilder>],
surface_id: Option<SurfaceId>, surface_id: Option<SurfaceId>,
shadow: Option<&Paint>,
blur: Option<&ImageFilter>,
) { ) {
let canvas = render_state let render_canvas = if let Some(rs) = render_state {
.surfaces rs.surfaces.canvas(surface_id.unwrap_or(SurfaceId::Fills))
.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 // Width
let paragraph_width = if let crate::shapes::Type::Text(text_content) = &shape.shape_type { let paragraph_width = if let crate::shapes::Type::Text(text_content) = &shape.shape_type {
text_content.width() text_content.width()
@@ -63,8 +96,6 @@ pub fn render(
global_offset_y = group_offset_y; global_offset_y = group_offset_y;
} }
} }
canvas.restore();
} }
fn draw_text_decorations( fn draw_text_decorations(
@@ -286,11 +317,11 @@ pub fn render_as_path(
// let text_content = text_content.new_bounds(shape.selrect()); // let text_content = text_content.new_bounds(shape.selrect());
// let paths = text_content.get_paths(antialias); // 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); // text::render(self, &paths, None, None);
// for stroke in shape.visible_strokes().rev() { // 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, // self, &shape, &paths, stroke, antialias,
// ); // );
// strokes::render_text_paths(self, &shape, stroke, &paths, None, None, 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) .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) { pub fn add_stroke(&mut self, s: Stroke) {
self.invalidate_extrect(); self.invalidate_extrect();
self.strokes.push(s) self.strokes.push(s)
@@ -984,6 +990,7 @@ impl Shape {
} }
} }
#[allow(dead_code)]
pub fn mask_filter(&self, scale: f32) -> Option<skia::MaskFilter> { pub fn mask_filter(&self, scale: f32) -> Option<skia::MaskFilter> {
if !self.blur.hidden { if !self.blur.hidden {
match self.blur.blur_type { match self.blur.blur_type {
@@ -1022,6 +1029,12 @@ impl Shape {
.filter(|shadow| shadow.style() == ShadowStyle::Drop) .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> { pub fn inner_shadows(&self) -> impl DoubleEndedIterator<Item = &Shadow> {
self.shadows self.shadows
.iter() .iter()
@@ -1162,11 +1175,6 @@ impl Shape {
!self.fills.is_empty() !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 { pub fn count_visible_inner_strokes(&self) -> usize {
self.visible_strokes() self.visible_strokes()
.filter(|s| s.kind == StrokeKind::Inner) .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> { pub fn inner_shadow_paints(&self) -> Vec<skia_safe::Paint> {
let inner_shadows: Vec<&crate::shapes::shadows::Shadow> = let inner_shadows: Vec<&crate::shapes::shadows::Shadow> =
self.inner_shadows().filter(|s| !s.hidden()).collect(); 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 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); let shader = get_fill_shader(fill, bounding_box);
if let Some(shader) = shader { if let Some(shader) = shader {
paint.set_shader(shader); paint.set_shader(shader);

View File

@@ -200,7 +200,7 @@ fn propagate_transform(
match content.grow_type() { match content.grow_type() {
GrowType::AutoHeight => { GrowType::AutoHeight => {
let paragraph_width = shape_bounds_after.width(); 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 height = auto_height(&mut paragraphs, paragraph_width);
let resize_transform = math::resize_matrix( let resize_transform = math::resize_matrix(
&shape_bounds_after, &shape_bounds_after,
@@ -213,7 +213,7 @@ fn propagate_transform(
} }
GrowType::AutoWidth => { GrowType::AutoWidth => {
let paragraph_width = content.width(); 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 height = auto_height(&mut paragraphs, paragraph_width);
let resize_transform = math::resize_matrix( let resize_transform = math::resize_matrix(
&shape_bounds_after, &shape_bounds_after,

View File

@@ -264,4 +264,11 @@ impl Stroke {
paint 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, 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 std::collections::HashSet;
use super::FontFamily; use super::FontFamily;
@@ -86,7 +90,7 @@ impl TextContent {
pub fn width(&self) -> f32 { pub fn width(&self) -> f32 {
if self.grow_type() == GrowType::AutoWidth { 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; let mut temp_paragraphs = temp_paragraphs;
auto_width(&mut temp_paragraphs, f32::MAX).ceil() auto_width(&mut temp_paragraphs, f32::MAX).ceil()
} else { } else {
@@ -104,7 +108,7 @@ impl TextContent {
pub fn visual_bounds(&self) -> (f32, f32) { pub fn visual_bounds(&self) -> (f32, f32) {
let paragraph_width = self.width(); 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); let paragraph_height = auto_height(&mut paragraphs, paragraph_width);
(paragraph_width, paragraph_height) (paragraph_width, paragraph_height)
} }
@@ -308,26 +312,20 @@ impl TextLeaf {
&self, &self,
content_bounds: &Rect, content_bounds: &Rect,
fallback_fonts: &HashSet<String>, fallback_fonts: &HashSet<String>,
_blur: Option<&ImageFilter>, remove_alpha: bool,
blur_mask: Option<&MaskFilter>,
shadow: Option<&Paint>,
) -> skia::textlayout::TextStyle { ) -> skia::textlayout::TextStyle {
let mut style = skia::textlayout::TextStyle::default(); let mut style = skia::textlayout::TextStyle::default();
if shadow.is_some() { let mut paint = paint::Paint::default();
let paint = shadow.unwrap().clone();
style.set_foreground_paint(&paint); if remove_alpha {
paint.set_color(skia::Color::BLACK);
paint.set_alpha(255);
} else { } else {
let paint = merge_fills(&self.fills, *content_bounds); 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);
} }
style.set_foreground_paint(&paint);
style.set_font_size(self.font_size); style.set_font_size(self.font_size);
style.set_letter_spacing(self.letter_spacing); style.set_letter_spacing(self.letter_spacing);
style.set_half_leading(false); style.set_half_leading(false);
@@ -359,12 +357,20 @@ impl TextLeaf {
&self, &self,
stroke_paint: &Paint, stroke_paint: &Paint,
fallback_fonts: &HashSet<String>, fallback_fonts: &HashSet<String>,
blur: Option<&ImageFilter>, remove_alpha: bool,
blur_mask: Option<&MaskFilter>,
shadow: Option<&Paint>,
) -> skia::textlayout::TextStyle { ) -> 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);
style.set_foreground_paint(stroke_paint); 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_font_size(self.font_size);
style.set_letter_spacing(self.letter_spacing); style.set_letter_spacing(self.letter_spacing);
style.set_decoration_type(match self.text_decoration { style.set_decoration_type(match self.text_decoration {
@@ -406,9 +412,6 @@ impl TextLeaf {
} }
pub fn is_transparent(&self) -> bool { pub fn is_transparent(&self) -> bool {
if self.fills.is_empty() {
return true;
}
self.fills.iter().all(|fill| match fill { self.fills.iter().all(|fill| match fill {
shapes::Fill::Solid(shapes::SolidColor(color)) => color.a() == 0, shapes::Fill::Solid(shapes::SolidColor(color)) => color.a() == 0,
_ => false, _ => false,

View File

@@ -20,7 +20,7 @@ impl TextPaths {
let mut paths = Vec::new(); let mut paths = Vec::new();
let mut offset_y = self.bounds.y(); 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 paragraphs in paragraphs.iter_mut() {
for paragraph_builder 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::{ use crate::{
render::filters::compose_filters,
shapes::{merge_fills, set_paint_fill, Stroke, StrokeKind, TextContent}, shapes::{merge_fills, set_paint_fill, Stroke, StrokeKind, TextContent},
utils::{get_fallback_fonts, get_font_collection}, utils::{get_fallback_fonts, get_font_collection},
}; };
@@ -51,13 +50,11 @@ pub fn build_paragraphs_with_width(
.collect() .collect()
} }
type ParagraphBuilderGroup = Vec<ParagraphBuilder>; pub type ParagraphBuilderGroup = Vec<ParagraphBuilder>;
pub fn paragraph_builder_group_from_text( pub fn paragraph_builder_group_from_text(
text_content: &TextContent, text_content: &TextContent,
blur: Option<&ImageFilter>, use_shadow: Option<bool>,
blur_mask: Option<&MaskFilter>,
shadow: Option<&Paint>,
) -> Vec<ParagraphBuilderGroup> { ) -> Vec<ParagraphBuilderGroup> {
let fonts = get_font_collection(); let fonts = get_font_collection();
let fallback_fonts = get_fallback_fonts(); 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 paragraph_style = paragraph.paragraph_to_style();
let mut builder = ParagraphBuilder::new(&paragraph_style, fonts); let mut builder = ParagraphBuilder::new(&paragraph_style, fonts);
for leaf in paragraph.children() { for leaf in paragraph.children() {
let text_style = leaf.to_style( let remove_alpha = use_shadow.unwrap_or(false) && !leaf.is_transparent();
&text_content.bounds(), let text_style = leaf.to_style(&text_content.bounds(), fallback_fonts, remove_alpha);
fallback_fonts,
blur,
blur_mask,
shadow,
);
let text = leaf.apply_text_transform(); let text = leaf.apply_text_transform();
builder.push_style(&text_style); builder.push_style(&text_style);
builder.add_text(&text); builder.add_text(&text);
@@ -88,43 +80,27 @@ pub fn stroke_paragraph_builder_group_from_text(
text_content: &TextContent, text_content: &TextContent,
stroke: &Stroke, stroke: &Stroke,
bounds: &Rect, bounds: &Rect,
blur: Option<&ImageFilter>,
blur_mask: Option<&MaskFilter>,
shadow: Option<&Paint>,
count_inner_strokes: usize, count_inner_strokes: usize,
use_shadow: Option<bool>,
) -> Vec<ParagraphBuilderGroup> { ) -> Vec<ParagraphBuilderGroup> {
let fallback_fonts = get_fallback_fonts(); let fallback_fonts = get_fallback_fonts();
let fonts = get_font_collection(); let fonts = get_font_collection();
let mut paragraph_group = Vec::new(); let mut paragraph_group = Vec::new();
let remove_stroke_alpha = use_shadow.unwrap_or(false) && !stroke.is_transparent();
for paragraph in text_content.paragraphs() { for paragraph in text_content.paragraphs() {
let mut stroke_paragraphs_map: std::collections::HashMap<usize, ParagraphBuilder> = let mut stroke_paragraphs_map: std::collections::HashMap<usize, ParagraphBuilder> =
std::collections::HashMap::new(); std::collections::HashMap::new();
for leaf in paragraph.children().iter() { for leaf in paragraph.children().iter() {
let mut text_paint = merge_fills(leaf.fills(), *bounds); let text_paint: skia_safe::Handle<_> = merge_fills(leaf.fills(), *bounds);
if let Some(blur_mask) = blur_mask { let stroke_paints = get_text_stroke_paints(
text_paint.set_mask_filter(blur_mask.clone()); stroke,
} bounds,
&text_paint,
let stroke_paints = if shadow.is_some() { count_inner_strokes,
get_text_stroke_paints_with_shadows( remove_stroke_alpha,
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: String = leaf.apply_text_transform(); let text: String = leaf.apply_text_transform();
@@ -134,8 +110,9 @@ pub fn stroke_paragraph_builder_group_from_text(
ParagraphBuilder::new(&paragraph_style, fonts) ParagraphBuilder::new(&paragraph_style, fonts)
}); });
let stroke_paint = stroke_paint.clone(); let stroke_paint = stroke_paint.clone();
let remove_alpha = use_shadow.unwrap_or(false) && !leaf.is_transparent();
let stroke_style = 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.push_style(&stroke_style);
builder.add_text(&text); builder.add_text(&text);
} }
@@ -158,101 +135,12 @@ fn get_built_paragraphs(
build_paragraphs_with_width(paragraphs, width) 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( fn get_text_stroke_paints(
stroke: &Stroke, stroke: &Stroke,
bounds: &Rect, bounds: &Rect,
text_paint: &Paint, text_paint: &Paint,
blur: Option<&ImageFilter>,
blur_mask: Option<&MaskFilter>,
count_inner_strokes: usize, count_inner_strokes: usize,
remove_stroke_alpha: bool,
) -> Vec<Paint> { ) -> Vec<Paint> {
let mut paints = Vec::new(); let mut paints = Vec::new();
@@ -269,34 +157,37 @@ fn get_text_stroke_paints(
let mut paint = text_paint.clone(); let mut paint = text_paint.clone();
paint.set_style(skia::PaintStyle::Fill); paint.set_style(skia::PaintStyle::Fill);
paint.set_anti_alias(true); paint.set_anti_alias(true);
if let Some(blur) = blur {
paint.set_image_filter(blur.clone());
}
paints.push(paint); paints.push(paint);
let mut paint = skia::Paint::default(); let mut paint = skia::Paint::default();
paint.set_style(skia::PaintStyle::Stroke); paint.set_style(skia::PaintStyle::Stroke);
paint.set_blend_mode(skia::BlendMode::SrcIn); paint.set_blend_mode(skia::BlendMode::SrcIn);
paint.set_anti_alias(true); paint.set_anti_alias(true);
paint.set_stroke_width(stroke.width * 2.0); paint.set_stroke_width(stroke.width * 2.0);
set_paint_fill(&mut paint, &stroke.fill, bounds); set_paint_fill(&mut paint, &stroke.fill, bounds, remove_stroke_alpha);
if let Some(blur) = blur {
paint.set_image_filter(blur.clone());
}
paints.push(paint); paints.push(paint);
} else { } 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_style(skia::PaintStyle::Fill);
paint.set_anti_alias(false); paint.set_anti_alias(false);
set_paint_fill(&mut paint, &stroke.fill, bounds);
paints.push(paint); paints.push(paint);
let mut paint = skia::Paint::default(); let mut paint = skia::Paint::default();
let image_filter = let image_filter =
skia_safe::image_filters::erode((stroke.width, stroke.width), None, None); skia_safe::image_filters::erode((stroke.width, stroke.width), None, None);
let filter = compose_filters(blur, image_filter.as_ref()); paint.set_image_filter(image_filter);
paint.set_image_filter(filter);
paint.set_anti_alias(false); paint.set_anti_alias(false);
paint.set_color(skia::Color::BLACK);
paint.set_alpha(255);
paint.set_blend_mode(skia::BlendMode::DstOut); paint.set_blend_mode(skia::BlendMode::DstOut);
paints.push(paint); paints.push(paint);
} }
@@ -306,12 +197,7 @@ fn get_text_stroke_paints(
paint.set_style(skia::PaintStyle::Stroke); paint.set_style(skia::PaintStyle::Stroke);
paint.set_anti_alias(true); paint.set_anti_alias(true);
paint.set_stroke_width(stroke.width); paint.set_stroke_width(stroke.width);
set_paint_fill(&mut paint, &stroke.fill, bounds, remove_stroke_alpha);
set_paint_fill(&mut paint, &stroke.fill, bounds);
if let Some(blur) = blur {
paint.set_image_filter(blur.clone());
}
paints.push(paint); paints.push(paint);
} }
StrokeKind::Outer => { StrokeKind::Outer => {
@@ -320,10 +206,7 @@ fn get_text_stroke_paints(
paint.set_blend_mode(skia::BlendMode::DstOver); paint.set_blend_mode(skia::BlendMode::DstOver);
paint.set_anti_alias(true); paint.set_anti_alias(true);
paint.set_stroke_width(stroke.width * 2.0); paint.set_stroke_width(stroke.width * 2.0);
set_paint_fill(&mut paint, &stroke.fill, bounds); set_paint_fill(&mut paint, &stroke.fill, bounds, remove_stroke_alpha);
if let Some(blur_mask) = blur_mask {
paint.set_mask_filter(blur_mask.clone());
}
paints.push(paint); paints.push(paint);
let mut paint = skia::Paint::default(); 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 { if let Type::Text(content) = &shape.shape_type {
// 1. Reset Paragraphs // 1. Reset Paragraphs
let paragraph_width = content.width(); 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); let built_paragraphs = build_paragraphs_with_width(&mut paragraphs, paragraph_width);
// 2. Max Width Calculation // 2. Max Width Calculation
@@ -58,14 +58,12 @@ pub extern "C" fn get_text_dimensions() -> *mut u8 {
// 3. Width and Height Calculation // 3. Width and Height Calculation
match content.grow_type() { match content.grow_type() {
GrowType::AutoHeight => { GrowType::AutoHeight => {
let mut paragraph_height = let mut paragraph_height = paragraph_builder_group_from_text(content, None);
paragraph_builder_group_from_text(content, None, None, None);
height = auto_height(&mut paragraph_height, paragraph_width).ceil(); height = auto_height(&mut paragraph_height, paragraph_width).ceil();
} }
GrowType::AutoWidth => { GrowType::AutoWidth => {
width = paragraph_width; width = paragraph_width;
let mut paragraph_height = let mut paragraph_height = paragraph_builder_group_from_text(content, None);
paragraph_builder_group_from_text(content, None, None, None);
height = auto_height(&mut paragraph_height, paragraph_width).ceil(); height = auto_height(&mut paragraph_height, paragraph_width).ceil();
} }
GrowType::Fixed => {} GrowType::Fixed => {}