| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:math' as math; |
| |
| import 'package:test/bootstrap/browser.dart'; |
| import 'package:test/test.dart'; |
| import 'package:ui/src/engine.dart'; |
| import 'package:ui/ui.dart' as ui; |
| import 'package:web_engine_tester/golden_tester.dart'; |
| |
| import '../../matchers.dart'; |
| |
| const ui.Rect region = ui.Rect.fromLTWH(0, 0, 500, 100); |
| |
| void main() { |
| internalBootstrapBrowserTest(() => testMain); |
| } |
| |
| Future<void> testMain() async { |
| setUpAll(() async { |
| await ui.webOnlyInitializePlatform(); |
| await renderer.fontCollection.debugDownloadTestFonts(); |
| renderer.fontCollection.registerDownloadedFonts(); |
| }); |
| |
| setUp(() async { |
| // To debug test failures uncomment the following to visualize clipping |
| // layers: |
| // debugShowClipLayers = true; |
| SurfaceSceneBuilder.debugForgetFrameScene(); |
| for (final DomNode scene in domDocument.querySelectorAll('flt-scene')) { |
| scene.remove(); |
| } |
| }); |
| |
| test('pushClipRect', () async { |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| builder.pushClipRect( |
| const ui.Rect.fromLTRB(10, 10, 60, 60), |
| ); |
| _drawTestPicture(builder); |
| builder.pop(); |
| |
| domDocument.body!.append(builder.build().webOnlyRootElement!); |
| |
| await matchGoldenFile('compositing_shifted_clip_rect.png', region: region); |
| }); |
| |
| test('pushClipRect with offset and transform', () async { |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| |
| builder.pushOffset(0, 60); |
| builder.pushTransform( |
| Matrix4.diagonal3Values(1, -1, 1).toFloat64(), |
| ); |
| builder.pushClipRect( |
| const ui.Rect.fromLTRB(10, 10, 60, 60), |
| ); |
| _drawTestPicture(builder); |
| builder.pop(); |
| builder.pop(); |
| builder.pop(); |
| |
| domDocument.body!.append(builder.build().webOnlyRootElement!); |
| |
| await matchGoldenFile('compositing_clip_rect_with_offset_and_transform.png', |
| region: region); |
| }); |
| |
| test('pushClipRect with offset and transform ClipOp none should not clip', |
| () async { |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| |
| builder.pushOffset(0, 80); |
| builder.pushTransform( |
| Matrix4.diagonal3Values(1, -1, 1).toFloat64(), |
| ); |
| builder.pushClipRect(const ui.Rect.fromLTRB(10, 10, 60, 60), |
| clipBehavior: ui.Clip.none); |
| _drawTestPicture(builder); |
| builder.pop(); |
| builder.pop(); |
| builder.pop(); |
| |
| domDocument.body!.append(builder.build().webOnlyRootElement!); |
| |
| await matchGoldenFile('compositing_clip_rect_clipop_none.png', |
| region: region); |
| }); |
| |
| test('pushClipRRect with offset and transform ClipOp none should not clip', |
| () async { |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| |
| builder.pushOffset(0, 80); |
| builder.pushTransform( |
| Matrix4.diagonal3Values(1, -1, 1).toFloat64(), |
| ); |
| builder.pushClipRRect( |
| ui.RRect.fromRectAndRadius( |
| const ui.Rect.fromLTRB(10, 10, 60, 60), |
| const ui.Radius.circular(1), |
| ), |
| clipBehavior: ui.Clip.none); |
| _drawTestPicture(builder); |
| builder.pop(); |
| builder.pop(); |
| builder.pop(); |
| |
| domDocument.body!.append(builder.build().webOnlyRootElement!); |
| |
| await matchGoldenFile('compositing_clip_rrect_clipop_none.png', |
| region: region); |
| }); |
| |
| test('pushClipRRect', () async { |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| builder.pushClipRRect( |
| ui.RRect.fromLTRBR(10, 10, 60, 60, const ui.Radius.circular(5)), |
| ); |
| _drawTestPicture(builder); |
| builder.pop(); |
| |
| domDocument.body!.append(builder.build().webOnlyRootElement!); |
| |
| await matchGoldenFile('compositing_shifted_clip_rrect.png', region: region); |
| }); |
| |
| test('pushPhysicalShape', () async { |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| builder.pushPhysicalShape( |
| path: ui.Path()..addRect(const ui.Rect.fromLTRB(10, 10, 60, 60)), |
| clipBehavior: ui.Clip.hardEdge, |
| color: const ui.Color.fromRGBO(0, 0, 0, 0.3), |
| elevation: 0, |
| ); |
| _drawTestPicture(builder); |
| builder.pop(); |
| |
| builder.pushOffset(70, 0); |
| builder.pushPhysicalShape( |
| path: ui.Path() |
| ..addRRect(ui.RRect.fromLTRBR(10, 10, 60, 60, const ui.Radius.circular(5))), |
| clipBehavior: ui.Clip.hardEdge, |
| color: const ui.Color.fromRGBO(0, 0, 0, 0.3), |
| elevation: 0, |
| ); |
| _drawTestPicture(builder); |
| builder.pop(); |
| builder.pop(); |
| |
| domDocument.body!.append(builder.build().webOnlyRootElement!); |
| |
| await matchGoldenFile('compositing_shifted_physical_shape_clip.png', |
| region: region); |
| }); |
| |
| test('pushPhysicalShape clipOp.none', () async { |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| builder.pushPhysicalShape( |
| path: ui.Path()..addRect(const ui.Rect.fromLTRB(10, 10, 60, 60)), |
| clipBehavior: ui.Clip.hardEdge, |
| color: const ui.Color.fromRGBO(0, 0, 0, 0.3), |
| elevation: 0, |
| ); |
| _drawTestPicture(builder); |
| builder.pop(); |
| |
| builder.pushOffset(70, 0); |
| builder.pushPhysicalShape( |
| path: ui.Path() |
| ..addRRect(ui.RRect.fromLTRBR(10, 10, 60, 60, const ui.Radius.circular(5))), |
| color: const ui.Color.fromRGBO(0, 0, 0, 0.3), |
| elevation: 0, |
| ); |
| _drawTestPicture(builder); |
| builder.pop(); |
| builder.pop(); |
| |
| domDocument.body!.append(builder.build().webOnlyRootElement!); |
| |
| await matchGoldenFile('compositing_shifted_physical_shape_clipnone.png', |
| region: region); |
| }); |
| |
| test('pushPhysicalShape with path and elevation', () async { |
| final ui.Path cutCornersButton = ui.Path() |
| ..moveTo(15, 10) |
| ..lineTo(60, 10) |
| ..lineTo(60, 60) |
| ..lineTo(15, 60) |
| ..lineTo(10, 55) |
| ..lineTo(10, 15); |
| |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| builder.pushPhysicalShape( |
| path: cutCornersButton, |
| clipBehavior: ui.Clip.hardEdge, |
| color: const ui.Color(0xFFA0FFFF), |
| elevation: 2, |
| ); |
| _drawTestPicture(builder); |
| builder.pop(); |
| |
| builder.pushOffset(70, 0); |
| builder.pushPhysicalShape( |
| path: cutCornersButton, |
| clipBehavior: ui.Clip.hardEdge, |
| color: const ui.Color(0xFFA0FFFF), |
| elevation: 8, |
| ); |
| _drawTestPicture(builder); |
| builder.pop(); |
| builder.pop(); |
| |
| builder.pushOffset(140, 0); |
| builder.pushPhysicalShape( |
| path: ui.Path()..addOval(const ui.Rect.fromLTRB(10, 10, 60, 60)), |
| clipBehavior: ui.Clip.hardEdge, |
| color: const ui.Color(0xFFA0FFFF), |
| elevation: 4, |
| ); |
| _drawTestPicture(builder); |
| builder.pop(); |
| builder.pop(); |
| |
| builder.pushOffset(210, 0); |
| builder.pushPhysicalShape( |
| path: ui.Path() |
| ..addRRect(ui.RRect.fromRectAndRadius( |
| const ui.Rect.fromLTRB(10, 10, 60, 60), const ui.Radius.circular(10.0))), |
| clipBehavior: ui.Clip.hardEdge, |
| color: const ui.Color(0xFFA0FFFF), |
| elevation: 4, |
| ); |
| _drawTestPicture(builder); |
| builder.pop(); |
| builder.pop(); |
| |
| domDocument.body!.append(builder.build().webOnlyRootElement!); |
| |
| await matchGoldenFile('compositing_physical_shape_path.png', |
| region: region); |
| }); |
| |
| test('pushPhysicalShape should update across frames', () async { |
| final ui.Path cutCornersButton = ui.Path() |
| ..moveTo(15, 10) |
| ..lineTo(60, 10) |
| ..lineTo(60, 60) |
| ..lineTo(15, 60) |
| ..lineTo(10, 55) |
| ..lineTo(10, 15); |
| |
| /// Start with shape that has elevation and red color. |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| final ui.PhysicalShapeEngineLayer oldShapeLayer = builder.pushPhysicalShape( |
| path: cutCornersButton, |
| clipBehavior: ui.Clip.hardEdge, |
| color: const ui.Color(0xFFFF0000), |
| elevation: 2, |
| ); |
| _drawTestPicture(builder); |
| builder.pop(); |
| |
| final DomElement viewElement = builder.build().webOnlyRootElement!; |
| domDocument.body!.append(viewElement); |
| await matchGoldenFile('compositing_physical_update_1.png', region: region); |
| viewElement.remove(); |
| |
| /// Update color to green. |
| final SurfaceSceneBuilder builder2 = SurfaceSceneBuilder(); |
| final ui.PhysicalShapeEngineLayer oldShapeLayer2 = builder2.pushPhysicalShape( |
| path: cutCornersButton, |
| clipBehavior: ui.Clip.hardEdge, |
| color: const ui.Color(0xFF00FF00), |
| elevation: 2, |
| oldLayer: oldShapeLayer, |
| ); |
| _drawTestPicture(builder2); |
| builder2.pop(); |
| |
| final DomElement viewElement2 = builder2.build().webOnlyRootElement!; |
| domDocument.body!.append(viewElement2); |
| await matchGoldenFile('compositing_physical_update_2.png', region: region); |
| viewElement2.remove(); |
| |
| /// Update elevation. |
| final SurfaceSceneBuilder builder3 = SurfaceSceneBuilder(); |
| final ui.PhysicalShapeEngineLayer oldShapeLayer3 = builder3.pushPhysicalShape( |
| path: cutCornersButton, |
| clipBehavior: ui.Clip.hardEdge, |
| color: const ui.Color(0xFF00FF00), |
| elevation: 6, |
| oldLayer: oldShapeLayer2, |
| ); |
| _drawTestPicture(builder3); |
| builder3.pop(); |
| |
| final DomElement viewElement3 = builder3.build().webOnlyRootElement!; |
| domDocument.body!.append(viewElement3); |
| await matchGoldenFile('compositing_physical_update_3.png', |
| region: region); |
| viewElement3.remove(); |
| |
| /// Update shape from arbitrary path to rect. |
| final SurfaceSceneBuilder builder4 = SurfaceSceneBuilder(); |
| final ui.PhysicalShapeEngineLayer oldShapeLayer4 = builder4.pushPhysicalShape( |
| path: ui.Path()..addOval(const ui.Rect.fromLTRB(10, 10, 60, 60)), |
| clipBehavior: ui.Clip.hardEdge, |
| color: const ui.Color(0xFF00FF00), |
| elevation: 6, |
| oldLayer: oldShapeLayer3, |
| ); |
| _drawTestPicture(builder4); |
| builder4.pop(); |
| |
| final DomElement viewElement4 = builder4.build().webOnlyRootElement!; |
| domDocument.body!.append(viewElement4); |
| await matchGoldenFile('compositing_physical_update_4.png', region: region); |
| viewElement4.remove(); |
| |
| /// Update shape back to arbitrary path. |
| final SurfaceSceneBuilder builder5 = SurfaceSceneBuilder(); |
| final ui.PhysicalShapeEngineLayer oldShapeLayer5 = builder5.pushPhysicalShape( |
| path: cutCornersButton, |
| clipBehavior: ui.Clip.hardEdge, |
| color: const ui.Color(0xFF00FF00), |
| elevation: 6, |
| oldLayer: oldShapeLayer4, |
| ); |
| _drawTestPicture(builder5); |
| builder5.pop(); |
| |
| final DomElement viewElement5 = builder5.build().webOnlyRootElement!; |
| domDocument.body!.append(viewElement5); |
| await matchGoldenFile('compositing_physical_update_3.png', |
| region: region); |
| viewElement5.remove(); |
| |
| /// Update shadow color. |
| final SurfaceSceneBuilder builder6 = SurfaceSceneBuilder(); |
| builder6.pushPhysicalShape( |
| path: cutCornersButton, |
| clipBehavior: ui.Clip.hardEdge, |
| color: const ui.Color(0xFF00FF00), |
| shadowColor: const ui.Color(0xFFFF0000), |
| elevation: 6, |
| oldLayer: oldShapeLayer5, |
| ); |
| _drawTestPicture(builder6); |
| builder6.pop(); |
| |
| final DomElement viewElement6 = builder6.build().webOnlyRootElement!; |
| domDocument.body!.append(viewElement6); |
| await matchGoldenFile('compositing_physical_update_5.png', region: region); |
| viewElement6.remove(); |
| }); |
| |
| test('pushImageFilter blur', () async { |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| builder.pushImageFilter( |
| ui.ImageFilter.blur(sigmaX: 1, sigmaY: 3), |
| ); |
| _drawTestPicture(builder); |
| builder.pop(); |
| |
| domDocument.body!.append(builder.build().webOnlyRootElement!); |
| |
| await matchGoldenFile('compositing_image_filter.png', region: region); |
| }); |
| |
| test('pushImageFilter matrix', () async { |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| builder.pushImageFilter( |
| ui.ImageFilter.matrix( |
| ( |
| Matrix4.identity() |
| ..translate(40, 10) |
| ..rotateZ(math.pi / 6) |
| ..scale(0.75, 0.75) |
| ).toFloat64()), |
| ); |
| _drawTestPicture(builder); |
| builder.pop(); |
| |
| domDocument.body!.append(builder.build().webOnlyRootElement!); |
| |
| await matchGoldenFile('compositing_image_filter_matrix.png', region: region); |
| }); |
| |
| test('pushImageFilter using mode ColorFilter', () async { |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| // Applying the colorFilter should turn all the circles red. |
| builder.pushImageFilter( |
| const ui.ColorFilter.mode( |
| ui.Color(0xFFFF0000), |
| ui.BlendMode.srcIn, |
| )); |
| _drawTestPicture(builder); |
| builder.pop(); |
| |
| domDocument.body!.append(builder.build().webOnlyRootElement!); |
| |
| await matchGoldenFile('compositing_image_filter_using_mode_color_filter.png', region: region); |
| }); |
| |
| test('pushImageFilter using matrix ColorFilter', () async { |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| // Apply a "greyscale" color filter. |
| final List<double> colorMatrix = <double>[ |
| 0.2126, 0.7152, 0.0722, 0, 0, // |
| 0.2126, 0.7152, 0.0722, 0, 0, // |
| 0.2126, 0.7152, 0.0722, 0, 0, // |
| 0, 0, 0, 1, 0, // |
| ]; |
| |
| builder.pushImageFilter(ui.ColorFilter.matrix(colorMatrix)); |
| _drawTestPicture(builder); |
| builder.pop(); |
| |
| domDocument.body!.append(builder.build().webOnlyRootElement!); |
| |
| await matchGoldenFile('compositing_image_filter_using_matrix_color_filter.png', region: region); |
| }); |
| |
| group('Cull rect computation', () { |
| _testCullRectComputation(); |
| }); |
| } |
| |
| void _testCullRectComputation() { |
| // Draw a picture larger that screen. Verify that cull rect is equal to screen |
| // bounds. |
| test('fills screen bounds', () async { |
| final ui.SceneBuilder builder = ui.SceneBuilder(); |
| drawWithBitmapCanvas(builder, (RecordingCanvas canvas) { |
| canvas.drawCircle( |
| ui.Offset.zero, 10000, SurfacePaint()..style = ui.PaintingStyle.fill); |
| }); |
| builder.build(); |
| |
| final PersistedPicture picture = enumeratePictures().single; |
| expect(picture.optimalLocalCullRect, const ui.Rect.fromLTRB(0, 0, 500, 100)); |
| }, skip: ''' |
| TODO(https://github.com/flutter/flutter/issues/40395) |
| Needs ability to set iframe to 500,100 size. Current screen seems to be 500,500'''); |
| |
| // Draw a picture that overflows the screen. Verify that cull rect is the |
| // intersection of screen bounds and paint bounds. |
| test('intersects with screen bounds', () async { |
| final ui.SceneBuilder builder = ui.SceneBuilder(); |
| drawWithBitmapCanvas(builder, (RecordingCanvas canvas) { |
| canvas.drawCircle(ui.Offset.zero, 20, SurfacePaint()..style = ui.PaintingStyle.fill); |
| }); |
| builder.build(); |
| |
| final PersistedPicture picture = enumeratePictures().single; |
| expect(picture.optimalLocalCullRect, const ui.Rect.fromLTRB(0, 0, 20, 20)); |
| }); |
| |
| // Draw a picture that's fully outside the screen bounds. Verify the cull rect |
| // is zero. |
| test('fully outside screen bounds', () async { |
| final ui.SceneBuilder builder = ui.SceneBuilder(); |
| drawWithBitmapCanvas(builder, (RecordingCanvas canvas) { |
| canvas.drawCircle( |
| const ui.Offset(-100, -100), 20, SurfacePaint()..style = ui.PaintingStyle.fill); |
| }); |
| builder.build(); |
| |
| final PersistedPicture picture = enumeratePictures().single; |
| expect(picture.optimalLocalCullRect, ui.Rect.zero); |
| expect(picture.debugExactGlobalCullRect, ui.Rect.zero); |
| }); |
| |
| // Draw a picture that's fully inside the screen. Verify that cull rect is |
| // equal to the paint bounds. |
| test('limits to paint bounds if no clip layers', () async { |
| final ui.SceneBuilder builder = ui.SceneBuilder(); |
| drawWithBitmapCanvas(builder, (RecordingCanvas canvas) { |
| canvas.drawCircle( |
| const ui.Offset(50, 50), 10, SurfacePaint()..style = ui.PaintingStyle.fill); |
| }); |
| builder.build(); |
| |
| final PersistedPicture picture = enumeratePictures().single; |
| expect(picture.optimalLocalCullRect, const ui.Rect.fromLTRB(40, 40, 60, 60)); |
| }); |
| |
| // Draw a picture smaller than the screen. Offset it such that it remains |
| // fully inside the screen bounds. Verify that cull rect is still just the |
| // paint bounds. |
| test('offset does not affect paint bounds', () async { |
| final ui.SceneBuilder builder = ui.SceneBuilder(); |
| |
| builder.pushOffset(10, 10); |
| drawWithBitmapCanvas(builder, (RecordingCanvas canvas) { |
| canvas.drawCircle( |
| const ui.Offset(50, 50), 10, SurfacePaint()..style = ui.PaintingStyle.fill); |
| }); |
| builder.pop(); |
| |
| builder.build(); |
| |
| final PersistedPicture picture = enumeratePictures().single; |
| expect(picture.optimalLocalCullRect, const ui.Rect.fromLTRB(40, 40, 60, 60)); |
| }); |
| |
| // Draw a picture smaller than the screen. Offset it such that the picture |
| // overflows screen bounds. Verify that the cull rect is the intersection |
| // between screen bounds and paint bounds. |
| test('offset overflows paint bounds', () async { |
| final ui.SceneBuilder builder = ui.SceneBuilder(); |
| |
| builder.pushOffset(0, 90); |
| drawWithBitmapCanvas(builder, (RecordingCanvas canvas) { |
| canvas.drawCircle(ui.Offset.zero, 20, SurfacePaint()..style = ui.PaintingStyle.fill); |
| }); |
| builder.pop(); |
| |
| builder.build(); |
| |
| final PersistedPicture picture = enumeratePictures().single; |
| expect( |
| picture.debugExactGlobalCullRect, const ui.Rect.fromLTRB(0, 70, 20, 100)); |
| expect(picture.optimalLocalCullRect, const ui.Rect.fromLTRB(0, -20, 20, 10)); |
| }, skip: ''' |
| TODO(https://github.com/flutter/flutter/issues/40395) |
| Needs ability to set iframe to 500,100 size. Current screen seems to be 500,500'''); |
| |
| // Draw a picture inside a layer clip but fill all available space inside it. |
| // Verify that the cull rect is equal to the layer clip. |
| test('fills layer clip rect', () async { |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| builder.pushClipRect( |
| const ui.Rect.fromLTWH(10, 10, 60, 60), |
| ); |
| |
| builder.pushClipRect( |
| const ui.Rect.fromLTWH(40, 40, 60, 60), |
| ); |
| |
| drawWithBitmapCanvas(builder, (RecordingCanvas canvas) { |
| canvas.drawCircle( |
| ui.Offset.zero, 10000, SurfacePaint()..style = ui.PaintingStyle.fill); |
| }); |
| |
| builder.pop(); // pushClipRect |
| builder.pop(); // pushClipRect |
| domDocument.body!.append(builder.build().webOnlyRootElement!); |
| |
| await matchGoldenFile('compositing_cull_rect_fills_layer_clip.png', |
| region: region); |
| |
| final PersistedPicture picture = enumeratePictures().single; |
| expect(picture.optimalLocalCullRect, const ui.Rect.fromLTRB(40, 40, 70, 70)); |
| }); |
| |
| // Draw a picture inside a layer clip but position the picture such that its |
| // paint bounds overflow the layer clip. Verify that the cull rect is the |
| // intersection between the layer clip and paint bounds. |
| test('intersects layer clip rect and paint bounds', () async { |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| builder.pushClipRect( |
| const ui.Rect.fromLTWH(10, 10, 60, 60), |
| ); |
| |
| builder.pushClipRect( |
| const ui.Rect.fromLTWH(40, 40, 60, 60), |
| ); |
| |
| drawWithBitmapCanvas(builder, (RecordingCanvas canvas) { |
| canvas.drawCircle( |
| const ui.Offset(80, 55), 30, SurfacePaint()..style = ui.PaintingStyle.fill); |
| }); |
| |
| builder.pop(); // pushClipRect |
| builder.pop(); // pushClipRect |
| domDocument.body!.append(builder.build().webOnlyRootElement!); |
| |
| await matchGoldenFile( |
| 'compositing_cull_rect_intersects_clip_and_paint_bounds.png', |
| region: region); |
| |
| final PersistedPicture picture = enumeratePictures().single; |
| expect(picture.optimalLocalCullRect, const ui.Rect.fromLTRB(50, 40, 70, 70)); |
| }); |
| |
| // Draw a picture inside a layer clip that's positioned inside the clip using |
| // an offset layer. Verify that the cull rect is the intersection between the |
| // layer clip and the offset paint bounds. |
| test('offsets picture inside layer clip rect', () async { |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| builder.pushClipRect( |
| const ui.Rect.fromLTWH(10, 10, 60, 60), |
| ); |
| |
| builder.pushClipRect( |
| const ui.Rect.fromLTWH(40, 40, 60, 60), |
| ); |
| |
| builder.pushOffset(55, 70); |
| |
| drawWithBitmapCanvas(builder, (RecordingCanvas canvas) { |
| canvas.drawCircle(ui.Offset.zero, 20, SurfacePaint()..style = ui.PaintingStyle.fill); |
| }); |
| |
| builder.pop(); // pushOffset |
| builder.pop(); // pushClipRect |
| builder.pop(); // pushClipRect |
| domDocument.body!.append(builder.build().webOnlyRootElement!); |
| |
| await matchGoldenFile('compositing_cull_rect_offset_inside_layer_clip.png', |
| region: region); |
| |
| final PersistedPicture picture = enumeratePictures().single; |
| expect(picture.optimalLocalCullRect, |
| const ui.Rect.fromLTRB(-15.0, -20.0, 15.0, 0.0)); |
| }); |
| |
| // Draw a picture inside a layer clip that's positioned an offset layer such |
| // that the picture is push completely outside the clip area. Verify that the |
| // cull rect is zero. |
| test('zero intersection with clip', () async { |
| final ui.SceneBuilder builder = ui.SceneBuilder(); |
| builder.pushClipRect( |
| const ui.Rect.fromLTWH(10, 10, 60, 60), |
| ); |
| |
| builder.pushClipRect( |
| const ui.Rect.fromLTWH(40, 40, 60, 60), |
| ); |
| |
| builder.pushOffset(100, 50); |
| |
| drawWithBitmapCanvas(builder, (RecordingCanvas canvas) { |
| canvas.drawCircle(ui.Offset.zero, 20, SurfacePaint()..style = ui.PaintingStyle.fill); |
| }); |
| |
| builder.pop(); // pushOffset |
| builder.pop(); // pushClipRect |
| builder.pop(); // pushClipRect |
| |
| builder.build(); |
| |
| final PersistedPicture picture = enumeratePictures().single; |
| expect(picture.optimalLocalCullRect, ui.Rect.zero); |
| expect(picture.debugExactGlobalCullRect, ui.Rect.zero); |
| }); |
| |
| // Draw a picture inside a rotated clip. Verify that the cull rect is big |
| // enough to fit the rotated clip. |
| test('rotates clip and the picture', () async { |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| builder.pushOffset(80, 50); |
| |
| builder.pushTransform( |
| Matrix4.rotationZ(-math.pi / 4).toFloat64(), |
| ); |
| |
| builder.pushClipRect( |
| const ui.Rect.fromLTRB(-10, -10, 10, 10), |
| ); |
| |
| builder.pushTransform( |
| Matrix4.rotationZ(math.pi / 4).toFloat64(), |
| ); |
| |
| drawWithBitmapCanvas(builder, (RecordingCanvas canvas) { |
| canvas.drawPaint(SurfacePaint() |
| ..color = const ui.Color.fromRGBO(0, 0, 255, 0.6) |
| ..style = ui.PaintingStyle.fill); |
| canvas.drawRect( |
| const ui.Rect.fromLTRB(-5, -5, 5, 5), |
| SurfacePaint() |
| ..color = const ui.Color.fromRGBO(0, 255, 0, 1.0) |
| ..style = ui.PaintingStyle.fill, |
| ); |
| }); |
| |
| builder.pop(); // pushTransform |
| builder.pop(); // pushClipRect |
| builder.pop(); // pushTransform |
| builder.pop(); // pushOffset |
| domDocument.body!.append(builder.build().webOnlyRootElement!); |
| |
| await matchGoldenFile('compositing_cull_rect_rotated.png', region: region); |
| |
| final PersistedPicture picture = enumeratePictures().single; |
| expect( |
| picture.optimalLocalCullRect, |
| within( |
| distance: 0.05, from: const ui.Rect.fromLTRB(-14.1, -14.1, 14.1, 14.1)), |
| ); |
| }); |
| |
| test('pushClipPath', () async { |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| final ui.Path path = ui.Path(); |
| path.addRect(const ui.Rect.fromLTRB(10, 10, 60, 60)); |
| builder.pushClipPath( |
| path, |
| ); |
| _drawTestPicture(builder); |
| builder.pop(); |
| |
| domDocument.body!.append(builder.build().webOnlyRootElement!); |
| |
| await matchGoldenFile('compositing_clip_path.png', region: region); |
| }); |
| |
| // Draw a picture inside a rotated clip. Verify that the cull rect is big |
| // enough to fit the rotated clip. |
| test('clips correctly when using 3d transforms', () async { |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| |
| builder.pushTransform(Matrix4.diagonal3Values( |
| EnginePlatformDispatcher.browserDevicePixelRatio, |
| EnginePlatformDispatcher.browserDevicePixelRatio, |
| 1.0) |
| .toFloat64()); |
| |
| // TODO(yjbanov): see the TODO below. |
| // final double screenWidth = domWindow.innerWidth.toDouble(); |
| // final double screenHeight = domWindow.innerHeight.toDouble(); |
| |
| final Matrix4 scaleTransform = Matrix4.identity().scaled(0.5, 0.2); |
| builder.pushTransform( |
| scaleTransform.toFloat64(), |
| ); |
| |
| builder.pushOffset(400, 200); |
| |
| builder.pushClipRect( |
| const ui.Rect.fromLTRB(-200, -200, 200, 200), |
| ); |
| |
| builder |
| .pushTransform(Matrix4.rotationY(45.0 * math.pi / 180.0).toFloat64()); |
| |
| builder.pushClipRect( |
| const ui.Rect.fromLTRB(-140, -140, 140, 140), |
| ); |
| |
| builder.pushTransform(Matrix4.translationValues(0, 0, -50).toFloat64()); |
| |
| drawWithBitmapCanvas(builder, (RecordingCanvas canvas) { |
| canvas.drawPaint(SurfacePaint() |
| ..color = const ui.Color.fromRGBO(0, 0, 255, 0.6) |
| ..style = ui.PaintingStyle.fill); |
| // ui.Rect will be clipped. |
| canvas.drawRect( |
| const ui.Rect.fromLTRB(-150, -150, 150, 150), |
| SurfacePaint() |
| ..color = const ui.Color.fromRGBO(0, 255, 0, 1.0) |
| ..style = ui.PaintingStyle.fill, |
| ); |
| // Should be outside the clip range. |
| canvas.drawRect( |
| const ui.Rect.fromLTRB(-150, -150, -140, -140), |
| SurfacePaint() |
| ..color = const ui.Color.fromARGB(0xE0, 255, 0, 0) |
| ..style = ui.PaintingStyle.fill, |
| ); |
| canvas.drawRect( |
| const ui.Rect.fromLTRB(140, -150, 150, -140), |
| SurfacePaint() |
| ..color = const ui.Color.fromARGB(0xE0, 255, 0, 0) |
| ..style = ui.PaintingStyle.fill, |
| ); |
| canvas.drawRect( |
| const ui.Rect.fromLTRB(-150, 140, -140, 150), |
| SurfacePaint() |
| ..color = const ui.Color.fromARGB(0xE0, 255, 0, 0) |
| ..style = ui.PaintingStyle.fill, |
| ); |
| canvas.drawRect( |
| const ui.Rect.fromLTRB(140, 140, 150, 150), |
| SurfacePaint() |
| ..color = const ui.Color.fromARGB(0xE0, 255, 0, 0) |
| ..style = ui.PaintingStyle.fill, |
| ); |
| // Should be inside clip range |
| canvas.drawRect( |
| const ui.Rect.fromLTRB(-100, -100, -90, -90), |
| SurfacePaint() |
| ..color = const ui.Color.fromARGB(0xE0, 0, 0, 0x80) |
| ..style = ui.PaintingStyle.fill, |
| ); |
| canvas.drawRect( |
| const ui.Rect.fromLTRB(90, -100, 100, -90), |
| SurfacePaint() |
| ..color = const ui.Color.fromARGB(0xE0, 0, 0, 0x80) |
| ..style = ui.PaintingStyle.fill, |
| ); |
| canvas.drawRect( |
| const ui.Rect.fromLTRB(-100, 90, -90, 100), |
| SurfacePaint() |
| ..color = const ui.Color.fromARGB(0xE0, 0, 0, 0x80) |
| ..style = ui.PaintingStyle.fill, |
| ); |
| canvas.drawRect( |
| const ui.Rect.fromLTRB(90, 90, 100, 100), |
| SurfacePaint() |
| ..color = const ui.Color.fromARGB(0xE0, 0, 0, 0x80) |
| ..style = ui.PaintingStyle.fill, |
| ); |
| }); |
| |
| builder.pop(); // pushTransform Z-50 |
| builder.pop(); // pushClipRect |
| builder.pop(); // pushTransform 3D rotate |
| builder.pop(); // pushClipRect |
| builder.pop(); // pushOffset |
| builder.pop(); // pushTransform scale |
| builder.pop(); // pushTransform scale devicepixelratio |
| domDocument.body!.append(builder.build().webOnlyRootElement!); |
| |
| await matchGoldenFile('compositing_3d_rotate1.png', region: region); |
| |
| // ignore: unused_local_variable |
| final PersistedPicture picture = enumeratePictures().single; |
| // TODO(yjbanov): https://github.com/flutter/flutter/issues/40395) |
| // Needs ability to set iframe to 500,100 size. Current screen seems to be 500,500. |
| // expect( |
| // picture.optimalLocalCullRect, |
| // within( |
| // distance: 0.05, |
| // from: ui.Rect.fromLTRB( |
| // -140, -140, screenWidth - 360.0, screenHeight + 40.0)), |
| // ); |
| }); |
| |
| // This test reproduces text blurriness when two pieces of text appear inside |
| // two nested clips: |
| // |
| // ┌───────────────────────┐ |
| // │ text in outer clip │ |
| // │ ┌────────────────────┐│ |
| // │ │ text in inner clip ││ |
| // │ └────────────────────┘│ |
| // └───────────────────────┘ |
| // |
| // This test clips using layers. See a similar test in `bitmap_canvas_golden_test.dart`, |
| // which clips using canvas. |
| // |
| // More details: https://github.com/flutter/flutter/issues/32274 |
| test( |
| 'renders clipped text with high quality', |
| () async { |
| // To reproduce blurriness we need real clipping. |
| final CanvasParagraph paragraph = |
| (ui.ParagraphBuilder(ui.ParagraphStyle(fontFamily: 'Roboto')) |
| // Use a decoration to force rendering in DOM mode. |
| ..pushStyle(ui.TextStyle(decoration: ui.TextDecoration.lineThrough, decorationColor: const ui.Color(0x00000000))) |
| ..addText('Am I blurry?')) |
| .build() as CanvasParagraph; |
| paragraph.layout(const ui.ParagraphConstraints(width: 1000)); |
| |
| final ui.Rect canvasSize = ui.Rect.fromLTRB( |
| 0, |
| 0, |
| paragraph.maxIntrinsicWidth + 16, |
| 2 * paragraph.height + 32, |
| ); |
| final ui.Rect outerClip = |
| ui.Rect.fromLTRB(0.5, 0.5, canvasSize.right, canvasSize.bottom); |
| final ui.Rect innerClip = ui.Rect.fromLTRB(0.5, canvasSize.bottom / 2 + 0.5, |
| canvasSize.right, canvasSize.bottom); |
| final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); |
| |
| builder.pushClipRect(outerClip); |
| |
| { |
| final EnginePictureRecorder recorder = EnginePictureRecorder(); |
| final RecordingCanvas canvas = recorder.beginRecording(outerClip); |
| canvas.drawParagraph(paragraph, const ui.Offset(8.5, 8.5)); |
| final ui.Picture picture = recorder.endRecording(); |
| expect(paragraph.canDrawOnCanvas, isFalse); |
| |
| builder.addPicture( |
| ui.Offset.zero, |
| picture, |
| ); |
| } |
| |
| builder.pushClipRect(innerClip); |
| { |
| final EnginePictureRecorder recorder = EnginePictureRecorder(); |
| final RecordingCanvas canvas = recorder.beginRecording(innerClip); |
| canvas.drawParagraph(paragraph, ui.Offset(8.5, 8.5 + innerClip.top)); |
| final ui.Picture picture = recorder.endRecording(); |
| expect(paragraph.canDrawOnCanvas, isFalse); |
| |
| builder.addPicture( |
| ui.Offset.zero, |
| picture, |
| ); |
| } |
| builder.pop(); // inner clip |
| builder.pop(); // outer clip |
| |
| final DomElement sceneElement = builder.build().webOnlyRootElement!; |
| expect( |
| sceneElement |
| .querySelectorAll('flt-paragraph') |
| .map<String>((DomElement e) => e.innerText) |
| .toList(), |
| <String>['Am I blurry?', 'Am I blurry?'], |
| reason: 'Expected to render text using HTML', |
| ); |
| domDocument.body!.append(sceneElement); |
| |
| await matchGoldenFile( |
| 'compositing_draw_high_quality_text.png', |
| region: canvasSize, |
| ); |
| }, |
| testOn: 'chrome', |
| ); |
| } |
| |
| void _drawTestPicture(ui.SceneBuilder builder) { |
| final EnginePictureRecorder recorder = EnginePictureRecorder(); |
| final RecordingCanvas canvas = |
| recorder.beginRecording(const ui.Rect.fromLTRB(0, 0, 100, 100)); |
| canvas.drawCircle( |
| const ui.Offset(10, 10), 10, SurfacePaint()..style = ui.PaintingStyle.fill); |
| canvas.drawCircle( |
| const ui.Offset(60, 10), |
| 10, |
| SurfacePaint() |
| ..style = ui.PaintingStyle.fill |
| ..color = const ui.Color.fromRGBO(255, 0, 0, 1)); |
| canvas.drawCircle( |
| const ui.Offset(10, 60), |
| 10, |
| SurfacePaint() |
| ..style = ui.PaintingStyle.fill |
| ..color = const ui.Color.fromRGBO(0, 255, 0, 1)); |
| canvas.drawCircle( |
| const ui.Offset(60, 60), |
| 10, |
| SurfacePaint() |
| ..style = ui.PaintingStyle.fill |
| ..color = const ui.Color.fromRGBO(0, 0, 255, 1)); |
| final ui.Picture picture = recorder.endRecording(); |
| |
| builder.addPicture( |
| ui.Offset.zero, |
| picture, |
| ); |
| } |
| |
| typedef PaintCallback = void Function(RecordingCanvas canvas); |
| |
| void drawWithBitmapCanvas(ui.SceneBuilder builder, PaintCallback callback, |
| {ui.Rect bounds = ui.Rect.largest}) { |
| final EnginePictureRecorder recorder = EnginePictureRecorder(); |
| final RecordingCanvas canvas = recorder.beginRecording(bounds); |
| |
| canvas.debugEnforceArbitraryPaint(); |
| callback(canvas); |
| final ui.Picture picture = recorder.endRecording(); |
| |
| builder.addPicture( |
| ui.Offset.zero, |
| picture, |
| ); |
| } |