blob: 0be145972fdb7435951fcb71e5d4b1d645de7625 [file] [log] [blame]
// 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()));
}
}