diff --git a/frontend/playwright/data/render-wasm/get-file-strokes-and-not-100-percent-opacities.json b/frontend/playwright/data/render-wasm/get-file-strokes-and-not-100-percent-opacities.json new file mode 100644 index 0000000000..d5e9e7a363 --- /dev/null +++ b/frontend/playwright/data/render-wasm/get-file-strokes-and-not-100-percent-opacities.json @@ -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" + } + } +} \ No newline at end of file diff --git a/frontend/playwright/ui/render-wasm-specs/texts.spec.js b/frontend/playwright/ui/render-wasm-specs/texts.spec.js index d837a64834..23a4701bbd 100644 --- a/frontend/playwright/ui/render-wasm-specs/texts.spec.js +++ b/frontend/playwright/ui/render-wasm-specs/texts.spec.js @@ -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, + }); +}); \ No newline at end of file diff --git a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-group-with-strokes-and-not-100-opacities-1.png b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-group-with-strokes-and-not-100-opacities-1.png new file mode 100644 index 0000000000..989559cc02 Binary files /dev/null and b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-group-with-strokes-and-not-100-opacities-1.png differ diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 12d4fcc2b7..ce5781ca63 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -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::>(); + .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::>(); + .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, ); } } diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs index b5077f0688..ea43322b70 100644 --- a/render-wasm/src/render/shadows.rs +++ b/render-wasm/src/render/shadows.rs @@ -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, ); } diff --git a/render-wasm/src/render/text.rs b/render-wasm/src/render/text.rs index 42c35b450f..832503505d 100644 --- a/render-wasm/src/render/text.rs +++ b/render-wasm/src/render/text.rs @@ -20,11 +20,12 @@ pub fn stroke_paragraph_builder_group_from_text( bounds: &Rect, count_inner_strokes: usize, use_shadow: Option, -) -> Vec { +) -> (Vec, Option) { 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 = None; for paragraph in text_content.paragraphs() { let mut stroke_paragraphs_map: std::collections::HashMap = @@ -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 { +) -> (Vec, Option) { let mut paints = Vec::new(); + let mut layer_opacity: Option = 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, + layer_opacity: Option, ) { 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, + layer_opacity: Option, ) { 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, + layer_opacity: Option, ) { 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], + layer_opacity: Option, ) { 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)); diff --git a/render-wasm/src/shapes/fills.rs b/render-wasm/src/shapes/fills.rs index cf8a930894..5b61b3ee2a 100644 --- a/render-wasm/src/shapes/fills.rs +++ b/render-wasm/src/shapes/fills.rs @@ -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)) => {