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 tags. - final List lines = paragraph._measurementResult!.lines!; + final List lines = + paragraph._measurementResult!.lines!; - final SurfacePaintData? backgroundPaint = paragraph._background?.paintData; + final SurfacePaintData? backgroundPaint = + paragraph._background?.paintData; if (backgroundPaint != null) { final ui.Rect rect = ui.Rect.fromLTWH( offset.dx, offset.dy, paragraph.width, paragraph.height); @@ -723,8 +871,8 @@ class BitmapCanvas extends EngineCanvas { /// If colors is specified, convert colors to premultiplied (alpha) colors /// and use a SkTriColorShader to render. @override - void drawVertices( - SurfaceVertices vertices, ui.BlendMode blendMode, SurfacePaintData paint) { + void drawVertices(SurfaceVertices vertices, ui.BlendMode blendMode, + SurfacePaintData paint) { // TODO(flutter_web): Implement shaders for [Paint.shader] and // blendMode. https://github.com/flutter/flutter/issues/40096 // Move rendering to OffscreenCanvas so that transform is preserved @@ -761,7 +909,8 @@ class BitmapCanvas extends EngineCanvas { ..blendMode = ui.BlendMode.srcOver; @override - void drawPoints(ui.PointMode pointMode, Float32List points, SurfacePaintData paint) { + void drawPoints( + ui.PointMode pointMode, Float32List points, SurfacePaintData paint) { if (pointMode == ui.PointMode.points) { _drawPointsPaint.style = ui.PaintingStyle.stroke; } else { @@ -780,6 +929,45 @@ class BitmapCanvas extends EngineCanvas { void endOfPaint() { _canvasPool.endOfPaint(); _elementCache?.commitFrame(); + // Wrap all elements in translate3d (workaround for webkit paint order bug). + if (_contains3dTransform && browserEngine == BrowserEngine.webkit) { + for (html.Element element in rootElement.children) { + html.DivElement paintOrderElement = html.DivElement() + ..style.transform = 'translate3d(0,0,0)'; + paintOrderElement.append(element); + rootElement.append(paintOrderElement); + _children.add(paintOrderElement); + } + } + if (rootElement.firstChild is html.HtmlElement && + (rootElement.firstChild as html.HtmlElement).tagName.toLowerCase() == + 'canvas') { + (rootElement.firstChild as html.HtmlElement).style.zIndex = '-1'; + } + } + + /// Computes paint bounds given [targetTransform] to completely cover window + /// viewport. + ui.Rect _computeScreenBounds(Matrix4 targetTransform) { + final Matrix4 inverted = targetTransform.clone()..invert(); + final double dpr = ui.window.devicePixelRatio; + final double width = ui.window.physicalSize.width * dpr; + final double height = ui.window.physicalSize.height * dpr; + Vector3 topLeft = inverted.perspectiveTransform(Vector3(0, 0, 0)); + Vector3 topRight = inverted.perspectiveTransform(Vector3(width, 0, 0)); + Vector3 bottomRight = + inverted.perspectiveTransform(Vector3(width, height, 0)); + Vector3 bottomLeft = inverted.perspectiveTransform(Vector3(0, height, 0)); + return ui.Rect.fromLTRB( + math.min(topLeft.x, + math.min(topRight.x, math.min(bottomRight.x, bottomLeft.x))), + math.min(topLeft.y, + math.min(topRight.y, math.min(bottomRight.y, bottomLeft.y))), + math.max(topLeft.x, + math.max(topRight.x, math.max(bottomRight.x, bottomLeft.x))), + math.max(topLeft.y, + math.max(topRight.y, math.max(bottomRight.y, bottomLeft.y))), + ); } } @@ -885,7 +1073,7 @@ String _stringForStrokeJoin(ui.StrokeJoin strokeJoin) { /// it's contents. The clipping rectangles are nested and returned together /// with a list of svg elements that provide clip-paths. List _clipContent(List<_SaveClipEntry> clipStack, - html.HtmlElement content, ui.Offset offset, Matrix4 currentTransform) { + html.Element content, ui.Offset offset, Matrix4 currentTransform) { html.Element? root, curElement; final List clipDefs = []; final int len = clipStack.length; @@ -902,6 +1090,9 @@ List _clipContent(List<_SaveClipEntry> clipStack, curElement = newElement; final ui.Rect? rect = entry.rect; Matrix4 newClipTransform = entry.currentTransform; + final TransformKind transformKind = + transformKindOf(newClipTransform.storage); + bool requiresTransformStyle = transformKind == TransformKind.complex; if (rect != null) { final double clipOffsetX = rect.left; final double clipOffsetY = rect.top; @@ -931,7 +1122,8 @@ List _clipContent(List<_SaveClipEntry> clipStack, curElement.style ..transform = matrix4ToCssTransform(newClipTransform) ..transformOrigin = '0 0 0'; - String svgClipPath = createSvgClipDef(curElement as html.HtmlElement, entry.path!); + String svgClipPath = + createSvgClipDef(curElement as html.HtmlElement, entry.path!); final html.Element clipElement = html.Element.html(svgClipPath, treeSanitizer: _NullTreeSanitizer()); clipDefs.add(clipElement); @@ -946,6 +1138,11 @@ List _clipContent(List<_SaveClipEntry> clipStack, reverseTransformDiv, (newClipTransform.clone()..invert()).storage, ); + if (requiresTransformStyle) { + // Instead of flattening matrix3d, preserve so it can be reversed. + curElement.style.transformStyle = 'preserve-3d'; + reverseTransformDiv.style.transformStyle = 'preserve-3d'; + } curElement.append(reverseTransformDiv); curElement = reverseTransformDiv; } @@ -975,4 +1172,3 @@ String _maskFilterToCanvasFilter(ui.MaskFilter? maskFilter) { return 'none'; } } - diff --git a/lib/web_ui/lib/src/engine/canvas_pool.dart b/lib/web_ui/lib/src/engine/canvas_pool.dart index 9a297398605c5..a645fc2aaaa55 100644 --- a/lib/web_ui/lib/src/engine/canvas_pool.dart +++ b/lib/web_ui/lib/src/engine/canvas_pool.dart @@ -33,8 +33,6 @@ class _CanvasPool extends _SaveStackTracking { html.HtmlElement? _rootElement; int _saveContextCount = 0; - // Number of elements that have been added to flt-canvas. - int _activeElementCount = 0; _CanvasPool(this._widthInBitmapPixels, this._heightInBitmapPixels); @@ -76,7 +74,6 @@ class _CanvasPool extends _SaveStackTracking { _context = null; _contextHandle = null; } - _activeElementCount++; } void allocateCanvas(html.HtmlElement rootElement) { @@ -134,15 +131,12 @@ class _CanvasPool extends _SaveStackTracking { _rootElement!.append(canvas); } - if (_activeElementCount == 0) { - canvas.style.zIndex = '-1'; - } else if (reused) { - // If a canvas is the first element we set z-index = -1 to workaround - // blink compositing bug. To make sure this does not leak when reused - // reset z-index. + if (reused) { + // If a canvas is the first element we set z-index = -1 in [BitmapCanvas] + // endOfPaint to workaround blink compositing bug. To make sure this + // does not leak when reused reset z-index. canvas.style.removeProperty('z-index'); } - ++_activeElementCount; final html.CanvasRenderingContext2D context = _context = canvas.context2D; _contextHandle = ContextStateHandle(this, context); @@ -270,7 +264,6 @@ class _CanvasPool extends _SaveStackTracking { _canvas = null; _context = null; _contextHandle = null; - _activeElementCount = 0; } void endOfPaint() { @@ -326,7 +319,7 @@ class _CanvasPool extends _SaveStackTracking { // Returns a "data://" URI containing a representation of the image in this // canvas in PNG format. - String toDataUrl() => _canvas!.toDataUrl(); + String toDataUrl() => _canvas?.toDataUrl() ?? ''; @override void save() { diff --git a/lib/web_ui/lib/src/engine/dom_canvas.dart b/lib/web_ui/lib/src/engine/dom_canvas.dart index 11668eb2472fc..c6d32a02e8f70 100644 --- a/lib/web_ui/lib/src/engine/dom_canvas.dart +++ b/lib/web_ui/lib/src/engine/dom_canvas.dart @@ -68,75 +68,16 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking { @override void drawRect(ui.Rect rect, SurfacePaintData paint) { - _drawRect(rect, paint, 'draw-rect'); - } - - html.Element _drawRect(ui.Rect rect, SurfacePaintData paint, String tagName) { - assert(paint.shader == null); - final html.Element rectangle = html.Element.tag(tagName); - assert(() { - rectangle.setAttribute('flt-rect', '$rect'); - rectangle.setAttribute('flt-paint', '$paint'); - return true; - }()); - String effectiveTransform; - final bool isStroke = paint.style == ui.PaintingStyle.stroke; - final double strokeWidth = paint.strokeWidth ?? 0.0; - final double left = math.min(rect.left, rect.right); - final double right = math.max(rect.left, rect.right); - final double top = math.min(rect.top, rect.bottom); - final double bottom = math.max(rect.top, rect.bottom); - if (currentTransform.isIdentity()) { - if (isStroke) { - effectiveTransform = - 'translate(${left - (strokeWidth / 2.0)}px, ${top - (strokeWidth / 2.0)}px)'; - } else { - effectiveTransform = 'translate(${left}px, ${top}px)'; - } - } else { - // Clone to avoid mutating _transform. - final Matrix4 translated = currentTransform.clone(); - if (isStroke) { - translated.translate( - left - (strokeWidth / 2.0), top - (strokeWidth / 2.0)); - } else { - translated.translate(left, top); - } - effectiveTransform = matrix4ToCssTransform(translated); - } - final html.CssStyleDeclaration style = rectangle.style; - style - ..position = 'absolute' - ..transformOrigin = '0 0 0' - ..transform = effectiveTransform; - - final String cssColor = - paint.color == null ? '#000000' : colorToCssString(paint.color)!; - - if (paint.maskFilter != null) { - style.filter = 'blur(${paint.maskFilter!.webOnlySigma}px)'; - } - - if (isStroke) { - style - ..width = '${right - left - strokeWidth}px' - ..height = '${bottom - top - strokeWidth}px' - ..border = '${strokeWidth}px solid $cssColor'; - } else { - style - ..width = '${right - left}px' - ..height = '${bottom - top}px' - ..backgroundColor = cssColor; - } - - currentElement.append(rectangle); - return rectangle; + currentElement.append(_buildDrawRectElement(rect, paint, 'draw-rect', + currentTransform)); } @override void drawRRect(ui.RRect rrect, SurfacePaintData paint) { - html.Element element = _drawRect(rrect.outerRect, paint, 'draw-rrect'); - element.style.borderRadius = '${rrect.blRadiusX.toStringAsFixed(3)}px'; + html.Element element = _buildDrawRectElement(rrect.outerRect, + paint, 'draw-rrect', currentTransform); + _applyRRectBorderRadius(element.style, rrect); + currentElement.append(element); } @override @@ -199,3 +140,108 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking { // No reuse of elements yet to handle here. Noop. } } + +html.HtmlElement _buildDrawRectElement(ui.Rect rect, SurfacePaintData paint, String tagName, + Matrix4 transform) { + assert(paint.shader == null); + final html.HtmlElement rectangle = html.Element.tag(tagName) as html.HtmlElement; + assert(() { + rectangle.setAttribute('flt-rect', '$rect'); + rectangle.setAttribute('flt-paint', '$paint'); + return true; + }()); + String effectiveTransform; + final bool isStroke = paint.style == ui.PaintingStyle.stroke; + final double strokeWidth = paint.strokeWidth ?? 0.0; + final double left = math.min(rect.left, rect.right); + final double right = math.max(rect.left, rect.right); + final double top = math.min(rect.top, rect.bottom); + final double bottom = math.max(rect.top, rect.bottom); + if (transform.isIdentity()) { + if (isStroke) { + effectiveTransform = + 'translate(${left - (strokeWidth / 2.0)}px, ${top - (strokeWidth / 2.0)}px)'; + } else { + effectiveTransform = 'translate(${left}px, ${top}px)'; + } + } else { + // Clone to avoid mutating _transform. + final Matrix4 translated = transform.clone(); + if (isStroke) { + translated.translate( + left - (strokeWidth / 2.0), top - (strokeWidth / 2.0)); + } else { + translated.translate(left, top); + } + effectiveTransform = matrix4ToCssTransform(translated); + } + final html.CssStyleDeclaration style = rectangle.style; + style + ..position = 'absolute' + ..transformOrigin = '0 0 0' + ..transform = effectiveTransform; + + final String cssColor = + paint.color == null ? '#000000' : colorToCssString(paint.color)!; + + if (paint.maskFilter != null) { + style.filter = 'blur(${paint.maskFilter!.webOnlySigma}px)'; + } + + if (isStroke) { + style + ..width = '${right - left - strokeWidth}px' + ..height = '${bottom - top - strokeWidth}px' + ..border = '${strokeWidth}px solid $cssColor'; + } else { + style + ..width = '${right - left}px' + ..height = '${bottom - top}px' + ..backgroundColor = cssColor; + } + return rectangle; +} + +void _applyRRectBorderRadius(html.CssStyleDeclaration style, ui.RRect rrect) { + if (rrect.tlRadiusX == rrect.trRadiusX && + rrect.tlRadiusX == rrect.blRadiusX && + rrect.tlRadiusX == rrect.brRadiusX && + rrect.tlRadiusX == rrect.tlRadiusY && + rrect.trRadiusX == rrect.trRadiusY && + rrect.blRadiusX == rrect.blRadiusY && + rrect.brRadiusX == rrect.brRadiusY) { + style.borderRadius = '${rrect.blRadiusX.toStringAsFixed(3)}px'; + return; + } + // Non-uniform. Apply each corner radius. + style.borderTopLeftRadius = '${rrect.tlRadiusX.toStringAsFixed(3)}px ' + '${rrect.tlRadiusY.toStringAsFixed(3)}px'; + style.borderTopRightRadius = '${rrect.trRadiusX.toStringAsFixed(3)}px ' + '${rrect.trRadiusY.toStringAsFixed(3)}px'; + style.borderBottomLeftRadius = '${rrect.blRadiusX.toStringAsFixed(3)}px ' + '${rrect.blRadiusY.toStringAsFixed(3)}px'; + style.borderBottomRightRadius = '${rrect.brRadiusX.toStringAsFixed(3)}px ' + '${rrect.brRadiusY.toStringAsFixed(3)}px'; +} + +html.Element _pathToSvgElement(SurfacePath path, SurfacePaintData paint, + String width, String height) { + final StringBuffer sb = StringBuffer(); + sb.write( + ''); + sb.write(''); + sb.write(''); + return html.Element.html(sb.toString(), treeSanitizer: _NullTreeSanitizer()); +} diff --git a/lib/web_ui/lib/src/engine/html/clip.dart b/lib/web_ui/lib/src/engine/html/clip.dart index 0cab237946277..bc8cbc456b5ed 100644 --- a/lib/web_ui/lib/src/engine/html/clip.dart +++ b/lib/web_ui/lib/src/engine/html/clip.dart @@ -25,18 +25,6 @@ mixin _DomClip on PersistedContainerSurface { @override html.Element createElement() { final html.Element element = defaultCreateElement('flt-clip'); - if (!debugShowClipLayers) { - // Hide overflow in production mode. When debugging we want to see the - // clipped picture in full. - element.style - ..overflow = 'hidden' - ..zIndex = '0'; - } else { - // Display the outline of the clipping region. When debugShowClipLayers is - // `true` we don't hide clip overflow (see above). This outline helps - // visualizing clip areas. - element.style.boxShadow = 'inset 0 0 10px green'; - } _childContainer = html.Element.tag('flt-clip-interior'); if (_debugExplainSurfaceStats) { // This creates an additional interior element. Count it too. @@ -57,14 +45,32 @@ mixin _DomClip on PersistedContainerSurface { // together. _childContainer = null; } + + void applyOverflow(html.Element element, ui.Clip? clipBehaviour) { + if (!debugShowClipLayers) { + // Hide overflow in production mode. When debugging we want to see the + // clipped picture in full. + if (clipBehaviour != ui.Clip.none) { + element.style + ..overflow = 'hidden' + ..zIndex = '0'; + } + } else { + // Display the outline of the clipping region. When debugShowClipLayers is + // `true` we don't hide clip overflow (see above). This outline helps + // visualizing clip areas. + element.style.boxShadow = 'inset 0 0 10px green'; + } + } } /// A surface that creates a rectangular clip. class PersistedClipRect extends PersistedContainerSurface with _DomClip implements ui.ClipRectEngineLayer { - PersistedClipRect(PersistedClipRect? oldLayer, this.rect) : super(oldLayer); - + PersistedClipRect(PersistedClipRect? oldLayer, this.rect, this.clipBehavior) + : super(oldLayer); + final ui.Clip? clipBehavior; final ui.Rect rect; @override @@ -87,6 +93,7 @@ class PersistedClipRect extends PersistedContainerSurface ..top = '${rect.top}px' ..width = '${rect.right - rect.left}px' ..height = '${rect.bottom - rect.top}px'; + applyOverflow(rootElement!, clipBehavior); // Translate the child container in the opposite direction to compensate for // the shift in the coordinate system introduced by the translation of the @@ -99,7 +106,7 @@ class PersistedClipRect extends PersistedContainerSurface @override void update(PersistedClipRect oldSurface) { super.update(oldSurface); - if (rect != oldSurface.rect) { + if (rect != oldSurface.rect || clipBehavior != oldSurface.clipBehavior) { apply(); } } @@ -134,7 +141,8 @@ class PersistedClipRRect extends PersistedContainerSurface @override void apply() { - rootElement!.style + html.CssStyleDeclaration style = rootElement!.style; + style ..left = '${rrect.left}px' ..top = '${rrect.top}px' ..width = '${rrect.width}px' @@ -143,6 +151,7 @@ class PersistedClipRRect extends PersistedContainerSurface ..borderTopRightRadius = '${rrect.trRadiusX}px' ..borderBottomRightRadius = '${rrect.brRadiusX}px' ..borderBottomLeftRadius = '${rrect.blRadiusX}px'; + applyOverflow(rootElement!, clipBehavior); // Translate the child container in the opposite direction to compensate for // the shift in the coordinate system introduced by the translation of the @@ -155,7 +164,7 @@ class PersistedClipRRect extends PersistedContainerSurface @override void update(PersistedClipRRect oldSurface) { super.update(oldSurface); - if (rrect != oldSurface.rrect) { + if (rrect != oldSurface.rrect || clipBehavior != oldSurface.clipBehavior) { apply(); } } diff --git a/lib/web_ui/lib/src/engine/html/scene_builder.dart b/lib/web_ui/lib/src/engine/html/scene_builder.dart index b9dd6f790aad9..705b3aa4bc589 100644 --- a/lib/web_ui/lib/src/engine/html/scene_builder.dart +++ b/lib/web_ui/lib/src/engine/html/scene_builder.dart @@ -113,7 +113,8 @@ class SurfaceSceneBuilder implements ui.SceneBuilder { }) { assert(clipBehavior != null); // ignore: unnecessary_null_comparison assert(clipBehavior != ui.Clip.none); - return _pushSurface(PersistedClipRect(oldLayer as PersistedClipRect?, rect)) as ui.ClipRectEngineLayer; + return _pushSurface(PersistedClipRect(oldLayer as PersistedClipRect?, rect, clipBehavior)) + as ui.ClipRectEngineLayer; } /// Pushes a rounded-rectangular clip operation onto the operation stack. diff --git a/lib/web_ui/lib/src/engine/picture.dart b/lib/web_ui/lib/src/engine/picture.dart index 946f348a66a3e..42304da062c9d 100644 --- a/lib/web_ui/lib/src/engine/picture.dart +++ b/lib/web_ui/lib/src/engine/picture.dart @@ -47,7 +47,7 @@ class EnginePicture implements ui.Picture { @override Future toImage(int width, int height) async { final ui.Rect imageRect = ui.Rect.fromLTRB(0, 0, width.toDouble(), height.toDouble()); - final BitmapCanvas canvas = BitmapCanvas(imageRect); + final BitmapCanvas canvas = BitmapCanvas.imageData(imageRect); recordingCanvas!.apply(canvas, imageRect); final String imageDataUrl = canvas.toDataUrl(); final html.ImageElement imageElement = html.ImageElement() diff --git a/lib/web_ui/test/engine/surface/scene_builder_test.dart b/lib/web_ui/test/engine/surface/scene_builder_test.dart index da2bdb275215e..89b358e35f7f6 100644 --- a/lib/web_ui/test/engine/surface/scene_builder_test.dart +++ b/lib/web_ui/test/engine/surface/scene_builder_test.dart @@ -133,7 +133,8 @@ void testMain() { () { final PersistedScene scene1 = PersistedScene(null); final PersistedClipRect clip1 = - PersistedClipRect(null, const Rect.fromLTRB(10, 10, 20, 20)); + PersistedClipRect(null, const Rect.fromLTRB(10, 10, 20, 20), + Clip.antiAlias); final PersistedOpacity opacity = PersistedOpacity(null, 100, Offset.zero); final MockPersistedPicture picture = MockPersistedPicture(); @@ -158,7 +159,8 @@ void testMain() { // because the clip didn't change no repaints should happen. final PersistedScene scene2 = PersistedScene(scene1); final PersistedClipRect clip2 = - PersistedClipRect(clip1, const Rect.fromLTRB(10, 10, 20, 20)); + PersistedClipRect(clip1, const Rect.fromLTRB(10, 10, 20, 20), + Clip.antiAlias); clip1.state = PersistedSurfaceState.pendingUpdate; scene2.appendChild(clip2); opacity.state = PersistedSurfaceState.pendingRetention; @@ -176,7 +178,8 @@ void testMain() { // This should cause the picture to repaint despite being retained. final PersistedScene scene3 = PersistedScene(scene2); final PersistedClipRect clip3 = - PersistedClipRect(clip2, const Rect.fromLTRB(10, 10, 50, 50)); + PersistedClipRect(clip2, const Rect.fromLTRB(10, 10, 50, 50), + Clip.antiAlias); clip2.state = PersistedSurfaceState.pendingUpdate; scene3.appendChild(clip3); opacity.state = PersistedSurfaceState.pendingRetention; @@ -234,6 +237,7 @@ void testMain() { builder.pop(); html.HtmlElement content = builder.build().webOnlyRootElement; + html.document.body.append(content); expect(content.querySelector('canvas').style.zIndex, '-1'); // Force update to scene which will utilize reuse code path. @@ -627,8 +631,16 @@ Picture _drawPicture() { final EnginePictureRecorder recorder = PictureRecorder(); final RecordingCanvas canvas = recorder.beginRecording(const Rect.fromLTRB(0, 0, 400, 400)); + Shader gradient = Gradient.radial( + Offset(100, 100), 50, [ + const Color.fromARGB(255, 0, 0, 0), + const Color.fromARGB(255, 0, 0, 255) + ]); canvas.drawCircle( - Offset(offsetX + 10, offsetY + 10), 10, Paint()..style = PaintingStyle.fill); + Offset(offsetX + 10, offsetY + 10), 10, + Paint() + ..style = PaintingStyle.fill + ..shader = gradient); canvas.drawCircle( Offset(offsetX + 60, offsetY + 10), 10, @@ -656,8 +668,16 @@ Picture _drawPathImagePath() { final EnginePictureRecorder recorder = PictureRecorder(); final RecordingCanvas canvas = recorder.beginRecording(const Rect.fromLTRB(0, 0, 400, 400)); + Shader gradient = Gradient.radial( + Offset(100, 100), 50, [ + const Color.fromARGB(255, 0, 0, 0), + const Color.fromARGB(255, 0, 0, 255) + ]); canvas.drawCircle( - Offset(offsetX + 10, offsetY + 10), 10, Paint()..style = PaintingStyle.fill); + Offset(offsetX + 10, offsetY + 10), 10, + Paint() + ..style = PaintingStyle.fill + ..shader = gradient); canvas.drawCircle( Offset(offsetX + 60, offsetY + 10), 10, @@ -671,6 +691,11 @@ Picture _drawPathImagePath() { ..style = PaintingStyle.fill ..color = const Color.fromRGBO(0, 255, 0, 1)); canvas.drawImage(createTestImage(), Offset(0, 0), Paint()); + canvas.drawCircle( + Offset(offsetX + 10, offsetY + 10), 10, + Paint() + ..style = PaintingStyle.fill + ..shader = gradient); canvas.drawCircle( Offset(offsetX + 60, offsetY + 60), 10, diff --git a/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart index 39cfcb4b27feb..2996be4e1be01 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_blend_golden_test.dart @@ -24,7 +24,7 @@ void testMain() async { // 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), - double maxDiffRatePercent = 0.0}) async { + double maxDiffRatePercent = 0.0, bool write = false}) async { final EngineCanvas engineCanvas = BitmapCanvas(screenRect); rc.endRecording(); @@ -35,7 +35,8 @@ void testMain() async { try { sceneElement.append(engineCanvas.rootElement); html.document.body.append(sceneElement); - await matchGoldenFile('$fileName.png', region: region, maxDiffRatePercent: maxDiffRatePercent); + await matchGoldenFile('$fileName.png', region: region, + maxDiffRatePercent: maxDiffRatePercent, write: write); } finally { // The page is reused across tests, so remove the element after taking the // Scuba screenshot. @@ -83,7 +84,8 @@ void testMain() async { ..color = const Color.fromARGB(128, 255, 0, 0)); rc.restore(); await _checkScreenshot(rc, 'canvas_blend_circle_diff_color', - maxDiffRatePercent: operatingSystem == OperatingSystem.macOs ? 2.95 : 0); + maxDiffRatePercent: operatingSystem == OperatingSystem.macOs ? 2.95 : + operatingSystem == OperatingSystem.iOs ? 1.0 : 0); }); test('Blend circle and text with multiply', () async { @@ -120,7 +122,8 @@ void testMain() async { Paint()..blendMode = BlendMode.multiply); rc.restore(); await _checkScreenshot(rc, 'canvas_blend_image_multiply', - maxDiffRatePercent: operatingSystem == OperatingSystem.macOs ? 2.95 : 0); + maxDiffRatePercent: operatingSystem == OperatingSystem.macOs ? 2.95 : + operatingSystem == OperatingSystem.iOs ? 2.0 : 0); }); } diff --git a/lib/web_ui/test/golden_tests/engine/canvas_draw_color_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_draw_color_test.dart new file mode 100644 index 0000000000000..112bffb5d58f7 --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/canvas_draw_color_test.dart @@ -0,0 +1,90 @@ +// 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. + +// @dart = 2.6 +import 'dart:html' as html; + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/ui.dart'; +import 'package:ui/src/engine.dart'; + +import 'package:web_engine_tester/golden_tester.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +void testMain() async { + setUp(() async { + debugShowClipLayers = true; + SurfaceSceneBuilder.debugForgetFrameScene(); + for (html.Node scene in html.document.querySelectorAll('flt-scene')) { + scene.remove(); + } + + await webOnlyInitializePlatform(); + webOnlyFontCollection.debugRegisterTestFonts(); + await webOnlyFontCollection.ensureFontsLoaded(); + }); + + test('drawColor should cover entire viewport', () async { + final Rect region = Rect.fromLTWH(0, 0, 400, 400); + + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + final Picture testPicture = _drawTestPicture(region, useColor: true); + builder.addPicture(Offset.zero, testPicture); + + html.document.body.append(builder + .build() + .webOnlyRootElement); + + await matchGoldenFile('canvas_draw_color.png', region: region); + }, skip: true); // TODO: matchGolden fails when a div covers viewport. + + test('drawPaint should cover entire viewport', () async { + final Rect region = Rect.fromLTWH(0, 0, 400, 400); + + final SurfaceSceneBuilder builder = SurfaceSceneBuilder(); + final Picture testPicture = _drawTestPicture(region, useColor: false); + builder.addPicture(Offset.zero, testPicture); + + html.document.body.append(builder + .build() + .webOnlyRootElement); + + await matchGoldenFile('canvas_draw_paint.png', region: region); + }, skip: true); // TODO: matchGolden fails when a div covers viewport.); +} + +Picture _drawTestPicture(Rect region, {bool useColor = false}) { + final EnginePictureRecorder recorder = PictureRecorder(); + final Rect r = Rect.fromLTWH(0, 0, 200, 200); + final RecordingCanvas canvas = recorder.beginRecording(r); + + canvas.drawRect( + region.deflate(8.0), + Paint() + ..style = PaintingStyle.fill + ..color = Color(0xFFE0E0E0) + ); + + canvas.transform(Matrix4.translationValues(50, 50, 0).storage); + + if (useColor) { + canvas.drawColor(const Color.fromRGBO(0, 255, 0, 1), BlendMode.srcOver); + } else { + canvas.drawPaint(Paint() + ..style = PaintingStyle.fill + ..color = const Color.fromRGBO(0, 0, 255, 1)); + } + + canvas.drawCircle( + Offset(r.width/2, r.height/2), r.width/2, + Paint() + ..style = PaintingStyle.fill + ..color = const Color.fromRGBO(255, 0, 0, 1)); + + return recorder.endRecording(); +} diff --git a/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart index 799ab41050a2e..4a61124d2578a 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_draw_image_golden_test.dart @@ -28,7 +28,8 @@ void testMain() async { // 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), - double maxDiffRatePercent = 0.0}) async { + double maxDiffRatePercent = 0.0, bool setupPerspective = false, + bool write = false}) async { final EngineCanvas engineCanvas = BitmapCanvas(screenRect); rc.endRecording(); @@ -37,10 +38,18 @@ void testMain() async { // Wrap in so that our CSS selectors kick in. final html.Element sceneElement = html.Element.tag('flt-scene'); try { + if (setupPerspective) { + // iFrame disables perspective, set it explicitly for test. + engineCanvas.rootElement.style.perspective = '400px'; + for (html.Element element in engineCanvas.rootElement.querySelectorAll( + 'div')) { + element.style.perspective = '400px'; + } + } sceneElement.append(engineCanvas.rootElement); html.document.body.append(sceneElement); await matchGoldenFile('$fileName.png', - region: region, maxDiffRatePercent: maxDiffRatePercent); + region: region, maxDiffRatePercent: maxDiffRatePercent, write: write); } finally { // The page is reused across tests, so remove the element after taking the // Scuba screenshot. @@ -400,6 +409,158 @@ void testMain() async { await _checkScreenshot(canvas, 'draw_clipped_and_transformed_image', region: region, maxDiffRatePercent: 1.0); }); + + /// Regression test for https://github.com/flutter/flutter/issues/61245 + test('Should render image with perspective', () async { + final Rect region = const Rect.fromLTRB(0, 0, 200, 200); + final RecordingCanvas canvas = RecordingCanvas(region); + canvas.translate(10, 10); + canvas.drawImage(createTestImage(), Offset(0, 0), new Paint()); + Matrix4 transform = Matrix4.identity() + ..setRotationY(0.8) + ..setEntry(3, 2, 0.0005); // perspective + canvas.transform(transform.storage); + canvas.drawImage(createTestImage(), Offset(0, 100), new Paint()); + await _checkScreenshot(canvas, 'draw_3d_image', + region: region, + maxDiffRatePercent: 6.0, + setupPerspective: true); + }); + + /// Regression test for https://github.com/flutter/flutter/issues/61245 + test('Should render image with perspective inside clip area', () async { + final Rect region = const Rect.fromLTRB(0, 0, 200, 200); + final RecordingCanvas canvas = RecordingCanvas(region); + canvas.drawRect(region, Paint()..color = Color(0xFFE0E0E0)); + canvas.translate(10, 10); + canvas.drawImage(createTestImage(), Offset(0, 0), new Paint()); + Matrix4 transform = Matrix4.identity() + ..setRotationY(0.8) + ..setEntry(3, 2, 0.0005); // perspective + canvas.transform(transform.storage); + canvas.clipRect(region, ClipOp.intersect); + canvas.drawRect(Rect.fromLTWH(0, 0, 100, 200), Paint()..color = Color(0x801080E0)); + canvas.drawImage(createTestImage(), Offset(0, 100), new Paint()); + canvas.drawRect(Rect.fromLTWH(50, 150, 50, 20), Paint()..color = Color(0x80000000)); + await _checkScreenshot(canvas, 'draw_3d_image_clipped', + region: region, + maxDiffRatePercent: 5.0, + setupPerspective: true); + }); + + test('Should render rect with perspective transform', () async { + final Rect region = const Rect.fromLTRB(0, 0, 400, 400); + final RecordingCanvas canvas = RecordingCanvas(region); + canvas.drawRect(region, Paint()..color = Color(0xFFE0E0E0)); + canvas.translate(20, 20); + canvas.drawRect(Rect.fromLTWH(0, 0, 100, 40), + Paint()..color = Color(0xFF000000)); + Matrix4 transform = Matrix4.identity() + ..setRotationY(0.8) + ..setEntry(3, 2, 0.001); // perspective + canvas.transform(transform.storage); + canvas.clipRect(region, ClipOp.intersect); + canvas.drawRect(Rect.fromLTWH(0, 60, 120, 40), Paint()..color = Color(0x801080E0)); + canvas.drawRect(Rect.fromLTWH(300, 250, 120, 40), Paint()..color = Color(0x80E010E0)); + canvas.drawRRect(RRect.fromRectAndRadius(Rect.fromLTWH(0, 120, 160, 40), Radius.circular(5)), + Paint()..color = Color(0x801080E0)); + canvas.drawRRect(RRect.fromRectAndRadius(Rect.fromLTWH(300, 320, 90, 40), Radius.circular(20)), + Paint()..color = Color(0x80E010E0)); + await _checkScreenshot(canvas, 'draw_3d_rect_clipped', + region: region, + maxDiffRatePercent: 1.0, + setupPerspective: true); + }); + + test('Should render color and ovals with perspective transform', () async { + final Rect region = const Rect.fromLTRB(0, 0, 400, 400); + final RecordingCanvas canvas = RecordingCanvas(region); + canvas.drawRect(region, Paint()..color = Color(0xFFFF0000)); + canvas.drawColor(Color(0xFFE0E0E0), BlendMode.src); + canvas.translate(20, 20); + canvas.drawRect(Rect.fromLTWH(0, 0, 100, 40), + Paint()..color = Color(0xFF000000)); + Matrix4 transform = Matrix4.identity() + ..setRotationY(0.8) + ..setEntry(3, 2, 0.001); // perspective + canvas.transform(transform.storage); + canvas.clipRect(region, ClipOp.intersect); + canvas.drawOval(Rect.fromLTWH(0, 120, 130, 40), + Paint()..color = Color(0x801080E0)); + canvas.drawOval(Rect.fromLTWH(300, 290, 90, 40), + Paint()..color = Color(0x80E010E0)); + canvas.drawCircle(Offset(60, 240), 50, Paint()..color = Color(0x801080E0)); + canvas.drawCircle(Offset(360, 370), 30, Paint()..color = Color(0x80E010E0)); + await _checkScreenshot(canvas, 'draw_3d_oval_clipped', + region: region, + maxDiffRatePercent: 1.0, + setupPerspective: true); + }); + + test('Should render path with perspective transform', () async { + final Rect region = const Rect.fromLTRB(0, 0, 400, 400); + final RecordingCanvas canvas = RecordingCanvas(region); + canvas.drawRect(region, Paint()..color = Color(0xFFFF0000)); + canvas.drawColor(Color(0xFFE0E0E0), BlendMode.src); + canvas.translate(20, 20); + canvas.drawRect(Rect.fromLTWH(0, 0, 100, 20), + Paint()..color = Color(0xFF000000)); + Matrix4 transform = Matrix4.identity() + ..setRotationY(0.8) + ..setEntry(3, 2, 0.001); // perspective + canvas.transform(transform.storage); + canvas.drawRect(Rect.fromLTWH(0, 120, 130, 40), + Paint()..color = Color(0x801080E0)); + canvas.drawOval(Rect.fromLTWH(300, 290, 90, 40), + Paint()..color = Color(0x80E010E0)); + Path path = Path(); + path.moveTo(50, 50); + path.lineTo(100, 50); + path.lineTo(100, 100); + path.close(); + canvas.drawPath(path, Paint()..color = Color(0x801080E0)); + + canvas.drawCircle(Offset(50, 50), 4, Paint()..color = Color(0xFF000000)); + canvas.drawCircle(Offset(100, 100), 4, Paint()..color = Color(0xFF000000)); + canvas.drawCircle(Offset(100, 50), 4, Paint()..color = Color(0xFF000000)); + await _checkScreenshot(canvas, 'draw_3d_path', + region: region, + maxDiffRatePercent: 1.0, + setupPerspective: true); + }); + + test('Should render path with perspective transform', () async { + final Rect region = const Rect.fromLTRB(0, 0, 400, 400); + final RecordingCanvas canvas = RecordingCanvas(region); + canvas.drawRect(region, Paint()..color = Color(0xFFFF0000)); + canvas.drawColor(Color(0xFFE0E0E0), BlendMode.src); + canvas.translate(20, 20); + canvas.drawRect(Rect.fromLTWH(0, 0, 100, 20), + Paint()..color = Color(0xFF000000)); + Matrix4 transform = Matrix4.identity() + ..setRotationY(0.8) + ..setEntry(3, 2, 0.001); // perspective + canvas.transform(transform.storage); + //canvas.clipRect(region, ClipOp.intersect); + canvas.drawRect(Rect.fromLTWH(0, 120, 130, 40), + Paint()..color = Color(0x801080E0)); + canvas.drawOval(Rect.fromLTWH(300, 290, 90, 40), + Paint()..color = Color(0x80E010E0)); + Path path = Path(); + path.moveTo(50, 50); + path.lineTo(100, 50); + path.lineTo(100, 100); + path.close(); + canvas.drawPath(path, Paint()..color = Color(0x801080E0)); + + canvas.drawCircle(Offset(50, 50), 4, Paint()..color = Color(0xFF000000)); + canvas.drawCircle(Offset(100, 100), 4, Paint()..color = Color(0xFF000000)); + canvas.drawCircle(Offset(100, 50), 4, Paint()..color = Color(0xFF000000)); + await _checkScreenshot(canvas, 'draw_3d_path_clipped', + region: region, + maxDiffRatePercent: 1.0, + setupPerspective: true); + }); } // 9 slice test image that has a shiny/glass look. diff --git a/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart b/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart index dde35315d38bc..0590725f909fc 100644 --- a/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/canvas_golden_test.dart @@ -93,28 +93,31 @@ void testMain() async { // compensate by shifting the contents of the canvas in the opposite // direction. canvas = BitmapCanvas(const Rect.fromLTWH(0.5, 0.5, 60, 60)); - + canvas.clipRect(const Rect.fromLTWH(0, 0, 50, 50), ClipOp.intersect); drawMisalignedLines(canvas); appendToScene(); - await matchGoldenFile('misaligned_canvas_test.png', region: region); + await matchGoldenFile('misaligned_canvas_test.png', region: region, + maxDiffRatePercent: 1.0); }); test('fill the whole canvas with color even when transformed', () async { canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50)); - + canvas.clipRect(const Rect.fromLTWH(0, 0, 50, 50), ClipOp.intersect); canvas.translate(25, 25); canvas.drawColor(const Color.fromRGBO(0, 255, 0, 1.0), BlendMode.src); appendToScene(); - await matchGoldenFile('bitmap_canvas_fills_color_when_transformed.png', region: region); + await matchGoldenFile('bitmap_canvas_fills_color_when_transformed.png', + region: region, + maxDiffRatePercent: 5.0); }); test('fill the whole canvas with paint even when transformed', () async { canvas = BitmapCanvas(const Rect.fromLTWH(0, 0, 50, 50)); - + canvas.clipRect(const Rect.fromLTWH(0, 0, 50, 50), ClipOp.intersect); canvas.translate(25, 25); canvas.drawPaint(SurfacePaintData() ..color = const Color.fromRGBO(0, 255, 0, 1.0) @@ -122,7 +125,9 @@ void testMain() async { appendToScene(); - await matchGoldenFile('bitmap_canvas_fills_paint_when_transformed.png', region: region); + await matchGoldenFile('bitmap_canvas_fills_paint_when_transformed.png', + region: region, + maxDiffRatePercent: 5.0); }); // This test reproduces text blurriness when two pieces of text appear inside @@ -245,7 +250,7 @@ void testMain() async { await matchGoldenFile( 'bitmap_canvas_draws_text_on_top_of_canvas.png', region: canvasSize, - maxDiffRatePercent: 0.0, + maxDiffRatePercent: 1.0, pixelComparison: PixelComparison.precise, ); }); diff --git a/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart b/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart index 04dce71d0b986..8c2784c7ba03a 100644 --- a/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/compositing_golden_test.dart @@ -22,7 +22,9 @@ void main() { void testMain() async { setUp(() async { - debugShowClipLayers = true; + // To debug test failures uncomment the following to visualize clipping + // layers: + // debugShowClipLayers = true; SurfaceSceneBuilder.debugForgetFrameScene(); for (html.Node scene in html.document.querySelectorAll('flt-scene')) { scene.remove(); @@ -545,7 +547,6 @@ void _testCullRectComputation() { 'renders clipped text with high quality', () async { // To reproduce blurriness we need real clipping. - debugShowClipLayers = false; final Paragraph paragraph = (ParagraphBuilder(ParagraphStyle(fontFamily: 'Roboto'))..addText('Am I blurry?')).build(); paragraph.layout(const ParagraphConstraints(width: 1000)); diff --git a/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart b/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart index c7854c961a798..e7397545124d9 100644 --- a/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart +++ b/lib/web_ui/test/golden_tests/engine/recording_canvas_golden_test.dart @@ -678,7 +678,7 @@ void testMain() async { await matchGoldenFile( 'paint_spread_bounds.png', region: const Rect.fromLTRB(0, 0, 250, 600), - maxDiffRatePercent: 0.01, + maxDiffRatePercent: 0.2, pixelComparison: PixelComparison.precise, ); } finally {