| // 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:async'; |
| import 'dart:io'; |
| import 'dart:math'; |
| import 'dart:typed_data'; |
| import 'dart:ui'; |
| |
| import 'package:path/path.dart' as path; |
| import 'package:test/test.dart'; |
| import 'package:vector_math/vector_math_64.dart'; |
| |
| import 'goldens.dart'; |
| import 'impeller_enabled.dart'; |
| |
| typedef CanvasCallback = void Function(Canvas canvas); |
| |
| Future<Image> createImage(int width, int height) { |
| final Completer<Image> completer = Completer<Image>(); |
| decodeImageFromPixels( |
| Uint8List.fromList(List<int>.generate( |
| width * height * 4, |
| (int pixel) => pixel % 255, |
| )), |
| width, |
| height, |
| PixelFormat.rgba8888, |
| (Image image) { |
| completer.complete(image); |
| }, |
| ); |
| |
| return completer.future; |
| } |
| |
| void testCanvas(CanvasCallback callback) { |
| try { |
| callback(Canvas(PictureRecorder(), Rect.zero)); |
| } catch (error) {} // ignore: empty_catches |
| } |
| |
| Future<Image> toImage(CanvasCallback callback, int width, int height) { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas( |
| recorder, Rect.fromLTRB(0, 0, width.toDouble(), height.toDouble())); |
| callback(canvas); |
| final Picture picture = recorder.endRecording(); |
| return picture.toImage(width, height); |
| } |
| |
| void testNoCrashes() { |
| test('canvas APIs should not crash', () async { |
| final Paint paint = Paint(); |
| const Rect rect = |
| Rect.fromLTRB(double.nan, double.nan, double.nan, double.nan); |
| final RRect rrect = RRect.fromRectAndCorners(rect); |
| const Offset offset = Offset(double.nan, double.nan); |
| final Path path = Path(); |
| const Color color = Color(0x00000000); |
| final Paragraph paragraph = ParagraphBuilder(ParagraphStyle()).build(); |
| |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas recorderCanvas = Canvas(recorder); |
| recorderCanvas.scale(1.0, 1.0); |
| final Picture picture = recorder.endRecording(); |
| final Image image = await picture.toImage(1, 1); |
| |
| try { |
| Canvas(PictureRecorder()); |
| } catch (error) {} // ignore: empty_catches |
| try { |
| Canvas(PictureRecorder(), rect); |
| } catch (error) {} // ignore: empty_catches |
| |
| try { |
| PictureRecorder() |
| ..endRecording() |
| ..endRecording() |
| ..endRecording(); |
| } catch (error) {} // ignore: empty_catches |
| |
| testCanvas((Canvas canvas) => canvas.clipPath(path)); |
| testCanvas((Canvas canvas) => canvas.clipRect(rect)); |
| testCanvas((Canvas canvas) => canvas.clipRRect(rrect)); |
| testCanvas((Canvas canvas) => canvas.drawArc(rect, 0.0, 0.0, false, paint)); |
| testCanvas((Canvas canvas) => canvas.drawAtlas(image, <RSTransform>[], |
| <Rect>[], <Color>[], BlendMode.src, rect, paint)); |
| testCanvas((Canvas canvas) => canvas.drawCircle(offset, double.nan, paint)); |
| testCanvas((Canvas canvas) => canvas.drawColor(color, BlendMode.src)); |
| testCanvas((Canvas canvas) => canvas.drawDRRect(rrect, rrect, paint)); |
| testCanvas((Canvas canvas) => canvas.drawImage(image, offset, paint)); |
| testCanvas( |
| (Canvas canvas) => canvas.drawImageNine(image, rect, rect, paint)); |
| testCanvas( |
| (Canvas canvas) => canvas.drawImageRect(image, rect, rect, paint)); |
| testCanvas((Canvas canvas) => canvas.drawLine(offset, offset, paint)); |
| testCanvas((Canvas canvas) => canvas.drawOval(rect, paint)); |
| testCanvas((Canvas canvas) => canvas.drawPaint(paint)); |
| testCanvas((Canvas canvas) => canvas.drawParagraph(paragraph, offset)); |
| testCanvas((Canvas canvas) => canvas.drawPath(path, paint)); |
| testCanvas((Canvas canvas) => canvas.drawPicture(picture)); |
| testCanvas((Canvas canvas) => |
| canvas.drawPoints(PointMode.points, <Offset>[], paint)); |
| testCanvas((Canvas canvas) => canvas.drawRawAtlas(image, Float32List(0), |
| Float32List(0), Int32List(0), BlendMode.src, rect, paint)); |
| testCanvas((Canvas canvas) => |
| canvas.drawRawPoints(PointMode.points, Float32List(0), paint)); |
| testCanvas((Canvas canvas) => canvas.drawRect(rect, paint)); |
| testCanvas((Canvas canvas) => canvas.drawRRect(rrect, paint)); |
| testCanvas( |
| (Canvas canvas) => canvas.drawShadow(path, color, double.nan, false)); |
| testCanvas( |
| (Canvas canvas) => canvas.drawShadow(path, color, double.nan, true)); |
| testCanvas((Canvas canvas) => canvas.drawVertices( |
| Vertices(VertexMode.triangles, <Offset>[]), BlendMode.screen, paint)); |
| testCanvas((Canvas canvas) => canvas.getSaveCount()); |
| testCanvas((Canvas canvas) => canvas.restore()); |
| testCanvas((Canvas canvas) => canvas.rotate(double.nan)); |
| testCanvas((Canvas canvas) => canvas.save()); |
| testCanvas((Canvas canvas) => canvas.saveLayer(rect, paint)); |
| testCanvas((Canvas canvas) => canvas.saveLayer(null, paint)); |
| testCanvas((Canvas canvas) => canvas.scale(double.nan, double.nan)); |
| testCanvas((Canvas canvas) => canvas.skew(double.nan, double.nan)); |
| testCanvas((Canvas canvas) => canvas.transform(Float64List(16))); |
| testCanvas((Canvas canvas) => canvas.translate(double.nan, double.nan)); |
| testCanvas((Canvas canvas) => canvas.drawVertices( |
| Vertices(VertexMode.triangles, <Offset>[], indices: <int>[]), |
| BlendMode.screen, |
| paint)); |
| testCanvas((Canvas canvas) => canvas.drawVertices( |
| Vertices(VertexMode.triangles, <Offset>[])..dispose(), |
| BlendMode.screen, |
| paint)); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/115143 |
| testCanvas((Canvas canvas) => canvas.drawPaint(Paint() |
| ..imageFilter = |
| const ColorFilter.mode(Color(0x00000000), BlendMode.xor))); |
| |
| // Regression test for https://github.com/flutter/flutter/issues/120278 |
| testCanvas((Canvas canvas) => canvas.drawPaint(Paint() |
| ..imageFilter = ImageFilter.compose( |
| outer: ImageFilter.matrix(Matrix4.identity().storage), |
| inner: ImageFilter.blur()))); |
| }); |
| } |
| |
| const String kFlutterBuildDirectory = 'kFlutterBuildDirectory'; |
| |
| String get _flutterBuildPath { |
| const String buildPath = String.fromEnvironment(kFlutterBuildDirectory); |
| if (buildPath.isEmpty) { |
| throw StateError('kFlutterBuildDirectory -D variable is not set.'); |
| } |
| return buildPath; |
| } |
| |
| void main() async { |
| final ImageComparer comparer = await ImageComparer.create(); |
| |
| testNoCrashes(); |
| |
| test('Simple .toImage', () async { |
| final Image image = await toImage((Canvas canvas) { |
| final Path circlePath = Path() |
| ..addOval( |
| Rect.fromCircle(center: const Offset(40.0, 40.0), radius: 20.0)); |
| final Paint paint = Paint() |
| ..isAntiAlias = false |
| ..style = PaintingStyle.fill; |
| canvas.drawPath(circlePath, paint); |
| }, 100, 100); |
| expect(image.width, equals(100)); |
| expect(image.height, equals(100)); |
| await comparer.addGoldenImage(image, 'canvas_test_toImage.png'); |
| }); |
| |
| Gradient makeGradient() { |
| return Gradient.linear( |
| Offset.zero, |
| const Offset(100, 100), |
| const <Color>[Color(0xFF4C4D52), Color(0xFF202124)], |
| ); |
| } |
| |
| test('Simple gradient, which is implicitly dithered', () async { |
| final Image image = await toImage((Canvas canvas) { |
| final Paint paint = Paint()..shader = makeGradient(); |
| canvas.drawPaint(paint); |
| }, 100, 100); |
| expect(image.width, equals(100)); |
| expect(image.height, equals(100)); |
| |
| await comparer.addGoldenImage(image, 'canvas_test_dithered_gradient.png'); |
| }); |
| |
| test('Null values allowed for drawAtlas methods', () async { |
| final Image image = await createImage(100, 100); |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| const Rect rect = Rect.fromLTWH(0, 0, 100, 100); |
| final RSTransform transform = RSTransform(1, 0, 0, 0); |
| const Color color = Color(0x00000000); |
| final Paint paint = Paint(); |
| canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[rect], |
| <Color>[color], BlendMode.src, rect, paint); |
| canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[rect], |
| <Color>[color], BlendMode.src, null, paint); |
| canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[rect], <Color>[], |
| null, rect, paint); |
| canvas.drawAtlas( |
| image, <RSTransform>[transform], <Rect>[rect], null, null, rect, paint); |
| canvas.drawRawAtlas(image, Float32List(0), Float32List(0), Int32List(0), |
| BlendMode.src, rect, paint); |
| canvas.drawRawAtlas(image, Float32List(0), Float32List(0), Int32List(0), |
| BlendMode.src, null, paint); |
| canvas.drawRawAtlas( |
| image, Float32List(0), Float32List(0), null, null, rect, paint); |
| |
| expect( |
| () => canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[rect], |
| <Color>[color], null, rect, paint), |
| throwsA(isA<AssertionError>()), |
| ); |
| }); |
| |
| test('Data lengths must match for drawAtlas methods', () async { |
| final Image image = await createImage(100, 100); |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| const Rect rect = Rect.fromLTWH(0, 0, 100, 100); |
| final RSTransform transform = RSTransform(1, 0, 0, 0); |
| const Color color = Color(0x00000000); |
| final Paint paint = Paint(); |
| canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[rect], |
| <Color>[color], BlendMode.src, rect, paint); |
| canvas.drawAtlas(image, <RSTransform>[transform, transform], |
| <Rect>[rect, rect], <Color>[color, color], BlendMode.src, rect, paint); |
| canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[rect], <Color>[], |
| null, rect, paint); |
| canvas.drawAtlas( |
| image, <RSTransform>[transform], <Rect>[rect], null, null, rect, paint); |
| canvas.drawRawAtlas(image, Float32List(0), Float32List(0), Int32List(0), |
| BlendMode.src, rect, paint); |
| canvas.drawRawAtlas(image, Float32List(4), Float32List(4), Int32List(1), |
| BlendMode.src, rect, paint); |
| canvas.drawRawAtlas( |
| image, Float32List(4), Float32List(4), null, null, rect, paint); |
| |
| expect( |
| () => canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[], |
| <Color>[color], BlendMode.src, rect, paint), |
| throwsArgumentError); |
| expect( |
| () => canvas.drawAtlas(image, <RSTransform>[], <Rect>[rect], |
| <Color>[color], BlendMode.src, rect, paint), |
| throwsArgumentError); |
| expect( |
| () => canvas.drawAtlas(image, <RSTransform>[transform], <Rect>[rect], |
| <Color>[color, color], BlendMode.src, rect, paint), |
| throwsArgumentError); |
| expect( |
| () => canvas.drawAtlas(image, <RSTransform>[transform], |
| <Rect>[rect, rect], <Color>[color], BlendMode.src, rect, paint), |
| throwsArgumentError); |
| expect( |
| () => canvas.drawAtlas(image, <RSTransform>[transform, transform], |
| <Rect>[rect], <Color>[color], BlendMode.src, rect, paint), |
| throwsArgumentError); |
| expect( |
| () => canvas.drawRawAtlas( |
| image, Float32List(3), Float32List(3), null, null, rect, paint), |
| throwsArgumentError); |
| expect( |
| () => canvas.drawRawAtlas( |
| image, Float32List(4), Float32List(0), null, null, rect, paint), |
| throwsArgumentError); |
| expect( |
| () => canvas.drawRawAtlas( |
| image, Float32List(0), Float32List(4), null, null, rect, paint), |
| throwsArgumentError); |
| expect( |
| () => canvas.drawRawAtlas(image, Float32List(4), Float32List(4), |
| Int32List(2), BlendMode.src, rect, paint), |
| throwsArgumentError); |
| }); |
| |
| test('Canvas preserves perspective data in Matrix4', () async { |
| const double rotateAroundX = pi / 6; // 30 degrees |
| const double rotateAroundY = pi / 9; // 20 degrees |
| const int width = 150; |
| const int height = 150; |
| const Color black = Color.fromARGB(255, 0, 0, 0); |
| const Color green = Color.fromARGB(255, 0, 255, 0); |
| void paint(Canvas canvas, CanvasCallback rotate) { |
| canvas.translate(width * 0.5, height * 0.5); |
| rotate(canvas); |
| const double width3 = width / 3.0; |
| const double width5 = width / 5.0; |
| const double width10 = width / 10.0; |
| canvas.drawRect(const Rect.fromLTRB(-width3, -width3, width3, width3), |
| Paint()..color = green); |
| canvas.drawRect(const Rect.fromLTRB(-width5, -width5, -width10, width5), |
| Paint()..color = black); |
| canvas.drawRect(const Rect.fromLTRB(-width5, -width5, width5, -width10), |
| Paint()..color = black); |
| } |
| |
| final Image incrementalMatrixImage = await toImage((Canvas canvas) { |
| paint(canvas, (Canvas canvas) { |
| final Matrix4 matrix = Matrix4.identity(); |
| matrix.setEntry(3, 2, 0.001); |
| canvas.transform(matrix.storage); |
| matrix.setRotationX(rotateAroundX); |
| canvas.transform(matrix.storage); |
| matrix.setRotationY(rotateAroundY); |
| canvas.transform(matrix.storage); |
| }); |
| }, width, height); |
| final Image combinedMatrixImage = await toImage((Canvas canvas) { |
| paint(canvas, (Canvas canvas) { |
| final Matrix4 matrix = Matrix4.identity(); |
| matrix.setEntry(3, 2, 0.001); |
| matrix.rotateX(rotateAroundX); |
| matrix.rotateY(rotateAroundY); |
| canvas.transform(matrix.storage); |
| }); |
| }, width, height); |
| |
| final bool areEqual = await comparer.fuzzyCompareImages( |
| incrementalMatrixImage, combinedMatrixImage); |
| |
| expect(areEqual, true); |
| }); |
| |
| test('Path effects from Paragraphs do not affect further rendering', |
| () async { |
| void drawText(Canvas canvas, String content, Offset offset, |
| {TextDecorationStyle style = TextDecorationStyle.solid}) { |
| final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle()); |
| builder.pushStyle(TextStyle( |
| decoration: TextDecoration.underline, |
| decorationColor: const Color(0xFF0000FF), |
| fontFamily: 'Ahem', |
| fontSize: 10, |
| color: const Color(0xFF000000), |
| decorationStyle: style, |
| )); |
| builder.addText(content); |
| final Paragraph paragraph = builder.build(); |
| paragraph.layout(const ParagraphConstraints(width: 100)); |
| canvas.drawParagraph(paragraph, offset); |
| } |
| |
| final Image image = await toImage((Canvas canvas) { |
| canvas.drawColor(const Color(0xFFFFFFFF), BlendMode.srcOver); |
| final Paint paint = Paint() |
| ..style = PaintingStyle.stroke |
| ..strokeWidth = 5; |
| drawText(canvas, 'Hello World', const Offset(20, 10)); |
| canvas.drawCircle( |
| const Offset(150, 25), 15, paint..color = const Color(0xFF00FF00)); |
| drawText(canvas, 'Regular text', const Offset(20, 60)); |
| canvas.drawCircle( |
| const Offset(150, 75), 15, paint..color = const Color(0xFFFFFF00)); |
| drawText(canvas, 'Dotted text', const Offset(20, 110), |
| style: TextDecorationStyle.dotted); |
| canvas.drawCircle( |
| const Offset(150, 125), 15, paint..color = const Color(0xFFFF0000)); |
| drawText(canvas, 'Dashed text', const Offset(20, 160), |
| style: TextDecorationStyle.dashed); |
| canvas.drawCircle( |
| const Offset(150, 175), 15, paint..color = const Color(0xFFFF0000)); |
| drawText(canvas, 'Wavy text', const Offset(20, 210), |
| style: TextDecorationStyle.wavy); |
| canvas.drawCircle( |
| const Offset(150, 225), 15, paint..color = const Color(0xFFFF0000)); |
| }, 200, 250); |
| expect(image.width, equals(200)); |
| expect(image.height, equals(250)); |
| |
| await comparer.addGoldenImage( |
| image, 'dotted_path_effect_mixed_with_stroked_geometry.png'); |
| }); |
| |
| test('Gradients with matrices in Paragraphs render correctly', () async { |
| final Image image = await toImage((Canvas canvas) { |
| final Paint p = Paint(); |
| final Float64List transform = Float64List.fromList(<double>[ |
| 86.80000129342079, |
| 0.0, |
| 0.0, |
| 0.0, |
| 0.0, |
| 94.5, |
| 0.0, |
| 0.0, |
| 0.0, |
| 0.0, |
| 1.0, |
| 0.0, |
| 60.0, |
| 224.310302734375, |
| 0.0, |
| 1.0 |
| ]); |
| p.shader = Gradient.radial( |
| const Offset(2.5, 0.33), |
| 0.8, |
| <Color>[ |
| const Color(0xffff0000), |
| const Color(0xff00ff00), |
| const Color(0xff0000ff), |
| const Color(0xffff00ff) |
| ], |
| <double>[0.0, 0.3, 0.7, 0.9], |
| TileMode.mirror, |
| transform, |
| const Offset(2.55, 0.4)); |
| final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle()); |
| builder.pushStyle(TextStyle( |
| foreground: p, |
| fontSize: 200, |
| )); |
| builder.addText('Woodstock!'); |
| final Paragraph paragraph = builder.build(); |
| paragraph.layout(const ParagraphConstraints(width: 1000)); |
| canvas.drawParagraph(paragraph, const Offset(10, 150)); |
| }, 600, 400); |
| expect(image.width, equals(600)); |
| expect(image.height, equals(400)); |
| |
| await comparer.addGoldenImage(image, 'text_with_gradient_with_matrix.png'); |
| }); |
| |
| test('toImageSync - too big', () async { |
| PictureRecorder recorder = PictureRecorder(); |
| Canvas canvas = Canvas(recorder); |
| canvas.drawPaint(Paint()..color = const Color(0xFF123456)); |
| final Picture picture = recorder.endRecording(); |
| final Image image = picture.toImageSync(300000, 4000000); |
| picture.dispose(); |
| |
| expect(image.width, 300000); |
| expect(image.height, 4000000); |
| |
| recorder = PictureRecorder(); |
| canvas = Canvas(recorder); |
| |
| if (impellerEnabled) { |
| // Impeller tries to automagically scale this. See |
| // https://github.com/flutter/flutter/issues/128885 |
| canvas.drawImage(image, Offset.zero, Paint()); |
| return; |
| } |
| // On a slower CI machine, the raster thread may get behind the UI thread |
| // here. However, once the image is in an error state it will immediately |
| // throw on subsequent attempts. |
| bool caughtException = false; |
| for (int iterations = 0; iterations < 1000; iterations += 1) { |
| try { |
| canvas.drawImage(image, Offset.zero, Paint()); |
| } on PictureRasterizationException catch (e) { |
| caughtException = true; |
| expect( |
| e.message, |
| contains( |
| 'unable to create bitmap render target at specified size ${image.width}x${image.height}'), |
| ); |
| break; |
| } |
| // Let the event loop turn. |
| await Future<void>.delayed(const Duration(milliseconds: 1)); |
| } |
| expect(caughtException, true); |
| expect( |
| () => canvas.drawImageRect(image, Rect.zero, Rect.zero, Paint()), |
| throwsException, |
| ); |
| expect( |
| () => canvas.drawImageNine(image, Rect.zero, Rect.zero, Paint()), |
| throwsException, |
| ); |
| expect( |
| () => canvas.drawAtlas( |
| image, <RSTransform>[], <Rect>[], null, null, null, Paint()), |
| throwsException, |
| ); |
| }); |
| |
| test('toImageSync - succeeds', () async { |
| PictureRecorder recorder = PictureRecorder(); |
| Canvas canvas = Canvas(recorder); |
| canvas.drawPaint(Paint()..color = const Color(0xFF123456)); |
| final Picture picture = recorder.endRecording(); |
| final Image image = picture.toImageSync(30, 40); |
| picture.dispose(); |
| |
| expect(image.width, 30); |
| expect(image.height, 40); |
| |
| recorder = PictureRecorder(); |
| canvas = Canvas(recorder); |
| expect( |
| () => canvas.drawImage(image, Offset.zero, Paint()), |
| returnsNormally, |
| ); |
| expect( |
| () => canvas.drawImageRect(image, Rect.zero, Rect.zero, Paint()), |
| returnsNormally, |
| ); |
| expect( |
| () => canvas.drawImageNine(image, Rect.zero, Rect.zero, Paint()), |
| returnsNormally, |
| ); |
| expect( |
| () => canvas.drawAtlas( |
| image, <RSTransform>[], <Rect>[], null, null, null, Paint()), |
| returnsNormally, |
| ); |
| }); |
| |
| test('toImageSync - toByteData', () async { |
| const Color color = Color(0xFF123456); |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| canvas.drawPaint(Paint()..color = color); |
| final Picture picture = recorder.endRecording(); |
| final Image image = picture.toImageSync(6, 8); |
| picture.dispose(); |
| |
| expect(image.width, 6); |
| expect(image.height, 8); |
| |
| final ByteData? data = await image.toByteData(); |
| |
| expect(data, isNotNull); |
| expect(data!.lengthInBytes, 6 * 8 * 4); |
| expect(data.buffer.asUint8List()[0], 0x12); |
| expect(data.buffer.asUint8List()[1], 0x34); |
| expect(data.buffer.asUint8List()[2], 0x56); |
| expect(data.buffer.asUint8List()[3], 0xFF); |
| }); |
| |
| test('toImage and toImageSync have identical contents', () async { |
| // Note: on linux this stil seems to be different. |
| // TODO(jonahwilliams): https://github.com/flutter/flutter/issues/108835 |
| if (Platform.isLinux) { |
| return; |
| } |
| |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| canvas.drawRect( |
| const Rect.fromLTWH(20, 20, 100, 100), |
| Paint()..color = const Color(0xA0FF6D00), |
| ); |
| final Picture picture = recorder.endRecording(); |
| final Image toImageImage = await picture.toImage(200, 200); |
| final Image toImageSyncImage = picture.toImageSync(200, 200); |
| |
| // To trigger observable difference in alpha, draw image |
| // on a second canvas. |
| Future<ByteData> drawOnCanvas(Image image) async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| canvas.drawPaint(Paint()..color = const Color(0x4FFFFFFF)); |
| canvas.drawImage(image, Offset.zero, Paint()); |
| final Image resultImage = await recorder.endRecording().toImage(200, 200); |
| return (await resultImage.toByteData())!; |
| } |
| |
| final ByteData dataSync = await drawOnCanvas(toImageImage); |
| final ByteData data = await drawOnCanvas(toImageSyncImage); |
| expect(data.buffer.asUint8List(), equals(dataSync.buffer.asUint8List())); |
| }); |
| |
| test('Canvas.drawParagraph throws when Paragraph.layout was not called', |
| () async { |
| // Regression test for https://github.com/flutter/flutter/issues/97172 |
| expect(() { |
| toImage((Canvas canvas) { |
| final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle()); |
| builder.addText('Woodstock!'); |
| final Paragraph woodstock = builder.build(); |
| canvas.drawParagraph(woodstock, const Offset(0, 50)); |
| }, 100, 100); |
| }, throwsA(isA<AssertionError>())); |
| }); |
| |
| Future<Image> drawText(String text) { |
| return toImage((Canvas canvas) { |
| final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle( |
| fontFamily: 'RobotoSerif', |
| fontStyle: FontStyle.normal, |
| fontWeight: FontWeight.normal, |
| fontSize: 15.0, |
| )); |
| builder.pushStyle(TextStyle(color: const Color(0xFF0000FF))); |
| builder.addText(text); |
| |
| final Paragraph paragraph = builder.build(); |
| paragraph.layout(const ParagraphConstraints(width: 20 * 5.0)); |
| |
| canvas.drawParagraph(paragraph, Offset.zero); |
| }, 100, 100); |
| } |
| |
| test('Canvas.drawParagraph renders tab as space instead of tofu', () async { |
| // Skia renders a tofu if the font does not have a glyph for a character. |
| // However, Flutter opts-in to a Skia feature to render tabs as a single space. |
| // See: https://github.com/flutter/flutter/issues/79153 |
| final File file = File(path.join(_flutterBuildPath, 'flutter', |
| 'third_party', 'txt', 'assets', 'Roboto-Regular.ttf')); |
| final Uint8List fontData = await file.readAsBytes(); |
| await loadFontFromList(fontData, fontFamily: 'RobotoSerif'); |
| |
| // The backspace character, \b, does not have a corresponding glyph and is rendered as a tofu. |
| final Image tabImage = await drawText('>\t<'); |
| final Image spaceImage = await drawText('> <'); |
| final Image tofuImage = await drawText('>\b<'); |
| |
| // The tab's image should be identical to the space's image but not the tofu's image. |
| final bool tabToSpaceComparison = |
| await comparer.fuzzyCompareImages(tabImage, spaceImage); |
| final bool tabToTofuComparison = |
| await comparer.fuzzyCompareImages(tabImage, tofuImage); |
| |
| expect(tabToSpaceComparison, isTrue); |
| expect(tabToTofuComparison, isFalse); |
| }); |
| |
| test('drawRect, drawOval, and clipRect render with unsorted rectangles', |
| () async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| |
| canvas.drawColor(const Color(0xFFE0E0E0), BlendMode.src); |
| |
| void draw(Rect rect, double x, double y, Color color) { |
| final Paint paint = Paint() |
| ..color = color |
| ..strokeWidth = 5.0; |
| |
| final Rect tallThin = Rect.fromLTRB( |
| min(rect.left, rect.right) - 10, |
| rect.top, |
| min(rect.left, rect.right) - 10, |
| rect.bottom, |
| ); |
| final Rect wideThin = Rect.fromLTRB( |
| rect.left, |
| min(rect.top, rect.bottom) - 10, |
| rect.right, |
| min(rect.top, rect.bottom) - 10, |
| ); |
| |
| canvas.save(); |
| canvas.translate(x, y); |
| |
| paint.style = PaintingStyle.fill; |
| canvas.drawRect(rect, paint); |
| canvas.drawRect(tallThin, paint); |
| canvas.drawRect(wideThin, paint); |
| |
| canvas.save(); |
| canvas.translate(0, 100); |
| paint.style = PaintingStyle.stroke; |
| canvas.drawRect(rect, paint); |
| canvas.drawRect(tallThin, paint); |
| canvas.drawRect(wideThin, paint); |
| canvas.restore(); |
| |
| canvas.save(); |
| canvas.translate(100, 0); |
| paint.style = PaintingStyle.fill; |
| canvas.drawOval(rect, paint); |
| canvas.drawOval(tallThin, paint); |
| canvas.drawOval(wideThin, paint); |
| canvas.restore(); |
| |
| canvas.save(); |
| canvas.translate(100, 100); |
| paint.style = PaintingStyle.stroke; |
| canvas.drawOval(rect, paint); |
| canvas.drawOval(tallThin, paint); |
| canvas.drawOval(wideThin, paint); |
| canvas.restore(); |
| |
| canvas.save(); |
| canvas.translate(50, 50); |
| |
| canvas.save(); |
| canvas.clipRect(rect); |
| canvas.drawPaint(paint); |
| canvas.restore(); |
| |
| canvas.save(); |
| canvas.clipRect(tallThin); |
| canvas.drawPaint(paint); |
| canvas.restore(); |
| |
| canvas.save(); |
| canvas.clipRect(wideThin); |
| canvas.drawPaint(paint); |
| canvas.restore(); |
| |
| canvas.restore(); |
| |
| canvas.restore(); |
| } |
| |
| draw(const Rect.fromLTRB(10, 10, 40, 40), 50, 50, const Color(0xFF2196F3)); |
| draw(const Rect.fromLTRB(40, 10, 10, 40), 250, 50, const Color(0xFF4CAF50)); |
| draw(const Rect.fromLTRB(10, 40, 40, 10), 50, 250, const Color(0xFF9C27B0)); |
| draw( |
| const Rect.fromLTRB(40, 40, 10, 10), 250, 250, const Color(0xFFFF9800)); |
| |
| final Picture picture = recorder.endRecording(); |
| final Image image = await picture.toImage(450, 450); |
| await comparer.addGoldenImage(image, 'render_unordered_rects.png'); |
| }); |
| |
| test('Canvas.translate affects canvas.getTransform', () async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| canvas.translate(12, 14.5); |
| final Float64List matrix = Matrix4.translationValues(12, 14.5, 0).storage; |
| final Float64List curMatrix = canvas.getTransform(); |
| expect(curMatrix, closeToTransform(matrix)); |
| canvas.translate(10, 10); |
| final Float64List newCurMatrix = canvas.getTransform(); |
| expect(newCurMatrix, isNot(closeToTransform(matrix))); |
| expect(curMatrix, closeToTransform(matrix)); |
| }); |
| |
| test('Canvas.scale affects canvas.getTransform', () async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| canvas.scale(12, 14.5); |
| final Float64List matrix = Matrix4.diagonal3Values(12, 14.5, 1).storage; |
| final Float64List curMatrix = canvas.getTransform(); |
| expect(curMatrix, closeToTransform(matrix)); |
| canvas.scale(10, 10); |
| final Float64List newCurMatrix = canvas.getTransform(); |
| expect(newCurMatrix, isNot(closeToTransform(matrix))); |
| expect(curMatrix, closeToTransform(matrix)); |
| }); |
| |
| test('Canvas.rotate affects canvas.getTransform', () async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| canvas.rotate(pi); |
| final Float64List matrix = Matrix4.rotationZ(pi).storage; |
| final Float64List curMatrix = canvas.getTransform(); |
| expect(curMatrix, closeToTransform(matrix)); |
| canvas.rotate(pi / 2); |
| final Float64List newCurMatrix = canvas.getTransform(); |
| expect(newCurMatrix, isNot(closeToTransform(matrix))); |
| expect(curMatrix, closeToTransform(matrix)); |
| }); |
| |
| test('Canvas.skew affects canvas.getTransform', () async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| canvas.skew(12, 14.5); |
| final Float64List matrix = (Matrix4.identity() |
| ..setEntry(0, 1, 12) |
| ..setEntry(1, 0, 14.5)) |
| .storage; |
| final Float64List curMatrix = canvas.getTransform(); |
| expect(curMatrix, closeToTransform(matrix)); |
| canvas.skew(10, 10); |
| final Float64List newCurMatrix = canvas.getTransform(); |
| expect(newCurMatrix, isNot(closeToTransform(matrix))); |
| expect(curMatrix, closeToTransform(matrix)); |
| }); |
| |
| test('Canvas.transform affects canvas.getTransform', () async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| final Float64List matrix = (Matrix4.identity() |
| ..translate(12.0, 14.5) |
| ..scale(12.0, 14.5)) |
| .storage; |
| canvas.transform(matrix); |
| final Float64List curMatrix = canvas.getTransform(); |
| expect(curMatrix, closeToTransform(matrix)); |
| canvas.translate(10, 10); |
| final Float64List newCurMatrix = canvas.getTransform(); |
| expect(newCurMatrix, isNot(closeToTransform(matrix))); |
| expect(curMatrix, closeToTransform(matrix)); |
| }); |
| |
| test('Canvas.clipRect affects canvas.getClipBounds', () async { |
| void testRect(Rect clipRect, bool doAA) { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| canvas.clipRect(clipRect, doAntiAlias: doAA); |
| |
| final Rect clipSortedBounds = Rect.fromLTRB( |
| min(clipRect.left, clipRect.right), |
| min(clipRect.top, clipRect.bottom), |
| max(clipRect.left, clipRect.right), |
| max(clipRect.top, clipRect.bottom), |
| ); |
| Rect clipExpandedBounds; |
| if (doAA) { |
| clipExpandedBounds = Rect.fromLTRB( |
| clipSortedBounds.left.floorToDouble(), |
| clipSortedBounds.top.floorToDouble(), |
| clipSortedBounds.right.ceilToDouble(), |
| clipSortedBounds.bottom.ceilToDouble(), |
| ); |
| } else { |
| clipExpandedBounds = clipSortedBounds; |
| } |
| |
| // Save initial return values for testing restored values |
| final Rect initialLocalBounds = canvas.getLocalClipBounds(); |
| final Rect initialDestinationBounds = canvas.getDestinationClipBounds(); |
| expect(initialLocalBounds, closeToRect(clipExpandedBounds)); |
| expect(initialDestinationBounds, closeToRect(clipExpandedBounds)); |
| |
| canvas.save(); |
| canvas.clipRect(const Rect.fromLTRB(0, 0, 15, 15)); |
| // Both clip bounds have changed |
| expect( |
| canvas.getLocalClipBounds(), isNot(closeToRect(clipExpandedBounds))); |
| expect(canvas.getDestinationClipBounds(), |
| isNot(closeToRect(clipExpandedBounds))); |
| // Previous return values have not changed |
| expect(initialLocalBounds, closeToRect(clipExpandedBounds)); |
| expect(initialDestinationBounds, closeToRect(clipExpandedBounds)); |
| canvas.restore(); |
| |
| // save/restore returned the values to their original values |
| expect(canvas.getLocalClipBounds(), initialLocalBounds); |
| expect(canvas.getDestinationClipBounds(), initialDestinationBounds); |
| |
| canvas.save(); |
| canvas.scale(2, 2); |
| final Rect scaledExpandedBounds = Rect.fromLTRB( |
| clipExpandedBounds.left / 2.0, |
| clipExpandedBounds.top / 2.0, |
| clipExpandedBounds.right / 2.0, |
| clipExpandedBounds.bottom / 2.0, |
| ); |
| expect(canvas.getLocalClipBounds(), closeToRect(scaledExpandedBounds)); |
| // Destination bounds are unaffected by transform |
| expect( |
| canvas.getDestinationClipBounds(), closeToRect(clipExpandedBounds)); |
| canvas.restore(); |
| |
| // save/restore returned the values to their original values |
| expect(canvas.getLocalClipBounds(), initialLocalBounds); |
| expect(canvas.getDestinationClipBounds(), initialDestinationBounds); |
| } |
| |
| testRect(const Rect.fromLTRB(10.2, 11.3, 20.4, 25.7), false); |
| testRect(const Rect.fromLTRB(10.2, 11.3, 20.4, 25.7), true); |
| |
| // LR swapped |
| testRect(const Rect.fromLTRB(20.4, 11.3, 10.2, 25.7), false); |
| testRect(const Rect.fromLTRB(20.4, 11.3, 10.2, 25.7), true); |
| |
| // TB swapped |
| testRect(const Rect.fromLTRB(10.2, 25.7, 20.4, 11.3), false); |
| testRect(const Rect.fromLTRB(10.2, 25.7, 20.4, 11.3), true); |
| |
| // LR and TB swapped |
| testRect(const Rect.fromLTRB(20.4, 25.7, 10.2, 11.3), false); |
| testRect(const Rect.fromLTRB(20.4, 25.7, 10.2, 11.3), true); |
| }); |
| |
| test('Canvas.clipRect with matrix affects canvas.getClipBounds', () async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| const Rect clipBounds1 = Rect.fromLTRB(0.0, 0.0, 10.0, 10.0); |
| const Rect clipBounds2 = Rect.fromLTRB(10.0, 10.0, 20.0, 20.0); |
| |
| canvas.save(); |
| canvas.clipRect(clipBounds1, doAntiAlias: false); |
| canvas.translate(0, 10.0); |
| canvas.clipRect(clipBounds1, doAntiAlias: false); |
| expect(canvas.getDestinationClipBounds().isEmpty, isTrue); |
| canvas.restore(); |
| |
| canvas.save(); |
| canvas.clipRect(clipBounds1, doAntiAlias: false); |
| canvas.translate(-10.0, -10.0); |
| canvas.clipRect(clipBounds2, doAntiAlias: false); |
| expect(canvas.getDestinationClipBounds(), clipBounds1); |
| canvas.restore(); |
| }); |
| |
| test('Canvas.clipRRect(doAA=true) affects canvas.getClipBounds', () async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| const Rect clipBounds = Rect.fromLTRB(10.2, 11.3, 20.4, 25.7); |
| const Rect clipExpandedBounds = Rect.fromLTRB(10, 11, 21, 26); |
| final RRect clip = |
| RRect.fromRectAndRadius(clipBounds, const Radius.circular(3)); |
| canvas.clipRRect(clip); |
| |
| // Save initial return values for testing restored values |
| final Rect initialLocalBounds = canvas.getLocalClipBounds(); |
| final Rect initialDestinationBounds = canvas.getDestinationClipBounds(); |
| expect(initialLocalBounds, closeToRect(clipExpandedBounds)); |
| expect(initialDestinationBounds, closeToRect(clipExpandedBounds)); |
| |
| canvas.save(); |
| canvas.clipRect(const Rect.fromLTRB(0, 0, 15, 15)); |
| // Both clip bounds have changed |
| expect(canvas.getLocalClipBounds(), isNot(closeToRect(clipExpandedBounds))); |
| expect(canvas.getDestinationClipBounds(), |
| isNot(closeToRect(clipExpandedBounds))); |
| // Previous return values have not changed |
| expect(initialLocalBounds, closeToRect(clipExpandedBounds)); |
| expect(initialDestinationBounds, closeToRect(clipExpandedBounds)); |
| canvas.restore(); |
| |
| // save/restore returned the values to their original values |
| expect(canvas.getLocalClipBounds(), initialLocalBounds); |
| expect(canvas.getDestinationClipBounds(), initialDestinationBounds); |
| |
| canvas.save(); |
| canvas.scale(2, 2); |
| const Rect scaledExpandedBounds = Rect.fromLTRB(5, 5.5, 10.5, 13); |
| expect(canvas.getLocalClipBounds(), closeToRect(scaledExpandedBounds)); |
| // Destination bounds are unaffected by transform |
| expect(canvas.getDestinationClipBounds(), closeToRect(clipExpandedBounds)); |
| canvas.restore(); |
| |
| // save/restore returned the values to their original values |
| expect(canvas.getLocalClipBounds(), initialLocalBounds); |
| expect(canvas.getDestinationClipBounds(), initialDestinationBounds); |
| }); |
| |
| test('Canvas.clipRRect(doAA=false) affects canvas.getClipBounds', () async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| const Rect clipBounds = Rect.fromLTRB(10.2, 11.3, 20.4, 25.7); |
| final RRect clip = |
| RRect.fromRectAndRadius(clipBounds, const Radius.circular(3)); |
| canvas.clipRRect(clip, doAntiAlias: false); |
| |
| // Save initial return values for testing restored values |
| final Rect initialLocalBounds = canvas.getLocalClipBounds(); |
| final Rect initialDestinationBounds = canvas.getDestinationClipBounds(); |
| expect(initialLocalBounds, closeToRect(clipBounds)); |
| expect(initialDestinationBounds, closeToRect(clipBounds)); |
| |
| canvas.save(); |
| canvas.clipRect(const Rect.fromLTRB(0, 0, 15, 15), doAntiAlias: false); |
| // Both clip bounds have changed |
| expect(canvas.getLocalClipBounds(), isNot(closeToRect(clipBounds))); |
| expect(canvas.getDestinationClipBounds(), isNot(closeToRect(clipBounds))); |
| // Previous return values have not changed |
| expect(initialLocalBounds, closeToRect(clipBounds)); |
| expect(initialDestinationBounds, closeToRect(clipBounds)); |
| canvas.restore(); |
| |
| // save/restore returned the values to their original values |
| expect(canvas.getLocalClipBounds(), initialLocalBounds); |
| expect(canvas.getDestinationClipBounds(), initialDestinationBounds); |
| |
| canvas.save(); |
| canvas.scale(2, 2); |
| const Rect scaledClipBounds = Rect.fromLTRB(5.1, 5.65, 10.2, 12.85); |
| expect(canvas.getLocalClipBounds(), closeToRect(scaledClipBounds)); |
| // Destination bounds are unaffected by transform |
| expect(canvas.getDestinationClipBounds(), closeToRect(clipBounds)); |
| canvas.restore(); |
| |
| // save/restore returned the values to their original values |
| expect(canvas.getLocalClipBounds(), initialLocalBounds); |
| expect(canvas.getDestinationClipBounds(), initialDestinationBounds); |
| }); |
| |
| test('Canvas.clipRRect with matrix affects canvas.getClipBounds', () async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| const Rect clipBounds1 = Rect.fromLTRB(0.0, 0.0, 10.0, 10.0); |
| const Rect clipBounds2 = Rect.fromLTRB(10.0, 10.0, 20.0, 20.0); |
| final RRect clip1 = |
| RRect.fromRectAndRadius(clipBounds1, const Radius.circular(3)); |
| final RRect clip2 = |
| RRect.fromRectAndRadius(clipBounds2, const Radius.circular(3)); |
| |
| canvas.save(); |
| canvas.clipRRect(clip1, doAntiAlias: false); |
| canvas.translate(0, 10.0); |
| canvas.clipRRect(clip1, doAntiAlias: false); |
| expect(canvas.getDestinationClipBounds().isEmpty, isTrue); |
| canvas.restore(); |
| |
| canvas.save(); |
| canvas.clipRRect(clip1, doAntiAlias: false); |
| canvas.translate(-10.0, -10.0); |
| canvas.clipRRect(clip2, doAntiAlias: false); |
| expect(canvas.getDestinationClipBounds(), clipBounds1); |
| canvas.restore(); |
| }); |
| |
| test('Canvas.clipPath(doAA=true) affects canvas.getClipBounds', () async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| const Rect clipBounds = Rect.fromLTRB(10.2, 11.3, 20.4, 25.7); |
| const Rect clipExpandedBounds = Rect.fromLTRB(10, 11, 21, 26); |
| final Path clip = Path() |
| ..addRect(clipBounds) |
| ..addOval(clipBounds); |
| canvas.clipPath(clip); |
| |
| // Save initial return values for testing restored values |
| final Rect initialLocalBounds = canvas.getLocalClipBounds(); |
| final Rect initialDestinationBounds = canvas.getDestinationClipBounds(); |
| expect(initialLocalBounds, closeToRect(clipExpandedBounds)); |
| expect(initialDestinationBounds, closeToRect(clipExpandedBounds)); |
| |
| canvas.save(); |
| canvas.clipRect(const Rect.fromLTRB(0, 0, 15, 15)); |
| // Both clip bounds have changed |
| expect(canvas.getLocalClipBounds(), isNot(closeToRect(clipExpandedBounds))); |
| expect(canvas.getDestinationClipBounds(), |
| isNot(closeToRect(clipExpandedBounds))); |
| // Previous return values have not changed |
| expect(initialLocalBounds, closeToRect(clipExpandedBounds)); |
| expect(initialDestinationBounds, closeToRect(clipExpandedBounds)); |
| canvas.restore(); |
| |
| // save/restore returned the values to their original values |
| expect(canvas.getLocalClipBounds(), initialLocalBounds); |
| expect(canvas.getDestinationClipBounds(), initialDestinationBounds); |
| |
| canvas.save(); |
| canvas.scale(2, 2); |
| const Rect scaledExpandedBounds = Rect.fromLTRB(5, 5.5, 10.5, 13); |
| expect(canvas.getLocalClipBounds(), closeToRect(scaledExpandedBounds)); |
| // Destination bounds are unaffected by transform |
| expect(canvas.getDestinationClipBounds(), closeToRect(clipExpandedBounds)); |
| canvas.restore(); |
| |
| // save/restore returned the values to their original values |
| expect(canvas.getLocalClipBounds(), initialLocalBounds); |
| expect(canvas.getDestinationClipBounds(), initialDestinationBounds); |
| }); |
| |
| test('Canvas.clipPath(doAA=false) affects canvas.getClipBounds', () async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| const Rect clipBounds = Rect.fromLTRB(10.2, 11.3, 20.4, 25.7); |
| final Path clip = Path() |
| ..addRect(clipBounds) |
| ..addOval(clipBounds); |
| canvas.clipPath(clip, doAntiAlias: false); |
| |
| // Save initial return values for testing restored values |
| final Rect initialLocalBounds = canvas.getLocalClipBounds(); |
| final Rect initialDestinationBounds = canvas.getDestinationClipBounds(); |
| expect(initialLocalBounds, closeToRect(clipBounds)); |
| expect(initialDestinationBounds, closeToRect(clipBounds)); |
| |
| canvas.save(); |
| canvas.clipRect(const Rect.fromLTRB(0, 0, 15, 15), doAntiAlias: false); |
| // Both clip bounds have changed |
| expect(canvas.getLocalClipBounds(), isNot(closeToRect(clipBounds))); |
| expect(canvas.getDestinationClipBounds(), isNot(closeToRect(clipBounds))); |
| // Previous return values have not changed |
| expect(initialLocalBounds, closeToRect(clipBounds)); |
| expect(initialDestinationBounds, closeToRect(clipBounds)); |
| canvas.restore(); |
| |
| // save/restore returned the values to their original values |
| expect(canvas.getLocalClipBounds(), initialLocalBounds); |
| expect(canvas.getDestinationClipBounds(), initialDestinationBounds); |
| |
| canvas.save(); |
| canvas.scale(2, 2); |
| const Rect scaledClipBounds = Rect.fromLTRB(5.1, 5.65, 10.2, 12.85); |
| expect(canvas.getLocalClipBounds(), closeToRect(scaledClipBounds)); |
| // Destination bounds are unaffected by transform |
| expect(canvas.getDestinationClipBounds(), closeToRect(clipBounds)); |
| canvas.restore(); |
| |
| // save/restore returned the values to their original values |
| expect(canvas.getLocalClipBounds(), initialLocalBounds); |
| expect(canvas.getDestinationClipBounds(), initialDestinationBounds); |
| }); |
| |
| test('Canvas.clipPath with matrix affects canvas.getClipBounds', () async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| const Rect clipBounds1 = Rect.fromLTRB(0.0, 0.0, 10.0, 10.0); |
| const Rect clipBounds2 = Rect.fromLTRB(10.0, 10.0, 20.0, 20.0); |
| final Path clip1 = Path() |
| ..addRect(clipBounds1) |
| ..addOval(clipBounds1); |
| final Path clip2 = Path() |
| ..addRect(clipBounds2) |
| ..addOval(clipBounds2); |
| |
| canvas.save(); |
| canvas.clipPath(clip1, doAntiAlias: false); |
| canvas.translate(0, 10.0); |
| canvas.clipPath(clip1, doAntiAlias: false); |
| expect(canvas.getDestinationClipBounds().isEmpty, isTrue); |
| canvas.restore(); |
| |
| canvas.save(); |
| canvas.clipPath(clip1, doAntiAlias: false); |
| canvas.translate(-10.0, -10.0); |
| canvas.clipPath(clip2, doAntiAlias: false); |
| expect(canvas.getDestinationClipBounds(), clipBounds1); |
| canvas.restore(); |
| }); |
| |
| test('Canvas.clipRect(diff) does not affect canvas.getClipBounds', () async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| const Rect clipBounds = Rect.fromLTRB(10.2, 11.3, 20.4, 25.7); |
| canvas.clipRect(clipBounds, doAntiAlias: false); |
| |
| // Save initial return values for testing restored values |
| final Rect initialLocalBounds = canvas.getLocalClipBounds(); |
| final Rect initialDestinationBounds = canvas.getDestinationClipBounds(); |
| expect(initialLocalBounds, closeToRect(clipBounds)); |
| expect(initialDestinationBounds, closeToRect(clipBounds)); |
| |
| canvas.clipRect(const Rect.fromLTRB(0, 0, 15, 15), |
| clipOp: ClipOp.difference, doAntiAlias: false); |
| expect(canvas.getLocalClipBounds(), initialLocalBounds); |
| expect(canvas.getDestinationClipBounds(), initialDestinationBounds); |
| }); |
| |
| test('RestoreToCount can work', () async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| canvas.save(); |
| canvas.save(); |
| canvas.save(); |
| canvas.save(); |
| canvas.save(); |
| expect(canvas.getSaveCount(), equals(6)); |
| canvas.restoreToCount(2); |
| expect(canvas.getSaveCount(), equals(2)); |
| canvas.restore(); |
| expect(canvas.getSaveCount(), equals(1)); |
| }); |
| |
| test('RestoreToCount count less than 1, the stack should be reset', () async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| canvas.save(); |
| canvas.save(); |
| canvas.save(); |
| canvas.save(); |
| canvas.save(); |
| expect(canvas.getSaveCount(), equals(6)); |
| canvas.restoreToCount(0); |
| expect(canvas.getSaveCount(), equals(1)); |
| }); |
| |
| test( |
| 'RestoreToCount count greater than current [getSaveCount], nothing would happend', |
| () async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| canvas.save(); |
| canvas.save(); |
| canvas.save(); |
| canvas.save(); |
| canvas.save(); |
| expect(canvas.getSaveCount(), equals(6)); |
| canvas.restoreToCount(canvas.getSaveCount() + 1); |
| expect(canvas.getSaveCount(), equals(6)); |
| }); |
| |
| test('TextDecoration renders non-solid lines', () async { |
| final File file = File(path.join(_flutterBuildPath, 'flutter', |
| 'third_party', 'txt', 'assets', 'Roboto-Regular.ttf')); |
| final Uint8List fontData = await file.readAsBytes(); |
| await loadFontFromList(fontData, fontFamily: 'RobotoSlab'); |
| |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| |
| for (final (int index, TextDecorationStyle style) |
| in TextDecorationStyle.values.indexed) { |
| final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle()); |
| builder.pushStyle(TextStyle( |
| decoration: TextDecoration.underline, |
| decorationStyle: style, |
| decorationThickness: 1.0, |
| decorationColor: const Color(0xFFFF0000), |
| fontFamily: 'RobotoSlab', |
| fontSize: 24.0, |
| foreground: Paint()..color = const Color(0xFF0000FF), |
| )); |
| |
| builder.addText(style.name); |
| final Paragraph paragraph = builder.build(); |
| paragraph.layout(const ParagraphConstraints(width: 1000)); |
| |
| // Draw and layout based on the index vertically. |
| canvas.drawParagraph(paragraph, Offset(0, index * 40.0)); |
| } |
| |
| final Picture picture = recorder.endRecording(); |
| final Image image = await picture.toImage(200, 200); |
| await comparer.addGoldenImage(image, 'text_decoration.png'); |
| }); |
| |
| test('Paint, when copied, has equivalent fields', () { |
| final Paint paint = Paint() |
| ..color = const Color(0xFF0000FF) |
| ..strokeWidth = 10.0 |
| ..strokeCap = StrokeCap.round |
| ..strokeJoin = StrokeJoin.round |
| ..style = PaintingStyle.stroke |
| ..blendMode = BlendMode.srcOver |
| ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10.0) |
| ..filterQuality = FilterQuality.high |
| ..colorFilter = const ColorFilter.mode(Color(0xFF00FF00), BlendMode.color) |
| ..imageFilter = ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0); |
| |
| final Paint paintCopy = Paint.from(paint); |
| expect(paintCopy.color, equals(const Color(0xFF0000FF))); |
| expect(paintCopy.strokeWidth, equals(10.0)); |
| expect(paintCopy.strokeCap, equals(StrokeCap.round)); |
| expect(paintCopy.strokeJoin, equals(StrokeJoin.round)); |
| expect(paintCopy.style, equals(PaintingStyle.stroke)); |
| expect(paintCopy.blendMode, equals(BlendMode.srcOver)); |
| expect(paintCopy.maskFilter, |
| equals(const MaskFilter.blur(BlurStyle.normal, 10.0))); |
| expect(paintCopy.filterQuality, equals(FilterQuality.high)); |
| expect(paintCopy.colorFilter, |
| equals(const ColorFilter.mode(Color(0xFF00FF00), BlendMode.color))); |
| expect(paintCopy.imageFilter, |
| equals(ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0))); |
| }); |
| |
| test('Paint, when copied, does not mutate the original instance', () { |
| final Paint paint = Paint() |
| ..color = const Color(0xFF0000FF) |
| ..strokeWidth = 10.0 |
| ..strokeCap = StrokeCap.round |
| ..strokeJoin = StrokeJoin.round |
| ..style = PaintingStyle.stroke |
| ..blendMode = BlendMode.srcOver |
| ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10.0) |
| ..filterQuality = FilterQuality.high |
| ..colorFilter = const ColorFilter.mode(Color(0xFF00FF00), BlendMode.color) |
| ..imageFilter = ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0); |
| |
| // Make a copy, and change every field of the copy. |
| Paint.from(paint) |
| ..color = const Color(0xFF00FF00) |
| ..strokeWidth = 20.0 |
| ..strokeCap = StrokeCap.butt |
| ..strokeJoin = StrokeJoin.bevel |
| ..style = PaintingStyle.fill |
| ..blendMode = BlendMode.srcIn |
| ..maskFilter = const MaskFilter.blur(BlurStyle.solid, 20.0) |
| ..filterQuality = FilterQuality.none |
| ..colorFilter = |
| const ColorFilter.mode(Color(0xFFFF0000), BlendMode.modulate) |
| ..imageFilter = ImageFilter.blur(sigmaX: 20.0, sigmaY: 20.0); |
| |
| // The original paint should not have changed. |
| expect(paint.color, equals(const Color(0xFF0000FF))); |
| expect(paint.strokeWidth, equals(10.0)); |
| expect(paint.strokeCap, equals(StrokeCap.round)); |
| expect(paint.strokeJoin, equals(StrokeJoin.round)); |
| expect(paint.style, equals(PaintingStyle.stroke)); |
| expect(paint.blendMode, equals(BlendMode.srcOver)); |
| expect(paint.maskFilter, |
| equals(const MaskFilter.blur(BlurStyle.normal, 10.0))); |
| expect(paint.filterQuality, equals(FilterQuality.high)); |
| expect(paint.colorFilter, |
| equals(const ColorFilter.mode(Color(0xFF00FF00), BlendMode.color))); |
| expect(paint.imageFilter, |
| equals(ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0))); |
| }); |
| |
| test('Paint, when copied, the original changing does not mutate the copy', |
| () { |
| final Paint paint = Paint() |
| ..color = const Color(0xFF0000FF) |
| ..strokeWidth = 10.0 |
| ..strokeCap = StrokeCap.round |
| ..strokeJoin = StrokeJoin.round |
| ..style = PaintingStyle.stroke |
| ..blendMode = BlendMode.srcOver |
| ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 10.0) |
| ..filterQuality = FilterQuality.high |
| ..colorFilter = const ColorFilter.mode(Color(0xFF00FF00), BlendMode.color) |
| ..imageFilter = ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0); |
| |
| // Make a copy, and change every field of the original. |
| final Paint paintCopy = Paint.from(paint); |
| paint |
| ..color = const Color(0xFF00FF00) |
| ..strokeWidth = 20.0 |
| ..strokeCap = StrokeCap.butt |
| ..strokeJoin = StrokeJoin.bevel |
| ..style = PaintingStyle.fill |
| ..blendMode = BlendMode.srcIn |
| ..maskFilter = const MaskFilter.blur(BlurStyle.solid, 20.0) |
| ..filterQuality = FilterQuality.none |
| ..colorFilter = |
| const ColorFilter.mode(Color(0xFFFF0000), BlendMode.modulate) |
| ..imageFilter = ImageFilter.blur(sigmaX: 20.0, sigmaY: 20.0); |
| |
| // The copy should not have changed. |
| expect(paintCopy.color, equals(const Color(0xFF0000FF))); |
| expect(paintCopy.strokeWidth, equals(10.0)); |
| expect(paintCopy.strokeCap, equals(StrokeCap.round)); |
| expect(paintCopy.strokeJoin, equals(StrokeJoin.round)); |
| expect(paintCopy.style, equals(PaintingStyle.stroke)); |
| expect(paintCopy.blendMode, equals(BlendMode.srcOver)); |
| expect(paintCopy.maskFilter, |
| equals(const MaskFilter.blur(BlurStyle.normal, 10.0))); |
| expect(paintCopy.filterQuality, equals(FilterQuality.high)); |
| expect(paintCopy.colorFilter, |
| equals(const ColorFilter.mode(Color(0xFF00FF00), BlendMode.color))); |
| expect(paintCopy.imageFilter, |
| equals(ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0))); |
| }); |
| |
| test('DrawAtlas correctly copies color values into display list format', |
| () async { |
| final Image testImage = await createTestImage(); |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| // Make a drawAtlas call that should be solid red. |
| canvas.drawAtlas( |
| testImage, |
| [ |
| RSTransform.fromComponents( |
| rotation: 0, |
| scale: 10, |
| anchorX: 0, |
| anchorY: 0, |
| translateX: 0, |
| translateY: 0, |
| ), |
| ], |
| [const Rect.fromLTWH(0, 0, 1, 1)], |
| [const Color.fromARGB(255, 255, 0, 0)], |
| BlendMode.dst, |
| null, |
| Paint(), |
| ); |
| |
| final Image resultImage = await recorder.endRecording().toImage(1, 1); |
| final ByteData? data = await resultImage.toByteData(); |
| if (data == null) { |
| fail('Expected non-null byte data'); |
| } |
| final int rgba = data.buffer.asUint32List()[0]; |
| expect(rgba, 0xFF0000FF); |
| }); |
| |
| test('DrawAtlas with no colors does not crash', |
| () async { |
| final Image testImage = await createTestImage(); |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas canvas = Canvas(recorder); |
| // Make a drawAtlas call that should be solid red. |
| canvas.drawAtlas( |
| testImage, |
| [ |
| RSTransform.fromComponents( |
| rotation: 0, |
| scale: 10, |
| anchorX: 0, |
| anchorY: 0, |
| translateX: 0, |
| translateY: 0, |
| ), |
| ], |
| [const Rect.fromLTWH(0, 0, 1, 1)], |
| [], |
| BlendMode.dst, |
| null, |
| Paint(), |
| ); |
| |
| final Image resultImage = await recorder.endRecording().toImage(1, 1); |
| final ByteData? data = await resultImage.toByteData(); |
| expect(data, isNotNull); |
| }); |
| |
| Image makeCheckerBoard(int width, int height) { |
| final recorder = PictureRecorder(); |
| final canvas = Canvas(recorder); |
| |
| const double left = 0; |
| final double centerX = width * 0.5; |
| final double right = width.toDouble(); |
| |
| const double top = 0; |
| final double centerY = height * 0.5; |
| final double bottom = height.toDouble(); |
| |
| canvas.drawRect(Rect.fromLTRB(left, top, centerX, centerY), |
| Paint()..color = const Color.fromARGB(255, 0, 255, 0)); |
| canvas.drawRect(Rect.fromLTRB(centerX, top, right, centerY), |
| Paint()..color = const Color.fromARGB(255, 255, 255, 0)); |
| canvas.drawRect(Rect.fromLTRB(left, centerY, centerX, bottom), |
| Paint()..color = const Color.fromARGB(255, 0, 0, 255)); |
| canvas.drawRect(Rect.fromLTRB(centerX, centerY, right, bottom), |
| Paint()..color = const Color.fromARGB(255, 255, 0, 0)); |
| |
| final picture = recorder.endRecording(); |
| return picture.toImageSync(width, height); |
| } |
| |
| Image renderingOpsWithTileMode(TileMode? tileMode) { |
| final recorder = PictureRecorder(); |
| final canvas = Canvas(recorder); |
| canvas.drawColor(const Color.fromARGB(255, 224, 224, 224), BlendMode.src); |
| |
| const Rect zone = Rect.fromLTWH(15, 15, 20, 20); |
| final Rect arena = zone.inflate(15); |
| const Rect ovalZone = Rect.fromLTWH(20, 15, 10, 20); |
| |
| final gradient = Gradient.linear( |
| zone.topLeft, |
| zone.bottomRight, |
| <Color>[ |
| const Color.fromARGB(255, 0, 255, 0), |
| const Color.fromARGB(255, 0, 0, 255), |
| ], |
| <double>[0, 1], |
| ); |
| final filter = ImageFilter.blur(sigmaX: 3.0, sigmaY: 3.0, tileMode: tileMode); |
| final Paint white = Paint()..color = const Color.fromARGB(255, 255, 255, 255); |
| final Paint grey = Paint()..color = const Color.fromARGB(255, 127, 127, 127); |
| final Paint unblurredFill = Paint()..shader = gradient; |
| final Paint blurredFill = Paint.from(unblurredFill) |
| ..imageFilter = filter; |
| final Paint unblurredStroke = Paint.from(unblurredFill) |
| ..style = PaintingStyle.stroke |
| ..strokeCap = StrokeCap.round |
| ..strokeJoin = StrokeJoin.round |
| ..strokeWidth = 10; |
| final Paint blurredStroke = Paint.from(unblurredStroke) |
| ..imageFilter = filter; |
| final Image image = makeCheckerBoard(20, 20); |
| const Rect imageBounds = Rect.fromLTRB(0, 0, 20, 20); |
| const Rect imageCenter = Rect.fromLTRB(5, 5, 9, 9); |
| final points = <Offset>[ |
| zone.topLeft, |
| zone.topCenter, |
| zone.topRight, |
| zone.centerLeft, |
| zone.center, |
| zone.centerRight, |
| zone.bottomLeft, |
| zone.bottomCenter, |
| zone.bottomRight, |
| ]; |
| final vertices = Vertices( |
| VertexMode.triangles, |
| <Offset> [ |
| zone.topLeft, |
| zone.bottomRight, |
| zone.topRight, |
| zone.topLeft, |
| zone.bottomRight, |
| zone.bottomLeft, |
| ], |
| colors: <Color>[ |
| const Color.fromARGB(255, 0, 255, 0), |
| const Color.fromARGB(255, 255, 0, 0), |
| const Color.fromARGB(255, 255, 255, 0), |
| const Color.fromARGB(255, 0, 255, 0), |
| const Color.fromARGB(255, 255, 0, 0), |
| const Color.fromARGB(255, 0, 0, 255), |
| ], |
| ); |
| final atlasXforms = <RSTransform>[ |
| RSTransform.fromComponents( |
| rotation: 0.0, |
| scale: 1.0, |
| anchorX: 0, |
| anchorY: 0, |
| translateX: zone.topLeft.dx, |
| translateY: zone.topLeft.dy, |
| ), |
| RSTransform.fromComponents( |
| rotation: pi / 2, |
| scale: 1.0, |
| anchorX: 0, |
| anchorY: 0, |
| translateX: zone.topRight.dx, |
| translateY: zone.topRight.dy, |
| ), |
| RSTransform.fromComponents( |
| rotation: pi, |
| scale: 1.0, |
| anchorX: 0, |
| anchorY: 0, |
| translateX: zone.bottomRight.dx, |
| translateY: zone.bottomRight.dy, |
| ), |
| RSTransform.fromComponents( |
| rotation: pi * 3 / 2, |
| scale: 1.0, |
| anchorX: 0, |
| anchorY: 0, |
| translateX: zone.bottomLeft.dx, |
| translateY: zone.bottomLeft.dy, |
| ), |
| RSTransform.fromComponents( |
| rotation: pi / 4, |
| scale: 1.0, |
| anchorX: 4, |
| anchorY: 4, |
| translateX: zone.center.dx, |
| translateY: zone.center.dy, |
| ), |
| ]; |
| const atlasRects = <Rect>[ |
| Rect.fromLTRB(6, 6, 14, 14), |
| Rect.fromLTRB(6, 6, 14, 14), |
| Rect.fromLTRB(6, 6, 14, 14), |
| Rect.fromLTRB(6, 6, 14, 14), |
| Rect.fromLTRB(6, 6, 14, 14), |
| ]; |
| |
| const double pad = 10; |
| final double offset = arena.width + pad; |
| const int columns = 5; |
| final Rect pairArena = Rect.fromLTRB(arena.left - 3, arena.top - 3, |
| arena.right + 3, arena.bottom + offset + 3); |
| |
| final List<void Function(Canvas canvas, Paint fill, Paint stroke)> renderers = [ |
| (canvas, fill, stroke) { |
| canvas.saveLayer(zone.inflate(5), fill); |
| canvas.drawLine(zone.topLeft, zone.bottomRight, unblurredStroke); |
| canvas.drawLine(zone.topRight, zone.bottomLeft, unblurredStroke); |
| canvas.restore(); |
| }, |
| (canvas, fill, stroke) => canvas.drawLine(zone.topLeft, zone.bottomRight, stroke), |
| (canvas, fill, stroke) => canvas.drawRect(zone, fill), |
| (canvas, fill, stroke) => canvas.drawOval(ovalZone, fill), |
| (canvas, fill, stroke) => canvas.drawCircle(zone.center, zone.width * 0.5, fill), |
| (canvas, fill, stroke) => canvas.drawRRect(RRect.fromRectXY(zone, 4.0, 4.0), fill), |
| (canvas, fill, stroke) => canvas.drawDRRect(RRect.fromRectXY(zone, 4.0, 4.0), |
| RRect.fromRectXY(zone.deflate(4), 4.0, 4.0), |
| fill), |
| (canvas, fill, stroke) => canvas.drawArc(zone, pi / 4, pi * 3 / 2, true, fill), |
| (canvas, fill, stroke) => canvas.drawPath(Path() |
| ..moveTo(zone.left, zone.top) |
| ..lineTo(zone.right, zone.top) |
| ..lineTo(zone.left, zone.bottom) |
| ..lineTo(zone.right, zone.bottom), |
| stroke), |
| (canvas, fill, stroke) => canvas.drawImage(image, zone.topLeft, fill), |
| (canvas, fill, stroke) => canvas.drawImageRect(image, imageBounds, zone.inflate(2), fill), |
| (canvas, fill, stroke) => canvas.drawImageNine(image, imageCenter, zone.inflate(2), fill), |
| (canvas, fill, stroke) => canvas.drawPoints(PointMode.points, points, stroke), |
| (canvas, fill, stroke) => canvas.drawVertices(vertices, BlendMode.dstOver, fill), |
| (canvas, fill, stroke) => canvas.drawAtlas(image, atlasXforms, atlasRects, |
| null, null, null, fill), |
| ]; |
| |
| canvas.save(); |
| canvas.translate(pad, pad); |
| int renderIndex = 0; |
| int rows = 0; |
| while (renderIndex < renderers.length) { |
| rows += 2; |
| canvas.save(); |
| for (int col = 0; col < columns && renderIndex < renderers.length; col++) { |
| final renderer = renderers[renderIndex++]; |
| canvas.drawRect(pairArena, grey); |
| canvas.drawRect(arena, white); |
| renderer(canvas, unblurredFill, unblurredStroke); |
| canvas.save(); |
| canvas.translate(0, offset); |
| canvas.drawRect(arena, white); |
| renderer(canvas, blurredFill, blurredStroke); |
| canvas.restore(); |
| canvas.translate(offset, 0); |
| } |
| canvas.restore(); |
| canvas.translate(0, offset * 2); |
| } |
| canvas.restore(); |
| |
| final picture = recorder.endRecording(); |
| return picture.toImageSync((offset * columns + pad).round(), |
| (offset * rows + pad).round()); |
| } |
| |
| test('Rendering ops with ImageFilter blur with default tile mode', () async { |
| final image = renderingOpsWithTileMode(null); |
| await comparer.addGoldenImage(image, 'canvas_test_blurred_rendering_with_default_tile_mode.png'); |
| }); |
| |
| test('Rendering ops with ImageFilter blur with clamp tile mode', () async { |
| final image = renderingOpsWithTileMode(TileMode.clamp); |
| await comparer.addGoldenImage(image, 'canvas_test_blurred_rendering_with_clamp_tile_mode.png'); |
| }); |
| |
| test('Rendering ops with ImageFilter blur with mirror tile mode', () async { |
| final image = renderingOpsWithTileMode(TileMode.mirror); |
| await comparer.addGoldenImage(image, 'canvas_test_blurred_rendering_with_mirror_tile_mode.png'); |
| }); |
| |
| test('Rendering ops with ImageFilter blur with repeated tile mode', () async { |
| final image = renderingOpsWithTileMode(TileMode.repeated); |
| await comparer.addGoldenImage(image, 'canvas_test_blurred_rendering_with_repeated_tile_mode.png'); |
| }); |
| |
| test('Rendering ops with ImageFilter blur with decal tile mode', () async { |
| final image = renderingOpsWithTileMode(TileMode.decal); |
| await comparer.addGoldenImage(image, 'canvas_test_blurred_rendering_with_decal_tile_mode.png'); |
| }); |
| } |
| |
| Future<Image> createTestImage() async { |
| final PictureRecorder recorder = PictureRecorder(); |
| final Canvas recorderCanvas = Canvas(recorder); |
| recorderCanvas.scale(1.0, 1.0); |
| final Picture picture = recorder.endRecording(); |
| return picture.toImage(1, 1); |
| } |
| |
| Matcher closeToRect(Rect rect) => _CloseToRectMatcher(rect); |
| |
| final class _CloseToRectMatcher extends Matcher { |
| const _CloseToRectMatcher(this._expectedRect); |
| final Rect _expectedRect; |
| |
| @override |
| bool matches(Object? item, Map<Object?, Object?> matchState) { |
| if (item is! Rect) { |
| return false; |
| } |
| return (item.left - _expectedRect.left).abs() < 1e-6 && |
| (item.top - _expectedRect.top).abs() < 1e-6 && |
| (item.right - _expectedRect.right).abs() < 1e-6 && |
| (item.bottom - _expectedRect.bottom).abs() < 1e-6; |
| } |
| |
| @override |
| Description describe(Description description) { |
| return description.add('Rect is close (within 1e-6) to $_expectedRect'); |
| } |
| } |
| |
| Matcher closeToTransform(Float64List expected) => |
| _CloseToTransformMatcher(expected); |
| |
| final class _CloseToTransformMatcher extends Matcher { |
| _CloseToTransformMatcher(this._expected); |
| final Float64List _expected; |
| |
| @override |
| bool matches(Object? item, Map<Object?, Object?> matchState) { |
| if (item is! Float64List) { |
| return false; |
| } |
| if (item.length != 16 || _expected.length != 16) { |
| return false; |
| } |
| for (int i = 0; i < 16; i++) { |
| if ((item[i] - _expected[i]).abs() > 1e-10) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| @override |
| Description describe(Description description) { |
| return description.add('Transform is close (within 1e-10) to $_expected'); |
| } |
| } |