未验证 提交 0747f2f4 编写于 作者: F Ferhat 提交者: GitHub

[web] Fix 3d transforms for html backend (#21499)

* Workaround for canvas element lacking support for 3d setTransform

* update golden test

* Add webkit workaround

* Implement DOM rendering for perspective

* cleanup

* update goldens lock

* Add check for shader and filtermask for dom use

* Fix svg viewBox. Move zIndex check to bitmap canvas

* Fix null check warning

* Fix scene_builder zIndex=-1 test to force canvas usage

* Add blendmode handling for DOM mode

* Update maxdiff and golden locks

* Remove unused import

* Add drawcolor/drawpaint test. Fix bounds for drawColor/drawPaint

* update golden locks

* adjust drawColor for dpr

* Update test to use canvas

* Fix toDataUrl NNBD

* Update Picture.toImage to use canvas to obstain image data

* Remove write:true from golden calls

* Add fill-rule for _pathToSvgElement

* Update golden locks

* Fix sceneBuilder pushClip / add missing clipBehaviour

* Fix test now that clipping works correctly

* move overflow handling for tests into DOMClip.addOverflow

* Add clipRect to test to keep render inside bitmap canvas area

* Update compositing test, fix drawColor coordinates

* update golden locks

* Skip test for matchGolden infra fail

* update golden lock

* merge

* update maxdiff for text over canvas

* update golden diff

* update paint spread bounds maxdiff

* update paint spread maxDiff
上级 3edc16ca
repository: https://github.com/flutter/goldens.git
revision: 1556280d6f1d70fac9ddff9b38639757e105b4b0
revision: 67f22ef933be27ba2be8b27df1b71b2c69eb86e5
......@@ -33,8 +33,6 @@ class _CanvasPool extends _SaveStackTracking {
html.HtmlElement? _rootElement;
int _saveContextCount = 0;
// Number of elements that have been added to flt-canvas.
int _activeElementCount = 0;
_CanvasPool(this._widthInBitmapPixels, this._heightInBitmapPixels);
......@@ -76,7 +74,6 @@ class _CanvasPool extends _SaveStackTracking {
_context = null;
_contextHandle = null;
}
_activeElementCount++;
}
void allocateCanvas(html.HtmlElement rootElement) {
......@@ -134,15 +131,12 @@ class _CanvasPool extends _SaveStackTracking {
_rootElement!.append(canvas);
}
if (_activeElementCount == 0) {
canvas.style.zIndex = '-1';
} else if (reused) {
// If a canvas is the first element we set z-index = -1 to workaround
// blink compositing bug. To make sure this does not leak when reused
// reset z-index.
if (reused) {
// If a canvas is the first element we set z-index = -1 in [BitmapCanvas]
// endOfPaint to workaround blink compositing bug. To make sure this
// does not leak when reused reset z-index.
canvas.style.removeProperty('z-index');
}
++_activeElementCount;
final html.CanvasRenderingContext2D context = _context = canvas.context2D;
_contextHandle = ContextStateHandle(this, context);
......@@ -270,7 +264,6 @@ class _CanvasPool extends _SaveStackTracking {
_canvas = null;
_context = null;
_contextHandle = null;
_activeElementCount = 0;
}
void endOfPaint() {
......@@ -326,7 +319,7 @@ class _CanvasPool extends _SaveStackTracking {
// Returns a "data://" URI containing a representation of the image in this
// canvas in PNG format.
String toDataUrl() => _canvas!.toDataUrl();
String toDataUrl() => _canvas?.toDataUrl() ?? '';
@override
void save() {
......
......@@ -68,75 +68,16 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking {
@override
void drawRect(ui.Rect rect, SurfacePaintData paint) {
_drawRect(rect, paint, 'draw-rect');
}
html.Element _drawRect(ui.Rect rect, SurfacePaintData paint, String tagName) {
assert(paint.shader == null);
final html.Element rectangle = html.Element.tag(tagName);
assert(() {
rectangle.setAttribute('flt-rect', '$rect');
rectangle.setAttribute('flt-paint', '$paint');
return true;
}());
String effectiveTransform;
final bool isStroke = paint.style == ui.PaintingStyle.stroke;
final double strokeWidth = paint.strokeWidth ?? 0.0;
final double left = math.min(rect.left, rect.right);
final double right = math.max(rect.left, rect.right);
final double top = math.min(rect.top, rect.bottom);
final double bottom = math.max(rect.top, rect.bottom);
if (currentTransform.isIdentity()) {
if (isStroke) {
effectiveTransform =
'translate(${left - (strokeWidth / 2.0)}px, ${top - (strokeWidth / 2.0)}px)';
} else {
effectiveTransform = 'translate(${left}px, ${top}px)';
}
} else {
// Clone to avoid mutating _transform.
final Matrix4 translated = currentTransform.clone();
if (isStroke) {
translated.translate(
left - (strokeWidth / 2.0), top - (strokeWidth / 2.0));
} else {
translated.translate(left, top);
}
effectiveTransform = matrix4ToCssTransform(translated);
}
final html.CssStyleDeclaration style = rectangle.style;
style
..position = 'absolute'
..transformOrigin = '0 0 0'
..transform = effectiveTransform;
final String cssColor =
paint.color == null ? '#000000' : colorToCssString(paint.color)!;
if (paint.maskFilter != null) {
style.filter = 'blur(${paint.maskFilter!.webOnlySigma}px)';
}
if (isStroke) {
style
..width = '${right - left - strokeWidth}px'
..height = '${bottom - top - strokeWidth}px'
..border = '${strokeWidth}px solid $cssColor';
} else {
style
..width = '${right - left}px'
..height = '${bottom - top}px'
..backgroundColor = cssColor;
}
currentElement.append(rectangle);
return rectangle;
currentElement.append(_buildDrawRectElement(rect, paint, 'draw-rect',
currentTransform));
}
@override
void drawRRect(ui.RRect rrect, SurfacePaintData paint) {
html.Element element = _drawRect(rrect.outerRect, paint, 'draw-rrect');
element.style.borderRadius = '${rrect.blRadiusX.toStringAsFixed(3)}px';
html.Element element = _buildDrawRectElement(rrect.outerRect,
paint, 'draw-rrect', currentTransform);
_applyRRectBorderRadius(element.style, rrect);
currentElement.append(element);
}
@override
......@@ -199,3 +140,108 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking {
// No reuse of elements yet to handle here. Noop.
}
}
html.HtmlElement _buildDrawRectElement(ui.Rect rect, SurfacePaintData paint, String tagName,
Matrix4 transform) {
assert(paint.shader == null);
final html.HtmlElement rectangle = html.Element.tag(tagName) as html.HtmlElement;
assert(() {
rectangle.setAttribute('flt-rect', '$rect');
rectangle.setAttribute('flt-paint', '$paint');
return true;
}());
String effectiveTransform;
final bool isStroke = paint.style == ui.PaintingStyle.stroke;
final double strokeWidth = paint.strokeWidth ?? 0.0;
final double left = math.min(rect.left, rect.right);
final double right = math.max(rect.left, rect.right);
final double top = math.min(rect.top, rect.bottom);
final double bottom = math.max(rect.top, rect.bottom);
if (transform.isIdentity()) {
if (isStroke) {
effectiveTransform =
'translate(${left - (strokeWidth / 2.0)}px, ${top - (strokeWidth / 2.0)}px)';
} else {
effectiveTransform = 'translate(${left}px, ${top}px)';
}
} else {
// Clone to avoid mutating _transform.
final Matrix4 translated = transform.clone();
if (isStroke) {
translated.translate(
left - (strokeWidth / 2.0), top - (strokeWidth / 2.0));
} else {
translated.translate(left, top);
}
effectiveTransform = matrix4ToCssTransform(translated);
}
final html.CssStyleDeclaration style = rectangle.style;
style
..position = 'absolute'
..transformOrigin = '0 0 0'
..transform = effectiveTransform;
final String cssColor =
paint.color == null ? '#000000' : colorToCssString(paint.color)!;
if (paint.maskFilter != null) {
style.filter = 'blur(${paint.maskFilter!.webOnlySigma}px)';
}
if (isStroke) {
style
..width = '${right - left - strokeWidth}px'
..height = '${bottom - top - strokeWidth}px'
..border = '${strokeWidth}px solid $cssColor';
} else {
style
..width = '${right - left}px'
..height = '${bottom - top}px'
..backgroundColor = cssColor;
}
return rectangle;
}
void _applyRRectBorderRadius(html.CssStyleDeclaration style, ui.RRect rrect) {
if (rrect.tlRadiusX == rrect.trRadiusX &&
rrect.tlRadiusX == rrect.blRadiusX &&
rrect.tlRadiusX == rrect.brRadiusX &&
rrect.tlRadiusX == rrect.tlRadiusY &&
rrect.trRadiusX == rrect.trRadiusY &&
rrect.blRadiusX == rrect.blRadiusY &&
rrect.brRadiusX == rrect.brRadiusY) {
style.borderRadius = '${rrect.blRadiusX.toStringAsFixed(3)}px';
return;
}
// Non-uniform. Apply each corner radius.
style.borderTopLeftRadius = '${rrect.tlRadiusX.toStringAsFixed(3)}px '
'${rrect.tlRadiusY.toStringAsFixed(3)}px';
style.borderTopRightRadius = '${rrect.trRadiusX.toStringAsFixed(3)}px '
'${rrect.trRadiusY.toStringAsFixed(3)}px';
style.borderBottomLeftRadius = '${rrect.blRadiusX.toStringAsFixed(3)}px '
'${rrect.blRadiusY.toStringAsFixed(3)}px';
style.borderBottomRightRadius = '${rrect.brRadiusX.toStringAsFixed(3)}px '
'${rrect.brRadiusY.toStringAsFixed(3)}px';
}
html.Element _pathToSvgElement(SurfacePath path, SurfacePaintData paint,
String width, String height) {
final StringBuffer sb = StringBuffer();
sb.write(
'<svg viewBox="0 0 $width $height" width="${width}px" height="${height}px">');
sb.write('<path ');
if (paint.style == ui.PaintingStyle.stroke) {
sb.write('stroke="${colorToCssString(paint.color)}" ');
sb.write('stroke-width="${paint.strokeWidth}" ');
} else if (paint.color != null) {
sb.write('fill="${colorToCssString(paint.color)}" ');
}
if (path.fillType == ui.PathFillType.evenOdd) {
sb.write('fill-rule="evenodd" ');
}
sb.write('d="');
pathToSvg(path, sb);
sb.write('"></path>');
sb.write('</svg>');
return html.Element.html(sb.toString(), treeSanitizer: _NullTreeSanitizer());
}
......@@ -25,18 +25,6 @@ mixin _DomClip on PersistedContainerSurface {
@override
html.Element createElement() {
final html.Element element = defaultCreateElement('flt-clip');
if (!debugShowClipLayers) {
// Hide overflow in production mode. When debugging we want to see the
// clipped picture in full.
element.style
..overflow = 'hidden'
..zIndex = '0';
} else {
// Display the outline of the clipping region. When debugShowClipLayers is
// `true` we don't hide clip overflow (see above). This outline helps
// visualizing clip areas.
element.style.boxShadow = 'inset 0 0 10px green';
}
_childContainer = html.Element.tag('flt-clip-interior');
if (_debugExplainSurfaceStats) {
// This creates an additional interior element. Count it too.
......@@ -57,14 +45,32 @@ mixin _DomClip on PersistedContainerSurface {
// together.
_childContainer = null;
}
void applyOverflow(html.Element element, ui.Clip? clipBehaviour) {
if (!debugShowClipLayers) {
// Hide overflow in production mode. When debugging we want to see the
// clipped picture in full.
if (clipBehaviour != ui.Clip.none) {
element.style
..overflow = 'hidden'
..zIndex = '0';
}
} else {
// Display the outline of the clipping region. When debugShowClipLayers is
// `true` we don't hide clip overflow (see above). This outline helps
// visualizing clip areas.
element.style.boxShadow = 'inset 0 0 10px green';
}
}
}
/// A surface that creates a rectangular clip.
class PersistedClipRect extends PersistedContainerSurface
with _DomClip
implements ui.ClipRectEngineLayer {
PersistedClipRect(PersistedClipRect? oldLayer, this.rect) : super(oldLayer);
PersistedClipRect(PersistedClipRect? oldLayer, this.rect, this.clipBehavior)
: super(oldLayer);
final ui.Clip? clipBehavior;
final ui.Rect rect;
@override
......@@ -87,6 +93,7 @@ class PersistedClipRect extends PersistedContainerSurface
..top = '${rect.top}px'
..width = '${rect.right - rect.left}px'
..height = '${rect.bottom - rect.top}px';
applyOverflow(rootElement!, clipBehavior);
// Translate the child container in the opposite direction to compensate for
// the shift in the coordinate system introduced by the translation of the
......@@ -99,7 +106,7 @@ class PersistedClipRect extends PersistedContainerSurface
@override
void update(PersistedClipRect oldSurface) {
super.update(oldSurface);
if (rect != oldSurface.rect) {
if (rect != oldSurface.rect || clipBehavior != oldSurface.clipBehavior) {
apply();
}
}
......@@ -134,7 +141,8 @@ class PersistedClipRRect extends PersistedContainerSurface
@override
void apply() {
rootElement!.style
html.CssStyleDeclaration style = rootElement!.style;
style
..left = '${rrect.left}px'
..top = '${rrect.top}px'
..width = '${rrect.width}px'
......@@ -143,6 +151,7 @@ class PersistedClipRRect extends PersistedContainerSurface
..borderTopRightRadius = '${rrect.trRadiusX}px'
..borderBottomRightRadius = '${rrect.brRadiusX}px'
..borderBottomLeftRadius = '${rrect.blRadiusX}px';
applyOverflow(rootElement!, clipBehavior);
// Translate the child container in the opposite direction to compensate for
// the shift in the coordinate system introduced by the translation of the
......@@ -155,7 +164,7 @@ class PersistedClipRRect extends PersistedContainerSurface
@override
void update(PersistedClipRRect oldSurface) {
super.update(oldSurface);
if (rrect != oldSurface.rrect) {
if (rrect != oldSurface.rrect || clipBehavior != oldSurface.clipBehavior) {
apply();
}
}
......
......@@ -113,7 +113,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder {
}) {
assert(clipBehavior != null); // ignore: unnecessary_null_comparison
assert(clipBehavior != ui.Clip.none);
return _pushSurface(PersistedClipRect(oldLayer as PersistedClipRect?, rect)) as ui.ClipRectEngineLayer;
return _pushSurface(PersistedClipRect(oldLayer as PersistedClipRect?, rect, clipBehavior))
as ui.ClipRectEngineLayer;
}
/// Pushes a rounded-rectangular clip operation onto the operation stack.
......
......@@ -47,7 +47,7 @@ class EnginePicture implements ui.Picture {
@override
Future<ui.Image> toImage(int width, int height) async {
final ui.Rect imageRect = ui.Rect.fromLTRB(0, 0, width.toDouble(), height.toDouble());
final BitmapCanvas canvas = BitmapCanvas(imageRect);
final BitmapCanvas canvas = BitmapCanvas.imageData(imageRect);
recordingCanvas!.apply(canvas, imageRect);
final String imageDataUrl = canvas.toDataUrl();
final html.ImageElement imageElement = html.ImageElement()
......
......@@ -133,7 +133,8 @@ void testMain() {
() {
final PersistedScene scene1 = PersistedScene(null);
final PersistedClipRect clip1 =
PersistedClipRect(null, const Rect.fromLTRB(10, 10, 20, 20));
PersistedClipRect(null, const Rect.fromLTRB(10, 10, 20, 20),
Clip.antiAlias);
final PersistedOpacity opacity = PersistedOpacity(null, 100, Offset.zero);
final MockPersistedPicture picture = MockPersistedPicture();
......@@ -158,7 +159,8 @@ void testMain() {
// because the clip didn't change no repaints should happen.
final PersistedScene scene2 = PersistedScene(scene1);
final PersistedClipRect clip2 =
PersistedClipRect(clip1, const Rect.fromLTRB(10, 10, 20, 20));
PersistedClipRect(clip1, const Rect.fromLTRB(10, 10, 20, 20),
Clip.antiAlias);
clip1.state = PersistedSurfaceState.pendingUpdate;
scene2.appendChild(clip2);
opacity.state = PersistedSurfaceState.pendingRetention;
......@@ -176,7 +178,8 @@ void testMain() {
// This should cause the picture to repaint despite being retained.
final PersistedScene scene3 = PersistedScene(scene2);
final PersistedClipRect clip3 =
PersistedClipRect(clip2, const Rect.fromLTRB(10, 10, 50, 50));
PersistedClipRect(clip2, const Rect.fromLTRB(10, 10, 50, 50),
Clip.antiAlias);
clip2.state = PersistedSurfaceState.pendingUpdate;
scene3.appendChild(clip3);
opacity.state = PersistedSurfaceState.pendingRetention;
......@@ -234,6 +237,7 @@ void testMain() {
builder.pop();
html.HtmlElement content = builder.build().webOnlyRootElement;
html.document.body.append(content);
expect(content.querySelector('canvas').style.zIndex, '-1');
// Force update to scene which will utilize reuse code path.
......@@ -627,8 +631,16 @@ Picture _drawPicture() {
final EnginePictureRecorder recorder = PictureRecorder();
final RecordingCanvas canvas =
recorder.beginRecording(const Rect.fromLTRB(0, 0, 400, 400));
Shader gradient = Gradient.radial(
Offset(100, 100), 50, [
const Color.fromARGB(255, 0, 0, 0),
const Color.fromARGB(255, 0, 0, 255)
]);
canvas.drawCircle(
Offset(offsetX + 10, offsetY + 10), 10, Paint()..style = PaintingStyle.fill);
Offset(offsetX + 10, offsetY + 10), 10,
Paint()
..style = PaintingStyle.fill
..shader = gradient);
canvas.drawCircle(
Offset(offsetX + 60, offsetY + 10),
10,
......@@ -656,8 +668,16 @@ Picture _drawPathImagePath() {
final EnginePictureRecorder recorder = PictureRecorder();
final RecordingCanvas canvas =
recorder.beginRecording(const Rect.fromLTRB(0, 0, 400, 400));
Shader gradient = Gradient.radial(
Offset(100, 100), 50, [
const Color.fromARGB(255, 0, 0, 0),
const Color.fromARGB(255, 0, 0, 255)
]);
canvas.drawCircle(
Offset(offsetX + 10, offsetY + 10), 10, Paint()..style = PaintingStyle.fill);
Offset(offsetX + 10, offsetY + 10), 10,
Paint()
..style = PaintingStyle.fill
..shader = gradient);
canvas.drawCircle(
Offset(offsetX + 60, offsetY + 10),
10,
......@@ -671,6 +691,11 @@ Picture _drawPathImagePath() {
..style = PaintingStyle.fill
..color = const Color.fromRGBO(0, 255, 0, 1));
canvas.drawImage(createTestImage(), Offset(0, 0), Paint());
canvas.drawCircle(
Offset(offsetX + 10, offsetY + 10), 10,
Paint()
..style = PaintingStyle.fill
..shader = gradient);
canvas.drawCircle(
Offset(offsetX + 60, offsetY + 60),
10,
......
......@@ -24,7 +24,7 @@ void testMain() async {
// 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),
double maxDiffRatePercent = 0.0}) async {
double maxDiffRatePercent = 0.0, bool write = false}) async {
final EngineCanvas engineCanvas = BitmapCanvas(screenRect);
rc.endRecording();
......@@ -35,7 +35,8 @@ void testMain() async {
try {
sceneElement.append(engineCanvas.rootElement);
html.document.body.append(sceneElement);
await matchGoldenFile('$fileName.png', region: region, maxDiffRatePercent: maxDiffRatePercent);
await matchGoldenFile('$fileName.png', region: region,
maxDiffRatePercent: maxDiffRatePercent, write: write);
} finally {
// The page is reused across tests, so remove the element after taking the
// Scuba screenshot.
......@@ -83,7 +84,8 @@ void testMain() async {
..color = const Color.fromARGB(128, 255, 0, 0));
rc.restore();
await _checkScreenshot(rc, 'canvas_blend_circle_diff_color',
maxDiffRatePercent: operatingSystem == OperatingSystem.macOs ? 2.95 : 0);
maxDiffRatePercent: operatingSystem == OperatingSystem.macOs ? 2.95 :
operatingSystem == OperatingSystem.iOs ? 1.0 : 0);
});
test('Blend circle and text with multiply', () async {
......@@ -120,7 +122,8 @@ void testMain() async {
Paint()..blendMode = BlendMode.multiply);
rc.restore();
await _checkScreenshot(rc, 'canvas_blend_image_multiply',
maxDiffRatePercent: operatingSystem == OperatingSystem.macOs ? 2.95 : 0);
maxDiffRatePercent: operatingSystem == OperatingSystem.macOs ? 2.95 :
operatingSystem == OperatingSystem.iOs ? 2.0 : 0);
});
}
......
// 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.
// @dart = 2.6
import 'dart:html' as html;
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/ui.dart';
import 'package:ui/src/engine.dart';
import 'package:web_engine_tester/golden_tester.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() async {
setUp(() async {
debugShowClipLayers = true;
SurfaceSceneBuilder.debugForgetFrameScene();
for (html.Node scene in html.document.querySelectorAll('flt-scene')) {
scene.remove();
}
await webOnlyInitializePlatform();
webOnlyFontCollection.debugRegisterTestFonts();
await webOnlyFontCollection.ensureFontsLoaded();
});
test('drawColor should cover entire viewport', () async {
final Rect region = Rect.fromLTWH(0, 0, 400, 400);
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final Picture testPicture = _drawTestPicture(region, useColor: true);
builder.addPicture(Offset.zero, testPicture);
html.document.body.append(builder
.build()
.webOnlyRootElement);
await matchGoldenFile('canvas_draw_color.png', region: region);
}, skip: true); // TODO: matchGolden fails when a div covers viewport.
test('drawPaint should cover entire viewport', () async {
final Rect region = Rect.fromLTWH(0, 0, 400, 400);
final SurfaceSceneBuilder builder = SurfaceSceneBuilder();
final Picture testPicture = _drawTestPicture(region, useColor: false);
builder.addPicture(Offset.zero, testPicture);
html.document.body.append(builder
.build()
.webOnlyRootElement);
await matchGoldenFile('canvas_draw_paint.png', region: region);
}, skip: true); // TODO: matchGolden fails when a div covers viewport.);
}
Picture _drawTestPicture(Rect region, {bool useColor = false}) {
final EnginePictureRecorder recorder = PictureRecorder();
final Rect r = Rect.fromLTWH(0, 0, 200, 200);
final RecordingCanvas canvas = recorder.beginRecording(r);
canvas.drawRect(
region.deflate(8.0),
Paint()
..style = PaintingStyle.fill
..color = Color(0xFFE0E0E0)
);
canvas.transform(Matrix4.translationValues(50, 50, 0).storage);
if (useColor) {
canvas.drawColor(const Color.fromRGBO(0, 255, 0, 1), BlendMode.srcOver);
} else {
canvas.drawPaint(Paint()
..style = PaintingStyle.fill
..color = const Color.fromRGBO(0, 0, 255, 1));
}
canvas.drawCircle(
Offset(r.width/2, r.height/2), r.width/2,
Paint()
..style = PaintingStyle.fill
..color = const Color.fromRGBO(255, 0, 0, 1));
return recorder.endRecording();
}
......@@ -28,7 +28,8 @@ void testMain() async {
// 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),
double maxDiffRatePercent = 0.0}) async {
double maxDiffRatePercent = 0.0, bool setupPerspective = false,
bool write = false}) async {
final EngineCanvas engineCanvas = BitmapCanvas(screenRect);
rc.endRecording();
......@@ -37,10 +38,18 @@ void testMain() async {
// Wrap in <flt-scene> so that our CSS selectors kick in.
final html.Element sceneElement = html.Element.tag('flt-scene');
try {
if (setupPerspective) {
// iFrame disables perspective, set it explicitly for test.
engineCanvas.rootElement.style.perspective = '400px';
for (html.Element element in engineCanvas.rootElement.querySelectorAll(
'div')) {
element.style.perspective = '400px';
}
}
sceneElement.append(engineCanvas.rootElement);
html.document.body.append(sceneElement);
await matchGoldenFile('$fileName.png',
region: region, maxDiffRatePercent: maxDiffRatePercent);
region: region, maxDiffRatePercent: maxDiffRatePercent, write: write);
} finally {
// The page is reused across tests, so remove the element after taking the
// Scuba screenshot.
......@@ -400,6 +409,158 @@ void testMain() async {
await _checkScreenshot(canvas, 'draw_clipped_and_transformed_image',
region: region, maxDiffRatePercent: 1.0);
});
/// Regression test for https://github.com/flutter/flutter/issues/61245
test('Should render image with perspective', () async {
final Rect region = const Rect.fromLTRB(0, 0, 200, 200);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.translate(10, 10);
canvas.drawImage(createTestImage(), Offset(0, 0), new Paint());
Matrix4 transform = Matrix4.identity()
..setRotationY(0.8)
..setEntry(3, 2, 0.0005); // perspective
canvas.transform(transform.storage);
canvas.drawImage(createTestImage(), Offset(0, 100), new Paint());
await _checkScreenshot(canvas, 'draw_3d_image',
region: region,
maxDiffRatePercent: 6.0,
setupPerspective: true);
});
/// Regression test for https://github.com/flutter/flutter/issues/61245
test('Should render image with perspective inside clip area', () async {
final Rect region = const Rect.fromLTRB(0, 0, 200, 200);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.drawRect(region, Paint()..color = Color(0xFFE0E0E0));
canvas.translate(10, 10);
canvas.drawImage(createTestImage(), Offset(0, 0), new Paint());
Matrix4 transform = Matrix4.identity()
..setRotationY(0.8)
..setEntry(3, 2, 0.0005); // perspective
canvas.transform(transform.storage);
canvas.clipRect(region, ClipOp.intersect);
canvas.drawRect(Rect.fromLTWH(0, 0, 100, 200), Paint()..color = Color(0x801080E0));
canvas.drawImage(createTestImage(), Offset(0, 100), new Paint());
canvas.drawRect(Rect.fromLTWH(50, 150, 50, 20), Paint()..color = Color(0x80000000));
await _checkScreenshot(canvas, 'draw_3d_image_clipped',
region: region,
maxDiffRatePercent: 5.0,
setupPerspective: true);
});
test('Should render rect with perspective transform', () async {
final Rect region = const Rect.fromLTRB(0, 0, 400, 400);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.drawRect(region, Paint()..color = Color(0xFFE0E0E0));
canvas.translate(20, 20);
canvas.drawRect(Rect.fromLTWH(0, 0, 100, 40),
Paint()..color = Color(0xFF000000));
Matrix4 transform = Matrix4.identity()
..setRotationY(0.8)
..setEntry(3, 2, 0.001); // perspective
canvas.transform(transform.storage);
canvas.clipRect(region, ClipOp.intersect);
canvas.drawRect(Rect.fromLTWH(0, 60, 120, 40), Paint()..color = Color(0x801080E0));
canvas.drawRect(Rect.fromLTWH(300, 250, 120, 40), Paint()..color = Color(0x80E010E0));
canvas.drawRRect(RRect.fromRectAndRadius(Rect.fromLTWH(0, 120, 160, 40), Radius.circular(5)),
Paint()..color = Color(0x801080E0));
canvas.drawRRect(RRect.fromRectAndRadius(Rect.fromLTWH(300, 320, 90, 40), Radius.circular(20)),
Paint()..color = Color(0x80E010E0));
await _checkScreenshot(canvas, 'draw_3d_rect_clipped',
region: region,
maxDiffRatePercent: 1.0,
setupPerspective: true);
});
test('Should render color and ovals with perspective transform', () async {
final Rect region = const Rect.fromLTRB(0, 0, 400, 400);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.drawRect(region, Paint()..color = Color(0xFFFF0000));
canvas.drawColor(Color(0xFFE0E0E0), BlendMode.src);
canvas.translate(20, 20);
canvas.drawRect(Rect.fromLTWH(0, 0, 100, 40),
Paint()..color = Color(0xFF000000));
Matrix4 transform = Matrix4.identity()
..setRotationY(0.8)
..setEntry(3, 2, 0.001); // perspective
canvas.transform(transform.storage);
canvas.clipRect(region, ClipOp.intersect);
canvas.drawOval(Rect.fromLTWH(0, 120, 130, 40),
Paint()..color = Color(0x801080E0));
canvas.drawOval(Rect.fromLTWH(300, 290, 90, 40),
Paint()..color = Color(0x80E010E0));
canvas.drawCircle(Offset(60, 240), 50, Paint()..color = Color(0x801080E0));
canvas.drawCircle(Offset(360, 370), 30, Paint()..color = Color(0x80E010E0));
await _checkScreenshot(canvas, 'draw_3d_oval_clipped',
region: region,
maxDiffRatePercent: 1.0,
setupPerspective: true);
});
test('Should render path with perspective transform', () async {
final Rect region = const Rect.fromLTRB(0, 0, 400, 400);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.drawRect(region, Paint()..color = Color(0xFFFF0000));
canvas.drawColor(Color(0xFFE0E0E0), BlendMode.src);
canvas.translate(20, 20);
canvas.drawRect(Rect.fromLTWH(0, 0, 100, 20),
Paint()..color = Color(0xFF000000));
Matrix4 transform = Matrix4.identity()
..setRotationY(0.8)
..setEntry(3, 2, 0.001); // perspective
canvas.transform(transform.storage);
canvas.drawRect(Rect.fromLTWH(0, 120, 130, 40),
Paint()..color = Color(0x801080E0));
canvas.drawOval(Rect.fromLTWH(300, 290, 90, 40),
Paint()..color = Color(0x80E010E0));
Path path = Path();
path.moveTo(50, 50);
path.lineTo(100, 50);
path.lineTo(100, 100);
path.close();
canvas.drawPath(path, Paint()..color = Color(0x801080E0));
canvas.drawCircle(Offset(50, 50), 4, Paint()..color = Color(0xFF000000));
canvas.drawCircle(Offset(100, 100), 4, Paint()..color = Color(0xFF000000));
canvas.drawCircle(Offset(100, 50), 4, Paint()..color = Color(0xFF000000));
await _checkScreenshot(canvas, 'draw_3d_path',
region: region,
maxDiffRatePercent: 1.0,
setupPerspective: true);
});
test('Should render path with perspective transform', () async {
final Rect region = const Rect.fromLTRB(0, 0, 400, 400);
final RecordingCanvas canvas = RecordingCanvas(region);
canvas.drawRect(region, Paint()..color = Color(0xFFFF0000));
canvas.drawColor(Color(0xFFE0E0E0), BlendMode.src);
canvas.translate(20, 20);
canvas.drawRect(Rect.fromLTWH(0, 0, 100, 20),
Paint()..color = Color(0xFF000000));
Matrix4 transform = Matrix4.identity()
..setRotationY(0.8)
..setEntry(3, 2, 0.001); // perspective
canvas.transform(transform.storage);
//canvas.clipRect(region, ClipOp.intersect);
canvas.drawRect(Rect.fromLTWH(0, 120, 130, 40),
Paint()..color = Color(0x801080E0));
canvas.drawOval(Rect.fromLTWH(300, 290, 90, 40),
Paint()..color = Color(0x80E010E0));
Path path = Path();
path.moveTo(50, 50);
path.lineTo(100, 50);
path.lineTo(100, 100);
path.close();
canvas.drawPath(path, Paint()..color = Color(0x801080E0));
canvas.drawCircle(Offset(50, 50), 4, Paint()..color = Color(0xFF000000));
canvas.drawCircle(Offset(100, 100), 4, Paint()..color = Color(0xFF000000));
canvas.drawCircle(Offset(100, 50), 4, Paint()..color = Color(0xFF000000));
await _checkScreenshot(canvas, 'draw_3d_path_clipped',
region: region,
maxDiffRatePercent: 1.0,
setupPerspective: true);
});
}
// 9 slice test image that has a shiny/glass look.
......
......@@ -93,28 +93,31 @@ void testMain() async {
// compensate by shifting the contents of the canvas in the opposite
// direction.
canvas = BitmapCanvas(const Rect.fromLTWH(0.5, 0.5, 60, 60));
canvas.clipRect(const Rect.fromLTWH(0, 0, 50, 50), ClipOp.intersect);
drawMisalignedLines(canvas);
appendToScene();
await matchGoldenFile('misaligned_canvas_test.png', region: region);
await matchGoldenFile('misaligned_canvas_test.png', region: region,
maxDiffRatePercent: 1.0);
});
test('fill the whole canvas with color even when transformed', () async {
canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50));
canvas.clipRect(const Rect.fromLTWH(0, 0, 50, 50), ClipOp.intersect);
canvas.translate(25, 25);
canvas.drawColor(const Color.fromRGBO(0, 255, 0, 1.0), BlendMode.src);
appendToScene();
await matchGoldenFile('bitmap_canvas_fills_color_when_transformed.png', region: region);
await matchGoldenFile('bitmap_canvas_fills_color_when_transformed.png',
region: region,
maxDiffRatePercent: 5.0);
});
test('fill the whole canvas with paint even when transformed', () async {
canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50));
canvas.clipRect(const Rect.fromLTWH(0, 0, 50, 50), ClipOp.intersect);
canvas.translate(25, 25);
canvas.drawPaint(SurfacePaintData()
..color = const Color.fromRGBO(0, 255, 0, 1.0)
......@@ -122,7 +125,9 @@ void testMain() async {
appendToScene();
await matchGoldenFile('bitmap_canvas_fills_paint_when_transformed.png', region: region);
await matchGoldenFile('bitmap_canvas_fills_paint_when_transformed.png',
region: region,
maxDiffRatePercent: 5.0);
});
// This test reproduces text blurriness when two pieces of text appear inside
......@@ -245,7 +250,7 @@ void testMain() async {
await matchGoldenFile(
'bitmap_canvas_draws_text_on_top_of_canvas.png',
region: canvasSize,
maxDiffRatePercent: 0.0,
maxDiffRatePercent: 1.0,
pixelComparison: PixelComparison.precise,
);
});
......
......@@ -22,7 +22,9 @@ void main() {
void testMain() async {
setUp(() async {
debugShowClipLayers = true;
// To debug test failures uncomment the following to visualize clipping
// layers:
// debugShowClipLayers = true;
SurfaceSceneBuilder.debugForgetFrameScene();
for (html.Node scene in html.document.querySelectorAll('flt-scene')) {
scene.remove();
......@@ -545,7 +547,6 @@ void _testCullRectComputation() {
'renders clipped text with high quality',
() async {
// To reproduce blurriness we need real clipping.
debugShowClipLayers = false;
final Paragraph paragraph =
(ParagraphBuilder(ParagraphStyle(fontFamily: 'Roboto'))..addText('Am I blurry?')).build();
paragraph.layout(const ParagraphConstraints(width: 1000));
......
......@@ -678,7 +678,7 @@ void testMain() async {
await matchGoldenFile(
'paint_spread_bounds.png',
region: const Rect.fromLTRB(0, 0, 250, 600),
maxDiffRatePercent: 0.01,
maxDiffRatePercent: 0.2,
pixelComparison: PixelComparison.precise,
);
} finally {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册