| // 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 |
| 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 [closeCurrentCanvas] |
| /// and adds the canvas(s) to [_activeCanvasList]. |
| /// |
| /// 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<html.CanvasElement> _activeCanvasList; |
| // List of canvases available to reuse from prior paint cycle. |
| List<html.CanvasElement> _reusablePool; |
| // Current canvas element or null if marked for lazy allocation. |
| html.CanvasElement _canvas; |
| |
| html.HtmlElement _rootElement; |
| int _saveContextCount = 0; |
| // Number of elements that have been added to flt-canvas. |
| int _activeElementCount = 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; |
| } |
| |
| // Prevents active canvas to be used for rendering and prepares a new |
| // canvas allocation on next drawing request that will require one. |
| // |
| // Saves current canvas so we can dispose |
| // and replay the clip/transform stack on top of new canvas. |
| void closeCurrentCanvas() { |
| assert(_rootElement != null); |
| // Place clean copy of current canvas with context stack restored and paint |
| // reset into pool. |
| if (_canvas != null) { |
| _restoreContextSave(); |
| _contextHandle.reset(); |
| _activeCanvasList ??= []; |
| _activeCanvasList.add(_canvas); |
| _canvas = null; |
| _context = null; |
| _contextHandle = null; |
| } |
| _activeElementCount++; |
| } |
| |
| void allocateCanvas(html.HtmlElement rootElement) { |
| _rootElement = rootElement; |
| } |
| |
| void _createCanvas() { |
| bool requiresClearRect = false; |
| bool reused = false; |
| if (_reusablePool != null && _reusablePool.isNotEmpty) { |
| _canvas = _reusablePool.removeAt(0); |
| requiresClearRect = true; |
| reused = 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 / EngineWindow.browserDevicePixelRatio; |
| final double cssHeight = |
| _heightInBitmapPixels / EngineWindow.browserDevicePixelRatio; |
| _canvas = html.CanvasElement( |
| width: _widthInBitmapPixels, |
| height: _heightInBitmapPixels, |
| ); |
| if (_canvas == null) { |
| // Evict BitmapCanvas(s) and retry. |
| _reduceCanvasMemoryUsage(); |
| _canvas = html.CanvasElement( |
| width: _widthInBitmapPixels, |
| height: _heightInBitmapPixels, |
| ); |
| } |
| _canvas.style |
| ..position = 'absolute' |
| ..width = '${cssWidth}px' |
| ..height = '${cssHeight}px'; |
| } |
| |
| // Before appending canvas, check if canvas is already on rootElement. This |
| // optimization prevents DOM .append call when a PersistentSurface is |
| // reused. Reading lastChild is faster than append call. |
| if (_rootElement.lastChild != _canvas) { |
| _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. |
| _canvas.style.removeProperty('z-index'); |
| } |
| ++_activeElementCount; |
| |
| _context = _canvas.context2D; |
| _contextHandle = ContextStateHandle(this, _context); |
| _initializeViewport(requiresClearRect); |
| _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 prevTransform, |
| Matrix4 transform, List<_SaveClipEntry> clipStack) { |
| final html.CanvasRenderingContext2D ctx = _context; |
| if (clipStack != null) { |
| for (int clipCount = clipStack.length; |
| clipDepth < clipCount; |
| clipDepth++) { |
| _SaveClipEntry clipEntry = clipStack[clipDepth]; |
| Matrix4 clipTimeTransform = clipEntry.currentTransform; |
| // If transform for entry recording change since last element, update. |
| // Comparing only matrix3 elements since Canvas API restricted. |
| if (clipTimeTransform[0] != prevTransform[0] || |
| clipTimeTransform[1] != prevTransform[1] || |
| clipTimeTransform[4] != prevTransform[4] || |
| clipTimeTransform[5] != prevTransform[5] || |
| clipTimeTransform[12] != prevTransform[12] || |
| clipTimeTransform[13] != prevTransform[13]) { |
| final double ratio = EngineWindow.browserDevicePixelRatio; |
| ctx.setTransform(ratio, 0, 0, ratio, 0, 0); |
| ctx.transform( |
| clipTimeTransform[0], |
| clipTimeTransform[1], |
| clipTimeTransform[4], |
| clipTimeTransform[5], |
| clipTimeTransform[12], |
| clipTimeTransform[13]); |
| prevTransform = clipTimeTransform; |
| } |
| 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(); |
| } |
| } |
| } |
| // If transform was changed between last clip operation and save call, |
| // update. |
| if (transform[0] != prevTransform[0] || |
| transform[1] != prevTransform[1] || |
| transform[4] != prevTransform[4] || |
| transform[5] != prevTransform[5] || |
| transform[12] != prevTransform[12] || |
| transform[13] != prevTransform[13]) { |
| final double ratio = EngineWindow.browserDevicePixelRatio; |
| ctx.setTransform(ratio, 0, 0, ratio, 0, 0); |
| ctx.transform(transform[0], transform[1], transform[4], transform[5], |
| transform[12], transform[13]); |
| } |
| return clipDepth; |
| } |
| |
| void _replayClipStack() { |
| // Replay save/clip stack on this canvas now. |
| html.CanvasRenderingContext2D ctx = _context; |
| int clipDepth = 0; |
| Matrix4 prevTransform = Matrix4.identity(); |
| for (int saveStackIndex = 0, len = _saveStack.length; |
| saveStackIndex < len; |
| saveStackIndex++) { |
| _SaveStackEntry saveEntry = _saveStack[saveStackIndex]; |
| clipDepth = _replaySingleSaveEntry( |
| clipDepth, prevTransform, saveEntry.transform, saveEntry.clipStack); |
| prevTransform = saveEntry.transform; |
| ctx.save(); |
| ++_saveContextCount; |
| } |
| _replaySingleSaveEntry( |
| clipDepth, prevTransform, _currentTransform, _clipStack); |
| } |
| |
| // Marks this pool for reuse. |
| void reuse() { |
| if (_canvas != null) { |
| _restoreContextSave(); |
| _contextHandle.reset(); |
| _activeCanvasList ??= []; |
| _activeCanvasList.add(_canvas); |
| _context = null; |
| _contextHandle = null; |
| } |
| _reusablePool = _activeCanvasList; |
| _activeCanvasList = null; |
| _canvas = null; |
| _context = null; |
| _contextHandle = null; |
| _activeElementCount = 0; |
| } |
| |
| void endOfPaint() { |
| if (_reusablePool != null) { |
| for (html.CanvasElement e in _reusablePool) { |
| if (browserEngine == BrowserEngine.webkit) { |
| e.width = e.height = 0; |
| } |
| 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(bool clearCanvas) { |
| 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); |
| if (clearCanvas) { |
| ctx.clearRect(0, 0, _widthInBitmapPixels, _heightInBitmapPixels); |
| } |
| |
| // This scale makes sure that 1 CSS pixel is translated to the correct |
| // number of bitmap pixels. |
| ctx.scale(EngineWindow.browserDevicePixelRatio, |
| EngineWindow.browserDevicePixelRatio); |
| } |
| |
| 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(Float32List 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 = colorToCssString(color); |
| 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(); |
| } |
| |
| void drawPoints(ui.PointMode pointMode, Float32List points, double radius) { |
| html.CanvasRenderingContext2D ctx = context; |
| final int len = points.length; |
| switch (pointMode) { |
| case ui.PointMode.points: |
| for (int i = 0; i < len; i += 2) { |
| final double x = points[i]; |
| final double y = points[i + 1]; |
| ctx.beginPath(); |
| ctx.arc(x, y, radius, 0, 2.0 * math.pi); |
| ctx.fill(); |
| } |
| break; |
| case ui.PointMode.lines: |
| ctx.beginPath(); |
| for (int i = 0; i < (len - 2); i += 4) { |
| ctx.moveTo(points[i], points[i + 1]); |
| ctx.lineTo(points[i + 2], points[i + 3]); |
| ctx.stroke(); |
| } |
| break; |
| case ui.PointMode.polygon: |
| ctx.beginPath(); |
| ctx.moveTo(points[0], points[1]); |
| for (int i = 2; i < len; i += 2) { |
| ctx.lineTo(points[i], points[i + 1]); |
| } |
| ctx.stroke(); |
| break; |
| } |
| } |
| |
| /// 'Runs' the given [path] by applying all of its commands to the canvas. |
| void _runPath(html.CanvasRenderingContext2D ctx, SurfacePath path) { |
| ctx.beginPath(); |
| final List<Subpath> subpaths = path.subpaths; |
| final int subpathCount = subpaths.length; |
| for (int subPathIndex = 0; subPathIndex < subpathCount; subPathIndex++) { |
| final Subpath subpath = subpaths[subPathIndex]; |
| final List<PathCommand> commands = subpath.commands; |
| final int commandCount = commands.length; |
| for (int c = 0; c < commandCount; c++) { |
| final PathCommand command = commands[c]; |
| 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; |
| if (c == 0) { |
| // Ellipses that start a new path need to set start point, |
| // otherwise it incorrectly uses last point. |
| ctx.moveTo(subpath.startX, subpath.startY); |
| } |
| DomRenderer.ellipse(ctx, |
| 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(); |
| DomRenderer.ellipse(context, 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(); |
| DomRenderer.ellipse(context, 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.paintPath(style, path.fillType); |
| } |
| |
| void drawShadow(ui.Path path, ui.Color color, double elevation, |
| bool transparentOccluder) { |
| final SurfaceShadowData shadow = computeShadow(path.getBounds(), elevation); |
| if (shadow != null) { |
| // On April 2020 Web canvas 2D did not support shadow color alpha. So |
| // instead we apply alpha separately using globalAlpha, then paint a |
| // solid shadow. |
| final ui.Color shadowColor = toShadowColor(color); |
| final double opacity = shadowColor.alpha / 255; |
| final String solidColor = colorComponentsToCssString( |
| shadowColor.red, |
| shadowColor.green, |
| shadowColor.blue, |
| 255, |
| ); |
| context.save(); |
| context.globalAlpha = opacity; |
| |
| // TODO(hterkelsen): 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.translate(shadow.offset.dx, shadow.offset.dy); |
| context.filter = _maskFilterToCanvasFilter( |
| ui.MaskFilter.blur(ui.BlurStyle.normal, shadow.blurWidth)); |
| context.strokeStyle = ''; |
| context.fillStyle = solidColor; |
| } else { |
| // TODO(yjbanov): the following comment by hterkelsen makes sense, but |
| // somehow we lost the implementation described in it. |
| // Perhaps we should revisit this and actually do what |
| // the comment says. |
| // TODO(hterkelsen): 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.filter = 'none'; |
| context.strokeStyle = ''; |
| context.fillStyle = solidColor; |
| context.shadowBlur = shadow.blurWidth; |
| context.shadowColor = solidColor; |
| context.shadowOffsetX = shadow.offset.dx; |
| context.shadowOffsetY = shadow.offset.dy; |
| } |
| _runPath(context, path); |
| context.fill(); |
| |
| // This also resets globalAlpha and shadow attributes. See: |
| // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/save#Drawing_state |
| 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; |
| } |
| _clearActiveCanvasList(); |
| } |
| |
| void _clearActiveCanvasList() { |
| if (_activeCanvasList != null) { |
| for (html.CanvasElement c in _activeCanvasList) { |
| if (browserEngine == BrowserEngine.webkit) { |
| c.width = c.height = 0; |
| } |
| c.remove(); |
| } |
| } |
| _activeCanvasList = 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 { |
| final html.CanvasRenderingContext2D context; |
| final _CanvasPool _canvasPool; |
| |
| ContextStateHandle(this._canvasPool, 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; |
| |
| 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; |
| } |
| } |
| |
| ui.MaskFilter _currentFilter; |
| SurfacePaintData _lastUsedPaint; |
| |
| /// The painting state. |
| /// |
| /// Used to validate that the [setUpPaint] and [tearDownPaint] are called in |
| /// a correct sequence. |
| bool _debugIsPaintSetUp = false; |
| |
| /// Whether to use WebKit's method of rendering [MaskFilter]. |
| /// |
| /// This is used in screenshot tests to test Safari codepaths. |
| static bool debugEmulateWebKitMaskFilter = false; |
| |
| bool get _renderMaskFilterForWebkit => browserEngine == BrowserEngine.webkit || debugEmulateWebKitMaskFilter; |
| |
| /// Sets paint properties on the current canvas. |
| /// |
| /// [tearDownPaint] must be called after calling this method. |
| void setUpPaint(SurfacePaintData paint) { |
| if (assertionsEnabled) { |
| assert(!_debugIsPaintSetUp); |
| _debugIsPaintSetUp = true; |
| } |
| |
| _lastUsedPaint = paint; |
| lineWidth = paint.strokeWidth ?? 1.0; |
| blendMode = paint.blendMode; |
| strokeCap = paint.strokeCap; |
| strokeJoin = paint.strokeJoin; |
| |
| if (paint.shader != null) { |
| final EngineGradient engineShader = paint.shader; |
| final Object paintStyle = |
| engineShader.createPaintStyle(_canvasPool.context); |
| fillStyle = paintStyle; |
| strokeStyle = paintStyle; |
| } else if (paint.color != null) { |
| final String colorString = colorToCssString(paint.color); |
| fillStyle = colorString; |
| strokeStyle = colorString; |
| } else { |
| fillStyle = ''; |
| strokeStyle = ''; |
| } |
| |
| final ui.MaskFilter maskFilter = paint?.maskFilter; |
| if (!_renderMaskFilterForWebkit) { |
| if (_currentFilter != maskFilter) { |
| _currentFilter = maskFilter; |
| context.filter = _maskFilterToCanvasFilter(maskFilter); |
| } |
| } else { |
| // WebKit does not support the `filter` property. Instead we apply a |
| // shadow to the shape of the same color as the paint and the same blur |
| // as the mask filter. |
| // |
| // Note that on WebKit the cached value of _currentFilter is not useful. |
| // Instead we destructure it into the shadow properties and cache those. |
| if (maskFilter != null) { |
| context.save(); |
| context.shadowBlur = convertSigmaToRadius(maskFilter.webOnlySigma); |
| if (paint?.color != null) { |
| // Shadow color must be fully opaque. |
| context.shadowColor = colorToCssString(paint.color.withAlpha(255)); |
| } else { |
| context.shadowColor = colorToCssString(const ui.Color(0xFF000000)); |
| } |
| |
| // On the web a shadow must always be painted together with the shape |
| // that casts it. In order to paint just the shadow, we offset the shape |
| // by a large enough value that it moved outside the canvas bounds, then |
| // offset the shadow in the opposite direction such that it lands exactly |
| // where the shape is. |
| const double kOutsideTheBoundsOffset = 50000; |
| |
| context.translate(-kOutsideTheBoundsOffset, 0); |
| |
| // Shadow offset is not affected by the current canvas context transform. |
| // We have to apply the transform ourselves. To do that we transform the |
| // tip of the vector from the shape to the shadow, then we transform the |
| // origin (0, 0). The desired shadow offset is the difference between the |
| // two. In vector notation, this is: |
| // |
| // transformedShadowDelta = M*shadowDelta - M*origin. |
| final Float32List tempVector = Float32List(2); |
| tempVector[0] = kOutsideTheBoundsOffset * window.devicePixelRatio; |
| _canvasPool.currentTransform.transform2(tempVector); |
| double shadowOffsetX = tempVector[0]; |
| double shadowOffsetY = tempVector[1]; |
| |
| tempVector[0] = tempVector[1] = 0; |
| _canvasPool.currentTransform.transform2(tempVector); |
| context.shadowOffsetX = shadowOffsetX - tempVector[0]; |
| context.shadowOffsetY = shadowOffsetY - tempVector[1]; |
| } |
| } |
| } |
| |
| /// Removes paint properties on the current canvas used by the last draw |
| /// command. |
| /// |
| /// Not all properties are cleared. Properties that are set by all paint |
| /// commands prior to painting do not need to be cleared. |
| /// |
| /// Must be called after calling [setUpPaint]. |
| void tearDownPaint() { |
| if (assertionsEnabled) { |
| assert(_debugIsPaintSetUp); |
| _debugIsPaintSetUp = false; |
| } |
| |
| final ui.MaskFilter maskFilter = _lastUsedPaint?.maskFilter; |
| if (maskFilter != null && _renderMaskFilterForWebkit) { |
| // On Safari (WebKit) we use a translated shadow to emulate |
| // MaskFilter.blur. We use restore to undo the translation and |
| // shadow attributes. |
| context.restore(); |
| } |
| } |
| |
| void paint(ui.PaintingStyle style) { |
| if (style == ui.PaintingStyle.stroke) { |
| context.stroke(); |
| } else { |
| context.fill(); |
| } |
| } |
| |
| void paintPath(ui.PaintingStyle style, ui.PathFillType pathFillType) { |
| if (style == ui.PaintingStyle.stroke) { |
| context.stroke(); |
| } else { |
| if (pathFillType == ui.PathFillType.nonZero) { |
| context.fill(); |
| } else { |
| context.fill('evenodd'); |
| } |
| } |
| } |
| |
| 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.shadowBlur = 0; |
| context.shadowColor = 'none'; |
| context.shadowOffsetX = 0; |
| context.shadowOffsetY = 0; |
| 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 Float32List storage = skewMatrix.storage; |
| storage[1] = sy; |
| storage[4] = sx; |
| _currentTransform.multiply(skewMatrix); |
| } |
| |
| /// Multiplies the [currentTransform] matrix by another matrix. |
| @mustCallSuper |
| void transform(Float32List matrix4) { |
| _currentTransform.multiply(Matrix4.fromFloat32List(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())); |
| } |
| } |