diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml
index 1f1b59c91e98d..f558ffe268551 100644
--- a/lib/web_ui/dev/goldens_lock.yaml
+++ b/lib/web_ui/dev/goldens_lock.yaml
@@ -1,2 +1,2 @@
repository: https://github.com/flutter/goldens.git
-revision: 1556280d6f1d70fac9ddff9b38639757e105b4b0
+revision: 67f22ef933be27ba2be8b27df1b71b2c69eb86e5
diff --git a/lib/web_ui/lib/src/engine/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/bitmap_canvas.dart
index b52588717183b..afe4a6e07844a 100644
--- a/lib/web_ui/lib/src/engine/bitmap_canvas.dart
+++ b/lib/web_ui/lib/src/engine/bitmap_canvas.dart
@@ -94,6 +94,16 @@ class BitmapCanvas extends EngineCanvas {
_childOverdraw = value;
}
+ /// Indicates bitmap canvas contains a 3d transform.
+ /// WebKit fails to preserve paint order when this happens and therefore
+ /// requires insertion of
to be
+ /// used for each child to force correct rendering order.
+ bool _contains3dTransform = false;
+
+ /// Indicates that contents should be rendered into canvas so a dataUrl
+ /// can be constructed from contents.
+ bool _preserveImageData = false;
+
/// Allocates a canvas with enough memory to paint a picture within the given
/// [bounds].
///
@@ -117,6 +127,13 @@ class BitmapCanvas extends EngineCanvas {
_setupInitialTransform();
}
+ /// Constructs bitmap canvas to capture image data.
+ factory BitmapCanvas.imageData(ui.Rect bounds) {
+ BitmapCanvas bitmapCanvas = BitmapCanvas(bounds);
+ bitmapCanvas._preserveImageData = true;
+ return bitmapCanvas;
+ }
+
/// Setup cache for reusing DOM elements across frames.
void setElementCache(CrossFrameCache
cache) {
_elementCache = cache;
@@ -139,8 +156,9 @@ class BitmapCanvas extends EngineCanvas {
final double canvasPositionCorrectionX = _bounds.left -
BitmapCanvas.kPaddingPixels -
_canvasPositionX!.toDouble();
- final double canvasPositionCorrectionY =
- _bounds.top - BitmapCanvas.kPaddingPixels - _canvasPositionY!.toDouble();
+ final double canvasPositionCorrectionY = _bounds.top -
+ BitmapCanvas.kPaddingPixels -
+ _canvasPositionY!.toDouble();
// This compensates for the translate on the `rootElement`.
_canvasPool.initialTransform = ui.Offset(
-_bounds.left + canvasPositionCorrectionX + BitmapCanvas.kPaddingPixels,
@@ -175,6 +193,7 @@ class BitmapCanvas extends EngineCanvas {
/// Prepare to reuse this canvas by clearing it's current contents.
@override
void clear() {
+ _contains3dTransform = false;
_canvasPool.clear();
final int len = _children.length;
for (int i = 0; i < len; i++) {
@@ -267,6 +286,10 @@ class BitmapCanvas extends EngineCanvas {
@override
void transform(Float32List matrix4) {
+ TransformKind transformKind = transformKindOf(matrix4);
+ if (transformKind == TransformKind.complex) {
+ _contains3dTransform = true;
+ }
_canvasPool.transform(matrix4);
}
@@ -295,37 +318,115 @@ class BitmapCanvas extends EngineCanvas {
_canvasPool.clipPath(path);
}
+ /// Whether drawing operation should use DOM node instead of Canvas.
+ ///
+ /// - Perspective transforms are not supported by canvas and require
+ /// DOM to render correctly.
+ /// - Pictures typically have large rect/rounded rectangles as background
+ /// prefer DOM if canvas has not been allocated yet.
+ bool _useDomForRendering(SurfacePaintData paint) =>
+ _preserveImageData == false && (
+ _contains3dTransform ||
+ (_canvasPool._canvas == null &&
+ paint.maskFilter == null &&
+ paint.shader == null &&
+ paint.style != ui.PaintingStyle.stroke));
+
@override
void drawColor(ui.Color color, ui.BlendMode blendMode) {
- _canvasPool.drawColor(color, blendMode);
+ final SurfacePaintData paintData = SurfacePaintData()
+ ..color = color
+ ..blendMode = blendMode;
+ if (_useDomForRendering(paintData)) {
+ drawRect(_computeScreenBounds(_canvasPool._currentTransform), paintData);
+ } else {
+ _canvasPool.drawColor(color, blendMode);
+ }
}
@override
void drawLine(ui.Offset p1, ui.Offset p2, SurfacePaintData paint) {
- _setUpPaint(paint);
- _canvasPool.strokeLine(p1, p2);
- _tearDownPaint();
+ if (_useDomForRendering(paint)) {
+ final SurfacePath path = SurfacePath()
+ ..moveTo(p1.dx, p1.dy)
+ ..lineTo(p2.dx, p2.dy);
+ drawPath(path, paint);
+ } else {
+ _setUpPaint(paint);
+ _canvasPool.strokeLine(p1, p2);
+ _tearDownPaint();
+ }
}
@override
void drawPaint(SurfacePaintData paint) {
- _setUpPaint(paint);
- _canvasPool.fill();
- _tearDownPaint();
+ if (_useDomForRendering(paint)) {
+ drawRect(_computeScreenBounds(_canvasPool._currentTransform), paint);
+ } else {
+ _setUpPaint(paint);
+ _canvasPool.fill();
+ _tearDownPaint();
+ }
}
@override
void drawRect(ui.Rect rect, SurfacePaintData paint) {
- _setUpPaint(paint);
- _canvasPool.drawRect(rect, paint.style);
- _tearDownPaint();
+ if (_useDomForRendering(paint)) {
+ html.HtmlElement element = _buildDrawRectElement(
+ rect, paint, 'draw-rect', _canvasPool._currentTransform);
+ _drawElement(
+ element,
+ ui.Offset(
+ math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
+ paint);
+ } else {
+ _setUpPaint(paint);
+ _canvasPool.drawRect(rect, paint.style);
+ _tearDownPaint();
+ }
+ }
+
+ /// Inserts a dom element at [offset] creating stack of divs for clipping
+ /// if required.
+ void _drawElement(
+ html.Element element, ui.Offset offset, SurfacePaintData paint) {
+ if (_canvasPool.isClipped) {
+ final List clipElements = _clipContent(
+ _canvasPool._clipStack!,
+ element,
+ ui.Offset.zero,
+ transformWithOffset(_canvasPool._currentTransform, offset));
+ for (html.Element clipElement in clipElements) {
+ rootElement.append(clipElement);
+ _children.add(clipElement);
+ }
+ } else {
+ rootElement.append(element);
+ _children.add(element);
+ }
+ ui.BlendMode? blendMode = paint.blendMode;
+ if (blendMode != null) {
+ element.style.mixBlendMode = _stringForBlendMode(blendMode) ?? '';
+ }
}
@override
void drawRRect(ui.RRect rrect, SurfacePaintData paint) {
- _setUpPaint(paint);
- _canvasPool.drawRRect(rrect, paint.style);
- _tearDownPaint();
+ final ui.Rect rect = rrect.outerRect;
+ if (_useDomForRendering(paint)) {
+ html.HtmlElement element = _buildDrawRectElement(
+ rect, paint, 'draw-rrect', _canvasPool._currentTransform);
+ _applyRRectBorderRadius(element.style, rrect);
+ _drawElement(
+ element,
+ ui.Offset(
+ math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
+ paint);
+ } else {
+ _setUpPaint(paint);
+ _canvasPool.drawRRect(rrect, paint.style);
+ _tearDownPaint();
+ }
}
@override
@@ -337,23 +438,62 @@ class BitmapCanvas extends EngineCanvas {
@override
void drawOval(ui.Rect rect, SurfacePaintData paint) {
- _setUpPaint(paint);
- _canvasPool.drawOval(rect, paint.style);
- _tearDownPaint();
+ if (_useDomForRendering(paint)) {
+ html.HtmlElement element = _buildDrawRectElement(
+ rect, paint, 'draw-oval', _canvasPool._currentTransform);
+ _drawElement(
+ element,
+ ui.Offset(
+ math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
+ paint);
+ element.style.borderRadius =
+ '${(rect.width / 2.0)}px / ${(rect.height / 2.0)}px';
+ } else {
+ _setUpPaint(paint);
+ _canvasPool.drawOval(rect, paint.style);
+ _tearDownPaint();
+ }
}
@override
void drawCircle(ui.Offset c, double radius, SurfacePaintData paint) {
- _setUpPaint(paint);
- _canvasPool.drawCircle(c, radius, paint.style);
- _tearDownPaint();
+ ui.Rect rect = ui.Rect.fromCircle(center: c, radius: radius);
+ if (_useDomForRendering(paint)) {
+ html.HtmlElement element = _buildDrawRectElement(
+ rect, paint, 'draw-circle', _canvasPool._currentTransform);
+ _drawElement(
+ element,
+ ui.Offset(
+ math.min(rect.left, rect.right), math.min(rect.top, rect.bottom)),
+ paint);
+ element.style.borderRadius = '50%';
+ } else {
+ _setUpPaint(paint);
+ _canvasPool.drawCircle(c, radius, paint.style);
+ _tearDownPaint();
+ }
}
@override
void drawPath(ui.Path path, SurfacePaintData paint) {
- _setUpPaint(paint);
- _canvasPool.drawPath(path, paint.style);
- _tearDownPaint();
+ if (_useDomForRendering(paint)) {
+ final Matrix4 transform = _canvasPool._currentTransform;
+ final SurfacePath surfacePath = path as SurfacePath;
+ final ui.Rect pathBounds = surfacePath.getBounds();
+ html.Element svgElm = _pathToSvgElement(
+ surfacePath, paint, '${pathBounds.right}', '${pathBounds.bottom}');
+ if (!_canvasPool.isClipped) {
+ svgElm.style
+ ..transform = matrix4ToCssTransform(transform)
+ ..transformOrigin = '0 0 0'
+ ..position = 'absolute';
+ }
+ _drawElement(svgElm, ui.Offset(0, 0), paint);
+ } else {
+ _setUpPaint(paint);
+ _canvasPool.drawPath(path, paint.style);
+ _tearDownPaint();
+ }
}
@override
@@ -366,8 +506,8 @@ class BitmapCanvas extends EngineCanvas {
void drawImage(ui.Image image, ui.Offset p, SurfacePaintData paint) {
final html.HtmlElement imageElement = _drawImage(image, p, paint);
if (paint.colorFilter != null) {
- _applyTargetSize(imageElement, image.width.toDouble(),
- image.height.toDouble());
+ _applyTargetSize(
+ imageElement, image.width.toDouble(), image.height.toDouble());
}
_childOverdraw = true;
_canvasPool.closeCurrentCanvas();
@@ -377,7 +517,8 @@ class BitmapCanvas extends EngineCanvas {
html.ImageElement _reuseOrCreateImage(HtmlImage htmlImage) {
final String cacheKey = htmlImage.imgElement.src!;
if (_elementCache != null) {
- html.ImageElement? imageElement = _elementCache!.reuse(cacheKey) as html.ImageElement?;
+ html.ImageElement? imageElement =
+ _elementCache!.reuse(cacheKey) as html.ImageElement?;
if (imageElement != null) {
return imageElement;
}
@@ -398,7 +539,8 @@ class BitmapCanvas extends EngineCanvas {
ui.Image image, ui.Offset p, SurfacePaintData paint) {
final HtmlImage htmlImage = image as HtmlImage;
final ui.BlendMode? blendMode = paint.blendMode;
- final EngineColorFilter? colorFilter = paint.colorFilter as EngineColorFilter?;
+ final EngineColorFilter? colorFilter =
+ paint.colorFilter as EngineColorFilter?;
final ui.BlendMode? colorFilterBlendMode = colorFilter?._blendMode;
html.HtmlElement imgElement;
if (colorFilterBlendMode == null) {
@@ -419,21 +561,19 @@ class BitmapCanvas extends EngineCanvas {
case ui.BlendMode.color:
case ui.BlendMode.luminosity:
case ui.BlendMode.xor:
- imgElement = _createImageElementWithSvgFilter(image,
- colorFilter!._color, colorFilterBlendMode, paint);
+ imgElement = _createImageElementWithSvgFilter(
+ image, colorFilter!._color, colorFilterBlendMode, paint);
break;
default:
- imgElement = _createBackgroundImageWithBlend(image,
- colorFilter!._color, colorFilterBlendMode, paint);
+ imgElement = _createBackgroundImageWithBlend(
+ image, colorFilter!._color, colorFilterBlendMode, paint);
break;
}
}
imgElement.style.mixBlendMode = _stringForBlendMode(blendMode) ?? '';
if (_canvasPool.isClipped) {
// Reset width/height since they may have been previously set.
- imgElement.style
- ..removeProperty('width')
- ..removeProperty('height');
+ imgElement.style..removeProperty('width')..removeProperty('height');
final List clipElements = _clipContent(
_canvasPool._clipStack!, imgElement, p, _canvasPool.currentTransform);
for (html.Element clipElement in clipElements) {
@@ -503,7 +643,8 @@ class BitmapCanvas extends EngineCanvas {
targetWidth *= image.width / src.width;
targetHeight *= image.height / src.height;
}
- _applyTargetSize(imgElement as html.HtmlElement, targetWidth, targetHeight);
+ _applyTargetSize(
+ imgElement as html.HtmlElement, targetWidth, targetHeight);
if (requiresClipping) {
restore();
}
@@ -511,8 +652,8 @@ class BitmapCanvas extends EngineCanvas {
_closeCurrentCanvas();
}
- void _applyTargetSize(html.HtmlElement imageElement, double targetWidth,
- double targetHeight) {
+ void _applyTargetSize(
+ html.HtmlElement imageElement, double targetWidth, double targetHeight) {
final html.CssStyleDeclaration imageStyle = imageElement.style;
final String widthPx = '${targetWidth.toStringAsFixed(2)}px';
final String heightPx = '${targetHeight.toStringAsFixed(2)}px';
@@ -542,8 +683,10 @@ class BitmapCanvas extends EngineCanvas {
// For clear,dstOut it generates a blank element.
// For src,srcOver it only sets background-color attribute.
// For dst,dstIn , it only sets source not background color.
- html.HtmlElement _createBackgroundImageWithBlend(HtmlImage image,
- ui.Color? filterColor, ui.BlendMode colorFilterBlendMode,
+ html.HtmlElement _createBackgroundImageWithBlend(
+ HtmlImage image,
+ ui.Color? filterColor,
+ ui.BlendMode colorFilterBlendMode,
SurfacePaintData paint) {
// When blending with color we can't use an image element.
// Instead use a div element with background image, color and
@@ -558,8 +701,8 @@ class BitmapCanvas extends EngineCanvas {
case ui.BlendMode.src:
case ui.BlendMode.srcOver:
style
- ..position = 'absolute'
- ..backgroundColor = colorToCssString(filterColor);
+ ..position = 'absolute'
+ ..backgroundColor = colorToCssString(filterColor);
break;
case ui.BlendMode.dst:
case ui.BlendMode.dstIn:
@@ -571,7 +714,8 @@ class BitmapCanvas extends EngineCanvas {
style
..position = 'absolute'
..backgroundImage = "url('${image.imgElement.src}')"
- ..backgroundBlendMode = _stringForBlendMode(colorFilterBlendMode) ?? ''
+ ..backgroundBlendMode =
+ _stringForBlendMode(colorFilterBlendMode) ?? ''
..backgroundColor = colorToCssString(filterColor);
break;
}
@@ -579,12 +723,14 @@ class BitmapCanvas extends EngineCanvas {
}
// Creates an image element and an svg filter to apply on the element.
- html.HtmlElement _createImageElementWithSvgFilter(HtmlImage image,
- ui.Color? filterColor, ui.BlendMode colorFilterBlendMode,
+ html.HtmlElement _createImageElementWithSvgFilter(
+ HtmlImage image,
+ ui.Color? filterColor,
+ ui.BlendMode colorFilterBlendMode,
SurfacePaintData paint) {
// For srcIn blendMode, we use an svg filter to apply to image element.
- String? svgFilter = svgFilterFromBlendMode(filterColor,
- colorFilterBlendMode);
+ String? svgFilter =
+ svgFilterFromBlendMode(filterColor, colorFilterBlendMode);
final html.Element filterElement =
html.Element.html(svgFilter, treeSanitizer: _NullTreeSanitizer());
rootElement.append(filterElement);
@@ -651,9 +797,11 @@ class BitmapCanvas extends EngineCanvas {
if (paragraph._drawOnCanvas && _childOverdraw == false) {
// !Do not move this assignment above this if clause since, accessing
// context will generate extra