mirror of
https://github.com/penpot/penpot.git
synced 2026-03-14 06:17:25 +00:00
🐛 Fix text stroke opacity causing different colors on overlapping glyphs
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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)) => {
|
||||
|
||||
Reference in New Issue
Block a user