diff --git a/engine/src/flutter/lib/web_ui/dev/goldens_lock.yaml b/engine/src/flutter/lib/web_ui/dev/goldens_lock.yaml index e82417f9286..e8b87138a00 100644 --- a/engine/src/flutter/lib/web_ui/dev/goldens_lock.yaml +++ b/engine/src/flutter/lib/web_ui/dev/goldens_lock.yaml @@ -1,2 +1,2 @@ repository: https://github.com/flutter/goldens.git -revision: 7935a97f89a6af5ae5182b2b5e59debda0189984 \ No newline at end of file +revision: 009fbdd595aeec364eaff6b8f337f8ceb3c44ab9 diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/bitmap_canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/bitmap_canvas.dart index 65de6c35129..b9daedc91f6 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/bitmap_canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/bitmap_canvas.dart @@ -72,6 +72,17 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { Object _prevFillStyle; Object _prevStrokeStyle; + // Indicates the instructions following drawImage or drawParagraph that + // a child element was created to paint. + // TODO(flutter_web): When childElements are created by + // drawImage/drawParagraph commands, compositing order is not correctly + // handled when we interleave these with other paint commands. + // To solve this, recording canvas will have to check the paint queue + // and send a hint to EngineCanvas that additional canvas layers need + // to be used to composite correctly. In practice this is very rare + // with Widgets but CustomPainter(s) can hit this code path. + bool _childOverdraw = false; + /// Allocates a canvas with enough memory to paint a picture within the given /// [bounds]. /// @@ -568,30 +579,81 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { void drawImage(ui.Image image, ui.Offset p, ui.PaintData paint) { _applyPaint(paint); final HtmlImage htmlImage = image; - final html.Element imgElement = htmlImage.imgElement.clone(true); - imgElement.style - ..position = 'absolute' - ..transform = 'translate(${p.dx}px, ${p.dy}px)'; - rootElement.append(imgElement); + final html.Element imgElement = htmlImage.cloneImageElement(); + _drawImage(imgElement, p); + _childOverdraw = true; + } + + void _drawImage(html.ImageElement imgElement, ui.Offset p) { + if (isClipped) { + final List clipElements = + _clipContent(_clipStack, imgElement, p, currentTransform); + for (html.Element clipElement in clipElements) { + rootElement.append(clipElement); + _children.add(clipElement); + } + } else { + final String cssTransform = + matrix4ToCssTransform(transformWithOffset(currentTransform, p)); + imgElement.style + ..transformOrigin = '0 0 0' + ..transform = cssTransform; + rootElement.append(imgElement); + _children.add(imgElement); + } } @override void drawImageRect( ui.Image image, ui.Rect src, ui.Rect dst, ui.PaintData paint) { - // TODO(het): Check if the src rect is the entire image, and if so just - // append the imgElement and set it's height and width. final HtmlImage htmlImage = image; - ctx.drawImageScaledFromSource( - htmlImage.imgElement, - src.left, - src.top, - src.width, - src.height, - dst.left, - dst.top, - dst.width, - dst.height, - ); + final bool requiresClipping = src.left != 0 || + src.top != 0 || + src.width != image.width || + src.height != image.height; + if (dst.width == image.width && + dst.height == image.height && + !requiresClipping) { + drawImage(image, dst.topLeft, paint); + } else { + _applyPaint(paint); + final html.Element imgElement = htmlImage.cloneImageElement(); + if (requiresClipping) { + save(); + clipRect(dst); + } + double targetLeft = dst.left; + double targetTop = dst.top; + if (requiresClipping) { + if (src.width != image.width) { + double leftMargin = -src.left * (dst.width / src.width); + targetLeft += leftMargin; + } + if (src.height != image.height) { + double topMargin = -src.top * (dst.height / src.height); + targetTop += topMargin; + } + } + _drawImage(imgElement, ui.Offset(targetLeft, targetTop)); + // To scale set width / height on destination image. + // For clipping we need to scale according to + // clipped-width/full image width and shift it according to left/top of + // source rectangle. + double targetWidth = dst.width; + double targetHeight = dst.height; + if (requiresClipping) { + targetWidth *= image.width / src.width; + targetHeight *= image.height / src.height; + } + final html.CssStyleDeclaration imageStyle = imgElement.style; + imageStyle + ..width = '${targetWidth.toStringAsFixed(2)}px' + ..height = '${targetHeight.toStringAsFixed(2)}px'; + if (requiresClipping) { + restore(); + } + } + _childOverdraw = true; } void _drawTextLine( @@ -625,7 +687,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { final ParagraphGeometricStyle style = paragraph._geometricStyle; - if (paragraph._drawOnCanvas) { + if (paragraph._drawOnCanvas && _childOverdraw == false) { final List lines = paragraph._lines ?? [paragraph._plainText]; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/html_image_codec.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/html_image_codec.dart index 23d422f4674..3ccd1faf11c 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -92,7 +92,7 @@ class SingleFrameInfo implements ui.FrameInfo { class HtmlImage implements ui.Image { final html.ImageElement imgElement; - + bool _requiresClone = false; HtmlImage(this.imgElement, this.width, this.height); @override @@ -117,6 +117,18 @@ class HtmlImage implements ui.Image { }); } + // Returns absolutely positioned actual image element on first call and + // clones on subsequent calls. + html.ImageElement cloneImageElement() { + if (_requiresClone) { + return imgElement.clone(true); + } else { + _requiresClone = true; + imgElement.style..position = 'absolute'; + return imgElement; + } + } + /// Returns an error message on failure, null on success. String _toByteData(int format, Callback callback) => null; } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/recording_canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/recording_canvas.dart index 067fe397eb8..891390fca44 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/recording_canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/recording_canvas.dart @@ -73,8 +73,9 @@ class RecordingCanvas { print(debugBuf); } else { try { - for (int i = 0; i < _commands.length; i++) { - _commands[i].apply(engineCanvas); + for (int i = 0, len = _commands.length; i < len; i++) { + PaintCommand command = _commands[i]; + command.apply(engineCanvas); } } catch (e) { // commands should never fail, but... diff --git a/engine/src/flutter/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart b/engine/src/flutter/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart new file mode 100644 index 00000000000..9601d71afa9 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart @@ -0,0 +1,136 @@ +// 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:js_util' as js_util; + +import 'package:ui/ui.dart' hide TextStyle; +import 'package:ui/src/engine.dart'; +import 'package:test/test.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 _checkScreenshot(RecordingCanvas rc, String fileName, + { Rect region = const Rect.fromLTWH(0, 0, 500, 500) }) async { + + final EngineCanvas engineCanvas = BitmapCanvas(screenRect); + + rc.apply(engineCanvas); + + // Wrap in 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('$fileName.png', region: region, maxDiffRate: 0.02); + } 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('Paints image', () async { + final RecordingCanvas rc = + RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + rc.save(); + rc.drawImage(createTestImage(), Offset(0, 0), new Paint()); + await _checkScreenshot(rc, 'draw_image'); + }); + + test('Paints image with transform', () async { + final RecordingCanvas rc = + RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + rc.save(); + rc.translate(50.0, 100.0); + rc.rotate(math.pi / 4.0); + rc.drawImage(createTestImage(), Offset(0, 0), new Paint()); + await _checkScreenshot(rc, 'draw_image_with_transform'); + }); + + test('Paints image with transform and offset', () async { + final RecordingCanvas rc = + RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + rc.save(); + rc.translate(50.0, 100.0); + rc.rotate(math.pi / 4.0); + rc.drawImage(createTestImage(), Offset(30, 20), new Paint()); + await _checkScreenshot(rc, 'draw_image_with_transform_and_offset'); + }); + + test('Paints image with transform using destination', () async { + final RecordingCanvas rc = + RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + rc.save(); + rc.translate(50.0, 100.0); + rc.rotate(math.pi / 4.0); + Image testImage = createTestImage(); + double testWidth = testImage.width.toDouble(); + double testHeight = testImage.height.toDouble(); + rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight), + Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint()); + await _checkScreenshot(rc, 'draw_image_rect_with_transform'); + }); + + test('Paints image with source and destination', () async { + final RecordingCanvas rc = + RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + rc.save(); + Image testImage = createTestImage(); + double testWidth = testImage.width.toDouble(); + double testHeight = testImage.height.toDouble(); + rc.drawImageRect(testImage, Rect.fromLTRB(testWidth / 2, 0, testWidth, testHeight), + Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint()); + await _checkScreenshot(rc, 'draw_image_rect_with_source'); + }); + + test('Paints image with transform using source and destination', () async { + final RecordingCanvas rc = + RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + rc.save(); + rc.translate(50.0, 100.0); + rc.rotate(math.pi / 6.0); + Image testImage = createTestImage(); + double testWidth = testImage.width.toDouble(); + double testHeight = testImage.height.toDouble(); + rc.drawImageRect(testImage, Rect.fromLTRB(testWidth / 2, 0, testWidth, testHeight), + Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint()); + await _checkScreenshot(rc, 'draw_image_rect_with_transform_source'); + }); +} + +HtmlImage createTestImage() { + const int width = 100; + const int height = 50; + html.CanvasElement canvas = new html.CanvasElement(width: width, height: height); + html.CanvasRenderingContext2D ctx = canvas.context2D; + ctx.fillStyle = '#E04040'; + ctx.fillRect(0, 0, 33, 50); + ctx.fill(); + ctx.fillStyle = '#40E080'; + ctx.fillRect(33, 0, 33, 50); + ctx.fill(); + ctx.fillStyle = '#2040E0'; + ctx.fillRect(66, 0, 33, 50); + ctx.fill(); + html.ImageElement imageElement = html.ImageElement(); + imageElement.src = js_util.callMethod(canvas, 'toDataURL', []); + return HtmlImage(imageElement, width, height); +} +