🐛 Fix text stroke opacity causing different colors on overlapping glyphs

This commit is contained in:
Alejandro Alonso
2026-03-10 09:13:26 +01:00
parent 052417cd10
commit 024f779cab
7 changed files with 885 additions and 23 deletions

View File

@@ -0,0 +1,737 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u0b5bcbca-32ab-81eb-8005-a15fc4484678",
"~: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": "holadios",
"~:revn": 54,
"~:modified-at": "~m1773136426990",
"~:vern": 0,
"~:id": "~ueffcbebc-b8c8-802f-8007-b0ebecd7ebf4",
"~: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",
"0008-fix-library-colors-v4",
"0009-clean-library-colors",
"0009-add-partial-text-touched-flags",
"0010-fix-swap-slots-pointing-non-existent-shapes",
"0011-fix-invalid-text-touched-flags",
"0012-fix-position-data",
"0013-fix-component-path",
"0013-clear-invalid-strokes-and-fills",
"0014-fix-tokens-lib-duplicate-ids",
"0014-clear-components-nil-objects",
"0015-fix-text-attrs-blank-strings",
"0016-copy-fills-from-position-data-to-text-node",
"0015-clean-shadow-color"
]
},
"~:version": 67,
"~:project-id": "~u0b5bcbca-32ab-81eb-8005-a15fc448f334",
"~:created-at": "~m1773127290716",
"~:backend": "legacy-db",
"~:data": {
"~:pages": [
"~u3e9e17c3-fc57-80ce-8007-101743996fe9"
],
"~:pages-index": {
"~u3e9e17c3-fc57-80ce-8007-101743996fe9": {
"~:id": "~u3e9e17c3-fc57-80ce-8007-101743996fe9",
"~:name": "Page 1",
"~: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
}
},
"~:page-id": "~u3e9e17c3-fc57-80ce-8007-101743996fe9",
"~: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": [
"~u7d004cdb-8305-806a-8007-b0f01ee65230",
"~u2ae0abdc-99ff-8009-8007-b0f7f123b5ea",
"~u2ae0abdc-99ff-8009-8007-b0f7f45177dc"
]
}
},
"~u7d004cdb-8305-806a-8007-b0f01ee65230": {
"~#shape": {
"~:y": -161.000001410182,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:grow-type": "~:auto-width",
"~:content": {
"~:type": "root",
"~:key": "6hv3a5x8wb",
"~: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": "219sqyfv11",
"~:font-size": "250",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#000000",
"~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro",
"~:text": "HELLO WORLD"
}
],
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:text-align": "left",
"~:font-id": "sourcesanspro",
"~:key": "21rct71nkal",
"~:font-size": "250",
"~: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": "#000000",
"~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro"
}
]
}
],
"~:vertical-align": "top"
},
"~:hide-in-viewer": false,
"~:name": "HELLO WORLD",
"~:width": 1529.00000393592,
"~:type": "~:text",
"~:points": [
{
"~#point": {
"~:x": 109.999998223851,
"~:y": -161.000001410182
}
},
{
"~#point": {
"~:x": 1639.00000215977,
"~:y": -161.000001410182
}
},
{
"~#point": {
"~:x": 1639.00000215977,
"~:y": 138.999988970841
}
},
{
"~#point": {
"~:x": 109.999998223851,
"~:y": 138.999988970841
}
}
],
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:id": "~u7d004cdb-8305-806a-8007-b0f01ee65230",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:position-data": [
{
"~:y": 150.869995117188,
"~:line-height": "1.2",
"~:font-style": "normal",
"~:text-transform": "none",
"~:text-align": "left",
"~:font-id": "sourcesanspro",
"~:font-size": "250",
"~:font-weight": "400",
"~:text-direction": "ltr",
"~:width": 1528.56005859375,
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:x": 110,
"~:fills": [
{
"~:fill-color": "#000000",
"~:fill-opacity": 1
}
],
"~:direction": "ltr",
"~:font-family": "sourcesanspro",
"~:height": 323.739990234375,
"~:text": "HELLO WORLD"
}
],
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [
{
"~:stroke-style": "~:solid",
"~:stroke-alignment": "~:center",
"~:stroke-width": 50,
"~:stroke-color": "#43e50b",
"~:stroke-opacity": 0.6
}
],
"~:x": 109.999998223851,
"~:selrect": {
"~#rect": {
"~:x": 109.999998223851,
"~:y": -161.000001410182,
"~:width": 1529.00000393592,
"~:height": 299.999990381022,
"~:x1": 109.999998223851,
"~:y1": -161.000001410182,
"~:x2": 1639.00000215977,
"~:y2": 138.999988970841
}
},
"~:flip-x": null,
"~:height": 299.999990381022,
"~:flip-y": null
}
},
"~u2ae0abdc-99ff-8009-8007-b0f7f123b5ea": {
"~#shape": {
"~:y": -462.000004970439,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:grow-type": "~:auto-width",
"~:content": {
"~:type": "root",
"~:key": "6hv3a5x8wb",
"~:children": [
{
"~:type": "paragraph-set",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:text-transform": "none",
"~:font-id": "sourcesanspro",
"~:key": "219sqyfv11",
"~:font-size": "250",
"~:font-weight": "400",
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#000000",
"~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro",
"~:text": "HELLO WORLD"
}
],
"~:text-transform": "none",
"~:text-align": "left",
"~:font-id": "sourcesanspro",
"~:key": "21rct71nkal",
"~:font-size": "250",
"~:font-weight": "400",
"~:text-direction": "ltr",
"~:type": "paragraph",
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#000000",
"~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro"
}
]
}
],
"~:vertical-align": "top"
},
"~:hide-in-viewer": false,
"~:name": "HELLO WORLD",
"~:width": 1529.00000393592,
"~:type": "~:text",
"~:points": [
{
"~#point": {
"~:x": 92.9999982852505,
"~:y": -462.000004970439
}
},
{
"~#point": {
"~:x": 1622.00000222117,
"~:y": -462.000004970439
}
},
{
"~#point": {
"~:x": 1622.00000222117,
"~:y": -162.000040815452
}
},
{
"~#point": {
"~:x": 92.9999982852505,
"~:y": -162.000040815452
}
}
],
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:id": "~u2ae0abdc-99ff-8009-8007-b0f7f123b5ea",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:position-data": [
{
"~:y": -150.130004882813,
"~:line-height": "1.2",
"~:font-style": "normal",
"~:text-transform": "none",
"~:text-align": "left",
"~:font-id": "sourcesanspro",
"~:font-size": "250",
"~:font-weight": "400",
"~:text-direction": "ltr",
"~:width": 1528.56005859375,
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:x": 93,
"~:fills": [
{
"~:fill-color": "#000000",
"~:fill-opacity": 1
}
],
"~:direction": "ltr",
"~:font-family": "sourcesanspro",
"~:height": 323.739990234375,
"~:text": "HELLO WORLD"
}
],
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [
{
"~:stroke-style": "~:solid",
"~:stroke-alignment": "~:inner",
"~:stroke-width": 50,
"~:stroke-color": "#43e50b",
"~:stroke-opacity": 0.6
}
],
"~:x": 92.9999982852505,
"~:selrect": {
"~#rect": {
"~:x": 92.9999982852505,
"~:y": -462.000004970439,
"~:width": 1529.00000393592,
"~:height": 299.999964154987,
"~:x1": 92.9999982852505,
"~:y1": -462.000004970439,
"~:x2": 1622.00000222117,
"~:y2": -162.000040815452
}
},
"~:fills": [],
"~:flip-x": null,
"~:height": 299.999964154987,
"~:flip-y": null
}
},
"~u2ae0abdc-99ff-8009-8007-b0f7f45177dc": {
"~#shape": {
"~:y": 169.999996321908,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:grow-type": "~:auto-width",
"~:content": {
"~:type": "root",
"~:key": "6hv3a5x8wb",
"~:children": [
{
"~:type": "paragraph-set",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:text-transform": "none",
"~:font-id": "sourcesanspro",
"~:key": "219sqyfv11",
"~:font-size": "250",
"~:font-weight": "400",
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#000000",
"~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro",
"~:text": "HELLO WORLD"
}
],
"~:text-transform": "none",
"~:text-align": "left",
"~:font-id": "sourcesanspro",
"~:key": "21rct71nkal",
"~:font-size": "250",
"~:font-weight": "400",
"~:text-direction": "ltr",
"~:type": "paragraph",
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#000000",
"~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro"
}
]
}
],
"~:vertical-align": "top"
},
"~:hide-in-viewer": false,
"~:name": "HELLO WORLD",
"~:width": 1529.00000393592,
"~:type": "~:text",
"~:points": [
{
"~#point": {
"~:x": 92.9999984013972,
"~:y": 169.999996321908
}
},
{
"~#point": {
"~:x": 1622.00000233732,
"~:y": 169.999996321908
}
},
{
"~#point": {
"~:x": 1622.00000233732,
"~:y": 470.000003392238
}
},
{
"~#point": {
"~:x": 92.9999984013972,
"~:y": 470.000003392238
}
}
],
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:id": "~u2ae0abdc-99ff-8009-8007-b0f7f45177dc",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:position-data": [
{
"~:y": 481.869995117188,
"~:line-height": "1.2",
"~:font-style": "normal",
"~:text-transform": "none",
"~:text-align": "left",
"~:font-id": "sourcesanspro",
"~:font-size": "250",
"~:font-weight": "400",
"~:text-direction": "ltr",
"~:width": 1528.56005859375,
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:x": 93,
"~:fills": [
{
"~:fill-color": "#000000",
"~:fill-opacity": 1
}
],
"~:direction": "ltr",
"~:font-family": "sourcesanspro",
"~:height": 323.739990234375,
"~:text": "HELLO WORLD"
}
],
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [
{
"~:stroke-style": "~:solid",
"~:stroke-alignment": "~:outer",
"~:stroke-width": 50,
"~:stroke-color": "#43e50b",
"~:stroke-opacity": 0.6
}
],
"~:x": 92.9999984013973,
"~:selrect": {
"~#rect": {
"~:x": 92.9999984013973,
"~:y": 169.999996321908,
"~:width": 1529.00000393592,
"~:height": 300.00000707033,
"~:x1": 92.9999984013973,
"~:y1": 169.999996321908,
"~:x2": 1622.00000233732,
"~:y2": 470.000003392238
}
},
"~:fills": [],
"~:flip-x": null,
"~:height": 300.00000707033,
"~:flip-y": null
}
}
}
}
},
"~:id": "~ueffcbebc-b8c8-802f-8007-b0ebecd7ebf4",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -587,3 +587,23 @@ test.skip("Updates text alignment edition - part 3", async ({ page }) => {
await expect(workspace.canvas).toHaveScreenshot({ timeout: 10000 });
});
test("Renders a file with group with strokes and not 100% opacities", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-strokes-and-not-100-percent-opacities.json");
await workspace.goToWorkspace({
id: "effcbebc-b8c8-802f-8007-b0ebecd7ebf4",
pageId: "3e9e17c3-fc57-80ce-8007-101743996fe9",
});
await workspace.waitForFirstRenderWithoutUI();
await expect(workspace.canvas).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0.01,
});
});

View File

@@ -868,7 +868,7 @@ impl RenderState {
let text_stroke_blur_outset =
Stroke::max_bounds_width(shape.visible_strokes(), false);
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
let mut stroke_paragraphs_list = shape
let (mut stroke_paragraphs_list, stroke_opacities): (Vec<_>, Vec<_>) = shape
.visible_strokes()
.rev()
.map(|stroke| {
@@ -880,7 +880,7 @@ impl RenderState {
None,
)
})
.collect::<Vec<_>>();
.unzip();
if fast_mode {
// Fast path: render fills and strokes only (skip shadows/blur).
text::render(
@@ -892,9 +892,13 @@ impl RenderState {
None,
None,
text_fill_inset,
None,
);
for stroke_paragraphs in stroke_paragraphs_list.iter_mut() {
for (stroke_paragraphs, layer_opacity) in stroke_paragraphs_list
.iter_mut()
.zip(stroke_opacities.iter())
{
text::render_with_bounds_outset(
Some(self),
None,
@@ -905,6 +909,7 @@ impl RenderState {
None,
text_stroke_blur_outset,
None,
*layer_opacity,
);
}
} else {
@@ -918,7 +923,10 @@ impl RenderState {
let blur_filter = shape.image_filter(1.);
let mut paragraphs_with_shadows =
text_content.paragraph_builder_group_from_text(Some(true));
let mut stroke_paragraphs_with_shadows_list = shape
let (mut stroke_paragraphs_with_shadows_list, _shadow_opacities): (
Vec<_>,
Vec<_>,
) = shape
.visible_strokes()
.rev()
.map(|stroke| {
@@ -930,7 +938,7 @@ impl RenderState {
Some(true),
)
})
.collect::<Vec<_>>();
.unzip();
if let Some(parent_shadows) = parent_shadows {
if !shape.has_visible_strokes() {
@@ -944,6 +952,7 @@ impl RenderState {
Some(&shadow),
blur_filter.as_ref(),
None,
None,
);
}
} else {
@@ -970,6 +979,7 @@ impl RenderState {
Some(shadow),
blur_filter.as_ref(),
None,
None,
);
}
}
@@ -984,6 +994,7 @@ impl RenderState {
None,
blur_filter.as_ref(),
text_fill_inset,
None,
);
// 3. Stroke drop shadows
@@ -998,7 +1009,10 @@ impl RenderState {
);
// 4. Stroke fills
for stroke_paragraphs in stroke_paragraphs_list.iter_mut() {
for (stroke_paragraphs, layer_opacity) in stroke_paragraphs_list
.iter_mut()
.zip(stroke_opacities.iter())
{
text::render_with_bounds_outset(
Some(self),
None,
@@ -1009,6 +1023,7 @@ impl RenderState {
blur_filter.as_ref(),
text_stroke_blur_outset,
None,
*layer_opacity,
);
}
@@ -1035,6 +1050,7 @@ impl RenderState {
Some(shadow),
blur_filter.as_ref(),
None,
None,
);
}
}

View File

@@ -155,6 +155,7 @@ pub fn render_text_shadows(
None,
blur_filter.as_ref(),
None,
None,
);
for stroke_paragraphs in stroke_paragraphs_group.iter_mut() {
@@ -167,6 +168,7 @@ pub fn render_text_shadows(
None,
blur_filter.as_ref(),
None,
None,
);
}

View File

@@ -20,11 +20,12 @@ pub fn stroke_paragraph_builder_group_from_text(
bounds: &Rect,
count_inner_strokes: usize,
use_shadow: Option<bool>,
) -> Vec<ParagraphBuilderGroup> {
) -> (Vec<ParagraphBuilderGroup>, Option<f32>) {
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();
let mut group_layer_opacity: Option<f32> = None;
for paragraph in text_content.paragraphs() {
let mut stroke_paragraphs_map: std::collections::HashMap<usize, ParagraphBuilder> =
@@ -32,7 +33,7 @@ pub fn stroke_paragraph_builder_group_from_text(
for span in paragraph.children().iter() {
let text_paint: skia_safe::Handle<_> = merge_fills(span.fills(), *bounds);
let stroke_paints = get_text_stroke_paints(
let (stroke_paints, stroke_layer_opacity) = get_text_stroke_paints(
stroke,
bounds,
&text_paint,
@@ -40,6 +41,10 @@ pub fn stroke_paragraph_builder_group_from_text(
remove_stroke_alpha,
);
if group_layer_opacity.is_none() {
group_layer_opacity = stroke_layer_opacity;
}
let text: String = span.apply_text_transform();
for (paint_idx, stroke_paint) in stroke_paints.iter().enumerate() {
@@ -67,7 +72,7 @@ pub fn stroke_paragraph_builder_group_from_text(
paragraph_group.push(stroke_paragraphs);
}
paragraph_group
(paragraph_group, group_layer_opacity)
}
fn get_text_stroke_paints(
@@ -76,8 +81,25 @@ fn get_text_stroke_paints(
text_paint: &Paint,
count_inner_strokes: usize,
remove_stroke_alpha: bool,
) -> Vec<Paint> {
) -> (Vec<Paint>, Option<f32>) {
let mut paints = Vec::new();
let mut layer_opacity: Option<f32> = None;
let stroke_opacity = stroke.fill.opacity();
let needs_opacity_layer = stroke_opacity < 1.0 && !remove_stroke_alpha;
let fill_for_paint = |paint: &mut Paint| {
if needs_opacity_layer {
let opaque_fill = stroke.fill.with_full_opacity();
set_paint_fill(paint, &opaque_fill, bounds, remove_stroke_alpha);
} else {
set_paint_fill(paint, &stroke.fill, bounds, remove_stroke_alpha);
}
};
if needs_opacity_layer {
layer_opacity = Some(stroke_opacity);
}
match stroke.kind {
StrokeKind::Inner => {
@@ -99,7 +121,7 @@ fn get_text_stroke_paints(
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, remove_stroke_alpha);
fill_for_paint(&mut paint);
paints.push(paint);
} else {
let mut paint = skia::Paint::default();
@@ -108,7 +130,12 @@ fn get_text_stroke_paints(
paint.set_alpha(255);
} else {
paint = text_paint.clone();
set_paint_fill(&mut paint, &stroke.fill, bounds, false);
if needs_opacity_layer {
let opaque_fill = stroke.fill.with_full_opacity();
set_paint_fill(&mut paint, &opaque_fill, bounds, false);
} else {
set_paint_fill(&mut paint, &stroke.fill, bounds, false);
}
}
paint.set_style(skia::PaintStyle::Fill);
@@ -132,7 +159,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, remove_stroke_alpha);
fill_for_paint(&mut paint);
paints.push(paint);
}
StrokeKind::Outer => {
@@ -141,7 +168,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, remove_stroke_alpha);
fill_for_paint(&mut paint);
paints.push(paint);
let mut paint = skia::Paint::default();
@@ -153,7 +180,7 @@ fn get_text_stroke_paints(
}
}
paints
(paints, layer_opacity)
}
#[allow(clippy::too_many_arguments)]
@@ -167,6 +194,7 @@ pub fn render_with_bounds_outset(
blur: Option<&ImageFilter>,
stroke_bounds_outset: f32,
fill_inset: Option<f32>,
layer_opacity: Option<f32>,
) {
if let Some(render_state) = render_state {
let target_surface = surface_id.unwrap_or(SurfaceId::Fills);
@@ -195,6 +223,7 @@ pub fn render_with_bounds_outset(
shadow,
Some(&blur_filter_clone),
fill_inset,
layer_opacity,
);
},
) {
@@ -204,12 +233,28 @@ pub fn render_with_bounds_outset(
}
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
render_text_on_canvas(canvas, shape, paragraph_builders, shadow, blur, fill_inset);
render_text_on_canvas(
canvas,
shape,
paragraph_builders,
shadow,
blur,
fill_inset,
layer_opacity,
);
return;
}
if let Some(canvas) = canvas {
render_text_on_canvas(canvas, shape, paragraph_builders, shadow, blur, fill_inset);
render_text_on_canvas(
canvas,
shape,
paragraph_builders,
shadow,
blur,
fill_inset,
layer_opacity,
);
}
}
@@ -223,6 +268,7 @@ pub fn render(
shadow: Option<&Paint>,
blur: Option<&ImageFilter>,
fill_inset: Option<f32>,
layer_opacity: Option<f32>,
) {
render_with_bounds_outset(
render_state,
@@ -234,6 +280,7 @@ pub fn render(
blur,
0.0,
fill_inset,
layer_opacity,
);
}
@@ -244,6 +291,7 @@ fn render_text_on_canvas(
shadow: Option<&Paint>,
blur: Option<&ImageFilter>,
fill_inset: Option<f32>,
layer_opacity: Option<f32>,
) {
if let Some(blur_filter) = blur {
let mut blur_paint = Paint::default();
@@ -255,7 +303,7 @@ fn render_text_on_canvas(
if let Some(shadow_paint) = shadow {
let layer_rec = SaveLayerRec::default().paint(shadow_paint);
canvas.save_layer(&layer_rec);
draw_text(canvas, shape, paragraph_builders);
draw_text(canvas, shape, paragraph_builders, layer_opacity);
canvas.restore();
} else if let Some(eps) = fill_inset.filter(|&e| e > 0.0) {
if let Some(erode) = skia_safe::image_filters::erode((eps, eps), None, None) {
@@ -263,13 +311,13 @@ fn render_text_on_canvas(
layer_paint.set_image_filter(erode);
let layer_rec = SaveLayerRec::default().paint(&layer_paint);
canvas.save_layer(&layer_rec);
draw_text(canvas, shape, paragraph_builders);
draw_text(canvas, shape, paragraph_builders, layer_opacity);
canvas.restore();
} else {
draw_text(canvas, shape, paragraph_builders);
draw_text(canvas, shape, paragraph_builders, layer_opacity);
}
} else {
draw_text(canvas, shape, paragraph_builders);
draw_text(canvas, shape, paragraph_builders, layer_opacity);
}
if blur.is_some() {
@@ -283,13 +331,20 @@ fn draw_text(
canvas: &Canvas,
shape: &Shape,
paragraph_builder_groups: &mut [Vec<ParagraphBuilder>],
layer_opacity: Option<f32>,
) {
let text_content = shape.get_text_content();
let layout_info =
calculate_text_layout_data(shape, text_content, paragraph_builder_groups, true);
let layer_rec = SaveLayerRec::default();
canvas.save_layer(&layer_rec);
if let Some(opacity) = layer_opacity {
let mut opacity_paint = Paint::default();
opacity_paint.set_alpha_f(opacity);
let layer_rec = SaveLayerRec::default().paint(&opacity_paint);
canvas.save_layer(&layer_rec);
} else {
canvas.save_layer(&SaveLayerRec::default());
}
for para in &layout_info.paragraphs {
para.paragraph.paint(canvas, (para.x, para.y));

View File

@@ -140,6 +140,38 @@ pub enum Fill {
}
impl Fill {
pub fn opacity(&self) -> f32 {
match self {
Fill::Solid(SolidColor(color)) => color.a() as f32 / 255.0,
Fill::LinearGradient(g) => g.opacity as f32 / 255.0,
Fill::RadialGradient(g) => g.opacity as f32 / 255.0,
Fill::Image(i) => i.opacity as f32 / 255.0,
}
}
pub fn with_full_opacity(&self) -> Fill {
match self {
Fill::Solid(SolidColor(color)) => Fill::Solid(SolidColor(skia::Color::from_argb(
255,
color.r(),
color.g(),
color.b(),
))),
Fill::LinearGradient(g) => Fill::LinearGradient(Gradient {
opacity: 255,
..g.clone()
}),
Fill::RadialGradient(g) => Fill::RadialGradient(Gradient {
opacity: 255,
..g.clone()
}),
Fill::Image(i) => Fill::Image(ImageFill {
opacity: 255,
..i.clone()
}),
}
}
pub fn to_paint(&self, rect: &Rect, anti_alias: bool) -> skia::Paint {
match self {
Self::Solid(SolidColor(color)) => {