diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 80ad768ee345a..cc1e18f1a79b7 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -387,6 +387,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/assets.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/bitmap_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/browser_detection.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/browser_location.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvas_pool.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/color_filter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/canvas_kit_canvas.dart diff --git a/lib/web_ui/dev/goldens_lock.yaml b/lib/web_ui/dev/goldens_lock.yaml index 6697a40e4bd90..c9a03622d779a 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: a121ff1169a1b478274a3e34c95d0a1d2d685948 +revision: c0032eeb9f9f064234991b8b5ddc15f714a53cf5 diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index 14808e413d86a..f000505c5e953 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -23,6 +23,7 @@ part 'engine/assets.dart'; part 'engine/bitmap_canvas.dart'; part 'engine/browser_detection.dart'; part 'engine/browser_location.dart'; +part 'engine/canvas_pool.dart'; part 'engine/color_filter.dart'; part 'engine/compositor/canvas.dart'; part 'engine/compositor/canvas_kit_canvas.dart'; diff --git a/lib/web_ui/lib/src/engine/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/bitmap_canvas.dart index e4dde41d591ca..682eca3297bde 100644 --- a/lib/web_ui/lib/src/engine/bitmap_canvas.dart +++ b/lib/web_ui/lib/src/engine/bitmap_canvas.dart @@ -5,7 +5,7 @@ part of engine; /// A raw HTML canvas that is directly written to. -class BitmapCanvas extends EngineCanvas with SaveStackTracking { +class BitmapCanvas extends EngineCanvas { /// The rectangle positioned relative to the parent layer's coordinate /// system's origin, within which this canvas paints. /// @@ -14,6 +14,14 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { set bounds(ui.Rect newValue) { assert(newValue != null); _bounds = newValue; + final int newCanvasPositionX = _bounds.left.floor() - kPaddingPixels; + final int newCanvasPositionY = _bounds.top.floor() - kPaddingPixels; + if (_canvasPositionX != newCanvasPositionX || + _canvasPositionY != newCanvasPositionY) { + _canvasPositionX = newCanvasPositionX; + _canvasPositionY = newCanvasPositionY; + _updateRootElementTransform(); + } } ui.Rect _bounds; @@ -25,8 +33,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { @override final html.Element rootElement = html.Element.tag('flt-canvas'); - html.CanvasElement _canvas; - html.CanvasRenderingContext2D _ctx; + final _CanvasPool _canvasPool; /// The size of the paint [bounds]. ui.Size get size => _bounds.size; @@ -43,22 +50,20 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { /// /// These pixels are different from the logical CSS pixels. Here a pixel /// literally means 1 point with a RGBA color. - int get widthInBitmapPixels => _widthInBitmapPixels; - int _widthInBitmapPixels; + final int _widthInBitmapPixels; /// The number of pixels along the width of the bitmap that the canvas element /// renders into. /// /// These pixels are different from the logical CSS pixels. Here a pixel /// literally means 1 point with a RGBA color. - int get heightInBitmapPixels => _heightInBitmapPixels; - int _heightInBitmapPixels; + final int _heightInBitmapPixels; /// The number of pixels in the bitmap that the canvas element renders into. /// /// These pixels are different from the logical CSS pixels. Here a pixel /// literally means 1 point with a RGBA color. - int get bitmapPixelCount => widthInBitmapPixels * heightInBitmapPixels; + int get bitmapPixelCount => _widthInBitmapPixels * _heightInBitmapPixels; int _saveCount = 0; @@ -66,11 +71,8 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { /// was created. final double _devicePixelRatio = html.window.devicePixelRatio; - // Cached current filter, fill and stroke style to reduce updates to - // CanvasRenderingContext2D that are slow even when resetting to null. - String _prevFilter = 'none'; - Object _prevFillStyle; - Object _prevStrokeStyle; + // Compensation for [_initializeViewport] snapping canvas position to 1 pixel. + int _canvasPositionX, _canvasPositionY; // Indicates the instructions following drawImage or drawParagraph that // a child element was created to paint. @@ -89,51 +91,62 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { /// This canvas can be reused by pictures with different paint bounds as long /// as the [Rect.size] of the bounds fully fit within the size used to /// initialize this canvas. - BitmapCanvas(this._bounds) : assert(_bounds != null) { + BitmapCanvas(this._bounds) + : assert(_bounds != null), + _widthInBitmapPixels = _widthToPhysical(_bounds.width), + _heightInBitmapPixels = _heightToPhysical(_bounds.height), + _canvasPool = _CanvasPool(_widthToPhysical(_bounds.width), + _heightToPhysical(_bounds.height)) { rootElement.style.position = 'absolute'; - // Adds one extra pixel to the requested size. This is to compensate for // _initializeViewport() snapping canvas position to 1 pixel, causing // painting to overflow by at most 1 pixel. + _canvasPositionX = _bounds.left.floor() - kPaddingPixels; + _canvasPositionY = _bounds.top.floor() - kPaddingPixels; + _updateRootElementTransform(); + _canvasPool.allocateCanvas(rootElement); + _setupInitialTransform(); + } - _widthInBitmapPixels = _widthToPhysical(_bounds.width); - _heightInBitmapPixels = _heightToPhysical(_bounds.height); - - // Compute the final CSS canvas size given the actual pixel count we - // allocated. This is done for the following reasons: + void _updateRootElementTransform() { + // Flutter emits paint operations positioned relative to the parent layer's + // coordinate system. However, canvas' coordinate system's origin is always + // in the top-left corner of the canvas. We therefore need to inject an + // initial translation so the paint operations are positioned as expected. // - // * To satisfy the invariant: pixel size = css size * device pixel ratio. - // * To make sure that when we scale the canvas by devicePixelRatio (see - // _initializeViewport below) the pixels line up. - final double cssWidth = _widthInBitmapPixels / html.window.devicePixelRatio; - final double cssHeight = - _heightInBitmapPixels / html.window.devicePixelRatio; - - _canvas = html.CanvasElement( - width: _widthInBitmapPixels, - height: _heightInBitmapPixels, + // The flooring of the value is to ensure that canvas' top-left corner + // lands on the physical pixel. TODO: !This is not accurate if there are + // transforms higher up in the stack. + rootElement.style.transform = + 'translate(${_canvasPositionX}px, ${_canvasPositionY}px)'; + } + + void _setupInitialTransform() { + final double canvasPositionCorrectionX = _bounds.left - + BitmapCanvas.kPaddingPixels - + _canvasPositionX.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, + -_bounds.top + canvasPositionCorrectionY + BitmapCanvas.kPaddingPixels, ); - _canvas.style - ..position = 'absolute' - ..width = '${cssWidth}px' - ..height = '${cssHeight}px'; - _ctx = _canvas.context2D; - rootElement.append(_canvas); - _initializeViewport(); } - int _widthToPhysical(double width) { + static int _widthToPhysical(double width) { final double boundsWidth = width + 1; return (boundsWidth * html.window.devicePixelRatio).ceil() + 2 * kPaddingPixels; } - int _heightToPhysical(double height) { + static int _heightToPhysical(double height) { final double boundsHeight = height + 1; return (boundsHeight * html.window.devicePixelRatio).ceil() + 2 * kPaddingPixels; } + // Used by picture to assess if canvas is large enough to reuse as is. bool doesFitBounds(ui.Rect newBounds) { assert(newBounds != null); return _widthInBitmapPixels >= _widthToPhysical(newBounds.width) && @@ -142,47 +155,20 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { @override void dispose() { - super.dispose(); - // Webkit has a threshold for the amount of canvas pixels an app can - // allocate. Even though our canvases are being garbage-collected as - // expected when we don't need them, Webkit keeps track of their sizes - // towards the threshold. Setting width and height to zero tricks Webkit - // into thinking that this canvas has a zero size so it doesn't count it - // towards the threshold. - if (browserEngine == BrowserEngine.webkit) { - _canvas.width = _canvas.height = 0; - } + _canvasPool.dispose(); } /// Prepare to reuse this canvas by clearing it's current contents. @override void clear() { - super.clear(); + _canvasPool.clear(); final int len = _children.length; for (int i = 0; i < len; i++) { _children[i].remove(); } _children.clear(); _cachedLastStyle = null; - // Restore to the state where we have only applied the scaling. - if (_ctx != null) { - _ctx.restore(); - _ctx.clearRect(0, 0, _widthInBitmapPixels, _heightInBitmapPixels); - try { - _ctx.font = ''; - } catch (e) { - // Firefox may explode here: - // https://bugzilla.mozilla.org/show_bug.cgi?id=941146 - if (!_isNsErrorFailureException(e)) { - rethrow; - } - } - _initializeViewport(); - } - if (_canvas != null) { - _canvas.style.transformOrigin = ''; - _canvas.style.transform = ''; - } + _setupInitialTransform(); } /// Checks whether this [BitmapCanvas] can still be recycled and reused. @@ -197,128 +183,41 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { return _devicePixelRatio == html.window.devicePixelRatio; } - /// Configures the canvas such that its coordinate system follows the scene's - /// coordinate system, and the pixel ratio is applied such that CSS pixels are - /// translated to bitmap pixels. - void _initializeViewport() { - // Save the canvas state with top-level transforms so we can undo - // any clips later when we reuse the canvas. - _ctx.save(); - - // We always start with identity transform because the surrounding transform - // is applied on the DOM elements. - _ctx.setTransform(1, 0, 0, 1, 0, 0); - - // This scale makes sure that 1 CSS pixel is translated to the correct - // number of bitmap pixels. - _ctx.scale(html.window.devicePixelRatio, html.window.devicePixelRatio); - - // Flutter emits paint operations positioned relative to the parent layer's - // coordinate system. However, canvas' coordinate system's origin is always - // in the top-left corner of the canvas. We therefore need to inject an - // initial translation so the paint operations are positioned as expected. - - // The flooring of the value is to ensure that canvas' top-left corner - // lands on the physical pixel. - final int canvasPositionX = _bounds.left.floor() - kPaddingPixels; - final int canvasPositionY = _bounds.top.floor() - kPaddingPixels; - final double canvasPositionCorrectionX = - _bounds.left - kPaddingPixels - canvasPositionX.toDouble(); - final double canvasPositionCorrectionY = - _bounds.top - kPaddingPixels - canvasPositionY.toDouble(); - - rootElement.style.transform = - 'translate(${canvasPositionX}px, ${canvasPositionY}px)'; - - // This compensates for the translate on the `rootElement`. - translate( - -_bounds.left + canvasPositionCorrectionX + kPaddingPixels, - -_bounds.top + canvasPositionCorrectionY + kPaddingPixels, - ); + /// Returns a data URI containing a representation of the image in this + /// canvas. + String toDataUrl() { + return _canvasPool.toDataUrl(); } - /// The `` element used by this bitmap canvas. - html.CanvasElement get canvas => _canvas; - - /// The 2D context of the `` element used by this bitmap canvas. - html.CanvasRenderingContext2D get ctx => _ctx; - /// Sets the global paint styles to correspond to [paint]. void _applyPaint(SurfacePaintData paint) { - ctx.globalCompositeOperation = - _stringForBlendMode(paint.blendMode) ?? 'source-over'; - ctx.lineWidth = paint.strokeWidth ?? 1.0; - final ui.StrokeCap cap = paint.strokeCap; - if (cap != null) { - ctx.lineCap = _stringForStrokeCap(cap); - } else { - ctx.lineCap = 'butt'; - } - final ui.StrokeJoin join = paint.strokeJoin; - if (join != null) { - ctx.lineJoin = _stringForStrokeJoin(join); - } else { - ctx.lineJoin = 'miter'; - } + ContextStateHandle contextHandle = _canvasPool.contextHandle; + contextHandle + ..lineWidth = paint.strokeWidth ?? 1.0 + ..blendMode = paint.blendMode + ..strokeCap = paint.strokeCap + ..strokeJoin = paint.strokeJoin + ..filter = _maskFilterToCss(paint.maskFilter); + if (paint.shader != null) { final EngineGradient engineShader = paint.shader; - final Object paintStyle = engineShader.createPaintStyle(ctx); - _setFillAndStrokeStyle(paintStyle, paintStyle); + final Object paintStyle = + engineShader.createPaintStyle(_canvasPool.context); + contextHandle.fillStyle = paintStyle; + contextHandle.strokeStyle = paintStyle; } else if (paint.color != null) { final String colorString = paint.color.toCssString(); - _setFillAndStrokeStyle(colorString, colorString); - } - if (paint.maskFilter != null) { - _setFilter('blur(${paint.maskFilter.webOnlySigma}px)'); - } - } - - void _strokeOrFill(SurfacePaintData paint, {bool resetPaint = true}) { - switch (paint.style) { - case ui.PaintingStyle.stroke: - ctx.stroke(); - break; - case ui.PaintingStyle.fill: - default: - ctx.fill(); - break; - } - if (resetPaint) { - _resetPaint(); - } - } - - /// Resets the paint styles that were set due to a previous paint command. - /// - /// For example, if a previous paint commands has a blur filter, we need to - /// undo that filter here. - /// - /// This needs to be called after [_applyPaint]. - void _resetPaint() { - _setFilter('none'); - _setFillAndStrokeStyle(null, null); - } - - void _setFilter(String value) { - if (_prevFilter != value) { - _prevFilter = ctx.filter = value; - } - } - - void _setFillAndStrokeStyle(Object fillStyle, Object strokeStyle) { - final html.CanvasRenderingContext2D _ctx = ctx; - if (!identical(_prevFillStyle, fillStyle)) { - _prevFillStyle = _ctx.fillStyle = fillStyle; - } - if (!identical(_prevStrokeStyle, strokeStyle)) { - _prevStrokeStyle = _ctx.strokeStyle = strokeStyle; + contextHandle.fillStyle = colorString; + contextHandle.strokeStyle = colorString; + } else { + contextHandle.fillStyle = ''; + contextHandle.strokeStyle = ''; } } @override int save() { - super.save(); - ctx.save(); + _canvasPool.save(); return _saveCount++; } @@ -328,8 +227,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { @override void restore() { - super.restore(); - ctx.restore(); + _canvasPool.restore(); _saveCount--; _cachedLastStyle = null; } @@ -341,262 +239,133 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { assert(_saveCount >= count); final int restores = _saveCount - count; for (int i = 0; i < restores; i++) { - ctx.restore(); + _canvasPool.restore(); } _saveCount = count; } @override void translate(double dx, double dy) { - super.translate(dx, dy); - ctx.translate(dx, dy); + _canvasPool.translate(dx, dy); } @override void scale(double sx, double sy) { - super.scale(sx, sy); - ctx.scale(sx, sy); + _canvasPool.scale(sx, sy); } @override void rotate(double radians) { - super.rotate(radians); - ctx.rotate(radians); + _canvasPool.rotate(radians); } @override void skew(double sx, double sy) { - super.skew(sx, sy); - ctx.transform(1, sy, sx, 1, 0, 0); - // | | | | | | - // | | | | | f - vertical translation - // | | | | e - horizontal translation - // | | | d - vertical scaling - // | | c - horizontal skewing - // | b - vertical skewing - // a - horizontal scaling - // - // Source: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/transform + _canvasPool.skew(sx, sy); } @override void transform(Float64List matrix4) { - super.transform(matrix4); - - // Canvas2D transform API: - // - // ctx.transform(a, b, c, d, e, f); - // - // In 3x3 matrix form assuming vector representation of (x, y, 1): - // - // a c e - // b d f - // 0 0 1 - // - // This translates to 4x4 matrix with vector representation of (x, y, z, 1) - // as: - // - // a c 0 e - // b d 0 f - // 0 0 1 0 - // 0 0 0 1 - // - // This matrix is sufficient to represent 2D rotates, translates, scales, - // and skews. - _ctx.transform( - matrix4[0], - matrix4[1], - matrix4[4], - matrix4[5], - matrix4[12], - matrix4[13], - ); + _canvasPool.transform(matrix4); } @override void clipRect(ui.Rect rect) { - super.clipRect(rect); - ctx.beginPath(); - ctx.rect(rect.left, rect.top, rect.width, rect.height); - ctx.clip(); + _canvasPool.clipRect(rect); } @override void clipRRect(ui.RRect rrect) { - super.clipRRect(rrect); - final ui.Path path = ui.Path()..addRRect(rrect); - _runPath(path); - ctx.clip(); + _canvasPool.clipRRect(rrect); } @override void clipPath(ui.Path path) { - super.clipPath(path); - _runPath(path); - ctx.clip(); + _canvasPool.clipPath(path); } @override void drawColor(ui.Color color, ui.BlendMode blendMode) { - ctx.globalCompositeOperation = _stringForBlendMode(blendMode); - - // Fill a virtually infinite rect with the color. - // - // We can't use (0, 0, width, height) because the current transform can - // cause it to not fill the entire clip. - ctx.fillRect(-10000, -10000, 20000, 20000); + _canvasPool.drawColor(color, blendMode); } @override void drawLine(ui.Offset p1, ui.Offset p2, SurfacePaintData paint) { _applyPaint(paint); - ctx.beginPath(); - ctx.moveTo(p1.dx, p1.dy); - ctx.lineTo(p2.dx, p2.dy); - ctx.stroke(); - _resetPaint(); + _canvasPool.strokeLine(p1, p2); } @override void drawPaint(SurfacePaintData paint) { _applyPaint(paint); - ctx.beginPath(); - - // Fill a virtually infinite rect with the color. - // - // We can't use (0, 0, width, height) because the current transform can - // cause it to not fill the entire clip. - ctx.fillRect(-10000, -10000, 20000, 20000); - _resetPaint(); + _canvasPool.fill(); } @override void drawRect(ui.Rect rect, SurfacePaintData paint) { _applyPaint(paint); - ctx.beginPath(); - ctx.rect(rect.left, rect.top, rect.width, rect.height); - _strokeOrFill(paint); + _canvasPool.drawRect(rect, paint.style); } @override void drawRRect(ui.RRect rrect, SurfacePaintData paint) { _applyPaint(paint); - _RRectToCanvasRenderer(ctx).render(rrect); - _strokeOrFill(paint); + _canvasPool.drawRRect(rrect, paint.style); } @override void drawDRRect(ui.RRect outer, ui.RRect inner, SurfacePaintData paint) { _applyPaint(paint); - _RRectRenderer renderer = _RRectToCanvasRenderer(ctx); - renderer.render(outer); - renderer.render(inner, startNewPath: false, reverse: true); - _strokeOrFill(paint); + _canvasPool.drawDRRect(outer, inner, paint.style); } @override void drawOval(ui.Rect rect, SurfacePaintData paint) { _applyPaint(paint); - ctx.beginPath(); - ctx.ellipse(rect.center.dx, rect.center.dy, rect.width / 2, rect.height / 2, - 0, 0, 2.0 * math.pi, false); - _strokeOrFill(paint); + _canvasPool.drawOval(rect, paint.style); } @override void drawCircle(ui.Offset c, double radius, SurfacePaintData paint) { _applyPaint(paint); - ctx.beginPath(); - ctx.ellipse(c.dx, c.dy, radius, radius, 0, 0, 2.0 * math.pi, false); - _strokeOrFill(paint); + _canvasPool.drawCircle(c, radius, paint.style); } @override void drawPath(ui.Path path, SurfacePaintData paint) { _applyPaint(paint); - _runPath(path); - _strokeOrFill(paint); + _canvasPool.drawPath(path, paint.style); } @override void drawShadow(ui.Path path, ui.Color color, double elevation, bool transparentOccluder) { - final List shadows = - ElevationShadow.computeCanvasShadows(elevation, color); - if (shadows.isNotEmpty) { - for (final CanvasShadow shadow in shadows) { - // TODO(het): Shadows with transparent occluders are not supported - // on webkit since filter is unsupported. - if (transparentOccluder && browserEngine != BrowserEngine.webkit) { - // We paint shadows using a path and a mask filter instead of the - // built-in shadow* properties. This is because the color alpha of the - // paint is added to the shadow. The effect we're looking for is to just - // paint the shadow without the path itself, but if we use a non-zero - // alpha for the paint the path is painted in addition to the shadow, - // which is undesirable. - final SurfacePaint paint = SurfacePaint() - ..color = shadow.color - ..style = ui.PaintingStyle.fill - ..strokeWidth = 0.0 - ..maskFilter = ui.MaskFilter.blur(ui.BlurStyle.normal, shadow.blur); - _ctx.save(); - _ctx.translate(shadow.offsetX, shadow.offsetY); - final SurfacePaintData paintData = paint.paintData; - _applyPaint(paintData); - _runPath(path); - _strokeOrFill(paintData, resetPaint: false); - _ctx.restore(); - } else { - // TODO(het): We fill the path with this paint, then later we clip - // by the same path and fill it with a fully opaque color (we know - // the color is fully opaque because `transparentOccluder` is false. - // However, due to anti-aliasing of the clip, a few pixels of the - // path we are about to paint may still be visible after we fill with - // the opaque occluder. For that reason, we fill with the shadow color, - // and set the shadow color to fully opaque. This way, the visible - // pixels are less opaque and less noticeable. - final SurfacePaint paint = SurfacePaint() - ..color = shadow.color - ..style = ui.PaintingStyle.fill - ..strokeWidth = 0.0; - _ctx.save(); - final SurfacePaintData paintData = paint.paintData; - _applyPaint(paintData); - _ctx.shadowBlur = shadow.blur; - _ctx.shadowColor = shadow.color.withAlpha(0xff).toCssString(); - _ctx.shadowOffsetX = shadow.offsetX; - _ctx.shadowOffsetY = shadow.offsetY; - _runPath(path); - _strokeOrFill(paintData, resetPaint: false); - _ctx.restore(); - } - } - _resetPaint(); - } + _canvasPool.drawShadow(path, color, elevation, transparentOccluder); } @override void drawImage(ui.Image image, ui.Offset p, SurfacePaintData paint) { - _applyPaint(paint); + //_applyPaint(paint); final HtmlImage htmlImage = image; final html.ImageElement imgElement = htmlImage.cloneImageElement(); - String blendMode = ctx.globalCompositeOperation; + String blendMode = _stringForBlendMode(paint.blendMode); imgElement.style.mixBlendMode = blendMode; _drawImage(imgElement, p); _childOverdraw = true; + _canvasPool.allocateExtraCanvas(); } void _drawImage(html.ImageElement imgElement, ui.Offset p) { - if (isClipped) { - final List clipElements = - _clipContent(_clipStack, imgElement, p, currentTransform); + if (_canvasPool.isClipped) { + final List clipElements = _clipContent( + _canvasPool._clipStack, imgElement, p, _canvasPool.currentTransform); for (html.Element clipElement in clipElements) { rootElement.append(clipElement); _children.add(clipElement); } } else { - final String cssTransform = - matrix4ToCssTransform3d(transformWithOffset(currentTransform, p)); + final String cssTransform = matrix4ToCssTransform3d( + transformWithOffset(_canvasPool.currentTransform, p)); imgElement.style ..transformOrigin = '0 0 0' ..transform = cssTransform; @@ -656,6 +425,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { if (requiresClipping) { restore(); } + _canvasPool.allocateExtraCanvas(); } _childOverdraw = true; } @@ -666,6 +436,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { double x, double y, ) { + html.CanvasRenderingContext2D ctx = _canvasPool.context; final double letterSpacing = style.letterSpacing; if (letterSpacing == null || letterSpacing == 0.0) { ctx.fillText(line.text, x, y); @@ -692,14 +463,13 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { @override void drawParagraph(EngineParagraph paragraph, ui.Offset offset) { assert(paragraph._isLaidOut); - + html.CanvasRenderingContext2D ctx = _canvasPool.context; final ParagraphGeometricStyle style = paragraph._geometricStyle; if (paragraph._drawOnCanvas && _childOverdraw == false) { 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); @@ -719,23 +489,25 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { _drawTextLine(style, lines[i], x, y); y += paragraph._lineHeight; } - _resetPaint(); return; } final html.Element paragraphElement = _drawParagraphElement(paragraph, offset); - if (isClipped) { - final List clipElements = - _clipContent(_clipStack, paragraphElement, offset, currentTransform); + if (_canvasPool.isClipped) { + final List clipElements = _clipContent( + _canvasPool._clipStack, + paragraphElement, + offset, + _canvasPool.currentTransform); for (html.Element clipElement in clipElements) { rootElement.append(clipElement); _children.add(clipElement); } } else { - final String cssTransform = - matrix4ToCssTransform3d(transformWithOffset(currentTransform, offset)); + final String cssTransform = matrix4ToCssTransform3d( + transformWithOffset(_canvasPool.currentTransform, offset)); paragraphElement.style ..transformOrigin = '0 0 0' ..transform = cssTransform; @@ -773,6 +545,7 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { 'Linear/Radial/SweepGradient and ImageShader not supported yet'); final Int32List colors = vertices.colors; final ui.VertexMode mode = vertices.mode; + html.CanvasRenderingContext2D ctx = _canvasPool.context; if (colors == null) { final Float32List positions = mode == ui.VertexMode.triangles ? vertices.positions @@ -780,69 +553,21 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { // Draw hairline for vertices if no vertex colors are specified. save(); final ui.Color color = paint.color ?? ui.Color(0xFF000000); - _setFillAndStrokeStyle('', color.toCssString()); - _glRenderer.drawHairline(_ctx, positions); + _canvasPool.contextHandle + ..fillStyle = null + ..strokeStyle = color.toCssString(); + _glRenderer.drawHairline(ctx, positions); restore(); return; } - _glRenderer.drawVertices(_ctx, _widthInBitmapPixels, _heightInBitmapPixels, - currentTransform, vertices, blendMode, paint); + _glRenderer.drawVertices(ctx, _widthInBitmapPixels, _heightInBitmapPixels, + _canvasPool.currentTransform, vertices, blendMode, paint); } - /// 'Runs' the given [path] by applying all of its commands to the canvas. - void _runPath(SurfacePath path) { - ctx.beginPath(); - for (Subpath subpath in path.subpaths) { - for (PathCommand command in subpath.commands) { - switch (command.type) { - case PathCommandTypes.bezierCurveTo: - final BezierCurveTo curve = command; - ctx.bezierCurveTo( - curve.x1, curve.y1, curve.x2, curve.y2, curve.x3, curve.y3); - break; - case PathCommandTypes.close: - ctx.closePath(); - break; - case PathCommandTypes.ellipse: - final Ellipse ellipse = command; - ctx.ellipse( - ellipse.x, - ellipse.y, - ellipse.radiusX, - ellipse.radiusY, - ellipse.rotation, - ellipse.startAngle, - ellipse.endAngle, - ellipse.anticlockwise); - break; - case PathCommandTypes.lineTo: - final LineTo lineTo = command; - ctx.lineTo(lineTo.x, lineTo.y); - break; - case PathCommandTypes.moveTo: - final MoveTo moveTo = command; - ctx.moveTo(moveTo.x, moveTo.y); - break; - case PathCommandTypes.rRect: - final RRectCommand rrectCommand = command; - _RRectToCanvasRenderer(ctx) - .render(rrectCommand.rrect, startNewPath: false); - break; - case PathCommandTypes.rect: - final RectCommand rectCommand = command; - ctx.rect(rectCommand.x, rectCommand.y, rectCommand.width, - rectCommand.height); - break; - case PathCommandTypes.quadraticCurveTo: - final QuadraticCurveTo quadraticCurveTo = command; - ctx.quadraticCurveTo(quadraticCurveTo.x1, quadraticCurveTo.y1, - quadraticCurveTo.x2, quadraticCurveTo.y2); - break; - default: - throw UnimplementedError('Unknown path command $command'); - } - } - } + @override + void endOfPaint() { + assert(_saveCount == 0); + _canvasPool.endOfPaint(); } } @@ -1027,3 +752,8 @@ String _cssTransformAtOffset( return matrix4ToCssTransform3d( transformWithOffset(transform, ui.Offset(offsetX, offsetY))); } + +String _maskFilterToCss(ui.MaskFilter maskFilter) { + if (maskFilter == null) return 'none'; + return 'blur(${maskFilter.webOnlySigma}px)'; +} diff --git a/lib/web_ui/lib/src/engine/canvas_pool.dart b/lib/web_ui/lib/src/engine/canvas_pool.dart new file mode 100644 index 0000000000000..e3586a4b6bf04 --- /dev/null +++ b/lib/web_ui/lib/src/engine/canvas_pool.dart @@ -0,0 +1,767 @@ +// 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. + +part of engine; + +/// Allocates and caches 0 or more canvas(s) for [BitmapCanvas]. +/// +/// [BitmapCanvas] signals allocation of first canvas using allocateCanvas. +/// When a painting command such as drawImage or drawParagraph requires +/// multiple canvases for correct compositing, it calls allocateExtraCanvas and +/// adds the canvas(s) to a [_pool] of active canvas(s). +/// +/// To make sure transformations and clips are preserved correctly when a new +/// canvas is allocated, [_CanvasPool] replays the current stack on the newly +/// allocated canvas. It also maintains a [_saveContextCount] so that +/// the context stack can be reinitialized to default when reused in the future. +/// +/// On a subsequent repaint, when a Picture determines that a [BitmapCanvas] +/// can be reused, [_CanvasPool] will move canvas(s) from pool to reusablePool +/// to prevent reallocation. +class _CanvasPool extends _SaveStackTracking { + html.CanvasRenderingContext2D _context; + ContextStateHandle _contextHandle; + final int _widthInBitmapPixels, _heightInBitmapPixels; + // List of canvases that have been allocated and used in this paint cycle. + List _pool; + // List of canvases available to reuse from prior paint cycle. + List _reusablePool; + // Current canvas element or null if marked for lazy allocation. + html.CanvasElement _canvas; + html.HtmlElement _rootElement; + int _saveContextCount = 0; + + _CanvasPool(this._widthInBitmapPixels, this._heightInBitmapPixels); + + html.CanvasRenderingContext2D get context { + if (_canvas == null) { + _createCanvas(); + assert(_context != null); + assert(_canvas != null); + } + return _context; + } + + ContextStateHandle get contextHandle { + if (_canvas == null) { + _createCanvas(); + assert(_context != null); + assert(_canvas != null); + } + return _contextHandle; + } + + // Allocating extra canvas items. Save current canvas so we can dispose + // and replay the clip/transform stack on top of new canvas. + void allocateExtraCanvas() { + assert(_rootElement != null); + // Place clean copy of current canvas with context stack restored and paint + // reset into pool. + if (_canvas != null) { + _restoreContextSave(); + _contextHandle.reset(); + _pool ??= []; + _pool.add(_canvas); + _canvas = null; + _context = null; + _contextHandle = null; + } + } + + void allocateCanvas(html.HtmlElement rootElement) { + _rootElement = rootElement; + } + + void _createCanvas() { + bool requiresClearRect = false; + if (_reusablePool != null && _reusablePool.isNotEmpty) { + _canvas = _reusablePool.removeAt(0); + requiresClearRect = true; + } else { + // Compute the final CSS canvas size given the actual pixel count we + // allocated. This is done for the following reasons: + // + // * To satisfy the invariant: pixel size = css size * device pixel ratio. + // * To make sure that when we scale the canvas by devicePixelRatio (see + // _initializeViewport below) the pixels line up. + final double cssWidth = + _widthInBitmapPixels / html.window.devicePixelRatio; + final double cssHeight = + _heightInBitmapPixels / html.window.devicePixelRatio; + _canvas = html.CanvasElement( + width: _widthInBitmapPixels, + height: _heightInBitmapPixels, + ); + _canvas.style + ..position = 'absolute' + ..width = '${cssWidth}px' + ..height = '${cssHeight}px'; + } + _rootElement.append(_canvas); + _context = _canvas.context2D; + _contextHandle = ContextStateHandle(_context); + _initializeViewport(); + if (requiresClearRect) { + // Now that the context is reset, clear old contents. + _context.clearRect(0, 0, _widthInBitmapPixels, _heightInBitmapPixels); + } + _replayClipStack(); + } + + @override + void clear() { + super.clear(); + + if (_canvas != null) { + // Restore to the state where we have only applied the scaling. + html.CanvasRenderingContext2D ctx = _context; + if (ctx != null) { + try { + ctx.font = ''; + } catch (e) { + // Firefox may explode here: + // https://bugzilla.mozilla.org/show_bug.cgi?id=941146 + if (!_isNsErrorFailureException(e)) { + rethrow; + } + } + } + } + reuse(); + resetTransform(); + } + + set initialTransform(ui.Offset transform) { + translate(transform.dx, transform.dy); + } + + int _replaySingleSaveEntry( + int clipDepth, Matrix4 transform, List<_SaveClipEntry> clipStack) { + final html.CanvasRenderingContext2D ctx = _context; + if (!transform.isIdentity()) { + ctx.setTransform(transform[0], transform[1], transform[4], transform[5], + transform[12], transform[13]); + } + if (clipStack != null) { + for (int clipCount = clipStack.length; + clipDepth < clipCount; + clipDepth++) { + _SaveClipEntry clipEntry = clipStack[clipDepth]; + if (clipEntry.rect != null) { + _clipRect(ctx, clipEntry.rect); + } else if (clipEntry.rrect != null) { + _clipRRect(ctx, clipEntry.rrect); + } else if (clipEntry.path != null) { + _runPath(ctx, clipEntry.path); + ctx.clip(); + } + } + } + return clipDepth; + } + + void _replayClipStack() { + // Replay save/clip stack on this canvas now. + html.CanvasRenderingContext2D ctx = _context; + int clipDepth = 0; + for (int saveStackIndex = 0, len = _saveStack.length; + saveStackIndex < len; + saveStackIndex++) { + _SaveStackEntry saveEntry = _saveStack[saveStackIndex]; + clipDepth = _replaySingleSaveEntry( + clipDepth, saveEntry.transform, saveEntry.clipStack); + ctx.save(); + ++_saveContextCount; + } + _replaySingleSaveEntry(clipDepth, _currentTransform, _clipStack); + } + + // Marks this pool for reuse. + void reuse() { + if (_canvas != null) { + _restoreContextSave(); + _contextHandle.reset(); + _pool ??= []; + _pool.add(_canvas); + _context = null; + _contextHandle = null; + } + _reusablePool = _pool; + _pool = null; + _canvas = null; + _context = null; + _contextHandle = null; + } + + void endOfPaint() { + if (_reusablePool != null) { + for (html.CanvasElement e in _reusablePool) { + e.remove(); + } + _reusablePool = null; + } + _restoreContextSave(); + } + + void _restoreContextSave() { + while (_saveContextCount != 0) { + _context.restore(); + --_saveContextCount; + } + } + + /// Configures the canvas such that its coordinate system follows the scene's + /// coordinate system, and the pixel ratio is applied such that CSS pixels are + /// translated to bitmap pixels. + void _initializeViewport() { + html.CanvasRenderingContext2D ctx = context; + // Save the canvas state with top-level transforms so we can undo + // any clips later when we reuse the canvas. + ctx.save(); + ++_saveContextCount; + + // We always start with identity transform because the surrounding transform + // is applied on the DOM elements. + ctx.setTransform(1, 0, 0, 1, 0, 0); + + // This scale makes sure that 1 CSS pixel is translated to the correct + // number of bitmap pixels. + ctx.scale(html.window.devicePixelRatio, html.window.devicePixelRatio); + } + + void resetTransform() { + if (_canvas != null) { + _canvas.style.transformOrigin = ''; + _canvas.style.transform = ''; + } + } + + // Returns a data URI containing a representation of the image in this + // canvas. + String toDataUrl() => _canvas.toDataUrl(); + + @override + void save() { + super.save(); + if (_canvas != null) { + context.save(); + ++_saveContextCount; + } + } + + @override + void restore() { + super.restore(); + if (_canvas != null) { + context.restore(); + contextHandle.reset(); + --_saveContextCount; + } + } + + @override + void translate(double dx, double dy) { + super.translate(dx, dy); + if (_canvas != null) { + context.translate(dx, dy); + } + } + + @override + void scale(double sx, double sy) { + super.scale(sx, sy); + if (_canvas != null) { + context.scale(sx, sy); + } + } + + @override + void rotate(double radians) { + super.rotate(radians); + if (_canvas != null) { + context.rotate(radians); + } + } + + @override + void skew(double sx, double sy) { + super.skew(sx, sy); + if (_canvas != null) { + context.transform(1, sy, sx, 1, 0, 0); + // | | | | | | + // | | | | | f - vertical translation + // | | | | e - horizontal translation + // | | | d - vertical scaling + // | | c - horizontal skewing + // | b - vertical skewing + // a - horizontal scaling + // + // Source: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/transform + } + } + + @override + void transform(Float64List matrix4) { + super.transform(matrix4); + // Canvas2D transform API: + // + // ctx.transform(a, b, c, d, e, f); + // + // In 3x3 matrix form assuming vector representation of (x, y, 1): + // + // a c e + // b d f + // 0 0 1 + // + // This translates to 4x4 matrix with vector representation of (x, y, z, 1) + // as: + // + // a c 0 e + // b d 0 f + // 0 0 1 0 + // 0 0 0 1 + // + // This matrix is sufficient to represent 2D rotates, translates, scales, + // and skews. + if (_canvas != null) { + context.transform(matrix4[0], matrix4[1], matrix4[4], matrix4[5], + matrix4[12], matrix4[13]); + } + } + + void clipRect(ui.Rect rect) { + super.clipRect(rect); + if (_canvas != null) { + _clipRect(context, rect); + } + } + + void _clipRect(html.CanvasRenderingContext2D ctx, ui.Rect rect) { + ctx.beginPath(); + ctx.rect(rect.left, rect.top, rect.width, rect.height); + ctx.clip(); + } + + void clipRRect(ui.RRect rrect) { + super.clipRRect(rrect); + if (_canvas != null) { + _clipRRect(context, rrect); + } + } + + void _clipRRect(html.CanvasRenderingContext2D ctx, ui.RRect rrect) { + final ui.Path path = ui.Path()..addRRect(rrect); + _runPath(ctx, path); + ctx.clip(); + } + + void clipPath(ui.Path path) { + super.clipPath(path); + if (_canvas != null) { + html.CanvasRenderingContext2D ctx = context; + _runPath(ctx, path); + ctx.clip(); + } + } + + void drawColor(ui.Color color, ui.BlendMode blendMode) { + html.CanvasRenderingContext2D ctx = context; + contextHandle.blendMode = blendMode; + contextHandle.fillStyle = color.toCssString(); + contextHandle.strokeStyle = ''; + ctx.beginPath(); + // Fill a virtually infinite rect with the color. + // + // We can't use (0, 0, width, height) because the current transform can + // cause it to not fill the entire clip. + ctx.fillRect(-10000, -10000, 20000, 20000); + } + + // Fill a virtually infinite rect with the color. + void fill() { + html.CanvasRenderingContext2D ctx = context; + ctx.beginPath(); + // We can't use (0, 0, width, height) because the current transform can + // cause it to not fill the entire clip. + ctx.fillRect(-10000, -10000, 20000, 20000); + } + + void strokeLine(ui.Offset p1, ui.Offset p2) { + html.CanvasRenderingContext2D ctx = context; + ctx.beginPath(); + ctx.moveTo(p1.dx, p1.dy); + ctx.lineTo(p2.dx, p2.dy); + ctx.stroke(); + } + + /// 'Runs' the given [path] by applying all of its commands to the canvas. + void _runPath(html.CanvasRenderingContext2D ctx, SurfacePath path) { + ctx.beginPath(); + for (Subpath subpath in path.subpaths) { + for (PathCommand command in subpath.commands) { + switch (command.type) { + case PathCommandTypes.bezierCurveTo: + final BezierCurveTo curve = command; + ctx.bezierCurveTo( + curve.x1, curve.y1, curve.x2, curve.y2, curve.x3, curve.y3); + break; + case PathCommandTypes.close: + ctx.closePath(); + break; + case PathCommandTypes.ellipse: + final Ellipse ellipse = command; + ctx.ellipse( + ellipse.x, + ellipse.y, + ellipse.radiusX, + ellipse.radiusY, + ellipse.rotation, + ellipse.startAngle, + ellipse.endAngle, + ellipse.anticlockwise); + break; + case PathCommandTypes.lineTo: + final LineTo lineTo = command; + ctx.lineTo(lineTo.x, lineTo.y); + break; + case PathCommandTypes.moveTo: + final MoveTo moveTo = command; + ctx.moveTo(moveTo.x, moveTo.y); + break; + case PathCommandTypes.rRect: + final RRectCommand rrectCommand = command; + _RRectToCanvasRenderer(ctx) + .render(rrectCommand.rrect, startNewPath: false); + break; + case PathCommandTypes.rect: + final RectCommand rectCommand = command; + ctx.rect(rectCommand.x, rectCommand.y, rectCommand.width, + rectCommand.height); + break; + case PathCommandTypes.quadraticCurveTo: + final QuadraticCurveTo quadraticCurveTo = command; + ctx.quadraticCurveTo(quadraticCurveTo.x1, quadraticCurveTo.y1, + quadraticCurveTo.x2, quadraticCurveTo.y2); + break; + default: + throw UnimplementedError('Unknown path command $command'); + } + } + } + } + + void drawRect(ui.Rect rect, ui.PaintingStyle style) { + context.beginPath(); + context.rect(rect.left, rect.top, rect.width, rect.height); + contextHandle.paint(style); + } + + void drawRRect(ui.RRect roundRect, ui.PaintingStyle style) { + _RRectToCanvasRenderer(context).render(roundRect); + contextHandle.paint(style); + } + + void drawDRRect(ui.RRect outer, ui.RRect inner, ui.PaintingStyle style) { + _RRectRenderer renderer = _RRectToCanvasRenderer(context); + renderer.render(outer); + renderer.render(inner, startNewPath: false, reverse: true); + contextHandle.paint(style); + } + + void drawOval(ui.Rect rect, ui.PaintingStyle style) { + context.beginPath(); + context.ellipse(rect.center.dx, rect.center.dy, rect.width / 2, + rect.height / 2, 0, 0, 2.0 * math.pi, false); + contextHandle.paint(style); + } + + void drawCircle(ui.Offset c, double radius, ui.PaintingStyle style) { + context.beginPath(); + context.ellipse(c.dx, c.dy, radius, radius, 0, 0, 2.0 * math.pi, false); + contextHandle.paint(style); + } + + void drawPath(ui.Path path, ui.PaintingStyle style) { + _runPath(context, path); + contextHandle.paint(style); + } + + void drawShadow(ui.Path path, ui.Color color, double elevation, + bool transparentOccluder) { + final List shadows = + ElevationShadow.computeCanvasShadows(elevation, color); + if (shadows.isNotEmpty) { + for (final CanvasShadow shadow in shadows) { + // TODO(het): Shadows with transparent occluders are not supported + // on webkit since filter is unsupported. + if (transparentOccluder && browserEngine != BrowserEngine.webkit) { + // We paint shadows using a path and a mask filter instead of the + // built-in shadow* properties. This is because the color alpha of the + // paint is added to the shadow. The effect we're looking for is to just + // paint the shadow without the path itself, but if we use a non-zero + // alpha for the paint the path is painted in addition to the shadow, + // which is undesirable. + context.save(); + context.translate(shadow.offsetX, shadow.offsetY); + context.filter = _maskFilterToCss( + ui.MaskFilter.blur(ui.BlurStyle.normal, shadow.blur)); + context.strokeStyle = ''; + context.fillStyle = shadow.color.toCssString(); + _runPath(context, path); + context.fill(); + context.restore(); + } else { + // TODO(het): We fill the path with this paint, then later we clip + // by the same path and fill it with a fully opaque color (we know + // the color is fully opaque because `transparentOccluder` is false. + // However, due to anti-aliasing of the clip, a few pixels of the + // path we are about to paint may still be visible after we fill with + // the opaque occluder. For that reason, we fill with the shadow color, + // and set the shadow color to fully opaque. This way, the visible + // pixels are less opaque and less noticeable. + context.save(); + context.filter = 'none'; + context.strokeStyle = ''; + context.fillStyle = shadow.color.toCssString(); + context.shadowBlur = shadow.blur; + context.shadowColor = shadow.color.withAlpha(0xff).toCssString(); + context.shadowOffsetX = shadow.offsetX; + context.shadowOffsetY = shadow.offsetY; + _runPath(context, path); + context.fill(); + context.restore(); + } + } + } + } + + void dispose() { + // Webkit has a threshold for the amount of canvas pixels an app can + // allocate. Even though our canvases are being garbage-collected as + // expected when we don't need them, Webkit keeps track of their sizes + // towards the threshold. Setting width and height to zero tricks Webkit + // into thinking that this canvas has a zero size so it doesn't count it + // towards the threshold. + if (browserEngine == BrowserEngine.webkit && _canvas != null) { + _canvas.width = _canvas.height = 0; + } + _clearPool(); + } + + void _clearPool() { + if (_pool != null) { + for (html.CanvasElement c in _pool) { + if (browserEngine == BrowserEngine.webkit) { + c.width = c.height = 0; + } + c.remove(); + } + } + _pool = null; + } +} + +// Optimizes applying paint parameters to html canvas. +// +// See https://www.w3.org/TR/2dcontext/ for defaults used in this class +// to initialize current values. +// +class ContextStateHandle { + html.CanvasRenderingContext2D context; + ContextStateHandle(this.context); + ui.BlendMode _currentBlendMode = ui.BlendMode.srcOver; + ui.StrokeCap _currentStrokeCap = ui.StrokeCap.butt; + ui.StrokeJoin _currentStrokeJoin = ui.StrokeJoin.miter; + // Fill style and stroke style are Object since they can have a String or + // shader object such as a gradient. + Object _currentFillStyle; + Object _currentStrokeStyle; + double _currentLineWidth = 1.0; + String _currentFilter = 'none'; + + set blendMode(ui.BlendMode blendMode) { + if (blendMode != _currentBlendMode) { + _currentBlendMode = blendMode; + context.globalCompositeOperation = + _stringForBlendMode(blendMode) ?? 'source-over'; + } + } + + set strokeCap(ui.StrokeCap strokeCap) { + strokeCap ??= ui.StrokeCap.butt; + if (strokeCap != _currentStrokeCap) { + _currentStrokeCap = strokeCap; + context.lineCap = _stringForStrokeCap(strokeCap); + } + } + + set lineWidth(double lineWidth) { + if (lineWidth != _currentLineWidth) { + _currentLineWidth = lineWidth; + context.lineWidth = lineWidth; + } + } + + set strokeJoin(ui.StrokeJoin strokeJoin) { + strokeJoin ??= ui.StrokeJoin.miter; + if (strokeJoin != _currentStrokeJoin) { + _currentStrokeJoin = strokeJoin; + context.lineJoin = _stringForStrokeJoin(strokeJoin); + } + } + + set fillStyle(Object colorOrGradient) { + if (!identical(colorOrGradient, _currentFillStyle)) { + _currentFillStyle = colorOrGradient; + context.fillStyle = colorOrGradient; + } + } + + set strokeStyle(Object colorOrGradient) { + if (!identical(colorOrGradient, _currentStrokeStyle)) { + _currentStrokeStyle = colorOrGradient; + context.strokeStyle = colorOrGradient; + } + } + + set filter(String filter) { + if (_currentFilter != filter) { + _currentFilter = filter; + context.filter = filter; + } + } + + void paint(ui.PaintingStyle style) { + if (style == ui.PaintingStyle.stroke) { + context.stroke(); + } else { + context.fill(); + } + } + + void reset() { + context.fillStyle = ''; + // Read back fillStyle/strokeStyle values from context so that input such + // as rgba(0, 0, 0, 0) is correctly compared and doesn't cause diff on + // setter. + _currentFillStyle = context.fillStyle; + context.strokeStyle = ''; + _currentStrokeStyle = context.strokeStyle; + context.filter = 'none'; + _currentFilter = 'none'; + context.globalCompositeOperation = 'source-over'; + _currentBlendMode = ui.BlendMode.srcOver; + context.lineWidth = 1.0; + _currentLineWidth = 1.0; + context.lineCap = 'butt'; + _currentStrokeCap = ui.StrokeCap.butt; + context.lineJoin = 'miter'; + _currentStrokeJoin = ui.StrokeJoin.miter; + } +} + +/// Provides save stack tracking functionality to implementations of +/// [EngineCanvas]. +class _SaveStackTracking { + // !Warning: this vector should not be mutated. + static final Vector3 _unitZ = Vector3(0.0, 0.0, 1.0); + + final List<_SaveStackEntry> _saveStack = <_SaveStackEntry>[]; + + /// The stack that maintains clipping operations used when text is painted + /// onto bitmap canvas but is composited as separate element. + List<_SaveClipEntry> _clipStack; + + /// Returns whether there are active clipping regions on the canvas. + bool get isClipped => _clipStack != null; + + /// Empties the save stack and the element stack, and resets the transform + /// and clip parameters. + @mustCallSuper + void clear() { + _saveStack.clear(); + _clipStack = null; + _currentTransform = Matrix4.identity(); + } + + /// The current transformation matrix. + Matrix4 get currentTransform => _currentTransform; + Matrix4 _currentTransform = Matrix4.identity(); + + /// Saves current clip and transform on the save stack. + @mustCallSuper + void save() { + _saveStack.add(_SaveStackEntry( + transform: _currentTransform.clone(), + clipStack: + _clipStack == null ? null : List<_SaveClipEntry>.from(_clipStack), + )); + } + + /// Restores current clip and transform from the save stack. + @mustCallSuper + void restore() { + if (_saveStack.isEmpty) { + return; + } + final _SaveStackEntry entry = _saveStack.removeLast(); + _currentTransform = entry.transform; + _clipStack = entry.clipStack; + } + + /// Multiplies the [currentTransform] matrix by a translation. + @mustCallSuper + void translate(double dx, double dy) { + _currentTransform.translate(dx, dy); + } + + /// Scales the [currentTransform] matrix. + @mustCallSuper + void scale(double sx, double sy) { + _currentTransform.scale(sx, sy); + } + + /// Rotates the [currentTransform] matrix. + @mustCallSuper + void rotate(double radians) { + _currentTransform.rotate(_unitZ, radians); + } + + /// Skews the [currentTransform] matrix. + @mustCallSuper + void skew(double sx, double sy) { + final Matrix4 skewMatrix = Matrix4.identity(); + final Float64List storage = skewMatrix.storage; + storage[1] = sy; + storage[4] = sx; + _currentTransform.multiply(skewMatrix); + } + + /// Multiplies the [currentTransform] matrix by another matrix. + @mustCallSuper + void transform(Float64List matrix4) { + _currentTransform.multiply(Matrix4.fromFloat64List(matrix4)); + } + + /// Adds a rectangle to clipping stack. + @mustCallSuper + void clipRect(ui.Rect rect) { + _clipStack ??= <_SaveClipEntry>[]; + _clipStack.add(_SaveClipEntry.rect(rect, _currentTransform.clone())); + } + + /// Adds a round rectangle to clipping stack. + @mustCallSuper + void clipRRect(ui.RRect rrect) { + _clipStack ??= <_SaveClipEntry>[]; + _clipStack.add(_SaveClipEntry.rrect(rrect, _currentTransform.clone())); + } + + /// Adds a path to clipping stack. + @mustCallSuper + void clipPath(ui.Path path) { + _clipStack ??= <_SaveClipEntry>[]; + _clipStack.add(_SaveClipEntry.path(path, _currentTransform.clone())); + } +} diff --git a/lib/web_ui/lib/src/engine/dom_canvas.dart b/lib/web_ui/lib/src/engine/dom_canvas.dart index 8618b35d6ae82..7a8f377662c95 100644 --- a/lib/web_ui/lib/src/engine/dom_canvas.dart +++ b/lib/web_ui/lib/src/engine/dom_canvas.dart @@ -175,8 +175,13 @@ class DomCanvas extends EngineCanvas with SaveElementStackTracking { } @override - void drawVertices(ui.Vertices vertices, ui.BlendMode blendMode, - SurfacePaintData paint) { + void drawVertices( + ui.Vertices vertices, ui.BlendMode blendMode, SurfacePaintData paint) { throw UnimplementedError(); } + + @override + void endOfPaint() { + // No reuse of elements yet to handle here. Noop. + } } diff --git a/lib/web_ui/lib/src/engine/engine_canvas.dart b/lib/web_ui/lib/src/engine/engine_canvas.dart index 222a4e3f41996..de2fff8f6201d 100644 --- a/lib/web_ui/lib/src/engine/engine_canvas.dart +++ b/lib/web_ui/lib/src/engine/engine_canvas.dart @@ -66,8 +66,12 @@ abstract class EngineCanvas { void drawParagraph(EngineParagraph paragraph, ui.Offset offset); - void drawVertices(ui.Vertices vertices, ui.BlendMode blendMode, - SurfacePaintData paint); + void drawVertices( + ui.Vertices vertices, ui.BlendMode blendMode, SurfacePaintData paint); + + /// Extension of Canvas API to mark the end of a stream of painting commands + /// to enable re-use/dispose optimizations. + void endOfPaint(); } /// Adds an [offset] transformation to a [transform] matrix and returns the diff --git a/lib/web_ui/lib/src/engine/houdini_canvas.dart b/lib/web_ui/lib/src/engine/houdini_canvas.dart index 5ab3c34d5358d..26e7e07da44dc 100644 --- a/lib/web_ui/lib/src/engine/houdini_canvas.dart +++ b/lib/web_ui/lib/src/engine/houdini_canvas.dart @@ -229,10 +229,13 @@ class HoudiniCanvas extends EngineCanvas with SaveElementStackTracking { } @override - void drawVertices(ui.Vertices vertices, ui.BlendMode blendMode, - SurfacePaintData paint) { + void drawVertices( + ui.Vertices vertices, ui.BlendMode blendMode, SurfacePaintData paint) { // TODO(flutter_web): implement. } + + @override + void endOfPaint() {} } class _SaveElementStackEntry { diff --git a/lib/web_ui/lib/src/engine/picture.dart b/lib/web_ui/lib/src/engine/picture.dart index 9175a3d383147..a9c41abea7ceb 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 { Future toImage(int width, int height) async { final BitmapCanvas canvas = BitmapCanvas(ui.Rect.fromLTRB(0, 0, width.toDouble(), height.toDouble())); recordingCanvas.apply(canvas); - final String imageDataUrl = canvas.canvas.toDataUrl(); + final String imageDataUrl = canvas.toDataUrl(); final html.ImageElement imageElement = html.ImageElement() ..src = imageDataUrl ..width = width diff --git a/lib/web_ui/lib/src/engine/surface/painting.dart b/lib/web_ui/lib/src/engine/surface/painting.dart index 0c759862d0016..0322cfac58c4f 100644 --- a/lib/web_ui/lib/src/engine/surface/painting.dart +++ b/lib/web_ui/lib/src/engine/surface/painting.dart @@ -900,7 +900,7 @@ class SurfacePath implements ui.Path { -BitmapCanvas.kPaddingPixels.toDouble()); _rawRecorder.drawPath( this, (SurfacePaint()..color = const ui.Color(0xFF000000)).paintData); - final bool result = _rawRecorder.ctx.isPointInPath(pointX, pointY); + final bool result = _rawRecorder._canvasPool.context.isPointInPath(pointX, pointY); _rawRecorder.dispose(); return result; } diff --git a/lib/web_ui/lib/src/engine/surface/picture.dart b/lib/web_ui/lib/src/engine/surface/picture.dart index a26b2fcd4e68e..336dd0ca74bc4 100644 --- a/lib/web_ui/lib/src/engine/surface/picture.dart +++ b/lib/web_ui/lib/src/engine/surface/picture.dart @@ -169,10 +169,10 @@ class PersistedStandardPicture extends PersistedPicture { // The canvas needs to be resized before painting. return 1.0; } else { - final int newPixelCount = oldCanvas._widthToPhysical(_exactLocalCullRect.width) - * oldCanvas._heightToPhysical(_exactLocalCullRect.height); + final int newPixelCount = BitmapCanvas._widthToPhysical(_exactLocalCullRect.width) + * BitmapCanvas._heightToPhysical(_exactLocalCullRect.height); final int oldPixelCount = - oldCanvas.widthInBitmapPixels * oldCanvas.heightInBitmapPixels; + oldCanvas._widthInBitmapPixels * oldCanvas._heightInBitmapPixels; if (oldPixelCount == 0) { return 1.0; @@ -320,7 +320,7 @@ class PersistedStandardPicture extends PersistedPicture { _surfaceStatsFor(this) ..allocateBitmapCanvasCount += 1 ..allocatedBitmapSizeInPixels = - canvas.widthInBitmapPixels * canvas.heightInBitmapPixels; + canvas._widthInBitmapPixels * canvas._heightInBitmapPixels; } return canvas; } diff --git a/lib/web_ui/lib/src/engine/surface/recording_canvas.dart b/lib/web_ui/lib/src/engine/surface/recording_canvas.dart index 07031bcb89890..f964323c49142 100644 --- a/lib/web_ui/lib/src/engine/surface/recording_canvas.dart +++ b/lib/web_ui/lib/src/engine/surface/recording_canvas.dart @@ -85,6 +85,7 @@ class RecordingCanvas { } } } + engineCanvas.endOfPaint(); } /// Prints recorded commands. diff --git a/lib/web_ui/test/canvas_test.dart b/lib/web_ui/test/canvas_test.dart index 8a00c8c5d77f2..12b84b1226631 100644 --- a/lib/web_ui/test/canvas_test.dart +++ b/lib/web_ui/test/canvas_test.dart @@ -41,7 +41,7 @@ void main() { canvas.clear(); recordingCanvas.apply(canvas); }, whenDone: () { - expect(mockCanvas.methodCallLog, hasLength(2)); + expect(mockCanvas.methodCallLog, hasLength(3)); MockCanvasCall call = mockCanvas.methodCallLog[0]; expect(call.methodName, 'clear'); @@ -64,8 +64,9 @@ void main() { canvas.clear(); recordingCanvas.apply(canvas); }, whenDone: () { - expect(mockCanvas.methodCallLog, hasLength(1)); + expect(mockCanvas.methodCallLog, hasLength(2)); expect(mockCanvas.methodCallLog[0].methodName, 'clear'); + expect(mockCanvas.methodCallLog[1].methodName, 'endOfPaint'); }); }); } diff --git a/lib/web_ui/test/engine/recording_canvas_test.dart b/lib/web_ui/test/engine/recording_canvas_test.dart index ed8c7ac5beb04..bfa13cb84dddf 100644 --- a/lib/web_ui/test/engine/recording_canvas_test.dart +++ b/lib/web_ui/test/engine/recording_canvas_test.dart @@ -9,7 +9,6 @@ import 'package:test/test.dart'; import '../mock_engine_canvas.dart'; void main() { - RecordingCanvas underTest; MockEngineCanvas mockCanvas; @@ -20,44 +19,51 @@ void main() { group('drawDRRect', () { final RRect rrect = RRect.fromLTRBR(10, 10, 50, 50, Radius.circular(3)); - final SurfacePaint somePaint = SurfacePaint()..color = const Color(0xFFFF0000); + final SurfacePaint somePaint = SurfacePaint() + ..color = const Color(0xFFFF0000); test('Happy case', () { underTest.drawDRRect(rrect, rrect.deflate(1), somePaint); underTest.apply(mockCanvas); _expectDrawCall(mockCanvas, { - 'outer': rrect, - 'inner': rrect.deflate(1), - 'paint': somePaint.paintData, - }); + 'outer': rrect, + 'inner': rrect.deflate(1), + 'paint': somePaint.paintData, + }); }); test('Inner RRect > Outer RRect', () { underTest.drawDRRect(rrect, rrect.inflate(1), somePaint); underTest.apply(mockCanvas); // Expect nothing to be called - expect(mockCanvas.methodCallLog.length, equals(0)); + expect(mockCanvas.methodCallLog.length, equals(1)); + expect(mockCanvas.methodCallLog.single.methodName, 'endOfPaint'); }); test('Inner RRect not completely inside Outer RRect', () { - underTest.drawDRRect(rrect, rrect.deflate(1).shift(const Offset(0.0, 10)), somePaint); + underTest.drawDRRect( + rrect, rrect.deflate(1).shift(const Offset(0.0, 10)), somePaint); underTest.apply(mockCanvas); // Expect nothing to be called - expect(mockCanvas.methodCallLog.length, equals(0)); + expect(mockCanvas.methodCallLog.length, equals(1)); + expect(mockCanvas.methodCallLog.single.methodName, 'endOfPaint'); }); test('Inner RRect same as Outer RRect', () { underTest.drawDRRect(rrect, rrect, somePaint); underTest.apply(mockCanvas); // Expect nothing to be called - expect(mockCanvas.methodCallLog.length, equals(0)); + expect(mockCanvas.methodCallLog.length, equals(1)); + expect(mockCanvas.methodCallLog.single.methodName, 'endOfPaint'); }); test('negative corners in inner RRect get passed through to draw', () { // This comes from github issue #40728 - final RRect outer = RRect.fromRectAndCorners(const Rect.fromLTWH(0, 0, 88, 48), - topLeft: Radius.circular(6), bottomLeft: Radius.circular(6)); + final RRect outer = RRect.fromRectAndCorners( + const Rect.fromLTWH(0, 0, 88, 48), + topLeft: Radius.circular(6), + bottomLeft: Radius.circular(6)); final RRect inner = outer.deflate(1); // If these assertions fail, check [_measureBorderRadius] in recording_canvas.dart @@ -76,24 +82,27 @@ void main() { }); test('preserve old scuba test behavior', () { - final RRect outer = RRect.fromRectAndCorners(const Rect.fromLTRB(10, 20, 30, 40)); - final RRect inner = RRect.fromRectAndCorners(const Rect.fromLTRB(12, 22, 28, 38)); + final RRect outer = + RRect.fromRectAndCorners(const Rect.fromLTRB(10, 20, 30, 40)); + final RRect inner = + RRect.fromRectAndCorners(const Rect.fromLTRB(12, 22, 28, 38)); underTest.drawDRRect(outer, inner, somePaint); underTest.apply(mockCanvas); _expectDrawCall(mockCanvas, { - 'outer': outer, - 'inner': inner, - 'paint': somePaint.paintData, - }); + 'outer': outer, + 'inner': inner, + 'paint': somePaint.paintData, + }); }); }); } // Expect a drawDRRect call to be registered in the mock call log, with the expectedArguments -void _expectDrawCall(MockEngineCanvas mock, Map expectedArguments) { - expect(mock.methodCallLog.length, equals(1)); +void _expectDrawCall( + MockEngineCanvas mock, Map expectedArguments) { + expect(mock.methodCallLog.length, equals(2)); MockCanvasCall mockCall = mock.methodCallLog[0]; expect(mockCall.methodName, equals('drawDRRect')); expect(mockCall.arguments, equals(expectedArguments)); 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 eccc179af8962..557304e424ee9 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 @@ -74,6 +74,7 @@ void main() async { Paint() ..style = PaintingStyle.fill ..color = const Color.fromARGB(128, 255, 0, 0)); + rc.restore(); await _checkScreenshot(rc, 'canvas_blend_circle_diff_color'); }); @@ -109,6 +110,7 @@ void main() async { ..color = const Color.fromARGB(128, 255, 0, 0)); rc.drawImage(createTestImage(), Offset(135.0, 130.0), Paint()..blendMode = BlendMode.multiply); + rc.restore(); await _checkScreenshot(rc, 'canvas_blend_image_multiply'); }); } 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 8b4d5e5d6a9cb..04881d5121f32 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 @@ -19,8 +19,7 @@ void main() 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) }) async { - + {Rect region = const Rect.fromLTWH(0, 0, 500, 500)}) async { final EngineCanvas engineCanvas = BitmapCanvas(screenRect); rc.apply(engineCanvas); @@ -47,35 +46,38 @@ void main() async { test('Paints image', () async { final RecordingCanvas rc = - RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); rc.save(); rc.drawImage(createTestImage(), Offset(0, 0), new Paint()); + rc.restore(); await _checkScreenshot(rc, 'draw_image'); }); test('Paints image with transform', () async { final RecordingCanvas rc = - RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + 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()); + rc.restore(); 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)); + 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()); + rc.restore(); 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)); + RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); rc.save(); rc.translate(50.0, 100.0); rc.rotate(math.pi / 4.0); @@ -84,53 +86,233 @@ void main() async { double testHeight = testImage.height.toDouble(); rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight), Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint()); + rc.restore(); 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)); + 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()); + rc.drawImageRect( + testImage, + Rect.fromLTRB(testWidth / 2, 0, testWidth, testHeight), + Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), + new Paint()); + rc.restore(); await _checkScreenshot(rc, 'draw_image_rect_with_source'); }); test('Paints image with source and destination and round clip', () async { final RecordingCanvas rc = - RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); Image testImage = createTestImage(); double testWidth = testImage.width.toDouble(); double testHeight = testImage.height.toDouble(); rc.save(); - rc.clipRRect(RRect.fromLTRBR(100, 30, 2 * testWidth, 2 * testHeight, Radius.circular(16))); - rc.drawImageRect(testImage, Rect.fromLTRB(testWidth / 2, 0, testWidth, testHeight), - Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint()); + rc.clipRRect(RRect.fromLTRBR( + 100, 30, 2 * testWidth, 2 * testHeight, Radius.circular(16))); + rc.drawImageRect( + testImage, + Rect.fromLTRB(testWidth / 2, 0, testWidth, testHeight), + Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), + new Paint()); + rc.restore(); await _checkScreenshot(rc, 'draw_image_rect_with_source_and_clip'); }); test('Paints image with transform using source and destination', () async { final RecordingCanvas rc = - RecordingCanvas(const Rect.fromLTRB(0, 0, 400, 300)); + 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()); + rc.drawImageRect( + testImage, + Rect.fromLTRB(testWidth / 2, 0, testWidth, testHeight), + Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), + new Paint()); + rc.restore(); await _checkScreenshot(rc, 'draw_image_rect_with_transform_source'); }); + + // Regression test for https://github.com/flutter/flutter/issues/44845 + // Circle should draw on top of image not below. + test('Paints on top of image', () 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(0, 0, testWidth, testHeight), + Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint()); + rc.drawCircle( + Offset(100, 100), + 50.0, + Paint() + ..strokeWidth = 3 + ..color = Color.fromARGB(128, 0, 0, 0)); + rc.restore(); + await _checkScreenshot(rc, 'draw_circle_on_image'); + }); + + // Regression test for https://github.com/flutter/flutter/issues/44845 + // Circle should below image not on top. + test('Paints below image', () 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.drawCircle( + Offset(100, 100), + 50.0, + Paint() + ..strokeWidth = 3 + ..color = Color.fromARGB(128, 0, 0, 0)); + rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight), + Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint()); + rc.restore(); + await _checkScreenshot(rc, 'draw_circle_below_image'); + }); + + // Regression test for https://github.com/flutter/flutter/issues/44845 + // Circle should draw on top of image with clip rect. + test('Paints on top of image with clip rect', () 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.clipRect(Rect.fromLTRB(75, 75, 160, 160)); + rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight), + Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint()); + rc.drawCircle( + Offset(100, 100), + 50.0, + Paint() + ..strokeWidth = 3 + ..color = Color.fromARGB(128, 0, 0, 0)); + rc.restore(); + await _checkScreenshot(rc, 'draw_circle_on_image_clip_rect'); + }); + + // Regression test for https://github.com/flutter/flutter/issues/44845 + // Circle should draw on top of image with clip rect and transform. + test('Paints on top of image with clip rect with transform', () 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(); + // Rotate around center of circle. + rc.translate(100, 100); + rc.rotate(math.pi / 4.0); + rc.translate(-100, -100); + rc.clipRect(Rect.fromLTRB(75, 75, 160, 160)); + rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight), + Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint()); + rc.drawCircle( + Offset(100, 100), + 50.0, + Paint() + ..strokeWidth = 3 + ..color = Color.fromARGB(128, 0, 0, 0)); + rc.restore(); + await _checkScreenshot(rc, 'draw_circle_on_image_clip_rect_with_transform'); + }); + + // Regression test for https://github.com/flutter/flutter/issues/44845 + // Circle should draw on top of image with stack of clip rect and transforms. + test('Paints on top of image with clip rect with stack', () 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(); + // Rotate around center of circle. + rc.translate(100, 100); + rc.rotate(-math.pi / 4.0); + rc.save(); + rc.translate(-100, -100); + rc.clipRect(Rect.fromLTRB(75, 75, 160, 160)); + rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight), + Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), new Paint()); + rc.drawCircle( + Offset(100, 100), + 50.0, + Paint() + ..strokeWidth = 3 + ..color = Color.fromARGB(128, 0, 0, 0)); + rc.restore(); + rc.restore(); + await _checkScreenshot(rc, 'draw_circle_on_image_clip_rect_with_stack'); + }); + + // Regression test for https://github.com/flutter/flutter/issues/44845 + // Circle should draw on top of image with clip rrect. + test('Paints on top of image with clip rrect', () 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.clipRRect(RRect.fromLTRBR(75, 75, 160, 160, Radius.circular(5))); + rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight), + Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), Paint()); + rc.drawCircle( + Offset(100, 100), + 50.0, + Paint() + ..strokeWidth = 3 + ..color = Color.fromARGB(128, 0, 0, 0)); + rc.restore(); + await _checkScreenshot(rc, 'draw_circle_on_image_clip_rrect'); + }); + + // Regression test for https://github.com/flutter/flutter/issues/44845 + // Circle should draw on top of image with clip rrect. + test('Paints on top of image with clip path', () 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(); + final Path path = Path(); + // Triangle. + path.moveTo(118, 57); + path.lineTo(75, 160); + path.lineTo(160, 160); + rc.clipPath(path); + rc.drawImageRect(testImage, Rect.fromLTRB(0, 0, testWidth, testHeight), + Rect.fromLTRB(100, 30, 2 * testWidth, 2 * testHeight), Paint()); + rc.drawCircle( + Offset(100, 100), + 50.0, + Paint() + ..strokeWidth = 3 + ..color = Color.fromARGB(128, 0, 0, 0)); + rc.restore(); + await _checkScreenshot(rc, 'draw_circle_on_image_clip_path'); + }); } -HtmlImage createTestImage() { - const int width = 100; - const int height = 50; - html.CanvasElement canvas = new html.CanvasElement(width: width, height: height); +HtmlImage createTestImage({int width = 100, 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); @@ -145,4 +327,3 @@ HtmlImage createTestImage() { imageElement.src = js_util.callMethod(canvas, 'toDataURL', []); return HtmlImage(imageElement, width, height); } - 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 fd0f2c52e9d35..b440dcf321f96 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 @@ -194,8 +194,10 @@ void main() async { test('drawColor should cover full size', () async { final RecordingCanvas rc = RecordingCanvas(screenRect); - rc.drawColor(const Color(0xFFFF0000), BlendMode.multiply); + final Paint testPaint = Paint()..color = const Color(0xFF80FF00); rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint); + rc.drawColor(const Color(0xFFFF0000), BlendMode.multiply); + rc.drawRect(const Rect.fromLTRB(10, 60, 30, 80), testPaint); expect(rc.computePaintBounds(), screenRect); await _checkScreenshot(rc, 'draw_color'); }); @@ -434,6 +436,7 @@ void main() async { ..style = PaintingStyle.stroke ..strokeWidth = 2.0 ..color = const Color(0xFF404000)); + rc.restore(); await _checkScreenshot(rc, 'path_with_line_and_roundrect'); }); } diff --git a/lib/web_ui/test/mock_engine_canvas.dart b/lib/web_ui/test/mock_engine_canvas.dart index 827d4e519d201..4513a861d1912 100644 --- a/lib/web_ui/test/mock_engine_canvas.dart +++ b/lib/web_ui/test/mock_engine_canvas.dart @@ -220,12 +220,17 @@ class MockEngineCanvas implements EngineCanvas { } @override - void drawVertices(Vertices vertices, BlendMode blendMode, - SurfacePaintData paint) { + void drawVertices( + Vertices vertices, BlendMode blendMode, SurfacePaintData paint) { _called('drawVertices', arguments: { 'vertices': vertices, 'blendMode': blendMode, 'paint': paint, }); } + + @override + void endOfPaint() { + _called('endOfPaint', arguments: {}); + } }