mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Add web engine screenshot (scuba) tests (flutter/engine#12353)
* Import all golden files and tests from internal repo. * Adapt test files to new screenshot API, and tweak some settings. (Check PR to see individual changes to each file) Fixes https://github.com/flutter/flutter/issues/40975
This commit is contained in:
parent
f6081ed304
commit
0c2c7cfd15
@ -49,7 +49,7 @@ import 'goldens.dart';
|
||||
const int _kChromeDevtoolsPort = 12345;
|
||||
const int _kMaxScreenshotWidth = 1024;
|
||||
const int _kMaxScreenshotHeight = 1024;
|
||||
const double _kMaxDiffRateFailure = 1.0/100000; // 0.001%
|
||||
const double _kMaxDiffRateFailure = 0.211/100; // 0.211%
|
||||
|
||||
class BrowserPlatform extends PlatformPlugin {
|
||||
/// Starts the server.
|
||||
|
||||
@ -0,0 +1,86 @@
|
||||
// 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 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:web_engine_tester/golden_tester.dart';
|
||||
|
||||
void main() async {
|
||||
final Rect region = Rect.fromLTWH(8, 8, 500, 100); // Compensate for old scuba tester padding
|
||||
|
||||
BitmapCanvas canvas;
|
||||
|
||||
final PaintData niceRRectPaint = PaintData()
|
||||
..color = const Color.fromRGBO(250, 186, 218, 1.0) // #fabada
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
// Some values to see how the algo behaves as radius get absurdly large
|
||||
const List<double> rRectRadii = <double>[0, 10, 20, 80, 8000];
|
||||
|
||||
const Radius someFixedRadius = Radius.circular(10);
|
||||
|
||||
setUp(() {
|
||||
canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 500, 100));
|
||||
canvas.translate(10, 10); // Center
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
canvas.rootElement.remove();
|
||||
});
|
||||
|
||||
test('round square with big (equal) radius ends up as a circle', () async {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
canvas.drawRRect(
|
||||
RRect.fromRectAndRadius(Rect.fromLTWH(100 * i.toDouble(), 0, 80, 80),
|
||||
Radius.circular(rRectRadii[i])),
|
||||
niceRRectPaint);
|
||||
}
|
||||
|
||||
html.document.body.append(canvas.rootElement);
|
||||
await matchGoldenFile('engine/canvas_rrect_round_square.png', region: region);
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
test('round rect with big radius scale down smaller radius', () async {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
final Radius growingRadius = Radius.circular(rRectRadii[i]);
|
||||
final RRect rrect = RRect.fromRectAndCorners(
|
||||
Rect.fromLTWH(100 * i.toDouble(), 0, 80, 80),
|
||||
bottomRight: someFixedRadius,
|
||||
topRight: growingRadius,
|
||||
bottomLeft: growingRadius);
|
||||
|
||||
canvas.drawRRect(rrect, niceRRectPaint);
|
||||
}
|
||||
|
||||
html.document.body.append(canvas.rootElement);
|
||||
await matchGoldenFile('engine/canvas_rrect_overlapping_radius.png', region: region);
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
test('diff round rect with big radius scale down smaller radius', () async {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
final Radius growingRadius = Radius.circular(rRectRadii[i]);
|
||||
final RRect outerRRect = RRect.fromRectAndCorners(
|
||||
Rect.fromLTWH(100 * i.toDouble(), 0, 80, 80),
|
||||
bottomRight: someFixedRadius,
|
||||
topRight: growingRadius,
|
||||
bottomLeft: growingRadius);
|
||||
|
||||
// Inner is half of outer, but offset a little so it looks nicer
|
||||
final RRect innerRRect = RRect.fromRectAndCorners(
|
||||
Rect.fromLTWH(100 * i.toDouble() + 5, 5, 40, 40),
|
||||
bottomRight: someFixedRadius / 2,
|
||||
topRight: growingRadius / 2,
|
||||
bottomLeft: growingRadius / 2);
|
||||
|
||||
canvas.drawDRRect(outerRRect, innerRRect, niceRRectPaint);
|
||||
}
|
||||
|
||||
html.document.body.append(canvas.rootElement);
|
||||
await matchGoldenFile('engine/canvas_drrect_overlapping_radius.png', region: region);
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
// 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 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:web_engine_tester/golden_tester.dart';
|
||||
|
||||
void main() async {
|
||||
final Rect region = Rect.fromLTWH(8, 8, 500, 100); // Compensate for old scuba tester padding
|
||||
|
||||
BitmapCanvas canvas;
|
||||
|
||||
setUp(() {
|
||||
html.document.body.style.transform = 'translate(10px, 10px)';
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
html.document.body.style.transform = 'none';
|
||||
canvas.rootElement.remove();
|
||||
});
|
||||
|
||||
/// Draws several lines, some aligned precisely with the pixel grid, and some
|
||||
/// that are offset by 0.5 vertically or horizontally.
|
||||
///
|
||||
/// The produced picture stresses the antialiasing generated by the browser
|
||||
/// when positioning and rasterizing `<canvas>` tags. Aliasing artifacts can
|
||||
/// be seen depending on pixel alignment and whether antialiasing happens
|
||||
/// before or after rasterization.
|
||||
void drawMisalignedLines(BitmapCanvas canvas) {
|
||||
final PaintData linePaint = (Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1)
|
||||
.webOnlyPaintData;
|
||||
|
||||
final PaintData fillPaint =
|
||||
(Paint()..style = PaintingStyle.fill).webOnlyPaintData;
|
||||
|
||||
canvas.drawRect(
|
||||
const Rect.fromLTWH(0, 0, 40, 40),
|
||||
linePaint,
|
||||
);
|
||||
|
||||
canvas.drawLine(
|
||||
const Offset(10, 0),
|
||||
const Offset(10, 40),
|
||||
linePaint,
|
||||
);
|
||||
|
||||
canvas.drawLine(
|
||||
const Offset(20.5, 0),
|
||||
const Offset(20, 40),
|
||||
linePaint,
|
||||
);
|
||||
|
||||
canvas.drawCircle(const Offset(30, 10), 3, fillPaint);
|
||||
canvas.drawCircle(const Offset(30.5, 30), 3, fillPaint);
|
||||
}
|
||||
|
||||
test('renders pixels that are not aligned inside the canvas', () async {
|
||||
canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 60, 60));
|
||||
|
||||
drawMisalignedLines(canvas);
|
||||
|
||||
html.document.body.append(canvas.rootElement);
|
||||
|
||||
await matchGoldenFile('engine/misaligned_pixels_in_canvas_test.png', region: region);
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
test('compensates for misalignment of the canvas', () async {
|
||||
// Notice the 0.5 offset in the bounds rectangle. It's what causes the
|
||||
// misalignment of canvas relative to the pixel grid. BitmapCanvas will
|
||||
// shift its position back to 0.0 and at the same time it will it will
|
||||
// compensate by shifting the contents of the canvas in the opposite
|
||||
// direction.
|
||||
canvas = BitmapCanvas(const Rect.fromLTWH(0.5, 0.5, 60, 60));
|
||||
|
||||
drawMisalignedLines(canvas);
|
||||
|
||||
html.document.body.append(canvas.rootElement);
|
||||
|
||||
await matchGoldenFile('engine/misaligned_canvas_test.png', region: region);
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
test('fill the whole canvas with color even when transformed', () async {
|
||||
canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50));
|
||||
|
||||
canvas.translate(25, 25);
|
||||
canvas.drawColor(const Color.fromRGBO(0, 255, 0, 1.0), BlendMode.src);
|
||||
|
||||
html.document.body.append(canvas.rootElement);
|
||||
|
||||
await matchGoldenFile('engine/bitmap_canvas_fills_color_when_transformed.png', region: region);
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
test('fill the whole canvas with paint even when transformed', () async {
|
||||
canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50));
|
||||
|
||||
canvas.translate(25, 25);
|
||||
canvas.drawPaint(PaintData()
|
||||
..color = const Color.fromRGBO(0, 255, 0, 1.0)
|
||||
..style = PaintingStyle.fill);
|
||||
|
||||
html.document.body.append(canvas.rootElement);
|
||||
|
||||
await matchGoldenFile('engine/bitmap_canvas_fills_paint_when_transformed.png', region: region);
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
}
|
||||
@ -0,0 +1,542 @@
|
||||
// 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 'package:ui/ui.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../matchers.dart';
|
||||
import 'package:web_engine_tester/golden_tester.dart';
|
||||
|
||||
final Rect region = Rect.fromLTWH(0, 0, 500, 100);
|
||||
|
||||
void main() async {
|
||||
debugShowClipLayers = true;
|
||||
|
||||
setUp(() {
|
||||
SceneBuilder.debugForgetFrameScene();
|
||||
for (html.Node scene in html.document.querySelectorAll('flt-scene')) {
|
||||
scene.remove();
|
||||
}
|
||||
});
|
||||
|
||||
test('pushClipRect', () async {
|
||||
final SceneBuilder builder = SceneBuilder();
|
||||
builder.pushClipRect(
|
||||
const Rect.fromLTRB(10, 10, 60, 60),
|
||||
);
|
||||
_drawTestPicture(builder);
|
||||
builder.pop();
|
||||
|
||||
html.document.body.append(builder.build().webOnlyRootElement);
|
||||
|
||||
await matchGoldenFile('engine/compositing_shifted_clip_rect.png', region: region);
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
test('pushClipRect with offset and transform', () async {
|
||||
final SceneBuilder builder = SceneBuilder();
|
||||
|
||||
builder.pushOffset(0, 60);
|
||||
builder.pushTransform(
|
||||
Matrix4.diagonal3Values(1, -1, 1).storage,
|
||||
);
|
||||
builder.pushClipRect(
|
||||
const Rect.fromLTRB(10, 10, 60, 60),
|
||||
);
|
||||
_drawTestPicture(builder);
|
||||
builder.pop();
|
||||
builder.pop();
|
||||
builder.pop();
|
||||
|
||||
html.document.body.append(builder.build().webOnlyRootElement);
|
||||
|
||||
await matchGoldenFile('engine/compositing_clip_rect_with_offset_and_transform.png', region: region);
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
test('pushClipRRect', () async {
|
||||
final SceneBuilder builder = SceneBuilder();
|
||||
builder.pushClipRRect(
|
||||
RRect.fromLTRBR(10, 10, 60, 60, const Radius.circular(5)),
|
||||
);
|
||||
_drawTestPicture(builder);
|
||||
builder.pop();
|
||||
|
||||
html.document.body.append(builder.build().webOnlyRootElement);
|
||||
|
||||
await matchGoldenFile('engine/compositing_shifted_clip_rrect.png', region: region);
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
test('pushPhysicalShape', () async {
|
||||
final SceneBuilder builder = SceneBuilder();
|
||||
builder.pushPhysicalShape(
|
||||
path: Path()..addRect(const Rect.fromLTRB(10, 10, 60, 60)),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: const Color.fromRGBO(0, 0, 0, 0.3),
|
||||
elevation: 0,
|
||||
);
|
||||
_drawTestPicture(builder);
|
||||
builder.pop();
|
||||
|
||||
builder.pushOffset(70, 0);
|
||||
builder.pushPhysicalShape(
|
||||
path: Path()
|
||||
..addRRect(RRect.fromLTRBR(10, 10, 60, 60, const Radius.circular(5))),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
color: const Color.fromRGBO(0, 0, 0, 0.3),
|
||||
elevation: 0,
|
||||
);
|
||||
_drawTestPicture(builder);
|
||||
builder.pop();
|
||||
builder.pop();
|
||||
|
||||
html.document.body.append(builder.build().webOnlyRootElement);
|
||||
|
||||
await matchGoldenFile('engine/compositing_shifted_physical_shape_clip.png', region: region);
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
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 SceneBuilder builder = SceneBuilder();
|
||||
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
|
||||
canvas.drawCircle(
|
||||
Offset.zero, 10000, Paint()..style = PaintingStyle.fill);
|
||||
});
|
||||
builder.build();
|
||||
|
||||
final PersistedStandardPicture picture = enumeratePictures().single;
|
||||
expect(picture.optimalLocalCullRect, const 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 SceneBuilder builder = SceneBuilder();
|
||||
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
|
||||
canvas.drawCircle(Offset.zero, 20, Paint()..style = PaintingStyle.fill);
|
||||
});
|
||||
builder.build();
|
||||
|
||||
final PersistedStandardPicture picture = enumeratePictures().single;
|
||||
expect(picture.optimalLocalCullRect, const 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 SceneBuilder builder = SceneBuilder();
|
||||
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
|
||||
canvas.drawCircle(
|
||||
const Offset(-100, -100), 20, Paint()..style = PaintingStyle.fill);
|
||||
});
|
||||
builder.build();
|
||||
|
||||
final PersistedStandardPicture picture = enumeratePictures().single;
|
||||
expect(picture.optimalLocalCullRect, Rect.zero);
|
||||
expect(picture.debugExactGlobalCullRect, 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 SceneBuilder builder = SceneBuilder();
|
||||
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
|
||||
canvas.drawCircle(
|
||||
const Offset(50, 50), 10, Paint()..style = PaintingStyle.fill);
|
||||
});
|
||||
builder.build();
|
||||
|
||||
final PersistedStandardPicture picture = enumeratePictures().single;
|
||||
expect(picture.optimalLocalCullRect, const 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 SceneBuilder builder = SceneBuilder();
|
||||
|
||||
builder.pushOffset(10, 10);
|
||||
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
|
||||
canvas.drawCircle(
|
||||
const Offset(50, 50), 10, Paint()..style = PaintingStyle.fill);
|
||||
});
|
||||
builder.pop();
|
||||
|
||||
builder.build();
|
||||
|
||||
final PersistedStandardPicture picture = enumeratePictures().single;
|
||||
expect(picture.optimalLocalCullRect, const 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 SceneBuilder builder = SceneBuilder();
|
||||
|
||||
builder.pushOffset(0, 90);
|
||||
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
|
||||
canvas.drawCircle(Offset.zero, 20, Paint()..style = PaintingStyle.fill);
|
||||
});
|
||||
builder.pop();
|
||||
|
||||
builder.build();
|
||||
|
||||
final PersistedStandardPicture picture = enumeratePictures().single;
|
||||
expect(
|
||||
picture.debugExactGlobalCullRect, const Rect.fromLTRB(0, 70, 20, 100));
|
||||
expect(picture.optimalLocalCullRect, const 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 SceneBuilder builder = SceneBuilder();
|
||||
builder.pushClipRect(
|
||||
const Rect.fromLTWH(10, 10, 60, 60),
|
||||
);
|
||||
|
||||
builder.pushClipRect(
|
||||
const Rect.fromLTWH(40, 40, 60, 60),
|
||||
);
|
||||
|
||||
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
|
||||
canvas.drawCircle(
|
||||
Offset.zero, 10000, Paint()..style = PaintingStyle.fill);
|
||||
});
|
||||
|
||||
builder.pop(); // pushClipRect
|
||||
builder.pop(); // pushClipRect
|
||||
html.document.body.append(builder.build().webOnlyRootElement);
|
||||
|
||||
await matchGoldenFile('engine/compositing_cull_rect_fills_layer_clip.png', region: region);
|
||||
|
||||
final PersistedStandardPicture picture = enumeratePictures().single;
|
||||
expect(picture.optimalLocalCullRect, const Rect.fromLTRB(40, 40, 70, 70));
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
// 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 SceneBuilder builder = SceneBuilder();
|
||||
builder.pushClipRect(
|
||||
const Rect.fromLTWH(10, 10, 60, 60),
|
||||
);
|
||||
|
||||
builder.pushClipRect(
|
||||
const Rect.fromLTWH(40, 40, 60, 60),
|
||||
);
|
||||
|
||||
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
|
||||
canvas.drawCircle(
|
||||
const Offset(80, 55), 30, Paint()..style = PaintingStyle.fill);
|
||||
});
|
||||
|
||||
builder.pop(); // pushClipRect
|
||||
builder.pop(); // pushClipRect
|
||||
html.document.body.append(builder.build().webOnlyRootElement);
|
||||
|
||||
await matchGoldenFile('engine/compositing_cull_rect_intersects_clip_and_paint_bounds.png', region: region);
|
||||
|
||||
final PersistedStandardPicture picture = enumeratePictures().single;
|
||||
expect(picture.optimalLocalCullRect, const Rect.fromLTRB(50, 40, 70, 70));
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
// 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 SceneBuilder builder = SceneBuilder();
|
||||
builder.pushClipRect(
|
||||
const Rect.fromLTWH(10, 10, 60, 60),
|
||||
);
|
||||
|
||||
builder.pushClipRect(
|
||||
const Rect.fromLTWH(40, 40, 60, 60),
|
||||
);
|
||||
|
||||
builder.pushOffset(55, 70);
|
||||
|
||||
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
|
||||
canvas.drawCircle(Offset.zero, 20, Paint()..style = PaintingStyle.fill);
|
||||
});
|
||||
|
||||
builder.pop(); // pushOffset
|
||||
builder.pop(); // pushClipRect
|
||||
builder.pop(); // pushClipRect
|
||||
html.document.body.append(builder.build().webOnlyRootElement);
|
||||
|
||||
await matchGoldenFile('engine/compositing_cull_rect_offset_inside_layer_clip.png', region: region);
|
||||
|
||||
final PersistedStandardPicture picture = enumeratePictures().single;
|
||||
expect(picture.optimalLocalCullRect,
|
||||
const Rect.fromLTRB(-15.0, -20.0, 15.0, 0.0));
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
// 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 SceneBuilder builder = SceneBuilder();
|
||||
builder.pushClipRect(
|
||||
const Rect.fromLTWH(10, 10, 60, 60),
|
||||
);
|
||||
|
||||
builder.pushClipRect(
|
||||
const Rect.fromLTWH(40, 40, 60, 60),
|
||||
);
|
||||
|
||||
builder.pushOffset(100, 50);
|
||||
|
||||
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
|
||||
canvas.drawCircle(Offset.zero, 20, Paint()..style = PaintingStyle.fill);
|
||||
});
|
||||
|
||||
builder.pop(); // pushOffset
|
||||
builder.pop(); // pushClipRect
|
||||
builder.pop(); // pushClipRect
|
||||
|
||||
builder.build();
|
||||
|
||||
final PersistedStandardPicture picture = enumeratePictures().single;
|
||||
expect(picture.optimalLocalCullRect, Rect.zero);
|
||||
expect(picture.debugExactGlobalCullRect, 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 SceneBuilder builder = SceneBuilder();
|
||||
builder.pushOffset(80, 50);
|
||||
|
||||
builder.pushTransform(
|
||||
Matrix4.rotationZ(-math.pi / 4).storage,
|
||||
);
|
||||
|
||||
builder.pushClipRect(
|
||||
const Rect.fromLTRB(-10, -10, 10, 10),
|
||||
);
|
||||
|
||||
builder.pushTransform(
|
||||
Matrix4.rotationZ(math.pi / 4).storage,
|
||||
);
|
||||
|
||||
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
|
||||
canvas.drawPaint(Paint()
|
||||
..color = const Color.fromRGBO(0, 0, 255, 0.6)
|
||||
..style = PaintingStyle.fill);
|
||||
canvas.drawRect(
|
||||
const Rect.fromLTRB(-5, -5, 5, 5),
|
||||
Paint()
|
||||
..color = const Color.fromRGBO(0, 255, 0, 1.0)
|
||||
..style = PaintingStyle.fill,
|
||||
);
|
||||
});
|
||||
|
||||
builder.pop(); // pushTransform
|
||||
builder.pop(); // pushClipRect
|
||||
builder.pop(); // pushTransform
|
||||
builder.pop(); // pushOffset
|
||||
html.document.body.append(builder.build().webOnlyRootElement);
|
||||
|
||||
await matchGoldenFile('engine/compositing_cull_rect_rotated.png', region: region);
|
||||
|
||||
final PersistedStandardPicture picture = enumeratePictures().single;
|
||||
expect(
|
||||
picture.optimalLocalCullRect,
|
||||
within(
|
||||
distance: 0.05, from: const Rect.fromLTRB(-14.1, -14.1, 14.1, 14.1)),
|
||||
);
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
test('pushClipPath', () async {
|
||||
final SceneBuilder builder = SceneBuilder();
|
||||
final Path path = Path();
|
||||
path..addRect(const Rect.fromLTRB(10, 10, 60, 60));
|
||||
builder.pushClipPath(
|
||||
path,
|
||||
);
|
||||
_drawTestPicture(builder);
|
||||
builder.pop();
|
||||
|
||||
html.document.body.append(builder.build().webOnlyRootElement);
|
||||
|
||||
await matchGoldenFile('engine/compositing_clip_path.png', region: region);
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
// 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 SceneBuilder builder = SceneBuilder();
|
||||
final double screenWidth = html.window.innerWidth.toDouble();
|
||||
final double screenHeight = html.window.innerHeight.toDouble();
|
||||
|
||||
final Matrix4 scaleTransform = Matrix4.identity().scaled(0.5, 0.2);
|
||||
builder.pushTransform(
|
||||
scaleTransform.storage,
|
||||
);
|
||||
|
||||
builder.pushOffset(400, 200);
|
||||
|
||||
builder.pushClipRect(
|
||||
const Rect.fromLTRB(-200, -200, 200, 200),
|
||||
);
|
||||
|
||||
builder.pushTransform(
|
||||
Matrix4.rotationY(45.0 * math.pi / 180.0).storage,
|
||||
);
|
||||
|
||||
builder.pushClipRect(
|
||||
const Rect.fromLTRB(-140, -140, 140, 140),
|
||||
);
|
||||
|
||||
builder.pushTransform(Matrix4.translationValues(0, 0, -50).storage);
|
||||
|
||||
drawWithBitmapCanvas(builder, (RecordingCanvas canvas) {
|
||||
canvas.drawPaint(Paint()
|
||||
..color = const Color.fromRGBO(0, 0, 255, 0.6)
|
||||
..style = PaintingStyle.fill);
|
||||
// Rect will be clipped.
|
||||
canvas.drawRect(
|
||||
const Rect.fromLTRB(-150, -150, 150, 150),
|
||||
Paint()
|
||||
..color = const Color.fromRGBO(0, 255, 0, 1.0)
|
||||
..style = PaintingStyle.fill,
|
||||
);
|
||||
// Should be outside the clip range.
|
||||
canvas.drawRect(
|
||||
const Rect.fromLTRB(-150, -150, -140, -140),
|
||||
Paint()
|
||||
..color = const Color.fromARGB(0xE0, 255, 0, 0)
|
||||
..style = PaintingStyle.fill,
|
||||
);
|
||||
canvas.drawRect(
|
||||
const Rect.fromLTRB(140, -150, 150, -140),
|
||||
Paint()
|
||||
..color = const Color.fromARGB(0xE0, 255, 0, 0)
|
||||
..style = PaintingStyle.fill,
|
||||
);
|
||||
canvas.drawRect(
|
||||
const Rect.fromLTRB(-150, 140, -140, 150),
|
||||
Paint()
|
||||
..color = const Color.fromARGB(0xE0, 255, 0, 0)
|
||||
..style = PaintingStyle.fill,
|
||||
);
|
||||
canvas.drawRect(
|
||||
const Rect.fromLTRB(140, 140, 150, 150),
|
||||
Paint()
|
||||
..color = const Color.fromARGB(0xE0, 255, 0, 0)
|
||||
..style = PaintingStyle.fill,
|
||||
);
|
||||
// Should be inside clip range
|
||||
canvas.drawRect(
|
||||
const Rect.fromLTRB(-100, -100, -90, -90),
|
||||
Paint()
|
||||
..color = const Color.fromARGB(0xE0, 0, 0, 0x80)
|
||||
..style = PaintingStyle.fill,
|
||||
);
|
||||
canvas.drawRect(
|
||||
const Rect.fromLTRB(90, -100, 100, -90),
|
||||
Paint()
|
||||
..color = const Color.fromARGB(0xE0, 0, 0, 0x80)
|
||||
..style = PaintingStyle.fill,
|
||||
);
|
||||
canvas.drawRect(
|
||||
const Rect.fromLTRB(-100, 90, -90, 100),
|
||||
Paint()
|
||||
..color = const Color.fromARGB(0xE0, 0, 0, 0x80)
|
||||
..style = PaintingStyle.fill,
|
||||
);
|
||||
canvas.drawRect(
|
||||
const Rect.fromLTRB(90, 90, 100, 100),
|
||||
Paint()
|
||||
..color = const Color.fromARGB(0xE0, 0, 0, 0x80)
|
||||
..style = 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
|
||||
html.document.body.append(builder.build().webOnlyRootElement);
|
||||
|
||||
await matchGoldenFile('engine/compositing_3d_rotate1.png', region: region);
|
||||
|
||||
final PersistedStandardPicture picture = enumeratePictures().single;
|
||||
// TODO(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: Rect.fromLTRB(
|
||||
// -140, -140, screenWidth - 360.0, screenHeight + 40.0)),
|
||||
// );
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
}
|
||||
|
||||
void _drawTestPicture(SceneBuilder builder) {
|
||||
final PictureRecorder recorder = PictureRecorder();
|
||||
final RecordingCanvas canvas =
|
||||
recorder.beginRecording(const Rect.fromLTRB(0, 0, 100, 100));
|
||||
canvas.drawCircle(
|
||||
const Offset(10, 10), 10, Paint()..style = PaintingStyle.fill);
|
||||
canvas.drawCircle(
|
||||
const Offset(60, 10),
|
||||
10,
|
||||
Paint()
|
||||
..style = PaintingStyle.fill
|
||||
..color = const Color.fromRGBO(255, 0, 0, 1));
|
||||
canvas.drawCircle(
|
||||
const Offset(10, 60),
|
||||
10,
|
||||
Paint()
|
||||
..style = PaintingStyle.fill
|
||||
..color = const Color.fromRGBO(0, 255, 0, 1));
|
||||
canvas.drawCircle(
|
||||
const Offset(60, 60),
|
||||
10,
|
||||
Paint()
|
||||
..style = PaintingStyle.fill
|
||||
..color = const Color.fromRGBO(0, 0, 255, 1));
|
||||
final Picture picture = recorder.endRecording();
|
||||
|
||||
builder.addPicture(
|
||||
Offset.zero,
|
||||
picture,
|
||||
);
|
||||
}
|
||||
|
||||
typedef PaintCallback = void Function(RecordingCanvas canvas);
|
||||
|
||||
void drawWithBitmapCanvas(SceneBuilder builder, PaintCallback callback,
|
||||
{Rect bounds = Rect.largest}) {
|
||||
final PictureRecorder recorder = PictureRecorder();
|
||||
final RecordingCanvas canvas = recorder.beginRecording(bounds);
|
||||
|
||||
canvas.debugEnforceArbitraryPaint();
|
||||
callback(canvas);
|
||||
final Picture picture = recorder.endRecording();
|
||||
|
||||
builder.addPicture(
|
||||
Offset.zero,
|
||||
picture,
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
// 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 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:web_engine_tester/golden_tester.dart';
|
||||
|
||||
void main() async {
|
||||
final Rect region = Rect.fromLTWH(8, 8, 600, 800); // Compensate for old scuba tester padding
|
||||
|
||||
Future<void> testPath(Path path, String scubaFileName) async {
|
||||
const Rect canvasBounds = Rect.fromLTWH(0, 0, 600, 800);
|
||||
final BitmapCanvas bitmapCanvas = BitmapCanvas(canvasBounds);
|
||||
final RecordingCanvas canvas = RecordingCanvas(canvasBounds);
|
||||
|
||||
Paint paint = Paint()
|
||||
..color = const Color(0x7F7F7F7F)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
paint = Paint()
|
||||
..strokeWidth = 2.0
|
||||
..color = const Color(0xFF7F007F)
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
html.document.body.append(bitmapCanvas.rootElement);
|
||||
canvas.apply(bitmapCanvas);
|
||||
await matchGoldenFile('engine/$scubaFileName.png', region: region);
|
||||
bitmapCanvas.rootElement.remove();
|
||||
}
|
||||
|
||||
test('render conic with control point horizontal center', () async {
|
||||
const double yStart = 20;
|
||||
|
||||
const Offset p0 = Offset(25, yStart + 25);
|
||||
const Offset pc = Offset(60, yStart + 150);
|
||||
const Offset p2 = Offset(100, yStart + 50);
|
||||
|
||||
final Path path = Path();
|
||||
path.moveTo(p0.dx, p0.dy);
|
||||
path.conicTo(pc.dx, pc.dy, p2.dx, p2.dy, 0.5);
|
||||
path.close();
|
||||
path.moveTo(p0.dx, p0.dy + 200);
|
||||
path.conicTo(pc.dx, pc.dy + 200, p2.dx, p2.dy + 200, 10);
|
||||
path.close();
|
||||
|
||||
await testPath(path, 'render_conic_1_w10');
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
test('render conic with control point left of start point', () async {
|
||||
const double yStart = 20;
|
||||
|
||||
const Offset p0 = Offset(60, yStart + 25);
|
||||
const Offset pc = Offset(25, yStart + 150);
|
||||
const Offset p2 = Offset(100, yStart + 50);
|
||||
|
||||
final Path path = Path();
|
||||
path.moveTo(p0.dx, p0.dy);
|
||||
path.conicTo(pc.dx, pc.dy, p2.dx, p2.dy, 0.5);
|
||||
path.close();
|
||||
path.moveTo(p0.dx, p0.dy + 200);
|
||||
path.conicTo(pc.dx, pc.dy + 200, p2.dx, p2.dy + 200, 10);
|
||||
path.close();
|
||||
|
||||
await testPath(path, 'render_conic_2_w10');
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
test('render conic with control point above start point', () async {
|
||||
const double yStart = 20;
|
||||
|
||||
const Offset p0 = Offset(25, yStart + 125);
|
||||
const Offset pc = Offset(60, yStart + 50);
|
||||
const Offset p2 = Offset(100, yStart + 150);
|
||||
|
||||
final Path path = Path();
|
||||
path.moveTo(p0.dx, p0.dy);
|
||||
path.conicTo(pc.dx, pc.dy, p2.dx, p2.dy, 0.5);
|
||||
path.close();
|
||||
path.moveTo(p0.dx, p0.dy + 200);
|
||||
path.conicTo(pc.dx, pc.dy + 200, p2.dx, p2.dy + 200, 10);
|
||||
path.close();
|
||||
|
||||
await testPath(path, 'render_conic_2');
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
}
|
||||
@ -0,0 +1,194 @@
|
||||
// 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:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart';
|
||||
|
||||
import 'scuba.dart';
|
||||
|
||||
typedef PaintTest = void Function(RecordingCanvas recordingCanvas);
|
||||
|
||||
void main() async {
|
||||
// Scuba doesn't give us viewport smaller than 472px wide.
|
||||
final EngineScubaTester scuba = await EngineScubaTester.initialize(
|
||||
viewportSize: const Size(600, 600),
|
||||
);
|
||||
|
||||
void paintTest(EngineCanvas canvas, PaintTest painter) {
|
||||
final RecordingCanvas recordingCanvas =
|
||||
RecordingCanvas(const Rect.fromLTWH(0, 0, 600, 600));
|
||||
painter(recordingCanvas);
|
||||
recordingCanvas.apply(canvas);
|
||||
}
|
||||
|
||||
testEachCanvas(
|
||||
'clips multiline text against rectangle',
|
||||
(EngineCanvas canvas) {
|
||||
// [DomCanvas] doesn't support clip commands.
|
||||
if (canvas is! DomCanvas) {
|
||||
paintTest(canvas, paintTextWithClipRect);
|
||||
return scuba.diffCanvasScreenshot(
|
||||
canvas, 'multiline_text_clipping_rect');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
testEachCanvas(
|
||||
'clips multiline text against rectangle with transform',
|
||||
(EngineCanvas canvas) {
|
||||
// [DomCanvas] doesn't support clip commands.
|
||||
if (canvas is! DomCanvas) {
|
||||
paintTest(canvas, paintTextWithClipRectTranslated);
|
||||
return scuba.diffCanvasScreenshot(
|
||||
canvas, 'multiline_text_clipping_rect_translate');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
testEachCanvas(
|
||||
'clips multiline text against round rectangle',
|
||||
(EngineCanvas canvas) {
|
||||
// [DomCanvas] doesn't support clip commands.
|
||||
if (canvas is! DomCanvas) {
|
||||
paintTest(canvas, paintTextWithClipRoundRect);
|
||||
return scuba.diffCanvasScreenshot(
|
||||
canvas, 'multiline_text_clipping_roundrect');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
testEachCanvas(
|
||||
'clips multiline text against path',
|
||||
(EngineCanvas canvas) {
|
||||
// [DomCanvas] doesn't support clip commands.
|
||||
if (canvas is! DomCanvas) {
|
||||
paintTest(canvas, paintTextWithClipPath);
|
||||
return scuba.diffCanvasScreenshot(
|
||||
canvas, 'multiline_text_clipping_path');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
testEachCanvas(
|
||||
'clips multiline text against stack of rects',
|
||||
(EngineCanvas canvas) {
|
||||
// [DomCanvas] doesn't support clip commands.
|
||||
if (canvas is! DomCanvas) {
|
||||
// TODO(flutter_web): https://github.com/flutter/flutter/issues/35086
|
||||
// This produces the wrong result when using [BitmapCanvas] but without
|
||||
// the new experimental canvas mode.
|
||||
paintTest(canvas, paintTextWithClipStack);
|
||||
return scuba.diffCanvasScreenshot(
|
||||
canvas, 'multiline_text_clipping_stack1');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const Rect testBounds = Rect.fromLTRB(50, 50, 230, 220);
|
||||
|
||||
void drawBackground(RecordingCanvas canvas) {
|
||||
canvas.drawRect(
|
||||
testBounds,
|
||||
Paint()
|
||||
..style = PaintingStyle.fill
|
||||
..color = const Color(0xFF9E9E9E));
|
||||
canvas.drawRect(
|
||||
testBounds.inflate(-40),
|
||||
Paint()
|
||||
..strokeWidth = 1
|
||||
..style = PaintingStyle.stroke
|
||||
..color = const Color(0xFF009688));
|
||||
}
|
||||
|
||||
void drawQuickBrownFox(RecordingCanvas canvas) {
|
||||
canvas.drawParagraph(
|
||||
paragraph(
|
||||
'The quick brown fox jumps over the lazy dog',
|
||||
textStyle: TextStyle(
|
||||
color: const Color(0xFF000000),
|
||||
decoration: TextDecoration.none,
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 30,
|
||||
background: Paint()..color = const Color.fromRGBO(50, 255, 50, 1.0),
|
||||
),
|
||||
maxWidth: 180,
|
||||
),
|
||||
Offset(testBounds.left, testBounds.top));
|
||||
}
|
||||
|
||||
void paintTextWithClipRect(RecordingCanvas canvas) {
|
||||
drawBackground(canvas);
|
||||
canvas.clipRect(testBounds.inflate(-40));
|
||||
drawQuickBrownFox(canvas);
|
||||
}
|
||||
|
||||
void paintTextWithClipRectTranslated(RecordingCanvas canvas) {
|
||||
drawBackground(canvas);
|
||||
canvas.clipRect(testBounds.inflate(-40));
|
||||
canvas.translate(30, 10);
|
||||
drawQuickBrownFox(canvas);
|
||||
}
|
||||
|
||||
const Color deepOrange = Color(0xFFFF5722);
|
||||
|
||||
void paintTextWithClipRoundRect(RecordingCanvas canvas) {
|
||||
final RRect roundRect = RRect.fromRectAndCorners(testBounds.inflate(-40),
|
||||
topLeft: Radius.zero,
|
||||
topRight: const Radius.elliptical(45, 40),
|
||||
bottomLeft: const Radius.elliptical(50, 40),
|
||||
bottomRight: const Radius.circular(30));
|
||||
drawBackground(canvas);
|
||||
canvas.drawRRect(
|
||||
roundRect,
|
||||
Paint()
|
||||
..color = deepOrange
|
||||
..style = PaintingStyle.fill);
|
||||
canvas.clipRRect(roundRect);
|
||||
drawQuickBrownFox(canvas);
|
||||
}
|
||||
|
||||
void paintTextWithClipPath(RecordingCanvas canvas) {
|
||||
drawBackground(canvas);
|
||||
final Path path = Path();
|
||||
const double delta = 40.0;
|
||||
final Rect clipBounds = testBounds.inflate(-delta);
|
||||
final double midX = (clipBounds.left + clipBounds.right) / 2.0;
|
||||
final double midY = (clipBounds.top + clipBounds.bottom) / 2.0;
|
||||
path.moveTo(clipBounds.left - delta, midY);
|
||||
path.quadraticBezierTo(midX, midY, midX, clipBounds.top - delta);
|
||||
path.quadraticBezierTo(midX, midY, clipBounds.right + delta, midY);
|
||||
path.quadraticBezierTo(midX, midY, midX, clipBounds.bottom + delta);
|
||||
path.quadraticBezierTo(midX, midY, clipBounds.left - delta, midY);
|
||||
path.close();
|
||||
canvas.drawPath(
|
||||
path,
|
||||
Paint()
|
||||
..color = deepOrange
|
||||
..style = PaintingStyle.fill);
|
||||
canvas.clipPath(path);
|
||||
drawQuickBrownFox(canvas);
|
||||
}
|
||||
|
||||
void paintTextWithClipStack(RecordingCanvas canvas) {
|
||||
drawBackground(canvas);
|
||||
final Rect inflatedRect = testBounds.inflate(-40);
|
||||
canvas.clipRect(inflatedRect);
|
||||
canvas.rotate(math.pi / 8.0);
|
||||
canvas.translate(40, -40);
|
||||
canvas.clipRect(inflatedRect);
|
||||
canvas.drawRect(
|
||||
inflatedRect,
|
||||
Paint()
|
||||
..color = deepOrange
|
||||
..style = PaintingStyle.fill);
|
||||
drawQuickBrownFox(canvas);
|
||||
}
|
||||
@ -0,0 +1,187 @@
|
||||
// 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 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:web_engine_tester/golden_tester.dart';
|
||||
|
||||
void main() async {
|
||||
final Rect region = Rect.fromLTWH(8, 8, 600, 800); // Compensate for old scuba tester padding
|
||||
|
||||
Future<void> testPath(Path path, String scubaFileName, {Paint paint}) async {
|
||||
const Rect canvasBounds = Rect.fromLTWH(0, 0, 600, 800);
|
||||
final BitmapCanvas bitmapCanvas = BitmapCanvas(canvasBounds);
|
||||
final RecordingCanvas canvas = RecordingCanvas(canvasBounds);
|
||||
|
||||
paint ??= Paint()
|
||||
..color = const Color(0x807F7F7F)
|
||||
..style = PaintingStyle.fill;
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
paint = Paint()
|
||||
..strokeWidth = 2
|
||||
..color = const Color(0xFFFF0000)
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
|
||||
final html.Element svgElement = pathToSvgElement(path, paint);
|
||||
|
||||
html.document.body.append(bitmapCanvas.rootElement);
|
||||
html.document.body.append(svgElement);
|
||||
|
||||
canvas.apply(bitmapCanvas);
|
||||
|
||||
await matchGoldenFile('engine/$scubaFileName.png', region: region);
|
||||
|
||||
bitmapCanvas.rootElement.remove();
|
||||
svgElement.remove();
|
||||
}
|
||||
|
||||
tearDown(() {
|
||||
html.document.body.children.clear();
|
||||
});
|
||||
|
||||
test('render line strokes', () async {
|
||||
final Path path = Path();
|
||||
path.moveTo(50, 60);
|
||||
path.lineTo(200, 300);
|
||||
await testPath(path, 'svg_stroke_line',
|
||||
paint: Paint()
|
||||
..color = const Color(0xFFFF0000)
|
||||
..strokeWidth = 2.0
|
||||
..style = PaintingStyle.stroke);
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
test('render quad bezier curve', () async {
|
||||
final Path path = Path();
|
||||
path.moveTo(50, 60);
|
||||
path.quadraticBezierTo(200, 60, 50, 200);
|
||||
await testPath(path, 'svg_quad_bezier');
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
test('render cubic curve', () async {
|
||||
final Path path = Path();
|
||||
path.moveTo(50, 60);
|
||||
path.cubicTo(200, 60, -100, -50, 150, 200);
|
||||
await testPath(path, 'svg_cubic_bezier');
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
test('render arcs', () async {
|
||||
final List<ArcSample> arcs = <ArcSample>[
|
||||
ArcSample(const Offset(0, 0),
|
||||
largeArc: false, clockwise: false, distance: 20),
|
||||
ArcSample(const Offset(200, 0),
|
||||
largeArc: true, clockwise: false, distance: 20),
|
||||
ArcSample(const Offset(0, 150),
|
||||
largeArc: false, clockwise: true, distance: 20),
|
||||
ArcSample(const Offset(200, 150),
|
||||
largeArc: true, clockwise: true, distance: 20),
|
||||
ArcSample(const Offset(0, 300),
|
||||
largeArc: false, clockwise: false, distance: -20),
|
||||
ArcSample(const Offset(200, 300),
|
||||
largeArc: true, clockwise: false, distance: -20),
|
||||
ArcSample(const Offset(0, 450),
|
||||
largeArc: false, clockwise: true, distance: -20),
|
||||
ArcSample(const Offset(200, 450),
|
||||
largeArc: true, clockwise: true, distance: -20)
|
||||
];
|
||||
int sampleIndex = 0;
|
||||
for (ArcSample sample in arcs) {
|
||||
++sampleIndex;
|
||||
final Path path = sample.createPath();
|
||||
await testPath(path, 'svg_arc_$sampleIndex');
|
||||
}
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
test('render rect', () async {
|
||||
final Path path = Path();
|
||||
path.addRect(const Rect.fromLTRB(15, 15, 60, 20));
|
||||
path.addRect(const Rect.fromLTRB(35, 160, 15, 100));
|
||||
await testPath(path, 'svg_rect');
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
|
||||
test('render notch', () async {
|
||||
final Path path = Path();
|
||||
path.moveTo(0, 0);
|
||||
path.lineTo(83, 0);
|
||||
path.quadraticBezierTo(98, 0, 99.97, 7.8);
|
||||
path.arcToPoint(const Offset(162, 7.8),
|
||||
radius: const Radius.circular(32),
|
||||
largeArc: false,
|
||||
clockwise: false,
|
||||
rotation: 0);
|
||||
path.lineTo(200, 7.8);
|
||||
path.lineTo(200, 80);
|
||||
path.lineTo(0, 80);
|
||||
path.lineTo(0, 10);
|
||||
await testPath(path, 'svg_notch');
|
||||
}, timeout: const Timeout(Duration(seconds: 10)));
|
||||
}
|
||||
|
||||
html.Element pathToSvgElement(Path path, Paint paint) {
|
||||
final Rect bounds = path.getBounds();
|
||||
final StringBuffer sb = StringBuffer();
|
||||
sb.write(
|
||||
'<svg viewBox="0 0 ${bounds.right} ${bounds.bottom}" width="${bounds.right}" height="${bounds.bottom}">');
|
||||
sb.write('<path ');
|
||||
if (paint.style == PaintingStyle.stroke) {
|
||||
sb.write('stroke="${paint.color.toCssString()}" ');
|
||||
sb.write('stroke-width="${paint.strokeWidth}" ');
|
||||
}
|
||||
if (paint.style == PaintingStyle.fill) {
|
||||
sb.write('fill="${paint.color.toCssString()}" ');
|
||||
}
|
||||
sb.write('d="');
|
||||
pathToSvg(path, sb); // This is what we're testing!
|
||||
sb.write('"></path>');
|
||||
sb.write('</svg>');
|
||||
final html.Element svgElement =
|
||||
html.Element.html(sb.toString(), treeSanitizer: _NullTreeSanitizer());
|
||||
svgElement.style.transform = 'translate(200px, 0px)';
|
||||
return svgElement;
|
||||
}
|
||||
|
||||
class _NullTreeSanitizer implements html.NodeTreeSanitizer {
|
||||
@override
|
||||
void sanitizeTree(html.Node node) {}
|
||||
}
|
||||
|
||||
class ArcSample {
|
||||
final Offset offset;
|
||||
final bool largeArc;
|
||||
final bool clockwise;
|
||||
final double distance;
|
||||
ArcSample(this.offset,
|
||||
{this.largeArc = false, this.clockwise = false, this.distance = 0});
|
||||
|
||||
Path createPath() {
|
||||
final Offset startP =
|
||||
Offset(75 - distance + offset.dx, 75 - distance + offset.dy);
|
||||
final Offset endP =
|
||||
Offset(75.0 + distance + offset.dx, 75.0 + distance + offset.dy);
|
||||
final Path path = Path();
|
||||
path.moveTo(startP.dx, startP.dy);
|
||||
path.arcToPoint(endP,
|
||||
rotation: 60,
|
||||
radius: const Radius.elliptical(40, 60),
|
||||
largeArc: largeArc,
|
||||
clockwise: clockwise);
|
||||
return path;
|
||||
}
|
||||
|
||||
// Returns bounds of start/end point of arc.
|
||||
Rect getBounds() {
|
||||
final Offset startP =
|
||||
Offset(75 - distance + offset.dx, 75 - distance + offset.dy);
|
||||
final Offset endP =
|
||||
Offset(75.0 + distance + offset.dx, 75.0 + distance + offset.dy);
|
||||
return Rect.fromLTRB(startP.dx, startP.dy, endP.dx, endP.dy);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,470 @@
|
||||
// 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:ui/ui.dart' hide TextStyle;
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import '../../matchers.dart';
|
||||
import 'package:web_engine_tester/golden_tester.dart';
|
||||
|
||||
void main() async {
|
||||
const double screenWidth = 600.0;
|
||||
const double screenHeight = 800.0;
|
||||
const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
|
||||
final Paint testPaint = Paint()..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) }) async {
|
||||
|
||||
final EngineCanvas engineCanvas = BitmapCanvas(screenRect);
|
||||
|
||||
// Draws the estimated bounds so we can spot the bug in Scuba.
|
||||
engineCanvas
|
||||
..save()
|
||||
..drawRect(
|
||||
rc.computePaintBounds(),
|
||||
PaintData()
|
||||
..color = const Color.fromRGBO(0, 0, 255, 1.0)
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 1.0,
|
||||
)
|
||||
..restore();
|
||||
|
||||
rc.apply(engineCanvas);
|
||||
|
||||
// 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('engine/paint_bounds_for_$fileName.png', region: region);
|
||||
} 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));
|
||||
expect(rc.computePaintBounds(), 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);
|
||||
// The off by one is due to the minimum stroke width of 1.
|
||||
expect(rc.computePaintBounds(), 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);
|
||||
// The off by one is due to the minimum stroke width of 1.
|
||||
expect(rc.computePaintBounds(),
|
||||
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);
|
||||
expect(rc.computePaintBounds(), 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);
|
||||
expect(rc.computePaintBounds(),
|
||||
const Rect.fromLTRB(10, 20, screenWidth, screenHeight));
|
||||
|
||||
rc = RecordingCanvas(screenRect);
|
||||
rc.drawRect(const Rect.fromLTRB(-200, -100, 30, 40), testPaint);
|
||||
expect(rc.computePaintBounds(), 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);
|
||||
expect(rc.computePaintBounds(), 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);
|
||||
expect(rc.computePaintBounds(), 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);
|
||||
// The extra 0.7 is due to stroke width of 1 rotated by 45 degrees.
|
||||
expect(rc.computePaintBounds(),
|
||||
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);
|
||||
expect(
|
||||
rc.computePaintBounds(),
|
||||
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);
|
||||
expect(
|
||||
rc.computePaintBounds(),
|
||||
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 Float64List matrix = Float64List(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);
|
||||
expect(rc.computePaintBounds(),
|
||||
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);
|
||||
expect(rc.computePaintBounds(), screenRect);
|
||||
await _checkScreenshot(rc, 'draw_paint');
|
||||
});
|
||||
|
||||
test('drawColor should cover full size', () async {
|
||||
final RecordingCanvas rc = RecordingCanvas(screenRect);
|
||||
rc.drawColor(const Color(0xFFFF0000), BlendMode.multiply);
|
||||
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
|
||||
expect(rc.computePaintBounds(), 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);
|
||||
expect(rc.computePaintBounds(), 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);
|
||||
expect(rc.computePaintBounds(), 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);
|
||||
expect(rc.computePaintBounds(), 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);
|
||||
expect(rc.computePaintBounds(), const Rect.fromLTRB(10, 20, 30, 40));
|
||||
await _checkScreenshot(rc, 'draw_drrect');
|
||||
});
|
||||
|
||||
test('Computes paint bounds for draw circle', () async {
|
||||
final RecordingCanvas rc = RecordingCanvas(screenRect);
|
||||
rc.drawCircle(const Offset(20, 20), 10.0, testPaint);
|
||||
expect(
|
||||
rc.computePaintBounds(), const Rect.fromLTRB(10.0, 10.0, 30.0, 30.0));
|
||||
rc.drawCircle(const Offset(200, 300), 100.0, testPaint);
|
||||
expect(
|
||||
rc.computePaintBounds(), 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), Paint());
|
||||
expect(
|
||||
rc.computePaintBounds(), 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), Paint());
|
||||
expect(
|
||||
rc.computePaintBounds(), 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));
|
||||
expect(
|
||||
rc.computePaintBounds(),
|
||||
const Rect.fromLTRB(textLeft, textTop, textLeft + widthConstraint, 21.0),
|
||||
);
|
||||
await _checkScreenshot(rc, 'draw_paragraph');
|
||||
});
|
||||
|
||||
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;
|
||||
const double widthConstraint =
|
||||
130.0; // do not go lower than the shortest word.
|
||||
paragraph.layout(const ParagraphConstraints(width: widthConstraint));
|
||||
rc.drawParagraph(paragraph, const Offset(textLeft, textTop));
|
||||
expect(
|
||||
rc.computePaintBounds(),
|
||||
const Rect.fromLTRB(textLeft, textTop, textLeft + widthConstraint, 35.0),
|
||||
);
|
||||
await _checkScreenshot(rc, 'draw_paragraph_multi_line');
|
||||
});
|
||||
|
||||
test('Should exclude painting outside simple clipRect', () async {
|
||||
final RecordingCanvas rc = RecordingCanvas(screenRect);
|
||||
rc.clipRect(const Rect.fromLTRB(50, 50, 100, 100));
|
||||
rc.drawLine(const Offset(10, 11), const Offset(20, 21), testPaint);
|
||||
|
||||
expect(rc.computePaintBounds(), Rect.zero);
|
||||
rc.drawLine(const Offset(52, 53), const Offset(55, 56), testPaint);
|
||||
|
||||
// Extra pixel due to default line length
|
||||
expect(rc.computePaintBounds(), 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));
|
||||
rc.drawRect(const Rect.fromLTRB(20, 60, 120, 70), testPaint);
|
||||
expect(rc.computePaintBounds(), 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));
|
||||
rc.drawRect(const Rect.fromLTRB(60, 20, 70, 200), testPaint);
|
||||
expect(rc.computePaintBounds(), 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));
|
||||
rc.scale(2.0, 2.0);
|
||||
rc.clipRect(const Rect.fromLTRB(30, 30, 45, 45));
|
||||
rc.drawRect(const Rect.fromLTRB(10, 30, 60, 35), testPaint);
|
||||
expect(rc.computePaintBounds(), 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);
|
||||
expect(
|
||||
rc.computePaintBounds(), const Rect.fromLTRB(15.0, 27.0, 106.0, 117.0));
|
||||
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))
|
||||
..drawRect(const Rect.fromLTRB(0, 0, 100, 100), Paint());
|
||||
expect(
|
||||
rc.computePaintBounds(), 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))
|
||||
..drawRect(const Rect.fromLTWH(-80, -80, 160, 160), Paint());
|
||||
expect(
|
||||
rc.computePaintBounds(),
|
||||
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), Paint());
|
||||
expect(
|
||||
rc.computePaintBounds(),
|
||||
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,
|
||||
Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.0
|
||||
..color = const Color(0xFFFF0000));
|
||||
path.reset();
|
||||
path.moveTo(0, 3);
|
||||
path.lineTo(97, 100);
|
||||
rc.drawPath(
|
||||
path,
|
||||
Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.0
|
||||
..color = const Color(0xFF00FF00));
|
||||
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,
|
||||
Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = 2.0
|
||||
..color = const Color(0xFF404000));
|
||||
await _checkScreenshot(rc, 'path_with_line_and_roundrect');
|
||||
});
|
||||
}
|
||||
|
||||
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() {}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
@ -0,0 +1,131 @@
|
||||
// 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:html' as html;
|
||||
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:web_engine_tester/golden_tester.dart';
|
||||
|
||||
/// Class that controls some details of how screenshotting is made.
|
||||
///
|
||||
/// (For Googlers: Not really related with internal Scuba anymore)
|
||||
class EngineScubaTester {
|
||||
/// The size of the browser window used in this scuba test.
|
||||
final ui.Size viewportSize;
|
||||
|
||||
EngineScubaTester(this.viewportSize);
|
||||
|
||||
static Future<EngineScubaTester> initialize(
|
||||
{ui.Size viewportSize = const ui.Size(2400, 1800)}) async {
|
||||
assert(viewportSize != null);
|
||||
|
||||
assert(() {
|
||||
if (viewportSize.width.ceil() != viewportSize.width ||
|
||||
viewportSize.height.ceil() != viewportSize.height) {
|
||||
throw Exception(
|
||||
'Scuba only supports integer screen sizes, but found: $viewportSize');
|
||||
}
|
||||
if (viewportSize.width < 472) {
|
||||
throw Exception('Scuba does not support screen width smaller than 472');
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
|
||||
return EngineScubaTester(viewportSize);
|
||||
}
|
||||
|
||||
Future<void> diffScreenshot(String fileName) async {
|
||||
await matchGoldenFile('engine/$fileName.png', region: ui.Rect.fromLTWH(0, 0, viewportSize.width, viewportSize.height));
|
||||
}
|
||||
|
||||
/// Prepares the DOM and inserts all the necessary nodes, then invokes scuba's
|
||||
/// screenshot diffing.
|
||||
///
|
||||
/// It also cleans up the DOM after itself.
|
||||
Future<void> diffCanvasScreenshot(
|
||||
EngineCanvas canvas,
|
||||
String fileName,
|
||||
) async {
|
||||
// Wrap in <flt-scene> so that our CSS selectors kick in.
|
||||
final html.Element sceneElement = html.Element.tag('flt-scene');
|
||||
try {
|
||||
sceneElement.append(canvas.rootElement);
|
||||
html.document.body.append(sceneElement);
|
||||
String screenshotName = '${fileName}_${canvas.runtimeType}';
|
||||
if (TextMeasurementService.enableExperimentalCanvasImplementation) {
|
||||
screenshotName += '+canvas_measurement';
|
||||
}
|
||||
await diffScreenshot(screenshotName);
|
||||
} finally {
|
||||
// The page is reused across tests, so remove the element after taking the
|
||||
// Scuba screenshot.
|
||||
sceneElement.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
typedef CanvasTest = FutureOr<void> Function(EngineCanvas canvas);
|
||||
|
||||
/// Runs the given test [body] with each type of canvas.
|
||||
void testEachCanvas(String description, CanvasTest body) {
|
||||
const ui.Rect bounds = ui.Rect.fromLTWH(0, 0, 600, 800);
|
||||
test('$description (bitmap)', () {
|
||||
try {
|
||||
TextMeasurementService.initialize(rulerCacheCapacity: 2);
|
||||
return body(BitmapCanvas(bounds));
|
||||
} finally {
|
||||
TextMeasurementService.clearCache();
|
||||
}
|
||||
});
|
||||
test('$description (bitmap + canvas measurement)', () async {
|
||||
try {
|
||||
TextMeasurementService.initialize(rulerCacheCapacity: 2);
|
||||
TextMeasurementService.enableExperimentalCanvasImplementation = true;
|
||||
await body(BitmapCanvas(bounds));
|
||||
} finally {
|
||||
TextMeasurementService.enableExperimentalCanvasImplementation = false;
|
||||
TextMeasurementService.clearCache();
|
||||
}
|
||||
});
|
||||
test('$description (dom)', () {
|
||||
try {
|
||||
TextMeasurementService.initialize(rulerCacheCapacity: 2);
|
||||
return body(DomCanvas());
|
||||
} finally {
|
||||
TextMeasurementService.clearCache();
|
||||
}
|
||||
});
|
||||
test('$description (houdini)', () {
|
||||
try {
|
||||
TextMeasurementService.initialize(rulerCacheCapacity: 2);
|
||||
return body(HoudiniCanvas(bounds));
|
||||
} finally {
|
||||
TextMeasurementService.clearCache();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final ui.TextStyle _defaultTextStyle = ui.TextStyle(
|
||||
color: const ui.Color(0xFF000000),
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 14,
|
||||
);
|
||||
|
||||
ui.Paragraph paragraph(
|
||||
String text, {
|
||||
ui.ParagraphStyle paragraphStyle,
|
||||
ui.TextStyle textStyle,
|
||||
double maxWidth = double.infinity,
|
||||
}) {
|
||||
final ui.ParagraphBuilder builder =
|
||||
ui.ParagraphBuilder(paragraphStyle ?? ui.ParagraphStyle());
|
||||
builder.pushStyle(textStyle ?? _defaultTextStyle);
|
||||
builder.addText(text);
|
||||
builder.pop();
|
||||
return builder.build()..layout(ui.ParagraphConstraints(width: maxWidth));
|
||||
}
|
||||
@ -0,0 +1,154 @@
|
||||
// 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 'package:ui/ui.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
|
||||
import 'scuba.dart';
|
||||
|
||||
typedef CanvasTest = FutureOr<void> Function(EngineCanvas canvas);
|
||||
|
||||
const String threeLines = 'First\nSecond\nThird';
|
||||
const String veryLongWithShortPrefix =
|
||||
'Lorem ipsum dolor\nsit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
|
||||
const String veryLongWithShortSuffix =
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et\ndolore magna aliqua.';
|
||||
const String veryLong =
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
|
||||
const String longUnbreakable = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA';
|
||||
|
||||
void main() async {
|
||||
final EngineScubaTester scuba = await EngineScubaTester.initialize(
|
||||
viewportSize: const Size(800, 800),
|
||||
);
|
||||
|
||||
final TextStyle warningStyle = TextStyle(
|
||||
color: const Color(0xFFFF0000),
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 10,
|
||||
);
|
||||
|
||||
Paragraph warning(String text) {
|
||||
return paragraph(text, textStyle: warningStyle);
|
||||
}
|
||||
|
||||
testEachCanvas('maxLines clipping', (EngineCanvas canvas) {
|
||||
Offset offset = Offset.zero;
|
||||
Paragraph p;
|
||||
|
||||
// All three lines are rendered.
|
||||
p = paragraph(threeLines);
|
||||
canvas.drawParagraph(p, offset);
|
||||
offset = offset.translate(0, p.height + 10);
|
||||
|
||||
// Only the first two lines are rendered.
|
||||
p = paragraph(threeLines, paragraphStyle: ParagraphStyle(maxLines: 2));
|
||||
canvas.drawParagraph(p, offset);
|
||||
offset = offset.translate(0, p.height + 10);
|
||||
|
||||
// The whole text is rendered.
|
||||
p = paragraph(veryLong, maxWidth: 200);
|
||||
canvas.drawParagraph(p, offset);
|
||||
offset = offset.translate(0, p.height + 10);
|
||||
|
||||
// Only the first two lines are rendered.
|
||||
p = paragraph(veryLong,
|
||||
paragraphStyle: ParagraphStyle(maxLines: 2), maxWidth: 200);
|
||||
canvas.drawParagraph(p, offset);
|
||||
offset = offset.translate(0, p.height + 10);
|
||||
|
||||
return scuba.diffCanvasScreenshot(canvas, 'text_max_lines');
|
||||
});
|
||||
|
||||
testEachCanvas('maxLines with overflow', (EngineCanvas canvas) {
|
||||
Offset offset = Offset.zero;
|
||||
Paragraph p;
|
||||
|
||||
// Only the first line is rendered with no ellipsis because the first line
|
||||
// doesn't overflow.
|
||||
p = paragraph(
|
||||
threeLines,
|
||||
paragraphStyle: ParagraphStyle(ellipsis: '...'),
|
||||
);
|
||||
canvas.drawParagraph(p, offset);
|
||||
offset = offset.translate(0, p.height + 10);
|
||||
|
||||
// The first two lines are rendered with an ellipsis on the 2nd line.
|
||||
p = paragraph(
|
||||
veryLongWithShortPrefix,
|
||||
paragraphStyle: ParagraphStyle(ellipsis: '...'),
|
||||
maxWidth: 200,
|
||||
);
|
||||
canvas.drawParagraph(p, offset);
|
||||
offset = offset.translate(0, p.height + 10);
|
||||
|
||||
// Only the first line is rendered with an ellipsis.
|
||||
if (!TextMeasurementService.enableExperimentalCanvasImplementation) {
|
||||
// This is now correct with the canvas-based measurement, so we shouldn't
|
||||
// print the "(wrong)" warning.
|
||||
p = warning('(wrong)');
|
||||
canvas.drawParagraph(p, offset);
|
||||
offset = offset.translate(0, p.height);
|
||||
}
|
||||
p = paragraph(
|
||||
veryLongWithShortSuffix,
|
||||
paragraphStyle: ParagraphStyle(ellipsis: '...'),
|
||||
maxWidth: 200,
|
||||
);
|
||||
canvas.drawParagraph(p, offset);
|
||||
offset = offset.translate(0, p.height + 10);
|
||||
|
||||
// Only the first two lines are rendered and the ellipsis appears on the 2nd
|
||||
// line.
|
||||
if (!TextMeasurementService.enableExperimentalCanvasImplementation) {
|
||||
// This is now correct with the canvas-based measurement, so we shouldn't
|
||||
// print the "(wrong)" warning.
|
||||
p = warning('(wrong)');
|
||||
canvas.drawParagraph(p, offset);
|
||||
offset = offset.translate(0, p.height);
|
||||
}
|
||||
p = paragraph(
|
||||
veryLong,
|
||||
paragraphStyle: ParagraphStyle(maxLines: 2, ellipsis: '...'),
|
||||
maxWidth: 200,
|
||||
);
|
||||
canvas.drawParagraph(p, offset);
|
||||
offset = offset.translate(0, p.height + 10);
|
||||
|
||||
return scuba.diffCanvasScreenshot(canvas, 'text_max_lines_with_ellipsis');
|
||||
});
|
||||
|
||||
testEachCanvas('long unbreakable text', (EngineCanvas canvas) {
|
||||
Offset offset = Offset.zero;
|
||||
Paragraph p;
|
||||
|
||||
// The whole line is rendered unbroken when there are no constraints.
|
||||
p = paragraph(longUnbreakable);
|
||||
canvas.drawParagraph(p, offset);
|
||||
offset = offset.translate(0, p.height + 10);
|
||||
|
||||
// The whole line is rendered with an ellipsis.
|
||||
p = paragraph(
|
||||
longUnbreakable,
|
||||
paragraphStyle: ParagraphStyle(ellipsis: '...'),
|
||||
maxWidth: 200,
|
||||
);
|
||||
canvas.drawParagraph(p, offset);
|
||||
offset = offset.translate(0, p.height + 10);
|
||||
|
||||
// The text is broken into multiple lines.
|
||||
p = paragraph(longUnbreakable, maxWidth: 200);
|
||||
canvas.drawParagraph(p, offset);
|
||||
offset = offset.translate(0, p.height + 10);
|
||||
|
||||
// Very narrow constraint (less than one character's width).
|
||||
p = paragraph('AA', maxWidth: 7);
|
||||
canvas.drawParagraph(p, offset);
|
||||
offset = offset.translate(0, p.height + 10);
|
||||
|
||||
return scuba.diffCanvasScreenshot(canvas, 'text_long_unbreakable');
|
||||
});
|
||||
}
|
||||
@ -0,0 +1,165 @@
|
||||
// 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 'package:ui/ui.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
|
||||
import 'scuba.dart';
|
||||
|
||||
void main() async {
|
||||
final EngineScubaTester scuba = await EngineScubaTester.initialize(
|
||||
viewportSize: const Size(800, 800),
|
||||
);
|
||||
|
||||
void drawLetterAndWordSpacing(EngineCanvas canvas) {
|
||||
Offset offset = Offset.zero;
|
||||
|
||||
for (double spacing = 0; spacing < 15; spacing += 5) {
|
||||
canvas.drawParagraph(
|
||||
paragraph('HelloWorld',
|
||||
textStyle: TextStyle(
|
||||
color: const Color(0xFF000000),
|
||||
decoration: TextDecoration.none,
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 30,
|
||||
letterSpacing: spacing)),
|
||||
offset,
|
||||
);
|
||||
offset = offset.translate(0, 40);
|
||||
}
|
||||
for (double spacing = 0; spacing < 30; spacing += 10) {
|
||||
final TextStyle textStyle = TextStyle(
|
||||
color: const Color(0xFF00FF00),
|
||||
decoration: TextDecoration.none,
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 30,
|
||||
wordSpacing: spacing);
|
||||
canvas.drawParagraph(
|
||||
paragraph('Hello World', textStyle: textStyle, maxWidth: 600),
|
||||
offset,
|
||||
);
|
||||
offset = offset.translate(0, 40);
|
||||
}
|
||||
}
|
||||
|
||||
testEachCanvas('draws text with letter/word spacing', (EngineCanvas canvas) {
|
||||
drawLetterAndWordSpacing(canvas);
|
||||
return scuba.diffCanvasScreenshot(
|
||||
canvas, 'paint_bounds_for_text_style_letter_spacing');
|
||||
});
|
||||
|
||||
void drawTextDecorationStyle(EngineCanvas canvas) {
|
||||
final List<TextDecorationStyle> decorationStyles = <TextDecorationStyle>[
|
||||
TextDecorationStyle.solid,
|
||||
TextDecorationStyle.double,
|
||||
TextDecorationStyle.dotted,
|
||||
TextDecorationStyle.dashed,
|
||||
TextDecorationStyle.wavy,
|
||||
];
|
||||
|
||||
Offset offset = Offset.zero;
|
||||
|
||||
for (TextDecorationStyle decorationStyle in decorationStyles) {
|
||||
final TextStyle textStyle = TextStyle(
|
||||
color: const Color.fromRGBO(50, 50, 255, 1.0),
|
||||
decoration: TextDecoration.underline,
|
||||
decorationStyle: decorationStyle,
|
||||
decorationColor: const Color.fromRGBO(50, 50, 50, 1.0),
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 30,
|
||||
);
|
||||
canvas.drawParagraph(
|
||||
paragraph('Hello World', textStyle: textStyle, maxWidth: 600),
|
||||
offset,
|
||||
);
|
||||
offset = offset.translate(0, 40);
|
||||
}
|
||||
}
|
||||
|
||||
testEachCanvas('draws text decoration style', (EngineCanvas canvas) {
|
||||
drawTextDecorationStyle(canvas);
|
||||
return scuba.diffCanvasScreenshot(
|
||||
canvas, 'paint_bounds_for_text_decorationStyle');
|
||||
});
|
||||
|
||||
void drawTextDecoration(EngineCanvas canvas) {
|
||||
final List<TextDecoration> decorations = <TextDecoration>[
|
||||
TextDecoration.overline,
|
||||
TextDecoration.underline,
|
||||
TextDecoration.combine(<TextDecoration>[
|
||||
TextDecoration.underline,
|
||||
TextDecoration.lineThrough
|
||||
]),
|
||||
TextDecoration.combine(
|
||||
<TextDecoration>[TextDecoration.underline, TextDecoration.overline]),
|
||||
TextDecoration.combine(
|
||||
<TextDecoration>[TextDecoration.overline, TextDecoration.lineThrough])
|
||||
];
|
||||
|
||||
Offset offset = Offset.zero;
|
||||
|
||||
for (TextDecoration decoration in decorations) {
|
||||
final TextStyle textStyle = TextStyle(
|
||||
color: const Color.fromRGBO(50, 50, 255, 1.0),
|
||||
decoration: decoration,
|
||||
decorationStyle: TextDecorationStyle.solid,
|
||||
decorationColor: const Color.fromRGBO(255, 160, 0, 1.0),
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 20,
|
||||
);
|
||||
canvas.drawParagraph(
|
||||
paragraph(
|
||||
'Hello World $decoration',
|
||||
textStyle: textStyle,
|
||||
maxWidth: 600,
|
||||
),
|
||||
offset,
|
||||
);
|
||||
offset = offset.translate(0, 40);
|
||||
}
|
||||
}
|
||||
|
||||
testEachCanvas('draws text decoration', (EngineCanvas canvas) {
|
||||
drawTextDecoration(canvas);
|
||||
return scuba.diffCanvasScreenshot(
|
||||
canvas, 'paint_bounds_for_text_decoration');
|
||||
});
|
||||
|
||||
void drawTextWithBackground(EngineCanvas canvas) {
|
||||
// Single-line text.
|
||||
canvas.drawParagraph(
|
||||
paragraph(
|
||||
'Hello World',
|
||||
maxWidth: 600,
|
||||
textStyle: TextStyle(
|
||||
color: const Color.fromRGBO(0, 0, 0, 1.0),
|
||||
background: Paint()..color = const Color.fromRGBO(255, 50, 50, 1.0),
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 30,
|
||||
),
|
||||
),
|
||||
Offset.zero,
|
||||
);
|
||||
|
||||
// Multi-line text.
|
||||
canvas.drawParagraph(
|
||||
paragraph(
|
||||
'Multi line Hello World paragraph',
|
||||
maxWidth: 200,
|
||||
textStyle: TextStyle(
|
||||
color: const Color.fromRGBO(0, 0, 0, 1.0),
|
||||
background: Paint()..color = const Color.fromRGBO(50, 50, 255, 1.0),
|
||||
fontFamily: 'Arial',
|
||||
fontSize: 30,
|
||||
),
|
||||
),
|
||||
const Offset(0, 40),
|
||||
);
|
||||
}
|
||||
|
||||
testEachCanvas('draws text with a background', (EngineCanvas canvas) {
|
||||
drawTextWithBackground(canvas);
|
||||
return scuba.diffCanvasScreenshot(canvas, 'text_background');
|
||||
});
|
||||
}
|
||||
@ -184,8 +184,8 @@ StreamChannel<dynamic> _connectToIframe(String url, int id) {
|
||||
_iframes[id] = iframe;
|
||||
iframe
|
||||
..src = url
|
||||
..width = '500'
|
||||
..height = '500';
|
||||
..width = '1000'
|
||||
..height = '1000';
|
||||
document.body.children.add(iframe);
|
||||
|
||||
// Use this to communicate securely with the iframe.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user