diff --git a/frontend/playwright/data/render-wasm/get-file-fill-blend-blurs.json b/frontend/playwright/data/render-wasm/get-file-fill-blend-blurs.json index a38f6b8de7..8359830e3c 100644 --- a/frontend/playwright/data/render-wasm/get-file-fill-blend-blurs.json +++ b/frontend/playwright/data/render-wasm/get-file-fill-blend-blurs.json @@ -26,8 +26,8 @@ "~:has-media-trimmed": false, "~:comment-thread-seqn": 0, "~:name": "test_color_blending", - "~:revn": 58, - "~:modified-at": "~m1770799634196", + "~:revn": 78, + "~:modified-at": "~m1770820738388", "~:vern": 0, "~:id": "~ub15901d7-d46d-8056-8007-8d5e34fc1f0c", "~:is-shared": false, @@ -119,13 +119,19 @@ "~ub15901d7-d46d-8056-8007-8d5e34fc1f0d": { "~:objects": { "~#penpot/objects-map/v2": { - "~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.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.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",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,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~u3b7d4c1f-3b79-80e5-8007-8d5e38c5a297\",\"~udb80df91-a3a3-803b-8007-8e379b5fd50f\",\"~udb80df91-a3a3-803b-8007-8e38034ff7c8\",\"~udb80df91-a3a3-803b-8007-8e37a71c9d28\",\"~udb80df91-a3a3-803b-8007-8e384d8c53b9\",\"~udb80df91-a3a3-803b-8007-8e37c09b4084\"]]]", + "~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.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.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",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,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~u3b7d4c1f-3b79-80e5-8007-8d5e38c5a297\",\"~udb80df91-a3a3-803b-8007-8e379b5fd50f\",\"~udb80df91-a3a3-803b-8007-8e38034ff7c8\",\"~udb80df91-a3a3-803b-8007-8e37a71c9d28\",\"~udb80df91-a3a3-803b-8007-8e384d8c53b9\",\"~udb80df91-a3a3-803b-8007-8e37c09b4084\",\"~u18522c44-655d-8050-8007-8e89f4bdc0c4\",\"~u097859f1-ca3b-80ba-8007-8e8beb99a3f5\",\"~u18522c44-655d-8050-8007-8e89f4bdc0c5\",\"~u097859f1-ca3b-80ba-8007-8e8bfca43303\",\"~ufb1f50bf-1bff-8030-8007-8e8c3bd8fcd7\",\"~u18522c44-655d-8050-8007-8e89f4bdc0c6\"]]]", + "~u097859f1-ca3b-80ba-8007-8e8bfca43303": "[\"~#shape\",[\"^ \",\"~:y\",-637.0000057220459,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",336.9999895095825,\"~:y\",-637.000005722046]],[\"^<\",[\"^ \",\"~:x\",636.9999995231628,\"~:y\",-637.000005722046]],[\"^<\",[\"^ \",\"~:x\",636.9999995231628,\"~:y\",-337.00000858306885]],[\"^<\",[\"^ \",\"~:x\",336.9999895095825,\"~:y\",-337.00000858306885]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e380b12ac2a\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"~:r1\",0,\"^B\",\"~u097859f1-ca3b-80ba-8007-8e8bfca43303\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-style\",\"~:solid\",\"~:stroke-alignment\",\"~:center\",\"~:stroke-width\",10,\"~:stroke-color\",\"#4bff00\",\"~:stroke-opacity\",1],[\"^ \",\"^J\",\"^K\",\"^L\",\"~:outer\",\"^N\",10,\"^O\",\"#333fbd\",\"^P\",1],[\"^ \",\"^J\",\"^K\",\"^L\",\"~:inner\",\"^N\",10,\"^O\",\"#ff0000\",\"^P\",1]],\"~:x\",336.9999895095825,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",336.9999895095825,\"~:y\",-637.0000057220459,\"^8\",300.0000100135803,\"~:height\",299.99999713897705,\"~:x1\",336.9999895095825,\"~:y1\",-637.0000057220459,\"~:x2\",636.9999995231628,\"~:y2\",-337.00000858306885]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^11\",\"#ff0000\",\"^12\",1]],\"~:flip-x\",null,\"^W\",299.99999713897705,\"~:flip-y\",null]]", "~udb80df91-a3a3-803b-8007-8e384d8c53b9": "[\"~#shape\",[\"^ \",\"~:y\",450.99999806284904,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",300.0000065565109,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1021.0000203847885,\"~:y\",450.99999806284904]],[\"^<\",[\"^ \",\"~:x\",1321.0000269412994,\"~:y\",450.99999806284904]],[\"^<\",[\"^ \",\"~:x\",1321.0000269412994,\"~:y\",751.0000142753124]],[\"^<\",[\"^ \",\"~:x\",1021.0000203847885,\"~:y\",751.0000142753124]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e37b7ddd15c\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"^@\",\"~udb80df91-a3a3-803b-8007-8e384d8c53b9\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-style\",\"~:solid\",\"~:stroke-alignment\",\"~:inner\",\"~:stroke-width\",20,\"~:stroke-color\",\"#333fbd\",\"~:stroke-opacity\",1]],\"~:x\",1021.0000203847885,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1021.0000203847885,\"~:y\",450.99999806284904,\"^8\",300.0000065565109,\"~:height\",300.0000162124634,\"~:x1\",1021.0000203847885,\"~:y1\",450.99999806284904,\"~:x2\",1321.0000269412994,\"~:y2\",751.0000142753124]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^W\",\"#ff0000\",\"^X\",1]],\"~:flip-x\",null,\"^Q\",300.0000162124634,\"~:flip-y\",null]]", - "~udb80df91-a3a3-803b-8007-8e379b5fd50f": "[\"~#shape\",[\"^ \",\"~:y\",82.00000368146124,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",679.0000200882939,\"~:y\",82.00000368146122]],[\"^<\",[\"^ \",\"~:x\",979.0000301018742,\"~:y\",82.00000368146122]],[\"^<\",[\"^ \",\"~:x\",979.0000301018742,\"~:y\",382.0000008204383]],[\"^<\",[\"^ \",\"~:x\",679.0000200882939,\"~:y\",382.0000008204383]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~udb80df91-a3a3-803b-8007-8e379b5fd50f\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",679.0000200882939,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",679.0000200882939,\"~:y\",82.00000368146124,\"^8\",300.0000100135803,\"~:height\",299.99999713897705,\"~:x1\",679.0000200882939,\"~:y1\",82.00000368146124,\"~:x2\",979.0000301018742,\"~:y2\",382.0000008204383]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^P\",\"#ff0000\",\"^Q\",1]],\"~:flip-x\",null,\"^J\",299.99999713897705,\"~:flip-y\",null]]", + "~udb80df91-a3a3-803b-8007-8e379b5fd50f": "[\"~#shape\",[\"^ \",\"~:y\",82.00000368146124,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",686.7500124588994,\"~:y\",82.00000368146122]],[\"^<\",[\"^ \",\"~:x\",986.7500224724797,\"~:y\",82.00000368146122]],[\"^<\",[\"^ \",\"~:x\",986.7500224724797,\"~:y\",382.0000008204383]],[\"^<\",[\"^ \",\"~:x\",686.7500124588994,\"~:y\",382.0000008204383]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~udb80df91-a3a3-803b-8007-8e379b5fd50f\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",686.7500124588994,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",686.7500124588994,\"~:y\",82.00000368146124,\"^8\",300.0000100135803,\"~:height\",299.99999713897705,\"~:x1\",686.7500124588994,\"~:y1\",82.00000368146124,\"~:x2\",986.7500224724797,\"~:y2\",382.0000008204383]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^P\",\"#ff0000\",\"^Q\",1]],\"~:flip-x\",null,\"^J\",299.99999713897705,\"~:flip-y\",null]]", "~u3b7d4c1f-3b79-80e5-8007-8d5e38c5a297": "[\"~#shape\",[\"^ \",\"~:y\",81.9999960520667,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",337.0000200882939,\"~:y\",81.99999605206669]],[\"^<\",[\"^ \",\"~:x\",637.0000301018742,\"~:y\",81.99999605206669]],[\"^<\",[\"^ \",\"~:x\",637.0000301018742,\"~:y\",381.99999319104376]],[\"^<\",[\"^ \",\"~:x\",337.0000200882939,\"~:y\",381.99999319104376]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:blur\",[\"^ \",\"~:id\",\"~u432cbb09-2ee7-80bf-8007-8d660b2f52ad\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"~:r1\",0,\"^B\",\"~u3b7d4c1f-3b79-80e5-8007-8d5e38c5a297\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",337.0000200882939,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",337.0000200882939,\"~:y\",81.9999960520667,\"^8\",300.0000100135803,\"~:height\",299.99999713897705,\"~:x1\",337.0000200882939,\"~:y1\",81.9999960520667,\"~:x2\",637.0000301018742,\"~:y2\",381.99999319104376]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^T\",\"#ff0000\",\"^U\",1]],\"~:flip-x\",null,\"^N\",299.99999713897705,\"~:flip-y\",null]]", + "~ufb1f50bf-1bff-8030-8007-8e8c3bd8fcd7": "[\"~#shape\",[\"^ \",\"~:y\",-629.9999999999998,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1037,\"~:y\",-630]],[\"^<\",[\"^ \",\"~:x\",1337.0000100135803,\"~:y\",-630]],[\"^<\",[\"^ \",\"~:x\",1337.0000100135803,\"~:y\",-330.0000028610228]],[\"^<\",[\"^ \",\"~:x\",1037,\"~:y\",-330.0000028610228]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e380b12ac2a\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"~:r1\",0,\"^B\",\"~ufb1f50bf-1bff-8030-8007-8e8c3bd8fcd7\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-style\",\"~:solid\",\"~:stroke-alignment\",\"~:outer\",\"~:stroke-width\",10,\"~:stroke-color\",\"#333fbd\",\"~:stroke-opacity\",1],[\"^ \",\"^J\",\"^K\",\"^L\",\"~:inner\",\"^N\",10,\"^O\",\"#ff0000\",\"^P\",1],[\"^ \",\"^J\",\"^K\",\"^L\",\"~:center\",\"^N\",10,\"^O\",\"#4bff00\",\"^P\",1]],\"~:x\",1037,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1037,\"~:y\",-629.9999999999998,\"^8\",300.0000100135803,\"~:height\",299.999997138977,\"~:x1\",1037,\"~:y1\",-629.9999999999998,\"~:x2\",1337.0000100135803,\"~:y2\",-330.0000028610228]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^11\",\"#ff0000\",\"^12\",1]],\"~:flip-x\",null,\"^W\",299.999997138977,\"~:flip-y\",null]]", + "~u097859f1-ca3b-80ba-8007-8e8beb99a3f5": "[\"~#shape\",[\"^ \",\"~:y\",-626.0000057220459,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",687.0000123977661,\"~:y\",-626.000005722046]],[\"^<\",[\"^ \",\"~:x\",987.0000224113464,\"~:y\",-626.000005722046]],[\"^<\",[\"^ \",\"~:x\",987.0000224113464,\"~:y\",-326.00000858306885]],[\"^<\",[\"^ \",\"~:x\",687.0000123977661,\"~:y\",-326.00000858306885]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e380b12ac2a\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"~:r1\",0,\"^B\",\"~u097859f1-ca3b-80ba-8007-8e8beb99a3f5\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-style\",\"~:solid\",\"~:stroke-alignment\",\"~:inner\",\"~:stroke-width\",10,\"~:stroke-color\",\"#333fbd\",\"~:stroke-opacity\",1],[\"^ \",\"^J\",\"^K\",\"^L\",\"^M\",\"^N\",10,\"^O\",\"#ff0000\",\"^P\",1]],\"~:x\",687.0000123977661,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",687.0000123977661,\"~:y\",-626.0000057220459,\"^8\",300.0000100135803,\"~:height\",299.99999713897705,\"~:x1\",687.0000123977661,\"~:y1\",-626.0000057220459,\"~:x2\",987.0000224113464,\"~:y2\",-326.00000858306885]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^[\",\"#ff0000\",\"^10\",1]],\"~:flip-x\",null,\"^U\",299.99999713897705,\"~:flip-y\",null]]", "~udb80df91-a3a3-803b-8007-8e37a71c9d28": "[\"~#shape\",[\"^ \",\"~:y\",450.99999806284904,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",300.0000065565109,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",337.0000203847885,\"~:y\",450.99999806284904]],[\"^<\",[\"^ \",\"~:x\",637.0000269412994,\"~:y\",450.99999806284904]],[\"^<\",[\"^ \",\"~:x\",637.0000269412994,\"~:y\",751.0000142753124]],[\"^<\",[\"^ \",\"~:x\",337.0000203847885,\"~:y\",751.0000142753124]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e37b7ddd15c\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"^@\",\"~udb80df91-a3a3-803b-8007-8e37a71c9d28\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",337.0000203847885,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",337.0000203847885,\"~:y\",450.99999806284904,\"^8\",300.0000065565109,\"~:height\",300.0000162124634,\"~:x1\",337.0000203847885,\"~:y1\",450.99999806284904,\"~:x2\",637.0000269412994,\"~:y2\",751.0000142753124]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^P\",\"#ff0000\",\"^Q\",1]],\"~:flip-x\",null,\"^J\",300.0000162124634,\"~:flip-y\",null]]", + "~u18522c44-655d-8050-8007-8e89f4bdc0c5": "[\"~#shape\",[\"^ \",\"~:y\",-287.0000057220459,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",337.00002002716064,\"~:y\",-287.000005722046]],[\"^<\",[\"^ \",\"~:x\",637.000030040741,\"~:y\",-287.000005722046]],[\"^<\",[\"^ \",\"~:x\",637.000030040741,\"~:y\",12.999991416931152]],[\"^<\",[\"^ \",\"~:x\",337.00002002716064,\"~:y\",12.999991416931152]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e380b12ac2a\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"~:r1\",0,\"^B\",\"~u18522c44-655d-8050-8007-8e89f4bdc0c5\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-style\",\"~:solid\",\"~:stroke-alignment\",\"~:outer\",\"~:stroke-width\",10,\"~:stroke-color\",\"#333fbd\",\"~:stroke-opacity\",1],[\"^ \",\"^J\",\"^K\",\"^L\",\"~:inner\",\"^N\",10,\"^O\",\"#ff0000\",\"^P\",1]],\"~:x\",337.00002002716064,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",337.00002002716064,\"~:y\",-287.0000057220459,\"^8\",300.0000100135803,\"~:height\",299.99999713897705,\"~:x1\",337.00002002716064,\"~:y1\",-287.0000057220459,\"~:x2\",637.000030040741,\"~:y2\",12.999991416931152]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^10\",\"#ff0000\",\"^11\",1]],\"~:flip-x\",null,\"^V\",299.99999713897705,\"~:flip-y\",null]]", "~udb80df91-a3a3-803b-8007-8e37c09b4084": "[\"~#shape\",[\"^ \",\"~:y\",450.99999806284904,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",300.0000065565109,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",679.0000203847885,\"~:y\",450.99999806284904]],[\"^<\",[\"^ \",\"~:x\",979.0000269412994,\"~:y\",450.99999806284904]],[\"^<\",[\"^ \",\"~:x\",979.0000269412994,\"~:y\",751.0000142753124]],[\"^<\",[\"^ \",\"~:x\",679.0000203847885,\"~:y\",751.0000142753124]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~udb80df91-a3a3-803b-8007-8e37c09b4084\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",679.0000203847885,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",679.0000203847885,\"~:y\",450.99999806284904,\"^8\",300.0000065565109,\"~:height\",300.0000162124634,\"~:x1\",679.0000203847885,\"~:y1\",450.99999806284904,\"~:x2\",979.0000269412994,\"~:y2\",751.0000142753124]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^L\",\"#ff0000\",\"^M\",1]],\"~:flip-x\",null,\"^F\",300.0000162124634,\"~:flip-y\",null]]", - "~udb80df91-a3a3-803b-8007-8e38034ff7c8": "[\"~#shape\",[\"^ \",\"~:y\",82.00000368146124,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1021.0000200882939,\"~:y\",82.00000368146122]],[\"^<\",[\"^ \",\"~:x\",1321.0000301018742,\"~:y\",82.00000368146122]],[\"^<\",[\"^ \",\"~:x\",1321.0000301018742,\"~:y\",382.0000008204383]],[\"^<\",[\"^ \",\"~:x\",1021.0000200882939,\"~:y\",382.0000008204383]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e380b12ac2a\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"~:r1\",0,\"^B\",\"~udb80df91-a3a3-803b-8007-8e38034ff7c8\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-style\",\"~:solid\",\"~:stroke-alignment\",\"~:inner\",\"~:stroke-width\",10,\"~:stroke-color\",\"#333fbd\",\"~:stroke-opacity\",1]],\"~:x\",1021.0000200882939,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1021.0000200882939,\"~:y\",82.00000368146124,\"^8\",300.0000100135803,\"~:height\",299.99999713897705,\"~:x1\",1021.0000200882939,\"~:y1\",82.00000368146124,\"~:x2\",1321.0000301018742,\"~:y2\",382.0000008204383]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^[\",\"#ff0000\",\"^10\",1]],\"~:flip-x\",null,\"^U\",299.99999713897705,\"~:flip-y\",null]]" + "~u18522c44-655d-8050-8007-8e89f4bdc0c4": "[\"~#shape\",[\"^ \",\"~:y\",-287.0000057220459,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",686.7500123977661,\"~:y\",-287.000005722046]],[\"^<\",[\"^ \",\"~:x\",986.7500224113464,\"~:y\",-287.000005722046]],[\"^<\",[\"^ \",\"~:x\",986.7500224113464,\"~:y\",12.999991416931152]],[\"^<\",[\"^ \",\"~:x\",686.7500123977661,\"~:y\",12.999991416931152]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e380b12ac2a\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"~:r1\",0,\"^B\",\"~u18522c44-655d-8050-8007-8e89f4bdc0c4\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-style\",\"~:solid\",\"~:stroke-alignment\",\"~:inner\",\"~:stroke-width\",10,\"~:stroke-color\",\"#333fbd\",\"~:stroke-opacity\",0.5],[\"^ \",\"^J\",\"^K\",\"^L\",\"^M\",\"^N\",10,\"^O\",\"#ff0000\",\"^P\",1]],\"~:x\",686.7500123977661,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",686.7500123977661,\"~:y\",-287.0000057220459,\"^8\",300.0000100135803,\"~:height\",299.99999713897705,\"~:x1\",686.7500123977661,\"~:y1\",-287.0000057220459,\"~:x2\",986.7500224113464,\"~:y2\",12.999991416931152]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^[\",\"#ff0000\",\"^10\",1]],\"~:flip-x\",null,\"^U\",299.99999713897705,\"~:flip-y\",null]]", + "~udb80df91-a3a3-803b-8007-8e38034ff7c8": "[\"~#shape\",[\"^ \",\"~:y\",82.00000368146124,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1036.5000048295049,\"~:y\",82.00000368146122]],[\"^<\",[\"^ \",\"~:x\",1336.5000148430852,\"~:y\",82.00000368146122]],[\"^<\",[\"^ \",\"~:x\",1336.5000148430852,\"~:y\",382.0000008204383]],[\"^<\",[\"^ \",\"~:x\",1036.5000048295049,\"~:y\",382.0000008204383]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e380b12ac2a\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"~:r1\",0,\"^B\",\"~udb80df91-a3a3-803b-8007-8e38034ff7c8\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-style\",\"~:solid\",\"~:stroke-alignment\",\"~:inner\",\"~:stroke-width\",10,\"~:stroke-color\",\"#333fbd\",\"~:stroke-opacity\",1]],\"~:x\",1036.5000048295049,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1036.5000048295049,\"~:y\",82.00000368146124,\"^8\",300.0000100135803,\"~:height\",299.99999713897705,\"~:x1\",1036.5000048295049,\"~:y1\",82.00000368146124,\"~:x2\",1336.5000148430852,\"~:y2\",382.0000008204383]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^[\",\"#ff0000\",\"^10\",1]],\"~:flip-x\",null,\"^U\",299.99999713897705,\"~:flip-y\",null]]", + "~u18522c44-655d-8050-8007-8e89f4bdc0c6": "[\"~#shape\",[\"^ \",\"~:y\",-287.0000057220459,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",300.0000100135803,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",1036.5000047683716,\"~:y\",-287.000005722046]],[\"^<\",[\"^ \",\"~:x\",1336.500014781952,\"~:y\",-287.000005722046]],[\"^<\",[\"^ \",\"~:x\",1336.500014781952,\"~:y\",12.999991416931152]],[\"^<\",[\"^ \",\"~:x\",1036.5000047683716,\"~:y\",12.999991416931152]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:blur\",[\"^ \",\"~:id\",\"~udb80df91-a3a3-803b-8007-8e380b12ac2a\",\"^9\",\"~:layer-blur\",\"~:value\",7,\"~:hidden\",false],\"~:r1\",0,\"^B\",\"~u18522c44-655d-8050-8007-8e89f4bdc0c6\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[[\"^ \",\"~:stroke-style\",\"~:solid\",\"~:stroke-alignment\",\"~:inner\",\"~:stroke-width\",10,\"~:stroke-color\",\"#333fbd\",\"~:stroke-opacity\",1],[\"^ \",\"^J\",\"^K\",\"^L\",\"~:outer\",\"^N\",10,\"^O\",\"#ff0000\",\"^P\",1]],\"~:x\",1036.5000047683716,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",1036.5000047683716,\"~:y\",-287.0000057220459,\"^8\",300.0000100135803,\"~:height\",299.99999713897705,\"~:x1\",1036.5000047683716,\"~:y1\",-287.0000057220459,\"~:x2\",1336.500014781952,\"~:y2\",12.999991416931152]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1],[\"^ \",\"^10\",\"#ff0000\",\"^11\",1]],\"~:flip-x\",null,\"^V\",299.99999713897705,\"~:flip-y\",null]]" } }, "~:id": "~ub15901d7-d46d-8056-8007-8d5e34fc1f0d", diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js index 48c7425429..67eccff5b9 100644 --- a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js +++ b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js @@ -197,7 +197,7 @@ test("Renders a file with blurs applied to any kind of shape", async ({ test("Renders a file with shadows applied to any kind of shape", async ({ page, -}) => { +}) => { const workspace = new WasmWorkspacePage(page); await workspace.setupEmptyFile(); await workspace.mockGetFile("render-wasm/get-file-shadows.json"); diff --git a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-shapes-with-multiple-fills-and-blur-1.png b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-shapes-with-multiple-fills-and-blur-1.png index 5e3531e8a6..353dfca842 100644 Binary files a/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-shapes-with-multiple-fills-and-blur-1.png and b/frontend/playwright/ui/render-wasm-specs/shapes.spec.js-snapshots/Renders-shapes-with-multiple-fills-and-blur-1.png differ diff --git a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-images-1.png b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-images-1.png index 86c2d90867..649a0751da 100644 Binary files a/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-images-1.png and b/frontend/playwright/ui/render-wasm-specs/texts.spec.js-snapshots/Renders-a-file-with-texts-with-images-1.png differ diff --git a/frontend/src/app/render_wasm/api/fonts.cljs b/frontend/src/app/render_wasm/api/fonts.cljs index c520923f74..09a0adfac2 100644 --- a/frontend/src/app/render_wasm/api/fonts.cljs +++ b/frontend/src/app/render_wasm/api/fonts.cljs @@ -320,7 +320,7 @@ :style-name style :weight weight :emoji? emoji? - :fallbck? fallback? + :fallback? fallback? :asset-id asset-id})) (defn store-font diff --git a/render-wasm/src/render.rs b/render-wasm/src/render.rs index 57b2a3723e..9a7b80c132 100644 --- a/render-wasm/src/render.rs +++ b/render-wasm/src/render.rs @@ -22,7 +22,7 @@ pub use surfaces::{SurfaceId, Surfaces}; use crate::performance; use crate::shapes::{ - all_with_ancestors, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, Type, + all_with_ancestors, Blur, BlurType, Corners, Fill, Shadow, Shape, SolidColor, Stroke, Type, }; use crate::state::{ShapesPoolMutRef, ShapesPoolRef}; use crate::tiles::{self, PendingTiles, TileRect}; @@ -700,16 +700,15 @@ impl RenderState { fills::render(self, shape, &shape.fills, antialias, SurfaceId::Current); - for stroke in shape.visible_strokes().rev() { - strokes::render( - self, - shape, - stroke, - Some(SurfaceId::Current), - None, - antialias, - ); - } + // Pass strokes in natural order; stroke merging handles top-most ordering internally. + let visible_strokes: Vec<&Stroke> = shape.visible_strokes().collect(); + strokes::render( + self, + shape, + &visible_strokes, + Some(SurfaceId::Current), + antialias, + ); self.surfaces.apply_mut(SurfaceId::Current as u32, |s| { s.canvas().restore(); @@ -1023,16 +1022,17 @@ impl RenderState { // over the children. Drawing twice would cause incorrect opacity blending. let skip_strokes = matches!(shape.shape_type, Type::Frame(_)) && shape.clip_content; if !skip_strokes { - for stroke in shape.visible_strokes().rev() { - strokes::render( - self, - shape, - stroke, - Some(strokes_surface_id), - None, - antialias, - ); - if !fast_mode { + // Pass strokes in natural order; stroke merging handles top-most ordering internally. + let visible_strokes: Vec<&Stroke> = shape.visible_strokes().collect(); + strokes::render( + self, + shape, + &visible_strokes, + Some(strokes_surface_id), + antialias, + ); + if !fast_mode { + for stroke in &visible_strokes { shadows::render_stroke_inner_shadows( self, shape, @@ -2355,11 +2355,8 @@ impl RenderState { } } - if zoom_changed { - // Zoom changed: clear all cached tiles since they're at wrong zoom level - self.surfaces.remove_cached_tiles(self.background_color); - } - // Pan only: no cache invalidation needed - tiles content unchanged + // Invalidate changed tiles - old content stays visible until new tiles render + self.surfaces.remove_cached_tiles(self.background_color); performance::end_measure!("rebuild_tiles_shallow"); } diff --git a/render-wasm/src/render/shadows.rs b/render-wasm/src/render/shadows.rs index 64a6d7533a..9a0862cbff 100644 --- a/render-wasm/src/render/shadows.rs +++ b/render-wasm/src/render/shadows.rs @@ -40,7 +40,7 @@ pub fn render_stroke_inner_shadows( if !shape.has_fills() { for shadow in shape.inner_shadows_visible() { let filter = shadow.get_inner_shadow_filter(); - strokes::render( + strokes::render_single( render_state, shape, stroke, diff --git a/render-wasm/src/render/strokes.rs b/render-wasm/src/render/strokes.rs index 94375c6d93..ff61502d7c 100644 --- a/render-wasm/src/render/strokes.rs +++ b/render-wasm/src/render/strokes.rs @@ -1,7 +1,7 @@ use crate::math::{Matrix, Point, Rect}; use crate::shapes::{ - Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, SvgAttrs, Type, + merge_fills, Corners, Fill, ImageFill, Path, Shape, Stroke, StrokeCap, StrokeKind, Type, }; use skia_safe::{self as skia, ImageFilter, RRect}; @@ -9,26 +9,20 @@ use super::{filters, RenderState, SurfaceId}; use crate::render::filters::compose_filters; use crate::render::{get_dest_rect, get_source_rect}; -// FIXME: See if we can simplify these arguments #[allow(clippy::too_many_arguments)] fn draw_stroke_on_rect( canvas: &skia::Canvas, stroke: &Stroke, rect: &Rect, - selrect: &Rect, corners: &Option, - svg_attrs: Option<&SvgAttrs>, + paint: &skia::Paint, scale: f32, shadow: Option<&ImageFilter>, blur: Option<&ImageFilter>, antialias: bool, ) { - // Draw the different kind of strokes for a rect is straightforward, we just need apply a stroke to: - // - The same rect if it's a center stroke - // - A bigger rect if it's an outer stroke - // - A smaller rect if it's an inner stroke let stroke_rect = stroke.aligned_rect(rect, scale); - let mut paint = stroke.to_paint(selrect, svg_attrs, antialias); + let mut paint = paint.clone(); // Apply both blur and shadow filters if present, composing them if necessary. let filter = compose_filters(blur, shadow); @@ -66,25 +60,19 @@ fn draw_stroke_on_rect( } } -// FIXME: See if we can simplify these arguments #[allow(clippy::too_many_arguments)] fn draw_stroke_on_circle( canvas: &skia::Canvas, stroke: &Stroke, rect: &Rect, - selrect: &Rect, - svg_attrs: Option<&SvgAttrs>, + paint: &skia::Paint, scale: f32, shadow: Option<&ImageFilter>, blur: Option<&ImageFilter>, antialias: bool, ) { - // Draw the different kind of strokes for an oval is straightforward, we just need apply a stroke to: - // - The same oval if it's a center stroke - // - A bigger oval if it's an outer stroke - // - A smaller oval if it's an inner stroke let stroke_rect = stroke.aligned_rect(rect, scale); - let mut paint = stroke.to_paint(selrect, svg_attrs, antialias); + let mut paint = paint.clone(); // Apply both blur and shadow filters if present, composing them if necessary. let filter = compose_filters(blur, shadow); @@ -154,15 +142,13 @@ fn draw_inner_stroke_path( } // For outer stroke we draw a center stroke (with double width) and use another path with blend mode clear to remove the inner stroke added -// FIXME: See if we can simplify these arguments #[allow(clippy::too_many_arguments)] -pub fn draw_stroke_on_path( +fn draw_stroke_on_path( canvas: &skia::Canvas, stroke: &Stroke, path: &Path, - selrect: &Rect, + paint: &skia::Paint, path_transform: Option<&Matrix>, - svg_attrs: Option<&SvgAttrs>, shadow: Option<&ImageFilter>, blur: Option<&ImageFilter>, antialias: bool, @@ -172,31 +158,28 @@ pub fn draw_stroke_on_path( let is_open = path.is_open(); - let mut paint: skia_safe::Handle<_> = - stroke.to_stroked_paint(is_open, selrect, svg_attrs, antialias); - + let mut draw_paint = paint.clone(); let filter = compose_filters(blur, shadow); - paint.set_image_filter(filter); + draw_paint.set_image_filter(filter); match stroke.render_kind(is_open) { StrokeKind::Inner => { - draw_inner_stroke_path(canvas, &skia_path, &paint, blur, antialias); + draw_inner_stroke_path(canvas, &skia_path, &draw_paint, blur, antialias); } StrokeKind::Center => { - canvas.draw_path(&skia_path, &paint); + canvas.draw_path(&skia_path, &draw_paint); } StrokeKind::Outer => { - draw_outer_stroke_path(canvas, &skia_path, &paint, blur, antialias); + draw_outer_stroke_path(canvas, &skia_path, &draw_paint, blur, antialias); } } handle_stroke_caps( &mut skia_path, stroke, - selrect, canvas, is_open, - svg_attrs, + paint, blur, antialias, ); @@ -239,17 +222,15 @@ fn handle_stroke_cap( } } -// FIXME: See if we can simplify these arguments #[allow(clippy::too_many_arguments)] fn handle_stroke_caps( path: &mut skia::Path, stroke: &Stroke, - selrect: &Rect, canvas: &skia::Canvas, is_open: bool, - svg_attrs: Option<&SvgAttrs>, + paint: &skia::Paint, blur: Option<&ImageFilter>, - antialias: bool, + _antialias: bool, ) { let mut points = vec![Point::default(); path.count_points()]; path.get_points(&mut points); @@ -262,7 +243,7 @@ fn handle_stroke_caps( let first_point = points.first().unwrap(); let last_point = points.last().unwrap(); - let mut paint_stroke = stroke.to_stroked_paint(is_open, selrect, svg_attrs, antialias); + let mut paint_stroke = paint.clone(); if let Some(filter) = blur { paint_stroke.set_image_filter(filter.clone()); @@ -437,30 +418,25 @@ fn draw_image_stroke_in_container( match &shape.shape_type { shape_type @ (Type::Rect(_) | Type::Frame(_)) => { + let paint = stroke.to_paint(&outer_rect, svg_attrs, antialias); draw_stroke_on_rect( canvas, stroke, container, - &outer_rect, &shape_type.corners(), - svg_attrs, + &paint, scale, None, None, antialias, ); } - Type::Circle => draw_stroke_on_circle( - canvas, - stroke, - container, - &outer_rect, - svg_attrs, - scale, - None, - None, - antialias, - ), + Type::Circle => { + let paint = stroke.to_paint(&outer_rect, svg_attrs, antialias); + draw_stroke_on_circle( + canvas, stroke, container, &paint, scale, None, None, antialias, + ); + } shape_type @ (Type::Path(_) | Type::Bool(_)) => { if let Some(p) = shape_type.path() { @@ -478,21 +454,21 @@ fn draw_image_stroke_in_container( } } let is_open = p.is_open(); - let mut paint = stroke.to_stroked_paint(is_open, &outer_rect, svg_attrs, antialias); + let paint = stroke.to_stroked_paint(is_open, &outer_rect, svg_attrs, antialias); canvas.draw_path(&path, &paint); if stroke.render_kind(is_open) == StrokeKind::Outer { // Small extra inner stroke to overlap with the fill // and avoid unnecesary artifacts. - paint.set_stroke_width(1. / scale); - canvas.draw_path(&path, &paint); + let mut thin_paint = paint.clone(); + thin_paint.set_stroke_width(1. / scale); + canvas.draw_path(&path, &thin_paint); } handle_stroke_caps( &mut path, stroke, - &outer_rect, canvas, is_open, - svg_attrs, + &paint, shape.image_filter(1.).as_ref(), antialias, ); @@ -541,8 +517,230 @@ fn draw_image_stroke_in_container( canvas.restore(); } -#[allow(clippy::too_many_arguments)] +/// Renders all strokes for a shape. Merges strokes that share the same +/// geometry (kind, width, style, caps) into a single draw call to avoid +/// anti-aliasing edge bleed between them. pub fn render( + render_state: &mut RenderState, + shape: &Shape, + strokes: &[&Stroke], + surface_id: Option, + antialias: bool, +) { + if strokes.is_empty() { + return; + } + + let has_image_fills = strokes.iter().any(|s| matches!(s.fill, Fill::Image(_))); + let can_merge = !has_image_fills && strokes.len() > 1 && strokes_share_geometry(strokes); + + if !can_merge { + // When blur is active, render all strokes into a single offscreen surface + // and apply blur once to the composite. This prevents blur from making + // edges semi-transparent and revealing strokes underneath. + if let Some(image_filter) = shape.image_filter(1.) { + let mut content_bounds = shape.selrect; + let max_margin = strokes + .iter() + .map(|s| s.bounds_width(shape.is_open())) + .fold(0.0f32, f32::max); + if max_margin > 0.0 { + content_bounds.inset((-max_margin, -max_margin)); + } + let max_cap = strokes + .iter() + .map(|s| s.cap_bounds_margin()) + .fold(0.0f32, f32::max); + if max_cap > 0.0 { + content_bounds.inset((-max_cap, -max_cap)); + } + let bounds = image_filter.compute_fast_bounds(content_bounds); + let target = surface_id.unwrap_or(SurfaceId::Strokes); + if filters::render_with_filter_surface( + render_state, + bounds, + target, + |state, temp_surface| { + // Use save_layer with the blur filter so it applies once + // to the composite of all strokes, not per-stroke. + let canvas = state.surfaces.canvas(temp_surface); + let mut blur_paint = skia::Paint::default(); + blur_paint.set_image_filter(image_filter.clone()); + let layer_rec = skia::canvas::SaveLayerRec::default().paint(&blur_paint); + canvas.save_layer(&layer_rec); + + for stroke in strokes.iter().rev() { + // bypass_filter=true prevents each stroke from creating + // its own filter surface. The blur on the paint inside + // draw functions is harmless — it composes with the + // layer's filter but the layer filter is the dominant one. + render_single_internal( + state, + shape, + stroke, + Some(temp_surface), + None, + antialias, + true, + true, + ); + } + + state.surfaces.canvas(temp_surface).restore(); + }, + ) { + return; + } + } + + // No blur or filter surface unavailable — draw strokes individually. + for stroke in strokes.iter().rev() { + render_single(render_state, shape, stroke, surface_id, None, antialias); + } + return; + } + + render_merged(render_state, shape, strokes, surface_id, antialias, false); +} + +fn strokes_share_geometry(strokes: &[&Stroke]) -> bool { + strokes.windows(2).all(|pair| { + pair[0].kind == pair[1].kind + && pair[0].width == pair[1].width + && pair[0].style == pair[1].style + && pair[0].cap_start == pair[1].cap_start + && pair[0].cap_end == pair[1].cap_end + }) +} + +fn render_merged( + render_state: &mut RenderState, + shape: &Shape, + strokes: &[&Stroke], + surface_id: Option, + antialias: bool, + bypass_filter: bool, +) { + let representative = *strokes + .last() + .expect("render_merged expects at least one stroke"); + + let blur_filter = if bypass_filter { + None + } else { + shape.image_filter(1.) + }; + + // Handle blur filter + if !bypass_filter { + if let Some(image_filter) = blur_filter.clone() { + let mut content_bounds = shape.selrect; + let stroke_margin = representative.bounds_width(shape.is_open()); + if stroke_margin > 0.0 { + content_bounds.inset((-stroke_margin, -stroke_margin)); + } + let cap_margin = representative.cap_bounds_margin(); + if cap_margin > 0.0 { + content_bounds.inset((-cap_margin, -cap_margin)); + } + let bounds = image_filter.compute_fast_bounds(content_bounds); + let target = surface_id.unwrap_or(SurfaceId::Strokes); + if filters::render_with_filter_surface( + render_state, + bounds, + target, + |state, temp_surface| { + let blur_filter = image_filter.clone(); + + state.surfaces.apply_mut(temp_surface as u32, |surface| { + let canvas = surface.canvas(); + let mut blur_paint = skia::Paint::default(); + blur_paint.set_image_filter(blur_filter.clone()); + let layer_rec = skia::canvas::SaveLayerRec::default().paint(&blur_paint); + canvas.save_layer(&layer_rec); + }); + + render_merged(state, shape, strokes, Some(temp_surface), antialias, true); + + state.surfaces.apply_mut(temp_surface as u32, |surface| { + surface.canvas().restore(); + }); + }, + ) { + return; + } + } + } + + // `merge_fills` puts fills[0] on top (each new fill goes under the accumulated shader + // via SrcOver), matching the non-merged path where strokes[0] is drawn last (on top). + let fills: Vec = strokes.iter().map(|s| s.fill.clone()).collect(); + + let merged = merge_fills(&fills, shape.selrect); + let scale = render_state.get_scale(); + let target_surface = surface_id.unwrap_or(SurfaceId::Strokes); + let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface); + let selrect = shape.selrect; + let svg_attrs = shape.svg_attrs.as_ref(); + let path_transform = shape.to_path_transform(); + + match &shape.shape_type { + shape_type @ (Type::Rect(_) | Type::Frame(_)) => { + let mut paint = representative.to_paint(&selrect, svg_attrs, antialias); + paint.set_shader(merged.shader()); + draw_stroke_on_rect( + canvas, + representative, + &selrect, + &shape_type.corners(), + &paint, + scale, + None, + blur_filter.as_ref(), + antialias, + ); + } + Type::Circle => { + let mut paint = representative.to_paint(&selrect, svg_attrs, antialias); + paint.set_shader(merged.shader()); + draw_stroke_on_circle( + canvas, + representative, + &selrect, + &paint, + scale, + None, + blur_filter.as_ref(), + antialias, + ); + } + Type::Text(_) => {} + shape_type @ (Type::Path(_) | Type::Bool(_)) => { + if let Some(path) = shape_type.path() { + let is_open = path.is_open(); + let mut paint = + representative.to_stroked_paint(is_open, &selrect, svg_attrs, antialias); + paint.set_shader(merged.shader()); + draw_stroke_on_path( + canvas, + representative, + path, + &paint, + path_transform.as_ref(), + None, + blur_filter.as_ref(), + antialias, + ); + } + } + _ => unreachable!("This shape should not have strokes"), + } +} + +/// Renders a single stroke. Used by the shadow module which needs per-stroke +/// shadow filters. +#[allow(clippy::too_many_arguments)] +pub fn render_single( render_state: &mut RenderState, shape: &Shape, stroke: &Stroke, @@ -550,7 +748,7 @@ pub fn render( shadow: Option<&ImageFilter>, antialias: bool, ) { - render_internal( + render_single_internal( render_state, shape, stroke, @@ -558,34 +756,12 @@ pub fn render( shadow, antialias, false, + false, ); } -/// Internal function to render a stroke with support for offscreen blur rendering. -/// -/// # Parameters -/// - `render_state`: The rendering state containing surfaces and context. -/// - `shape`: The shape to render the stroke for. -/// - `stroke`: The stroke configuration (width, fill, style, etc.). -/// - `surface_id`: Optional target surface ID. Defaults to `SurfaceId::Strokes` if `None`. -/// - `shadow`: Optional shadow filter to apply to the stroke. -/// - `antialias`: Whether to use antialiasing for rendering. -/// - `bypass_filter`: -/// - If `false`, attempts to use offscreen filter surface for blur effects. -/// - If `true`, renders directly to the target surface (used for recursive calls to avoid infinite loops when rendering into the filter surface). -/// -/// # Behavior -/// When `bypass_filter` is `false` and the shape has a blur filter: -/// 1. Calculates bounds including stroke width and cap margins. -/// 2. Attempts to render into an offscreen filter surface at unscaled coordinates. -/// 3. If successful, composites the result back to the target surface and returns early. -/// 4. If the offscreen render fails or `bypass_filter` is `true`, renders directly to the target -/// surface using the appropriate drawing function for the shape type. -/// -/// The recursive call with `bypass_filter=true` ensures that when rendering into the filter -/// surface, we don't attempt to create another filter surface, avoiding infinite recursion. #[allow(clippy::too_many_arguments)] -fn render_internal( +fn render_single_internal( render_state: &mut RenderState, shape: &Shape, stroke: &Stroke, @@ -593,10 +769,10 @@ fn render_internal( shadow: Option<&ImageFilter>, antialias: bool, bypass_filter: bool, + skip_blur: bool, ) { if !bypass_filter { if let Some(image_filter) = shape.image_filter(1.) { - // We have to calculate the bounds considering the stroke and the cap margins. let mut content_bounds = shape.selrect; let stroke_margin = stroke.bounds_width(shape.is_open()); if stroke_margin > 0.0 { @@ -614,7 +790,7 @@ fn render_internal( bounds, target, |state, temp_surface| { - render_internal( + render_single_internal( state, shape, stroke, @@ -622,6 +798,7 @@ fn render_internal( shadow, antialias, true, + true, ); }, ) { @@ -637,6 +814,12 @@ fn render_internal( let path_transform = shape.to_path_transform(); let svg_attrs = shape.svg_attrs.as_ref(); + let blur = if skip_blur { + None + } else { + shape.image_filter(1.) + }; + if !matches!(shape.shape_type, Type::Text(_)) && shadow.is_none() && matches!(stroke.fill, Fill::Image(_)) @@ -654,42 +837,45 @@ fn render_internal( } else { match &shape.shape_type { shape_type @ (Type::Rect(_) | Type::Frame(_)) => { + let paint = stroke.to_paint(&selrect, svg_attrs, antialias); draw_stroke_on_rect( canvas, stroke, &selrect, - &selrect, &shape_type.corners(), - svg_attrs, + &paint, scale, shadow, - shape.image_filter(1.).as_ref(), + blur.as_ref(), + antialias, + ); + } + Type::Circle => { + let paint = stroke.to_paint(&selrect, svg_attrs, antialias); + draw_stroke_on_circle( + canvas, + stroke, + &selrect, + &paint, + scale, + shadow, + blur.as_ref(), antialias, ); } - Type::Circle => draw_stroke_on_circle( - canvas, - stroke, - &selrect, - &selrect, - svg_attrs, - scale, - shadow, - shape.image_filter(1.).as_ref(), - antialias, - ), Type::Text(_) => {} shape_type @ (Type::Path(_) | Type::Bool(_)) => { if let Some(path) = shape_type.path() { + let is_open = path.is_open(); + let paint = stroke.to_stroked_paint(is_open, &selrect, svg_attrs, antialias); draw_stroke_on_path( canvas, stroke, path, - &selrect, + &paint, path_transform.as_ref(), - svg_attrs, shadow, - shape.image_filter(1.).as_ref(), + blur.as_ref(), antialias, ); }