diff --git a/lib/web_ui/lib/src/engine/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/bitmap_canvas.dart index 7973d824f7dcb0b27eeebd05787d2c7bff663700..5f64e95d7c7acd2667a73789cfa99e774ee4ae34 100644 --- a/lib/web_ui/lib/src/engine/bitmap_canvas.dart +++ b/lib/web_ui/lib/src/engine/bitmap_canvas.dart @@ -574,10 +574,8 @@ class BitmapCanvas extends EngineCanvas { void _applyFilter(html.Element element, SurfacePaintData paint) { if (paint.maskFilter != null) { final bool isStroke = paint.style == ui.PaintingStyle.stroke; - String cssColor = paint.color == null - ? '#000000' - : colorToCssString( - paint.color)!; + String cssColor = + paint.color == null ? '#000000' : colorToCssString(paint.color)!; final double sigma = paint.maskFilter!.webOnlySigma; if (browserEngine == BrowserEngine.webkit && !isStroke) { // A bug in webkit leaves artifacts when this element is animated @@ -1195,14 +1193,33 @@ List _clipContent(List<_SaveClipEntry> clipStack, ..height = '${roundRect.bottom - clipOffsetY}px'; setElementTransform(curElement, newClipTransform.storage); } else if (entry.path != null) { - curElement.style - ..transform = matrix4ToCssTransform(newClipTransform) - ..transformOrigin = '0 0 0'; - String svgClipPath = - createSvgClipDef(curElement as html.HtmlElement, entry.path!); - final html.Element clipElement = - html.Element.html(svgClipPath, treeSanitizer: _NullTreeSanitizer()); - clipDefs.add(clipElement); + // Clipping optimization when we know that the path is an oval. + // We use a div with border-radius set to 50% with a size that is + // set to path bounds and set overflow to hidden. + final SurfacePath surfacePath = entry.path as SurfacePath; + if (surfacePath.pathRef.isOval != -1) { + final ui.Rect ovalBounds = surfacePath.getBounds(); + final double clipOffsetX = ovalBounds.left; + final double clipOffsetY = ovalBounds.top; + newClipTransform = newClipTransform.clone() + ..translate(clipOffsetX, clipOffsetY); + curElement.style + ..overflow = 'hidden' + ..width = '${ovalBounds.width}px' + ..height = '${ovalBounds.height}px' + ..borderRadius = '50%'; + setElementTransform(curElement, newClipTransform.storage); + } else { + // Abitrary path clipping. + curElement.style + ..transform = matrix4ToCssTransform(newClipTransform) + ..transformOrigin = '0 0 0'; + String svgClipPath = + createSvgClipDef(curElement as html.HtmlElement, entry.path!); + final html.Element clipElement = + html.Element.html(svgClipPath, treeSanitizer: _NullTreeSanitizer()); + clipDefs.add(clipElement); + } } // Reverse the transform of the clipping element so children can use // effective transform to render. diff --git a/lib/web_ui/test/golden_tests/engine/canvas_clip_path_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_clip_path_test.dart index 6345c6361646bff96279fef246006f13cf26e7bf..88dc7fe169eb4451699fc2ee80b4a781604c9994 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_clip_path_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_clip_path_test.dart @@ -11,39 +11,17 @@ import 'package:test/test.dart'; import 'package:ui/ui.dart' hide TextStyle; import 'package:ui/src/engine.dart' as engine; -import 'package:web_engine_tester/golden_tester.dart'; +import 'screenshot.dart'; void main() { internalBootstrapBrowserTest(() => testMain); } void testMain() async { - const double screenWidth = 600.0; - const double screenHeight = 800.0; + const double screenWidth = 500.0; + const double screenHeight = 500.0; const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); - // Commit a recording canvas to a bitmap, and compare with the expected - Future _checkScreenshot(engine.RecordingCanvas rc, String fileName, - {Rect region = const Rect.fromLTWH(0, 0, 500, 500)}) async { - final engine.EngineCanvas engineCanvas = engine.BitmapCanvas(screenRect, - engine.RenderStrategy()); - - rc.endRecording(); - rc.apply(engineCanvas, screenRect); - - // Wrap in so that our CSS selectors kick in. - final html.Element sceneElement = html.Element.tag('flt-scene'); - try { - sceneElement.append(engineCanvas.rootElement); - html.document.body.append(sceneElement); - await matchGoldenFile('$fileName.png', region: region, maxDiffRatePercent: 0.0); - } finally { - // The page is reused across tests, so remove the element after taking the - // Scuba screenshot. - sceneElement.remove(); - } - } - setUp(() async { debugEmulateFlutterTesterEnvironment = true; await webOnlyInitializePlatform(); @@ -55,7 +33,7 @@ void testMain() async { // Should clip image with oval. test('Clips image with oval clip path', () async { final engine.RecordingCanvas rc = - engine.RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + engine.RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500)); rc.save(); Image testImage = createTestImage(); double testWidth = testImage.width.toDouble(); @@ -66,13 +44,14 @@ void testMain() async { rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight), Rect.fromLTWH(100, 30, testWidth, testHeight), Paint()); rc.restore(); - await _checkScreenshot(rc, 'image_clipped_by_oval'); + await canvasScreenshot(rc, 'image_clipped_by_oval', + region: screenRect); }); // Regression test for https://github.com/flutter/flutter/issues/48683 test('Clips triangle with oval clip path', () async { final engine.RecordingCanvas rc = - engine.RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + engine.RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500)); rc.save(); double testWidth = 200; double testHeight = 150; @@ -90,7 +69,61 @@ void testMain() async { ..color = Color(0xFF00FF00) ..style = PaintingStyle.fill); rc.restore(); - await _checkScreenshot(rc, 'triangle_clipped_by_oval'); + await canvasScreenshot(rc, 'triangle_clipped_by_oval', + region: screenRect); + }); + + // Regression test for https://github.com/flutter/flutter/issues/78782 + test('Clips on Safari when clip bounds off screen', () async { + final engine.RecordingCanvas rc = + engine.RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500)); + rc.save(); + double testWidth = 200; + double testHeight = 150; + + final Path paintPath = new Path(); + paintPath.addRect(Rect.fromLTWH(-50, 0, testWidth, testHeight)); + paintPath.close(); + rc.drawPath(paintPath, + Paint() + ..color = Color(0xFF000000) + ..style = PaintingStyle.stroke); + + final Path path = Path(); + path.moveTo(-200, 0); + path.lineTo(100, 75); + path.lineTo(-200, 150); + path.close(); + rc.clipPath(path); + rc.drawImageRect(createTestImage(), Rect.fromLTRB(0, 0, testWidth, testHeight), + Rect.fromLTWH(-50, 0, testWidth, testHeight), Paint()); + rc.restore(); + await canvasScreenshot(rc, 'image_clipped_by_triangle_off_screen'); + }); + + // Tests oval clipping using border radius 50%. + test('Clips against oval', () async { + final engine.RecordingCanvas rc = + engine.RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500)); + rc.save(); + double testWidth = 200; + double testHeight = 150; + + final Path paintPath = new Path(); + paintPath.addRect(Rect.fromLTWH(-50, 0, testWidth, testHeight)); + paintPath.close(); + rc.drawPath(paintPath, + Paint() + ..color = Color(0xFF000000) + ..style = PaintingStyle.stroke); + + final Path path = Path(); + path.addOval(Rect.fromLTRB(-200, 0, 100, 150)); + rc.clipPath(path); + rc.drawImageRect(createTestImage(), Rect.fromLTRB(0, 0, testWidth, testHeight), + Rect.fromLTWH(-50, 0, testWidth, testHeight), Paint()); + rc.restore(); + await canvasScreenshot(rc, 'image_clipped_by_oval_path'); }); }