mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[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:
parent
a4a346f317
commit
1f2d6ce8f8
@ -1,2 +1,2 @@
|
||||
repository: https://github.com/flutter/goldens.git
|
||||
revision: 7935a97f89a6af5ae5182b2b5e59debda0189984
|
||||
revision: 009fbdd595aeec364eaff6b8f337f8ceb3c44ab9
|
||||
|
||||
@ -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];
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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...
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user