Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 7413304

Browse files
authored
[web] Support gif/webp animations, Speed up image drawing in BitmapCanvas. (#13748)
* Add draw image test * Optimize drawImageScaled * optimize cloning in HtmlImage, implement drawImageRect using image tag
1 parent 618e666 commit 7413304

File tree

5 files changed

+234
-23
lines changed

5 files changed

+234
-23
lines changed

lib/web_ui/dev/goldens_lock.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
repository: https://github.com/flutter/goldens.git
2-
revision: 7935a97f89a6af5ae5182b2b5e59debda0189984
2+
revision: 009fbdd595aeec364eaff6b8f337f8ceb3c44ab9

lib/web_ui/lib/src/engine/bitmap_canvas.dart

Lines changed: 81 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,17 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking {
7272
Object _prevFillStyle;
7373
Object _prevStrokeStyle;
7474

75+
// Indicates the instructions following drawImage or drawParagraph that
76+
// a child element was created to paint.
77+
// TODO(flutter_web): When childElements are created by
78+
// drawImage/drawParagraph commands, compositing order is not correctly
79+
// handled when we interleave these with other paint commands.
80+
// To solve this, recording canvas will have to check the paint queue
81+
// and send a hint to EngineCanvas that additional canvas layers need
82+
// to be used to composite correctly. In practice this is very rare
83+
// with Widgets but CustomPainter(s) can hit this code path.
84+
bool _childOverdraw = false;
85+
7586
/// Allocates a canvas with enough memory to paint a picture within the given
7687
/// [bounds].
7788
///
@@ -568,30 +579,81 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking {
568579
void drawImage(ui.Image image, ui.Offset p, ui.PaintData paint) {
569580
_applyPaint(paint);
570581
final HtmlImage htmlImage = image;
571-
final html.Element imgElement = htmlImage.imgElement.clone(true);
572-
imgElement.style
573-
..position = 'absolute'
574-
..transform = 'translate(${p.dx}px, ${p.dy}px)';
575-
rootElement.append(imgElement);
582+
final html.Element imgElement = htmlImage.cloneImageElement();
583+
_drawImage(imgElement, p);
584+
_childOverdraw = true;
585+
}
586+
587+
void _drawImage(html.ImageElement imgElement, ui.Offset p) {
588+
if (isClipped) {
589+
final List<html.Element> clipElements =
590+
_clipContent(_clipStack, imgElement, p, currentTransform);
591+
for (html.Element clipElement in clipElements) {
592+
rootElement.append(clipElement);
593+
_children.add(clipElement);
594+
}
595+
} else {
596+
final String cssTransform =
597+
matrix4ToCssTransform(transformWithOffset(currentTransform, p));
598+
imgElement.style
599+
..transformOrigin = '0 0 0'
600+
..transform = cssTransform;
601+
rootElement.append(imgElement);
602+
_children.add(imgElement);
603+
}
576604
}
577605

578606
@override
579607
void drawImageRect(
580608
ui.Image image, ui.Rect src, ui.Rect dst, ui.PaintData paint) {
581-
// TODO(het): Check if the src rect is the entire image, and if so just
582-
// append the imgElement and set it's height and width.
583609
final HtmlImage htmlImage = image;
584-
ctx.drawImageScaledFromSource(
585-
htmlImage.imgElement,
586-
src.left,
587-
src.top,
588-
src.width,
589-
src.height,
590-
dst.left,
591-
dst.top,
592-
dst.width,
593-
dst.height,
594-
);
610+
final bool requiresClipping = src.left != 0 ||
611+
src.top != 0 ||
612+
src.width != image.width ||
613+
src.height != image.height;
614+
if (dst.width == image.width &&
615+
dst.height == image.height &&
616+
!requiresClipping) {
617+
drawImage(image, dst.topLeft, paint);
618+
} else {
619+
_applyPaint(paint);
620+
final html.Element imgElement = htmlImage.cloneImageElement();
621+
if (requiresClipping) {
622+
save();
623+
clipRect(dst);
624+
}
625+
double targetLeft = dst.left;
626+
double targetTop = dst.top;
627+
if (requiresClipping) {
628+
if (src.width != image.width) {
629+
double leftMargin = -src.left * (dst.width / src.width);
630+
targetLeft += leftMargin;
631+
}
632+
if (src.height != image.height) {
633+
double topMargin = -src.top * (dst.height / src.height);
634+
targetTop += topMargin;
635+
}
636+
}
637+
_drawImage(imgElement, ui.Offset(targetLeft, targetTop));
638+
// To scale set width / height on destination image.
639+
// For clipping we need to scale according to
640+
// clipped-width/full image width and shift it according to left/top of
641+
// source rectangle.
642+
double targetWidth = dst.width;
643+
double targetHeight = dst.height;
644+
if (requiresClipping) {
645+
targetWidth *= image.width / src.width;
646+
targetHeight *= image.height / src.height;
647+
}
648+
final html.CssStyleDeclaration imageStyle = imgElement.style;
649+
imageStyle
650+
..width = '${targetWidth.toStringAsFixed(2)}px'
651+
..height = '${targetHeight.toStringAsFixed(2)}px';
652+
if (requiresClipping) {
653+
restore();
654+
}
655+
}
656+
_childOverdraw = true;
595657
}
596658

597659
void _drawTextLine(
@@ -625,7 +687,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking {
625687

626688
final ParagraphGeometricStyle style = paragraph._geometricStyle;
627689

628-
if (paragraph._drawOnCanvas) {
690+
if (paragraph._drawOnCanvas && _childOverdraw == false) {
629691
final List<String> lines =
630692
paragraph._lines ?? <String>[paragraph._plainText];
631693

lib/web_ui/lib/src/engine/html_image_codec.dart

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ class SingleFrameInfo implements ui.FrameInfo {
9292

9393
class HtmlImage implements ui.Image {
9494
final html.ImageElement imgElement;
95-
95+
bool _requiresClone = false;
9696
HtmlImage(this.imgElement, this.width, this.height);
9797

9898
@override
@@ -117,6 +117,18 @@ class HtmlImage implements ui.Image {
117117
});
118118
}
119119

120+
// Returns absolutely positioned actual image element on first call and
121+
// clones on subsequent calls.
122+
html.ImageElement cloneImageElement() {
123+
if (_requiresClone) {
124+
return imgElement.clone(true);
125+
} else {
126+
_requiresClone = true;
127+
imgElement.style..position = 'absolute';
128+
return imgElement;
129+
}
130+
}
131+
120132
/// Returns an error message on failure, null on success.
121133
String _toByteData(int format, Callback<Uint8List> callback) => null;
122134
}

lib/web_ui/lib/src/engine/recording_canvas.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@ class RecordingCanvas {
7373
print(debugBuf);
7474
} else {
7575
try {
76-
for (int i = 0; i < _commands.length; i++) {
77-
_commands[i].apply(engineCanvas);
76+
for (int i = 0, len = _commands.length; i < len; i++) {
77+
PaintCommand command = _commands[i];
78+
command.apply(engineCanvas);
7879
}
7980
} catch (e) {
8081
// commands should never fail, but...
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:html' as html;
6+
import 'dart:math' as math;
7+
import 'dart:js_util' as js_util;
8+
9+
import 'package:ui/ui.dart' hide TextStyle;
10+
import 'package:ui/src/engine.dart';
11+
import 'package:test/test.dart';
12+
13+
import 'package:web_engine_tester/golden_tester.dart';
14+
15+
void main() async {
16+
const double screenWidth = 600.0;
17+
const double screenHeight = 800.0;
18+
const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight);
19+
final Paint testPaint = Paint()..color = const Color(0xFFFF0000);
20+
21+
// Commit a recording canvas to a bitmap, and compare with the expected
22+
Future<void> _checkScreenshot(RecordingCanvas rc, String fileName,
23+
{ Rect region = const Rect.fromLTWH(0, 0, 500, 500) }) async {
24+
25+
final EngineCanvas engineCanvas = BitmapCanvas(screenRect);
26+
27+
rc.apply(engineCanvas);
28+
29+
// Wrap in <flt-scene> so that our CSS selectors kick in.
30+
final html.Element sceneElement = html.Element.tag('flt-scene');
31+
try {
32+
sceneElement.append(engineCanvas.rootElement);
33+
html.document.body.append(sceneElement);
34+
await matchGoldenFile('$fileName.png', region: region, maxDiffRate: 0.02);
35+
} finally {
36+
// The page is reused across tests, so remove the element after taking the
37+
// Scuba screenshot.
38+
sceneElement.remove();
39+
}
40+
}
41+
42+
setUp(() async {
43+
debugEmulateFlutterTesterEnvironment = true;
44+
await webOnlyInitializePlatform();
45+
webOnlyFontCollection.debugRegisterTestFonts();
46+
await webOnlyFontCollection.ensureFontsLoaded();
47+
});
48+
49+
test('Paints image', () async {
50+
final RecordingCanvas rc =
51+
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
52+
rc.save();
53+
rc.drawImage(createTestImage(), Offset(0, 0), new Paint());
54+
await _checkScreenshot(rc, 'draw_image');
55+
});
56+
57+
test('Paints image with transform', () async {
58+
final RecordingCanvas rc =
59+
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
60+
rc.save();
61+
rc.translate(50.0, 100.0);
62+
rc.rotate(math.pi / 4.0);
63+
rc.drawImage(createTestImage(), Offset(0, 0), new Paint());
64+
await _checkScreenshot(rc, 'draw_image_with_transform');
65+
});
66+
67+
test('Paints image with transform and offset', () async {
68+
final RecordingCanvas rc =
69+
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
70+
rc.save();
71+
rc.translate(50.0, 100.0);
72+
rc.rotate(math.pi / 4.0);
73+
rc.drawImage(createTestImage(), Offset(30, 20), new Paint());
74+
await _checkScreenshot(rc, 'draw_image_with_transform_and_offset');
75+
});
76+
77+
test('Paints image with transform using destination', () async {
78+
final RecordingCanvas rc =
79+
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
80+
rc.save();
81+
rc.translate(50.0, 100.0);
82+
rc.rotate(math.pi / 4.0);
83+
Image testImage = createTestImage();
84+
double testWidth = testImage.width.toDouble();
85+
double testHeight = testImage.height.toDouble();
86+
rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight),
87+
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint());
88+
await _checkScreenshot(rc, 'draw_image_rect_with_transform');
89+
});
90+
91+
test('Paints image with source and destination', () async {
92+
final RecordingCanvas rc =
93+
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
94+
rc.save();
95+
Image testImage = createTestImage();
96+
double testWidth = testImage.width.toDouble();
97+
double testHeight = testImage.height.toDouble();
98+
rc.drawImageRect(testImage, Rect.fromLTRB(testWidth / 2, 0, testWidth, testHeight),
99+
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint());
100+
await _checkScreenshot(rc, 'draw_image_rect_with_source');
101+
});
102+
103+
test('Paints image with transform using source and destination', () async {
104+
final RecordingCanvas rc =
105+
RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300));
106+
rc.save();
107+
rc.translate(50.0, 100.0);
108+
rc.rotate(math.pi / 6.0);
109+
Image testImage = createTestImage();
110+
double testWidth = testImage.width.toDouble();
111+
double testHeight = testImage.height.toDouble();
112+
rc.drawImageRect(testImage, Rect.fromLTRB(testWidth / 2, 0, testWidth, testHeight),
113+
Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint());
114+
await _checkScreenshot(rc, 'draw_image_rect_with_transform_source');
115+
});
116+
}
117+
118+
HtmlImage createTestImage() {
119+
const int width = 100;
120+
const int height = 50;
121+
html.CanvasElement canvas = new html.CanvasElement(width: width, height: height);
122+
html.CanvasRenderingContext2D ctx = canvas.context2D;
123+
ctx.fillStyle = '#E04040';
124+
ctx.fillRect(0, 0, 33, 50);
125+
ctx.fill();
126+
ctx.fillStyle = '#40E080';
127+
ctx.fillRect(33, 0, 33, 50);
128+
ctx.fill();
129+
ctx.fillStyle = '#2040E0';
130+
ctx.fillRect(66, 0, 33, 50);
131+
ctx.fill();
132+
html.ImageElement imageElement = html.ImageElement();
133+
imageElement.src = js_util.callMethod(canvas, 'toDataURL', []);
134+
return HtmlImage(imageElement, width, height);
135+
}
136+

0 commit comments

Comments
 (0)