blob: 9e33a58eaf0683f1fca91fdbf4ee282677672f95 [file] [log] [blame]
// 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:html' as html;
import 'dart:math' as math;
import 'dart:typed_data';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' hide TextStyle;
import 'package:web_engine_tester/golden_tester.dart';
import '../matchers.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
Future<void> testMain() async {
const double screenWidth = 600.0;
const double screenHeight = 800.0;
const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
final SurfacePaint testPaint = SurfacePaint()..color = const Color(0xFFFF0000);
// Commit a recording canvas to a bitmap, and compare with the expected
Future<void> _checkScreenshot(RecordingCanvas rc, String fileName,
{ Rect region = const Rect.fromLTWH(0, 0, 500, 500),
bool write = false }) async {
final EngineCanvas engineCanvas = BitmapCanvas(screenRect,
RenderStrategy());
// Draws the estimated bounds so we can spot the bug in Scuba.
engineCanvas
..save()
..drawRect(
rc.pictureBounds!,
SurfacePaintData()
..color = const Color.fromRGBO(0, 0, 255, 1.0)
..style = PaintingStyle.stroke
..strokeWidth = 1.0,
)
..restore();
rc.apply(engineCanvas, screenRect);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final html.Element sceneElement = html.Element.tag('flt-scene');
try {
sceneElement.append(engineCanvas.rootElement);
html.document.body!.append(sceneElement);
await matchGoldenFile('paint_bounds_for_$fileName.png', region: region,
write: write);
} finally {
// The page is reused across tests, so remove the element after taking the
// Scuba screenshot.
sceneElement.remove();
}
}
setUp(() async {
debugEmulateFlutterTesterEnvironment = true;
await webOnlyInitializePlatform();
webOnlyFontCollection.debugRegisterTestFonts();
await webOnlyFontCollection.ensureFontsLoaded();
});
test('Empty canvas reports correct paint bounds', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTWH(1, 2, 300, 400));
rc.endRecording();
expect(rc.pictureBounds, Rect.zero);
await _checkScreenshot(rc, 'empty_canvas');
});
test('Computes paint bounds for draw line', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawLine(const Offset(50, 100), const Offset(120, 140), testPaint);
rc.endRecording();
// The off by one is due to the minimum stroke width of 1.
expect(rc.pictureBounds, const Rect.fromLTRB(49, 99, 121, 141));
await _checkScreenshot(rc, 'draw_line');
});
test('Computes paint bounds for draw line when line exceeds limits',
() async {
// Uses max bounds when computing paint bounds
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawLine(const Offset(50, 100), const Offset(screenWidth + 100.0, 140),
testPaint);
rc.endRecording();
// The off by one is due to the minimum stroke width of 1.
expect(
rc.pictureBounds, const Rect.fromLTRB(49.0, 99.0, screenWidth, 141.0));
await _checkScreenshot(rc, 'draw_line_exceeding_limits');
});
test('Computes paint bounds for draw rect', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(10, 20, 30, 40));
await _checkScreenshot(rc, 'draw_rect');
});
test('Computes paint bounds for draw rect when exceeds limits', () async {
// Uses max bounds when computing paint bounds
RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawRect(
const Rect.fromLTRB(10, 20, 30 + screenWidth, 40 + screenHeight),
testPaint);
rc.endRecording();
expect(rc.pictureBounds,
const Rect.fromLTRB(10, 20, screenWidth, screenHeight));
rc = RecordingCanvas(screenRect);
rc.drawRect(const Rect.fromLTRB(-200, -100, 30, 40), testPaint);
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(0, 0, 30, 40));
await _checkScreenshot(rc, 'draw_rect_exceeding_limits');
});
test('Computes paint bounds for translate', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.translate(5, 7);
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(15, 27, 35, 47));
await _checkScreenshot(rc, 'translate');
});
test('Computes paint bounds for scale', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.scale(2, 2);
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(20, 40, 60, 80));
await _checkScreenshot(rc, 'scale');
});
test('Computes paint bounds for rotate', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.rotate(math.pi / 4.0);
rc.drawLine(
const Offset(1, 0), Offset(50 * math.sqrt(2) - 1, 0), testPaint);
rc.endRecording();
// The extra 0.7 is due to stroke width of 1 rotated by 45 degrees.
expect(rc.pictureBounds,
within(distance: 0.1, from: const Rect.fromLTRB(0, 0, 50.7, 50.7)));
await _checkScreenshot(rc, 'rotate');
});
test('Computes paint bounds for horizontal skew', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.skew(1.0, 0.0);
rc.drawRect(const Rect.fromLTRB(20, 20, 40, 40), testPaint);
rc.endRecording();
expect(
rc.pictureBounds,
within(
distance: 0.1, from: const Rect.fromLTRB(40.0, 20.0, 80.0, 40.0)));
await _checkScreenshot(rc, 'skew_horizontally');
});
test('Computes paint bounds for vertical skew', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.skew(0.0, 1.0);
rc.drawRect(const Rect.fromLTRB(20, 20, 40, 40), testPaint);
rc.endRecording();
expect(
rc.pictureBounds,
within(
distance: 0.1, from: const Rect.fromLTRB(20.0, 40.0, 40.0, 80.0)));
await _checkScreenshot(rc, 'skew_vertically');
});
test('Computes paint bounds for a complex transform', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
final Float32List matrix = Float32List(16);
// translate(210, 220) , scale(2, 3), rotate(math.pi / 4.0)
matrix[0] = 1.4;
matrix[1] = 2.12;
matrix[2] = 0.0;
matrix[3] = 0.0;
matrix[4] = -1.4;
matrix[5] = 2.12;
matrix[6] = 0.0;
matrix[7] = 0.0;
matrix[8] = 0.0;
matrix[9] = 0.0;
matrix[10] = 2.0;
matrix[11] = 0.0;
matrix[12] = 210.0;
matrix[13] = 220.0;
matrix[14] = 0.0;
matrix[15] = 1.0;
rc.transform(matrix);
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
rc.endRecording();
expect(
rc.pictureBounds,
within(
distance: 0.001,
from: const Rect.fromLTRB(168.0, 283.6, 224.0, 368.4)));
await _checkScreenshot(rc, 'complex_transform');
});
test('drawPaint should cover full size', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawPaint(testPaint);
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
rc.endRecording();
expect(rc.pictureBounds, screenRect);
await _checkScreenshot(rc, 'draw_paint');
});
test('drawColor should cover full size', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
final SurfacePaint testPaint = SurfacePaint()..color = const Color(0xFF80FF00);
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
rc.drawColor(const Color(0xFFFF0000), BlendMode.multiply);
rc.drawRect(const Rect.fromLTRB(10, 60, 30, 80), testPaint);
rc.endRecording();
expect(rc.pictureBounds, screenRect);
await _checkScreenshot(rc, 'draw_color');
});
test('Computes paint bounds for draw oval', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawOval(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(10, 20, 30, 40));
await _checkScreenshot(rc, 'draw_oval');
});
test('Computes paint bounds for draw round rect', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawRRect(
RRect.fromRectAndRadius(
const Rect.fromLTRB(10, 20, 30, 40), const Radius.circular(5.0)),
testPaint);
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(10, 20, 30, 40));
await _checkScreenshot(rc, 'draw_round_rect');
});
test(
'Computes empty paint bounds when inner rect outside of outer rect for '
'drawDRRect', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawDRRect(RRect.fromRectAndCorners(const Rect.fromLTRB(10, 20, 30, 40)),
RRect.fromRectAndCorners(const Rect.fromLTRB(1, 2, 3, 4)), testPaint);
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(0, 0, 0, 0));
await _checkScreenshot(rc, 'draw_drrect_empty');
});
test('Computes paint bounds using outer rect for drawDRRect', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawDRRect(
RRect.fromRectAndCorners(const Rect.fromLTRB(10, 20, 30, 40)),
RRect.fromRectAndCorners(const Rect.fromLTRB(12, 22, 28, 38)),
testPaint);
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(10, 20, 30, 40));
await _checkScreenshot(rc, 'draw_drrect');
});
test('Computes paint bounds for draw circle', () async {
// Paint bounds of one circle.
RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawCircle(const Offset(20, 20), 10.0, testPaint);
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(10.0, 10.0, 30.0, 30.0));
// Paint bounds of a union of two circles.
rc = RecordingCanvas(screenRect);
rc.drawCircle(const Offset(20, 20), 10.0, testPaint);
rc.drawCircle(const Offset(200, 300), 100.0, testPaint);
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(10.0, 10.0, 300.0, 400.0));
await _checkScreenshot(rc, 'draw_circle');
});
test('Computes paint bounds for draw image', () {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawImage(TestImage(), const Offset(50, 100), SurfacePaint());
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(50.0, 100.0, 70.0, 110.0));
});
test('Computes paint bounds for draw image rect', () {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawImageRect(TestImage(), const Rect.fromLTRB(1, 1, 20, 10),
const Rect.fromLTRB(5, 6, 400, 500), SurfacePaint());
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(5.0, 6.0, 400.0, 500.0));
});
test('Computes paint bounds for single-line draw paragraph', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
final Paragraph paragraph = createTestParagraph();
const double textLeft = 5.0;
const double textTop = 7.0;
const double widthConstraint = 300.0;
paragraph.layout(const ParagraphConstraints(width: widthConstraint));
rc.drawParagraph(paragraph, const Offset(textLeft, textTop));
rc.endRecording();
expect(
rc.pictureBounds,
const Rect.fromLTRB(textLeft, textTop, textLeft + widthConstraint, 21.0),
);
await _checkScreenshot(rc, 'draw_paragraph');
}, // TODO: https://github.com/flutter/flutter/issues/65789
skip: browserEngine == BrowserEngine.webkit &&
operatingSystem == OperatingSystem.iOs);
test('Computes paint bounds for multi-line draw paragraph', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
final Paragraph paragraph = createTestParagraph();
const double textLeft = 5.0;
const double textTop = 7.0;
// Do not go lower than the shortest word.
const double widthConstraint = 130.0;
paragraph.layout(const ParagraphConstraints(width: widthConstraint));
rc.drawParagraph(paragraph, const Offset(textLeft, textTop));
rc.endRecording();
expect(
rc.pictureBounds,
const Rect.fromLTRB(textLeft, textTop, textLeft + widthConstraint, 35.0),
);
await _checkScreenshot(rc, 'draw_paragraph_multi_line');
}, // TODO: https://github.com/flutter/flutter/issues/65789
skip: browserEngine == BrowserEngine.webkit &&
operatingSystem == OperatingSystem.iOs);
test('Should exclude painting outside simple clipRect', () async {
// One clipped line.
RecordingCanvas rc = RecordingCanvas(screenRect);
rc.clipRect(const Rect.fromLTRB(50, 50, 100, 100), ClipOp.intersect);
rc.drawLine(const Offset(10, 11), const Offset(20, 21), testPaint);
rc.endRecording();
expect(rc.pictureBounds, Rect.zero);
// Two clipped lines.
rc = RecordingCanvas(screenRect);
rc.clipRect(const Rect.fromLTRB(50, 50, 100, 100), ClipOp.intersect);
rc.drawLine(const Offset(10, 11), const Offset(20, 21), testPaint);
rc.drawLine(const Offset(52, 53), const Offset(55, 56), testPaint);
rc.endRecording();
// Extra pixel due to default line length
expect(rc.pictureBounds, const Rect.fromLTRB(51, 52, 56, 57));
await _checkScreenshot(rc, 'clip_rect_simple');
});
test('Should include intersection of clipRect and painting', () async {
RecordingCanvas rc = RecordingCanvas(screenRect);
rc.clipRect(const Rect.fromLTRB(50, 50, 100, 100), ClipOp.intersect);
rc.drawRect(const Rect.fromLTRB(20, 60, 120, 70), testPaint);
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(50, 60, 100, 70));
await _checkScreenshot(rc, 'clip_rect_intersects_paint_left_to_right');
rc = RecordingCanvas(screenRect);
rc.clipRect(const Rect.fromLTRB(50, 50, 100, 100), ClipOp.intersect);
rc.drawRect(const Rect.fromLTRB(60, 20, 70, 200), testPaint);
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(60, 50, 70, 100));
await _checkScreenshot(rc, 'clip_rect_intersects_paint_top_to_bottom');
});
test('Should intersect rects for multiple clipRect calls', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.clipRect(const Rect.fromLTRB(50, 50, 100, 100), ClipOp.intersect);
rc.scale(2.0, 2.0);
rc.clipRect(const Rect.fromLTRB(30, 30, 45, 45), ClipOp.intersect);
rc.drawRect(const Rect.fromLTRB(10, 30, 60, 35), testPaint);
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(60, 60, 90, 70));
await _checkScreenshot(rc, 'clip_rects_intersect');
});
// drawShadow
test('Computes paint bounds for drawShadow', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
final Path path = Path();
path.addRect(const Rect.fromLTRB(20, 30, 100, 110));
rc.drawShadow(path, const Color(0xFFFF0000), 2.0, true);
rc.endRecording();
expect(
rc.pictureBounds,
within(
distance: 0.05, from: const Rect.fromLTRB(17.9, 28.5, 103.5, 114.1)),
);
await _checkScreenshot(rc, 'path_with_shadow');
});
test('Clip with negative scale reports correct paint bounds', () async {
// The following draws a filled rectangle that occupies the bottom half of
// the canvas. Notice that both the clip and the rectangle are drawn
// forward. What makes them appear at the bottom is the translation and a
// vertical flip via a negative scale. This replicates the Material
// overscroll glow effect at the bottom of a list, where it is drawn upside
// down.
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 100, 100));
rc
..translate(0, 100)
..scale(1, -1)
..clipRect(const Rect.fromLTRB(0, 0, 100, 50), ClipOp.intersect)
..drawRect(const Rect.fromLTRB(0, 0, 100, 100), SurfacePaint());
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(0.0, 50.0, 100.0, 100.0));
await _checkScreenshot(rc, 'scale_negative');
});
test('Clip with a rotation reports correct paint bounds', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 100, 100));
rc
..translate(50, 50)
..rotate(math.pi / 4.0)
..clipRect(const Rect.fromLTWH(-20, -20, 40, 40), ClipOp.intersect)
..drawRect(const Rect.fromLTWH(-80, -80, 160, 160), SurfacePaint());
rc.endRecording();
expect(
rc.pictureBounds,
within(
distance: 0.001,
from: Rect.fromCircle(
center: const Offset(50, 50), radius: 20 * math.sqrt(2))),
);
await _checkScreenshot(rc, 'clip_rect_rotated');
});
test('Rotated line reports correct paint bounds', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 100, 100));
rc
..translate(50, 50)
..rotate(math.pi / 4.0)
..drawLine(const Offset(0, 0), const Offset(20, 20), SurfacePaint());
rc.endRecording();
expect(
rc.pictureBounds,
within(distance: 0.1, from: const Rect.fromLTRB(34.4, 48.6, 65.6, 79.7)),
);
await _checkScreenshot(rc, 'line_rotated');
});
test('Should support reusing path and reset when drawing into canvas.',
() async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 100, 100));
final Path path = Path();
path.moveTo(3, 0);
path.lineTo(100, 97);
rc.drawPath(
path,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFFFF0000));
path.reset();
path.moveTo(0, 3);
path.lineTo(97, 100);
rc.drawPath(
path,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFF00FF00));
rc.endRecording();
await _checkScreenshot(rc, 'reuse_path');
});
test('Should draw RRect after line when beginning new path.', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 200, 400));
rc.save();
rc.translate(50.0, 100.0);
final Path path = Path();
// Draw a vertical small line (caret).
path.moveTo(8, 4);
path.lineTo(8, 24);
// Draw round rect below caret.
path.addRRect(
RRect.fromLTRBR(0.5, 100.5, 80.7, 150.7, const Radius.circular(10)));
rc.drawPath(
path,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFF404000));
rc.restore();
rc.endRecording();
await _checkScreenshot(rc, 'path_with_line_and_roundrect');
});
// Regression test for https://github.com/flutter/flutter/issues/64371.
test('Should draw line following a polygon without closing path.', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTRB(0, 0, 200, 400));
rc.save();
rc.translate(50.0, 100.0);
final Path path = Path();
// Draw a vertical small line (caret).
path.addPolygon(const <Offset>[Offset(0, 10), Offset(20,5), Offset(50,10)],
false);
path.lineTo(60, 80);
path.lineTo(0, 80);
path.close();
rc.drawPath(
path,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFF404000));
rc.restore();
rc.endRecording();
await _checkScreenshot(rc, 'path_with_addpolygon');
});
test('should include paint spread in bounds estimates', () async {
final SurfaceSceneBuilder sb = SurfaceSceneBuilder();
final List<PaintSpreadPainter> painters = <PaintSpreadPainter>[
(RecordingCanvas canvas, SurfacePaint paint) {
canvas.drawLine(
const Offset(0.0, 0.0),
const Offset(20.0, 20.0),
paint,
);
},
(RecordingCanvas canvas, SurfacePaint paint) {
canvas.drawRect(
const Rect.fromLTRB(0.0, 0.0, 20.0, 20.0),
paint,
);
},
(RecordingCanvas canvas, SurfacePaint paint) {
canvas.drawRRect(
RRect.fromLTRBR(0.0, 0.0, 20.0, 20.0, const Radius.circular(7.0)),
paint,
);
},
(RecordingCanvas canvas, SurfacePaint paint) {
canvas.drawDRRect(
RRect.fromLTRBR(0.0, 0.0, 20.0, 20.0, const Radius.circular(5.0)),
RRect.fromLTRBR(4.0, 4.0, 16.0, 16.0, const Radius.circular(5.0)),
paint,
);
},
(RecordingCanvas canvas, SurfacePaint paint) {
canvas.drawOval(
const Rect.fromLTRB(0.0, 5.0, 20.0, 15.0),
paint,
);
},
(RecordingCanvas canvas, SurfacePaint paint) {
canvas.drawCircle(
const Offset(10.0, 10.0),
10.0,
paint,
);
},
(RecordingCanvas canvas, SurfacePaint paint) {
final SurfacePath path = SurfacePath()
..moveTo(10, 0)
..lineTo(20, 10)
..lineTo(10, 20)
..lineTo(0, 10)
..close();
canvas.drawPath(path, paint);
},
// Images are not affected by mask filter or stroke width. They use image
// filter instead.
(RecordingCanvas canvas, SurfacePaint paint) {
canvas.drawImage(_createRealTestImage(), Offset.zero, paint);
},
(RecordingCanvas canvas, SurfacePaint paint) {
canvas.drawImageRect(
_createRealTestImage(),
const Rect.fromLTRB(0, 0, 20, 20),
const Rect.fromLTRB(5, 5, 15, 15),
paint,
);
},
];
Picture drawBounds(Rect bounds) {
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(Rect.largest);
canvas.drawRect(
bounds,
SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 1.0
..color = const Color.fromARGB(255, 0, 255, 0),
);
return recorder.endRecording();
}
for (int i = 0; i < painters.length; i++) {
sb.pushOffset(0.0, 20.0 + 60.0 * i);
final PaintSpreadPainter painter = painters[i];
// Paint with zero paint spread.
{
sb.pushOffset(20.0, 0.0);
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(Rect.largest);
final SurfacePaint zeroSpreadPaint = SurfacePaint();
painter(canvas, zeroSpreadPaint);
sb.addPicture(Offset.zero, recorder.endRecording());
sb.addPicture(Offset.zero, drawBounds(canvas.pictureBounds!));
sb.pop();
}
// Paint with a thick stroke paint.
{
sb.pushOffset(80.0, 0.0);
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(Rect.largest);
final SurfacePaint thickStrokePaint = SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 5.0;
painter(canvas, thickStrokePaint);
sb.addPicture(Offset.zero, recorder.endRecording());
sb.addPicture(Offset.zero, drawBounds(canvas.pictureBounds!));
sb.pop();
}
// Paint with a mask filter blur.
{
sb.pushOffset(140.0, 0.0);
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(Rect.largest);
final SurfacePaint maskFilterBlurPaint = SurfacePaint()
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 5.0);
painter(canvas, maskFilterBlurPaint);
sb.addPicture(Offset.zero, recorder.endRecording());
sb.addPicture(Offset.zero, drawBounds(canvas.pictureBounds!));
sb.pop();
}
// Paint with a thick stroke paint and a mask filter blur.
{
sb.pushOffset(200.0, 0.0);
final EnginePictureRecorder recorder = EnginePictureRecorder();
final RecordingCanvas canvas = recorder.beginRecording(Rect.largest);
final SurfacePaint thickStrokeAndBlurPaint = SurfacePaint()
..style = PaintingStyle.stroke
..strokeWidth = 5.0
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 5.0);
painter(canvas, thickStrokeAndBlurPaint);
sb.addPicture(Offset.zero, recorder.endRecording());
sb.addPicture(Offset.zero, drawBounds(canvas.pictureBounds!));
sb.pop();
}
sb.pop();
}
final html.Element sceneElement = sb.build().webOnlyRootElement!;
html.document.body!.append(sceneElement);
try {
await matchGoldenFile(
'paint_spread_bounds.png',
region: const Rect.fromLTRB(0, 0, 250, 600),
maxDiffRatePercent: 0.21,
pixelComparison: PixelComparison.precise,
);
} finally {
sceneElement.remove();
}
});
}
typedef PaintSpreadPainter = void Function(
RecordingCanvas canvas, SurfacePaint paint);
const String _base64Encoded20x20TestImage = 'iVBORw0KGgoAAAANSUhEUgAAABQAAAAUC'
'AIAAAAC64paAAAACXBIWXMAAC4jAAAuIwF4pT92AAAA'
'B3RJTUUH5AMFFBksg4i3gQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAj'
'SURBVDjLY2TAC/7jlWVioACMah4ZmhnxpyHG0QAb1UyZZgBjWAIm/clP0AAAAABJRU5ErkJggg==';
HtmlImage _createRealTestImage() {
return HtmlImage(
html.ImageElement()
..src = 'data:text/plain;base64,$_base64Encoded20x20TestImage',
20,
20,
);
}
class TestImage implements Image {
@override
int get width => 20;
@override
int get height => 10;
@override
Future<ByteData> toByteData(
{ImageByteFormat format = ImageByteFormat.rawRgba}) async {
throw UnsupportedError('Cannot encode test image');
}
@override
String toString() => '[$width\u00D7$height]';
@override
void dispose() {}
@override
bool get debugDisposed => false;
@override
Image clone() => this;
@override
bool isCloneOf(Image other) => other == this;
@override
List<StackTrace>/*?*/ debugGetOpenHandleStackTraces() => <StackTrace>[];
}
Paragraph createTestParagraph() {
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(
fontFamily: 'Ahem',
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
fontSize: 14.0,
));
builder.addText('A short sentence.');
return builder.build();
}