[web] Support gif/webp animations, Speed up image drawing in BitmapCanvas. (flutter/engine#13748)

* Add draw image test
* Optimize drawImageScaled
* optimize cloning in HtmlImage, implement drawImageRect using image tag
This commit is contained in:
Ferhat 2019-11-08 12:52:01 -08:00 committed by GitHub
parent a4a346f317
commit 1f2d6ce8f8
5 changed files with 234 additions and 23 deletions

View File

@ -1,2 +1,2 @@
repository: https://github.com/flutter/goldens.git
revision: 7935a97f89a6af5ae5182b2b5e59debda0189984
revision: 009fbdd595aeec364eaff6b8f337f8ceb3c44ab9

View File

@ -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<html.Element> 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<String> lines =
paragraph._lines ?? <String>[paragraph._plainText];

View File

@ -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<Uint8List> callback) => null;
}

View File

@ -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...

View File

@ -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<void> _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 <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('$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);
}