Optimize static content scrolling (flutter/engine#17621)

* store paint command bounds
* do not apply commands outside the clip region
* better cull rect prediction
* enforce RecordingCanvas.endRecording
This commit is contained in:
Yegor 2020-04-14 20:08:10 -07:00 committed by GitHub
parent 169b3b4521
commit d8b5667fb2
21 changed files with 533 additions and 177 deletions

View File

@ -545,7 +545,7 @@ class BitmapCanvas extends EngineCanvas {
/// Paints the [picture] into this canvas.
void drawPicture(ui.Picture picture) {
final EnginePicture enginePicture = picture;
enginePicture.recordingCanvas.apply(this);
enginePicture.recordingCanvas.apply(this, bounds);
}
/// Draws vertices on a gl context.

View File

@ -32,6 +32,7 @@ class EnginePictureRecorder implements ui.PictureRecorder {
return null;
}
_isRecording = false;
_canvas.endRecording();
return EnginePicture(_canvas, cullRect);
}
}
@ -46,8 +47,9 @@ class EnginePicture implements ui.Picture {
@override
Future<ui.Image> toImage(int width, int height) async {
final BitmapCanvas canvas = BitmapCanvas(ui.Rect.fromLTRB(0, 0, width.toDouble(), height.toDouble()));
recordingCanvas.apply(canvas);
final ui.Rect imageRect = ui.Rect.fromLTRB(0, 0, width.toDouble(), height.toDouble());
final BitmapCanvas canvas = BitmapCanvas(imageRect);
recordingCanvas.apply(canvas, imageRect);
final String imageDataUrl = canvas.toDataUrl();
final html.ImageElement imageElement = html.ImageElement()
..src = imageDataUrl

View File

@ -145,7 +145,7 @@ class PersistedHoudiniPicture extends PersistedPicture {
_canvas = canvas;
domRenderer.clearDom(rootElement);
rootElement.append(_canvas.rootElement);
picture.recordingCanvas.apply(_canvas);
picture.recordingCanvas.apply(_canvas, _optimalLocalCullRect);
canvas.commit();
}
}
@ -231,7 +231,7 @@ class PersistedStandardPicture extends PersistedPicture {
_canvas = DomCanvas();
domRenderer.clearDom(rootElement);
rootElement.append(_canvas.rootElement);
picture.recordingCanvas.apply(_canvas);
picture.recordingCanvas.apply(_canvas, _optimalLocalCullRect);
}
void _applyBitmapPaint(EngineCanvas oldCanvas) {
@ -244,7 +244,7 @@ class PersistedStandardPicture extends PersistedPicture {
oldCanvas.bounds = _optimalLocalCullRect;
_canvas = oldCanvas;
_canvas.clear();
picture.recordingCanvas.apply(_canvas);
picture.recordingCanvas.apply(_canvas, _optimalLocalCullRect);
} else {
// We can't use the old canvas because the size has changed, so we put
// it in a cache for later reuse.
@ -265,7 +265,7 @@ class PersistedStandardPicture extends PersistedPicture {
domRenderer.clearDom(rootElement);
rootElement.append(_canvas.rootElement);
_canvas.clear();
picture.recordingCanvas.apply(_canvas);
picture.recordingCanvas.apply(_canvas, _optimalLocalCullRect);
},
));
}
@ -352,7 +352,7 @@ class PersistedStandardPicture extends PersistedPicture {
/// to draw shapes and text.
abstract class PersistedPicture extends PersistedLeafSurface {
PersistedPicture(this.dx, this.dy, this.picture, this.hints)
: localPaintBounds = picture.recordingCanvas.computePaintBounds();
: localPaintBounds = picture.recordingCanvas.pictureBounds;
EngineCanvas _canvas;
@ -491,7 +491,7 @@ abstract class PersistedPicture extends PersistedLeafSurface {
// The new cull rect contains area not covered by a previous rect. Perhaps
// the clip is growing, moving around the picture, or both. In this case
// a part of the picture may not been painted. We will need to
// a part of the picture may not have been painted. We will need to
// request a new canvas and paint the picture on it. However, this is also
// a strong signal that the clip will continue growing as typically
// Flutter uses animated transitions. So instead of allocating the canvas
@ -500,25 +500,19 @@ abstract class PersistedPicture extends PersistedLeafSurface {
// will hit the above case where the new cull rect is fully contained
// within the cull rect we compute now.
// If any of the borders moved.
// TODO(yjbanov): consider switching to Mouad's snap-to-10px strategy. It
// might be sufficient, if not more effective.
const double kPredictedGrowthFactor = 3.0;
final double leftwardTrend = kPredictedGrowthFactor *
math.max(oldOptimalLocalCullRect.left - _exactLocalCullRect.left, 0);
final double upwardTrend = kPredictedGrowthFactor *
math.max(oldOptimalLocalCullRect.top - _exactLocalCullRect.top, 0);
final double rightwardTrend = kPredictedGrowthFactor *
math.max(_exactLocalCullRect.right - oldOptimalLocalCullRect.right, 0);
final double bottomwardTrend = kPredictedGrowthFactor *
math.max(
_exactLocalCullRect.bottom - oldOptimalLocalCullRect.bottom, 0);
// Compute the delta, by which each of the side of the clip rect has "moved"
// since the last time we updated the cull rect.
final double leftwardDelta = oldOptimalLocalCullRect.left - _exactLocalCullRect.left;
final double upwardDelta = oldOptimalLocalCullRect.top - _exactLocalCullRect.top;
final double rightwardDelta = _exactLocalCullRect.right - oldOptimalLocalCullRect.right;
final double bottomwardDelta = _exactLocalCullRect.bottom - oldOptimalLocalCullRect.bottom;
// Compute the new optimal rect to paint into.
final ui.Rect newLocalCullRect = ui.Rect.fromLTRB(
oldOptimalLocalCullRect.left - leftwardTrend,
oldOptimalLocalCullRect.top - upwardTrend,
oldOptimalLocalCullRect.right + rightwardTrend,
oldOptimalLocalCullRect.bottom + bottomwardTrend,
_exactLocalCullRect.left - _predictTrend(leftwardDelta, _exactLocalCullRect.width),
_exactLocalCullRect.top - _predictTrend(upwardDelta, _exactLocalCullRect.height),
_exactLocalCullRect.right + _predictTrend(rightwardDelta, _exactLocalCullRect.width),
_exactLocalCullRect.bottom + _predictTrend(bottomwardDelta, _exactLocalCullRect.height),
).intersect(localPaintBounds);
final bool localCullRectChanged = _optimalLocalCullRect != newLocalCullRect;
@ -526,6 +520,25 @@ abstract class PersistedPicture extends PersistedLeafSurface {
return localCullRectChanged;
}
/// Predicts the delta a particular side of a clip rect will move given the
/// [delta] it moved by last, and the respective [extent] (width or height)
/// of the clip.
static double _predictTrend(double delta, double extent) {
if (delta <= 0.0) {
// Shrinking. Give it 10% of the extent in case the trend is reversed.
return extent * 0.1;
} else {
// Growing. Predict 10 more frames of similar deltas. Give it at least
// 50% of the extent (protect from extremely slow growth trend such as
// slow scrolling). Give no more than the full extent (protects from
// fast scrolling that could lead to overallocation).
return math.min(
math.max(extent * 0.5, delta * 10.0),
extent,
);
}
}
/// Number of bitmap pixel painted by this picture.
///
/// If the implementation does not paint onto a bitmap canvas, it should

View File

@ -23,10 +23,33 @@ double _measureBorderRadius(double x, double y) {
///
/// See [Canvas] for docs for these methods.
class RecordingCanvas {
/// Maximum paintable bounds for this canvas.
/// Computes [_pictureBounds].
final _PaintBounds _paintBounds;
/// Maximum paintable bounds for the picture painted by this recording.
///
/// The bounds contain the full picture. The commands recorded for the picture
/// are later pruned based on the clip applied to the picture. See the [apply]
/// method for more details.
ui.Rect get pictureBounds {
assert(
_debugRecordingEnded,
'Picture bounds not available yet. Call [endRecording] before accessing picture bounds.',
);
return _pictureBounds;
}
ui.Rect _pictureBounds;
final List<PaintCommand> _commands = <PaintCommand>[];
/// In debug mode returns the list of recorded paint commands for testing.
List<PaintCommand> get debugPaintCommands {
if (assertionsEnabled) {
return _commands;
}
throw UnsupportedError('For debugging only.');
}
RecordingCanvas(ui.Rect bounds) : _paintBounds = _PaintBounds(bounds);
/// Whether this canvas is doing arbitrary paint operations not expressible
@ -53,30 +76,78 @@ class RecordingCanvas {
bool get didDraw => _didDraw;
bool _didDraw = false;
/// Computes paint bounds based on estimated [bounds] and transforms.
ui.Rect computePaintBounds() {
return _paintBounds.computeBounds();
/// When assertions are enabled used to ensure that [endRecording] is called
/// before calling [apply] or [pictureBounds].
bool _debugRecordingEnded = false;
/// Stops recording drawing commands and computes paint bounds.
///
/// This must be called prior to passing the picture to the [SceneBuilder]
/// for rendering. In a production app, this is done automatically by
/// [PictureRecorder] when the framework calls [PictureRecorder.endRecording].
/// However, if you are writing a unit-test and using [RecordingCanvas]
/// directly it is up to you to call this method explicitly.
void endRecording() {
_pictureBounds = _paintBounds.computeBounds();
if (assertionsEnabled) {
_debugRecordingEnded = true;
}
}
/// Applies the recorded commands onto an [engineCanvas].
void apply(EngineCanvas engineCanvas) {
///
/// The [clipRect] specifies the clip applied to the picture (screen clip at
/// a minimum). The commands that fall outside the clip are skipped and are
/// not applied to the [engineCanvas]. A command must have a non-zero
/// intersection with the clip in order to be applied.
void apply(EngineCanvas engineCanvas, ui.Rect clipRect) {
assert(_debugRecordingEnded);
if (_debugDumpPaintCommands) {
final StringBuffer debugBuf = StringBuffer();
int skips = 0;
debugBuf.writeln(
'--- Applying RecordingCanvas to ${engineCanvas.runtimeType} '
'with bounds $_paintBounds');
'with bounds $_paintBounds and clip $clipRect (w = ${clipRect.width},'
' h = ${clipRect.height})');
for (int i = 0; i < _commands.length; i++) {
final PaintCommand command = _commands[i];
if (command is DrawCommand) {
if (command.isInvisible(clipRect)) {
// The drawing command is outside the clip region. No need to apply.
debugBuf.writeln('SKIPPED: ctx.$command;');
skips += 1;
continue;
}
}
debugBuf.writeln('ctx.$command;');
command.apply(engineCanvas);
}
if (skips > 0) {
debugBuf.writeln('Total commands skipped: $skips');
}
debugBuf.writeln('--- End of command stream');
print(debugBuf);
} else {
try {
for (int i = 0, len = _commands.length; i < len; i++) {
PaintCommand command = _commands[i];
command.apply(engineCanvas);
if (rectContainsOther(clipRect, _pictureBounds)) {
// No need to check if commands fit in the clip rect if we already
// know that the entire picture fits it.
for (int i = 0, len = _commands.length; i < len; i++) {
_commands[i].apply(engineCanvas);
}
} else {
// The picture doesn't fit the clip rect. Check that drawing commands
// fit before applying them.
for (int i = 0, len = _commands.length; i < len; i++) {
final PaintCommand command = _commands[i];
if (command is DrawCommand) {
if (command.isInvisible(clipRect)) {
// The drawing command is outside the clip region. No need to apply.
continue;
}
}
command.apply(engineCanvas);
}
}
} catch (e) {
// commands should never fail, but...
@ -103,12 +174,14 @@ class RecordingCanvas {
}
void save() {
assert(!_debugRecordingEnded);
_paintBounds.saveTransformsAndClip();
_commands.add(const PaintSave());
_saveCount++;
}
void saveLayerWithoutBounds(SurfacePaint paint) {
assert(!_debugRecordingEnded);
_hasArbitraryPaint = true;
// TODO(het): Implement this correctly using another canvas.
_commands.add(const PaintSave());
@ -117,6 +190,7 @@ class RecordingCanvas {
}
void saveLayer(ui.Rect bounds, SurfacePaint paint) {
assert(!_debugRecordingEnded);
_hasArbitraryPaint = true;
// TODO(het): Implement this correctly using another canvas.
_commands.add(const PaintSave());
@ -125,6 +199,7 @@ class RecordingCanvas {
}
void restore() {
assert(!_debugRecordingEnded);
_paintBounds.restoreTransformsAndClip();
if (_commands.isNotEmpty && _commands.last is PaintSave) {
// A restore followed a save without any drawing operations in between.
@ -139,56 +214,71 @@ class RecordingCanvas {
}
void translate(double dx, double dy) {
assert(!_debugRecordingEnded);
_paintBounds.translate(dx, dy);
_commands.add(PaintTranslate(dx, dy));
}
void scale(double sx, double sy) {
assert(!_debugRecordingEnded);
_paintBounds.scale(sx, sy);
_commands.add(PaintScale(sx, sy));
}
void rotate(double radians) {
assert(!_debugRecordingEnded);
_paintBounds.rotateZ(radians);
_commands.add(PaintRotate(radians));
}
void transform(Float64List matrix4) {
assert(!_debugRecordingEnded);
_paintBounds.transform(matrix4);
_commands.add(PaintTransform(matrix4));
}
void skew(double sx, double sy) {
assert(!_debugRecordingEnded);
_hasArbitraryPaint = true;
_paintBounds.skew(sx, sy);
_commands.add(PaintSkew(sx, sy));
}
void clipRect(ui.Rect rect) {
_paintBounds.clipRect(rect);
assert(!_debugRecordingEnded);
final PaintClipRect command = PaintClipRect(rect);
_paintBounds.clipRect(rect, command);
_hasArbitraryPaint = true;
_commands.add(PaintClipRect(rect));
_commands.add(command);
}
void clipRRect(ui.RRect rrect) {
_paintBounds.clipRect(rrect.outerRect);
assert(!_debugRecordingEnded);
final PaintClipRRect command = PaintClipRRect(rrect);
_paintBounds.clipRect(rrect.outerRect, command);
_hasArbitraryPaint = true;
_commands.add(PaintClipRRect(rrect));
_commands.add(command);
}
void clipPath(ui.Path path, {bool doAntiAlias = true}) {
_paintBounds.clipRect(path.getBounds());
assert(!_debugRecordingEnded);
final PaintClipPath command = PaintClipPath(path);
_paintBounds.clipRect(path.getBounds(), command);
_hasArbitraryPaint = true;
_commands.add(PaintClipPath(path));
_commands.add(command);
}
void drawColor(ui.Color color, ui.BlendMode blendMode) {
_paintBounds.grow(_paintBounds.maxPaintBounds);
_commands.add(PaintDrawColor(color, blendMode));
assert(!_debugRecordingEnded);
final PaintDrawColor command = PaintDrawColor(color, blendMode);
_commands.add(command);
_paintBounds.grow(_paintBounds.maxPaintBounds, command);
}
void drawLine(ui.Offset p1, ui.Offset p2, SurfacePaint paint) {
assert(!_debugRecordingEnded);
final double paintSpread = math.max(_getPaintSpread(paint), 1.0);
final PaintDrawLine command = PaintDrawLine(p1, p2, paint.paintData);
// TODO(yjbanov): This can be optimized. Currently we create a box around
// the line and then apply the transform on the box to get
// the bounding box. If you have a 45-degree line and a
@ -201,34 +291,40 @@ class RecordingCanvas {
math.min(p1.dy, p2.dy) - paintSpread,
math.max(p1.dx, p2.dx) + paintSpread,
math.max(p1.dy, p2.dy) + paintSpread,
command,
);
_hasArbitraryPaint = true;
_didDraw = true;
_commands.add(PaintDrawLine(p1, p2, paint.paintData));
_commands.add(command);
}
void drawPaint(SurfacePaint paint) {
assert(!_debugRecordingEnded);
_hasArbitraryPaint = true;
_didDraw = true;
_paintBounds.grow(_paintBounds.maxPaintBounds);
_commands.add(PaintDrawPaint(paint.paintData));
final PaintDrawPaint command = PaintDrawPaint(paint.paintData);
_paintBounds.grow(_paintBounds.maxPaintBounds, command);
_commands.add(command);
}
void drawRect(ui.Rect rect, SurfacePaint paint) {
assert(!_debugRecordingEnded);
if (paint.shader != null) {
_hasArbitraryPaint = true;
}
_didDraw = true;
final double paintSpread = _getPaintSpread(paint);
final PaintDrawRect command = PaintDrawRect(rect, paint.paintData);
if (paintSpread != 0.0) {
_paintBounds.grow(rect.inflate(paintSpread));
_paintBounds.grow(rect.inflate(paintSpread), command);
} else {
_paintBounds.grow(rect);
_paintBounds.grow(rect, command);
}
_commands.add(PaintDrawRect(rect, paint.paintData));
_commands.add(command);
}
void drawRRect(ui.RRect rrect, SurfacePaint paint) {
assert(!_debugRecordingEnded);
if (paint.shader != null || !rrect.webOnlyUniformRadii) {
_hasArbitraryPaint = true;
}
@ -238,11 +334,13 @@ class RecordingCanvas {
final double top = math.min(rrect.top, rrect.bottom) - paintSpread;
final double right = math.max(rrect.left, rrect.right) + paintSpread;
final double bottom = math.max(rrect.top, rrect.bottom) + paintSpread;
_paintBounds.growLTRB(left, top, right, bottom);
_commands.add(PaintDrawRRect(rrect, paint.paintData));
final PaintDrawRRect command = PaintDrawRRect(rrect, paint.paintData);
_paintBounds.growLTRB(left, top, right, bottom, command);
_commands.add(command);
}
void drawDRRect(ui.RRect outer, ui.RRect inner, SurfacePaint paint) {
assert(!_debugRecordingEnded);
// Check the inner bounds are contained within the outer bounds
// see: https://cs.chromium.org/chromium/src/third_party/skia/src/core/SkCanvas.cpp?l=1787-1789
ui.Rect innerRect = inner.outerRect;
@ -283,41 +381,50 @@ class RecordingCanvas {
_hasArbitraryPaint = true;
_didDraw = true;
final double paintSpread = _getPaintSpread(paint);
final PaintDrawDRRect command = PaintDrawDRRect(outer, inner, paint.paintData);
_paintBounds.growLTRB(
outer.left - paintSpread,
outer.top - paintSpread,
outer.right + paintSpread,
outer.bottom + paintSpread,
command,
);
_commands.add(PaintDrawDRRect(outer, inner, paint.paintData));
_commands.add(command);
}
void drawOval(ui.Rect rect, SurfacePaint paint) {
assert(!_debugRecordingEnded);
_hasArbitraryPaint = true;
_didDraw = true;
final double paintSpread = _getPaintSpread(paint);
final PaintDrawOval command = PaintDrawOval(rect, paint.paintData);
if (paintSpread != 0.0) {
_paintBounds.grow(rect.inflate(paintSpread));
_paintBounds.grow(rect.inflate(paintSpread), command);
} else {
_paintBounds.grow(rect);
_paintBounds.grow(rect, command);
}
_commands.add(PaintDrawOval(rect, paint.paintData));
_commands.add(command);
}
void drawCircle(ui.Offset c, double radius, SurfacePaint paint) {
assert(!_debugRecordingEnded);
_hasArbitraryPaint = true;
_didDraw = true;
final double paintSpread = _getPaintSpread(paint);
final PaintDrawCircle command = PaintDrawCircle(c, radius, paint.paintData);
final double distance = radius + paintSpread;
_paintBounds.growLTRB(
c.dx - radius - paintSpread,
c.dy - radius - paintSpread,
c.dx + radius + paintSpread,
c.dy + radius + paintSpread,
c.dx - distance,
c.dy - distance,
c.dx + distance,
c.dy + distance,
command,
);
_commands.add(PaintDrawCircle(c, radius, paint.paintData));
_commands.add(command);
}
void drawPath(ui.Path path, SurfacePaint paint) {
assert(!_debugRecordingEnded);
if (paint.shader == null) {
// For Rect/RoundedRect paths use drawRect/drawRRect code paths for
// DomCanvas optimization.
@ -340,31 +447,37 @@ class RecordingCanvas {
if (paintSpread != 0.0) {
pathBounds = pathBounds.inflate(paintSpread);
}
_paintBounds.grow(pathBounds);
// Clone path so it can be reused for subsequent draw calls.
final ui.Path clone = SurfacePath._shallowCopy(path);
final PaintDrawPath command = PaintDrawPath(clone, paint.paintData);
_paintBounds.grow(pathBounds, command);
clone.fillType = path.fillType;
_commands.add(PaintDrawPath(clone, paint.paintData));
_commands.add(command);
}
void drawImage(ui.Image image, ui.Offset offset, SurfacePaint paint) {
assert(!_debugRecordingEnded);
_hasArbitraryPaint = true;
_didDraw = true;
final double left = offset.dx;
final double top = offset.dy;
_paintBounds.growLTRB(left, top, left + image.width, top + image.height);
_commands.add(PaintDrawImage(image, offset, paint.paintData));
final command = PaintDrawImage(image, offset, paint.paintData);
_paintBounds.growLTRB(left, top, left + image.width, top + image.height, command);
_commands.add(command);
}
void drawImageRect(
ui.Image image, ui.Rect src, ui.Rect dst, SurfacePaint paint) {
assert(!_debugRecordingEnded);
_hasArbitraryPaint = true;
_didDraw = true;
_paintBounds.grow(dst);
_commands.add(PaintDrawImageRect(image, src, dst, paint.paintData));
final PaintDrawImageRect command = PaintDrawImageRect(image, src, dst, paint.paintData);
_paintBounds.grow(dst, command);
_commands.add(command);
}
void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) {
assert(!_debugRecordingEnded);
final EngineParagraph engineParagraph = paragraph;
if (!engineParagraph._isLaidOut) {
// Ignore non-laid out paragraphs. This matches Flutter's behavior.
@ -377,42 +490,53 @@ class RecordingCanvas {
}
final double left = offset.dx;
final double top = offset.dy;
final PaintDrawParagraph command = PaintDrawParagraph(engineParagraph, offset);
_paintBounds.growLTRB(
left, top, left + engineParagraph.width, top + engineParagraph.height);
_commands.add(PaintDrawParagraph(engineParagraph, offset));
left,
top,
left + engineParagraph.width,
top + engineParagraph.height,
command,
);
_commands.add(command);
}
void drawShadow(ui.Path path, ui.Color color, double elevation,
bool transparentOccluder) {
assert(!_debugRecordingEnded);
_hasArbitraryPaint = true;
_didDraw = true;
final ui.Rect shadowRect =
computePenumbraBounds(path.getBounds(), elevation);
_paintBounds.grow(shadowRect);
_commands.add(PaintDrawShadow(path, color, elevation, transparentOccluder));
final PaintDrawShadow command = PaintDrawShadow(path, color, elevation, transparentOccluder);
_paintBounds.grow(shadowRect, command);
_commands.add(command);
}
void drawVertices(
ui.Vertices vertices, ui.BlendMode blendMode, SurfacePaint paint) {
assert(!_debugRecordingEnded);
_hasArbitraryPaint = true;
_didDraw = true;
_growPaintBoundsByPoints(vertices.positions, 0, paint);
_commands.add(PaintVertices(vertices, blendMode, paint.paintData));
final PaintDrawVertices command = PaintDrawVertices(vertices, blendMode, paint.paintData);
_growPaintBoundsByPoints(vertices.positions, 0, paint, command);
_commands.add(command);
}
void drawRawPoints(
ui.PointMode pointMode, Float32List points, SurfacePaint paint) {
assert(!_debugRecordingEnded);
if (paint.strokeWidth == null) {
return;
}
_hasArbitraryPaint = true;
_didDraw = true;
_growPaintBoundsByPoints(points, paint.strokeWidth, paint);
_commands
.add(PaintPoints(pointMode, points, paint.strokeWidth, paint.color));
final PaintDrawPoints command = PaintDrawPoints(pointMode, points, paint.strokeWidth, paint.color);
_growPaintBoundsByPoints(points, paint.strokeWidth, paint, command);
_commands.add(command);
}
void _growPaintBoundsByPoints(Float32List points, double thickness, SurfacePaint paint) {
void _growPaintBoundsByPoints(Float32List points, double thickness, SurfacePaint paint, DrawCommand command) {
double minValueX, maxValueX, minValueY, maxValueY;
minValueX = maxValueX = points[0];
minValueY = maxValueY = points[1];
@ -436,6 +560,7 @@ class RecordingCanvas {
minValueY - distance - paintSpread,
maxValueX + distance + paintSpread,
maxValueY + distance + paintSpread,
command,
);
}
@ -458,6 +583,43 @@ abstract class PaintCommand {
void serializeToCssPaint(List<List<dynamic>> serializedCommands);
}
/// A [PaintCommand] that affect pixels on the screen (unlike, for example, the
/// [SaveCommand]).
abstract class DrawCommand extends PaintCommand {
/// Whether the command is completely clipped out of the picture.
bool isClippedOut = false;
/// The left bound of the graphic produced by this command in picture-global
/// coordinates.
double leftBound = double.negativeInfinity;
/// The top bound of the graphic produced by this command in picture-global
/// coordinates.
double topBound = double.negativeInfinity;
/// The right bound of the graphic produced by this command in picture-global
/// coordinates.
double rightBound = double.infinity;
/// The bottom bound of the graphic produced by this command in
/// picture-global coordinates.
double bottomBound = double.infinity;
/// Whether this command intersects with the [clipRect].
bool isInvisible(ui.Rect clipRect) {
if (isClippedOut) {
return true;
}
// Check top and bottom first because vertical scrolling is more common
// than horizontal scrolling.
return bottomBound < clipRect.top ||
topBound > clipRect.bottom ||
rightBound < clipRect.left ||
leftBound > clipRect.right;
}
}
class PaintSave extends PaintCommand {
const PaintSave();
@ -632,7 +794,7 @@ class PaintSkew extends PaintCommand {
}
}
class PaintClipRect extends PaintCommand {
class PaintClipRect extends DrawCommand {
final ui.Rect rect;
PaintClipRect(this.rect);
@ -657,7 +819,7 @@ class PaintClipRect extends PaintCommand {
}
}
class PaintClipRRect extends PaintCommand {
class PaintClipRRect extends DrawCommand {
final ui.RRect rrect;
PaintClipRRect(this.rrect);
@ -685,7 +847,7 @@ class PaintClipRRect extends PaintCommand {
}
}
class PaintClipPath extends PaintCommand {
class PaintClipPath extends DrawCommand {
final SurfacePath path;
PaintClipPath(this.path);
@ -710,7 +872,7 @@ class PaintClipPath extends PaintCommand {
}
}
class PaintDrawColor extends PaintCommand {
class PaintDrawColor extends DrawCommand {
final ui.Color color;
final ui.BlendMode blendMode;
@ -737,7 +899,7 @@ class PaintDrawColor extends PaintCommand {
}
}
class PaintDrawLine extends PaintCommand {
class PaintDrawLine extends DrawCommand {
final ui.Offset p1;
final ui.Offset p2;
final SurfacePaintData paint;
@ -771,7 +933,7 @@ class PaintDrawLine extends PaintCommand {
}
}
class PaintDrawPaint extends PaintCommand {
class PaintDrawPaint extends DrawCommand {
final SurfacePaintData paint;
PaintDrawPaint(this.paint);
@ -796,11 +958,11 @@ class PaintDrawPaint extends PaintCommand {
}
}
class PaintVertices extends PaintCommand {
class PaintDrawVertices extends DrawCommand {
final ui.Vertices vertices;
final ui.BlendMode blendMode;
final SurfacePaintData paint;
PaintVertices(this.vertices, this.blendMode, this.paint);
PaintDrawVertices(this.vertices, this.blendMode, this.paint);
@override
void apply(EngineCanvas canvas) {
@ -822,12 +984,12 @@ class PaintVertices extends PaintCommand {
}
}
class PaintPoints extends PaintCommand {
class PaintDrawPoints extends DrawCommand {
final Float32List points;
final ui.PointMode pointMode;
final double strokeWidth;
final ui.Color color;
PaintPoints(this.pointMode, this.points, this.strokeWidth, this.color);
PaintDrawPoints(this.pointMode, this.points, this.strokeWidth, this.color);
@override
void apply(EngineCanvas canvas) {
@ -849,7 +1011,7 @@ class PaintPoints extends PaintCommand {
}
}
class PaintDrawRect extends PaintCommand {
class PaintDrawRect extends DrawCommand {
final ui.Rect rect;
final SurfacePaintData paint;
@ -879,7 +1041,7 @@ class PaintDrawRect extends PaintCommand {
}
}
class PaintDrawRRect extends PaintCommand {
class PaintDrawRRect extends DrawCommand {
final ui.RRect rrect;
final SurfacePaintData paint;
@ -909,7 +1071,7 @@ class PaintDrawRRect extends PaintCommand {
}
}
class PaintDrawDRRect extends PaintCommand {
class PaintDrawDRRect extends DrawCommand {
final ui.RRect outer;
final ui.RRect inner;
final SurfacePaintData paint;
@ -941,7 +1103,7 @@ class PaintDrawDRRect extends PaintCommand {
}
}
class PaintDrawOval extends PaintCommand {
class PaintDrawOval extends DrawCommand {
final ui.Rect rect;
final SurfacePaintData paint;
@ -971,7 +1133,7 @@ class PaintDrawOval extends PaintCommand {
}
}
class PaintDrawCircle extends PaintCommand {
class PaintDrawCircle extends DrawCommand {
final ui.Offset c;
final double radius;
final SurfacePaintData paint;
@ -1004,7 +1166,7 @@ class PaintDrawCircle extends PaintCommand {
}
}
class PaintDrawPath extends PaintCommand {
class PaintDrawPath extends DrawCommand {
final SurfacePath path;
final SurfacePaintData paint;
@ -1034,7 +1196,7 @@ class PaintDrawPath extends PaintCommand {
}
}
class PaintDrawShadow extends PaintCommand {
class PaintDrawShadow extends DrawCommand {
PaintDrawShadow(
this.path, this.color, this.elevation, this.transparentOccluder);
@ -1074,7 +1236,7 @@ class PaintDrawShadow extends PaintCommand {
}
}
class PaintDrawImage extends PaintCommand {
class PaintDrawImage extends DrawCommand {
final ui.Image image;
final ui.Offset offset;
final SurfacePaintData paint;
@ -1103,7 +1265,7 @@ class PaintDrawImage extends PaintCommand {
}
}
class PaintDrawImageRect extends PaintCommand {
class PaintDrawImageRect extends DrawCommand {
final ui.Image image;
final ui.Rect src;
final ui.Rect dst;
@ -1133,7 +1295,7 @@ class PaintDrawImageRect extends PaintCommand {
}
}
class PaintDrawParagraph extends PaintCommand {
class PaintDrawParagraph extends DrawCommand {
final EngineParagraph paragraph;
final ui.Offset offset;
@ -1765,7 +1927,7 @@ class _PaintBounds {
_currentMatrix.multiply(skewMatrix);
}
void clipRect(ui.Rect rect) {
void clipRect(ui.Rect rect, DrawCommand command) {
// If we have an active transform, calculate screen relative clipping
// rectangle and union with current clipping rectangle.
if (!_currentMatrixIsIdentity) {
@ -1807,16 +1969,25 @@ class _PaintBounds {
_currentClipBottom = rect.bottom;
}
}
if (_currentClipLeft >= _currentClipRight || _currentClipTop >= _currentClipBottom) {
command.isClippedOut = true;
} else {
command.leftBound = _currentClipLeft;
command.topBound = _currentClipTop;
command.rightBound = _currentClipRight;
command.bottomBound = _currentClipBottom;
}
}
/// Grow painted area to include given rectangle.
void grow(ui.Rect r) {
growLTRB(r.left, r.top, r.right, r.bottom);
void grow(ui.Rect r, DrawCommand command) {
growLTRB(r.left, r.top, r.right, r.bottom, command);
}
/// Grow painted area to include given rectangle.
void growLTRB(double left, double top, double right, double bottom) {
void growLTRB(double left, double top, double right, double bottom, DrawCommand command) {
if (left == right || top == bottom) {
command.isClippedOut = true;
return;
}
@ -1836,15 +2007,19 @@ class _PaintBounds {
if (_clipRectInitialized) {
if (transformedPointLeft > _currentClipRight) {
command.isClippedOut = true;
return;
}
if (transformedPointRight < _currentClipLeft) {
command.isClippedOut = true;
return;
}
if (transformedPointTop > _currentClipBottom) {
command.isClippedOut = true;
return;
}
if (transformedPointBottom < _currentClipTop) {
command.isClippedOut = true;
return;
}
if (transformedPointLeft < _currentClipLeft) {
@ -1861,6 +2036,11 @@ class _PaintBounds {
}
}
command.leftBound = transformedPointLeft;
command.topBound = transformedPointTop;
command.rightBound = transformedPointRight;
command.bottomBound = transformedPointBottom;
if (_didPaintInsideClipArea) {
_left = math.min(
math.min(_left, transformedPointLeft), transformedPointRight);

View File

@ -36,16 +36,17 @@ void main() {
}
testCanvas('draws laid out paragraph', (EngineCanvas canvas) {
final RecordingCanvas recordingCanvas =
RecordingCanvas(const ui.Rect.fromLTWH(0, 0, 100, 100));
final ui.Rect screenRect = const ui.Rect.fromLTWH(0, 0, 100, 100);
final RecordingCanvas recordingCanvas = RecordingCanvas(screenRect);
final ui.ParagraphBuilder builder =
ui.ParagraphBuilder(ui.ParagraphStyle());
builder.addText('sample');
paragraph = builder.build();
paragraph.layout(const ui.ParagraphConstraints(width: 100));
recordingCanvas.drawParagraph(paragraph, const ui.Offset(10, 10));
recordingCanvas.endRecording();
canvas.clear();
recordingCanvas.apply(canvas);
recordingCanvas.apply(canvas, screenRect);
}, whenDone: () {
expect(mockCanvas.methodCallLog, hasLength(3));
@ -60,15 +61,16 @@ void main() {
testCanvas('ignores paragraphs that were not laid out',
(EngineCanvas canvas) {
final RecordingCanvas recordingCanvas =
RecordingCanvas(const ui.Rect.fromLTWH(0, 0, 100, 100));
final ui.Rect screenRect = const ui.Rect.fromLTWH(0, 0, 100, 100);
final RecordingCanvas recordingCanvas = RecordingCanvas(screenRect);
final ui.ParagraphBuilder builder =
ui.ParagraphBuilder(ui.ParagraphStyle());
builder.addText('sample');
final ui.Paragraph paragraph = builder.build();
recordingCanvas.drawParagraph(paragraph, const ui.Offset(10, 10));
recordingCanvas.endRecording();
canvas.clear();
recordingCanvas.apply(canvas);
recordingCanvas.apply(canvas, screenRect);
}, whenDone: () {
expect(mockCanvas.methodCallLog, hasLength(2));
expect(mockCanvas.methodCallLog[0].methodName, 'clear');

View File

@ -12,9 +12,10 @@ import '../mock_engine_canvas.dart';
void main() {
RecordingCanvas underTest;
MockEngineCanvas mockCanvas;
final Rect screenRect = Rect.largest;
setUp(() {
underTest = RecordingCanvas(Rect.largest);
underTest = RecordingCanvas(screenRect);
mockCanvas = MockEngineCanvas();
});
@ -25,9 +26,10 @@ void main() {
test('Happy case', () {
underTest.drawDRRect(rrect, rrect.deflate(1), somePaint);
underTest.apply(mockCanvas);
underTest.endRecording();
underTest.apply(mockCanvas, screenRect);
_expectDrawCall(mockCanvas, <String, dynamic>{
_expectDrawDRRectCall(mockCanvas, <String, dynamic>{
'outer': rrect,
'inner': rrect.deflate(1),
'paint': somePaint.paintData,
@ -36,7 +38,8 @@ void main() {
test('Inner RRect > Outer RRect', () {
underTest.drawDRRect(rrect, rrect.inflate(1), somePaint);
underTest.apply(mockCanvas);
underTest.endRecording();
underTest.apply(mockCanvas, screenRect);
// Expect nothing to be called
expect(mockCanvas.methodCallLog.length, equals(1));
expect(mockCanvas.methodCallLog.single.methodName, 'endOfPaint');
@ -45,7 +48,8 @@ void main() {
test('Inner RRect not completely inside Outer RRect', () {
underTest.drawDRRect(
rrect, rrect.deflate(1).shift(const Offset(0.0, 10)), somePaint);
underTest.apply(mockCanvas);
underTest.endRecording();
underTest.apply(mockCanvas, screenRect);
// Expect nothing to be called
expect(mockCanvas.methodCallLog.length, equals(1));
expect(mockCanvas.methodCallLog.single.methodName, 'endOfPaint');
@ -53,7 +57,8 @@ void main() {
test('Inner RRect same as Outer RRect', () {
underTest.drawDRRect(rrect, rrect, somePaint);
underTest.apply(mockCanvas);
underTest.endRecording();
underTest.apply(mockCanvas, screenRect);
// Expect nothing to be called
expect(mockCanvas.methodCallLog.length, equals(1));
expect(mockCanvas.methodCallLog.single.methodName, 'endOfPaint');
@ -72,10 +77,11 @@ void main() {
expect(inner.trRadius, equals(Radius.circular(-1)));
underTest.drawDRRect(outer, inner, somePaint);
underTest.apply(mockCanvas);
underTest.endRecording();
underTest.apply(mockCanvas, screenRect);
// Expect to draw, even when inner has negative radii (which get ignored by canvas)
_expectDrawCall(mockCanvas, <String, dynamic>{
_expectDrawDRRectCall(mockCanvas, <String, dynamic>{
'outer': outer,
'inner': inner,
'paint': somePaint.paintData,
@ -89,19 +95,105 @@ void main() {
RRect.fromRectAndCorners(const Rect.fromLTRB(12, 22, 28, 38));
underTest.drawDRRect(outer, inner, somePaint);
underTest.apply(mockCanvas);
underTest.endRecording();
underTest.apply(mockCanvas, screenRect);
_expectDrawCall(mockCanvas, <String, dynamic>{
_expectDrawDRRectCall(mockCanvas, <String, dynamic>{
'outer': outer,
'inner': inner,
'paint': somePaint.paintData,
});
});
});
test('Filters out paint commands outside the clip rect', () {
// Outside to the left
underTest.drawRect(Rect.fromLTWH(0.0, 20.0, 10.0, 10.0), Paint());
// Outside above
underTest.drawRect(Rect.fromLTWH(20.0, 0.0, 10.0, 10.0), Paint());
// Visible
underTest.drawRect(Rect.fromLTWH(20.0, 20.0, 10.0, 10.0), Paint());
// Inside the layer clip rect but zero-size
underTest.drawRect(Rect.fromLTRB(20.0, 20.0, 30.0, 20.0), Paint());
// Inside the layer clip but clipped out by a canvas clip
underTest.save();
underTest.clipRect(Rect.fromLTWH(0, 0, 10, 10));
underTest.drawRect(Rect.fromLTWH(20.0, 20.0, 10.0, 10.0), Paint());
underTest.restore();
// Outside to the right
underTest.drawRect(Rect.fromLTWH(40.0, 20.0, 10.0, 10.0), Paint());
// Outside below
underTest.drawRect(Rect.fromLTWH(20.0, 40.0, 10.0, 10.0), Paint());
underTest.endRecording();
expect(underTest.debugPaintCommands, hasLength(10));
final PaintDrawRect outsideLeft = underTest.debugPaintCommands[0];
expect(outsideLeft.isClippedOut, false);
expect(outsideLeft.leftBound, 0);
expect(outsideLeft.topBound, 20);
expect(outsideLeft.rightBound, 10);
expect(outsideLeft.bottomBound, 30);
final PaintDrawRect outsideAbove = underTest.debugPaintCommands[1];
expect(outsideAbove.isClippedOut, false);
final PaintDrawRect visible = underTest.debugPaintCommands[2];
expect(visible.isClippedOut, false);
final PaintDrawRect zeroSize = underTest.debugPaintCommands[3];
expect(zeroSize.isClippedOut, true);
expect(underTest.debugPaintCommands[4], isA<PaintSave>());
final PaintClipRect clip = underTest.debugPaintCommands[5];
expect(clip.isClippedOut, false);
final PaintDrawRect clippedOut = underTest.debugPaintCommands[6];
expect(clippedOut.isClippedOut, true);
expect(underTest.debugPaintCommands[7], isA<PaintRestore>());
final PaintDrawRect outsideRight = underTest.debugPaintCommands[8];
expect(outsideRight.isClippedOut, false);
final PaintDrawRect outsideBelow = underTest.debugPaintCommands[9];
expect(outsideBelow.isClippedOut, false);
// Give it the entire screen so everything paints.
underTest.apply(mockCanvas, screenRect);
expect(mockCanvas.methodCallLog, hasLength(11));
expect(mockCanvas.methodCallLog[0].methodName, 'drawRect');
expect(mockCanvas.methodCallLog[1].methodName, 'drawRect');
expect(mockCanvas.methodCallLog[2].methodName, 'drawRect');
expect(mockCanvas.methodCallLog[3].methodName, 'drawRect');
expect(mockCanvas.methodCallLog[4].methodName, 'save');
expect(mockCanvas.methodCallLog[5].methodName, 'clipRect');
expect(mockCanvas.methodCallLog[6].methodName, 'drawRect');
expect(mockCanvas.methodCallLog[7].methodName, 'restore');
expect(mockCanvas.methodCallLog[8].methodName, 'drawRect');
expect(mockCanvas.methodCallLog[9].methodName, 'drawRect');
expect(mockCanvas.methodCallLog[10].methodName, 'endOfPaint');
// Clip out a middle region that only contains 'drawRect'
mockCanvas.methodCallLog.clear();
underTest.apply(mockCanvas, Rect.fromLTRB(15, 15, 35, 35));
expect(mockCanvas.methodCallLog, hasLength(4));
expect(mockCanvas.methodCallLog[0].methodName, 'drawRect');
expect(mockCanvas.methodCallLog[1].methodName, 'save');
expect(mockCanvas.methodCallLog[2].methodName, 'restore');
expect(mockCanvas.methodCallLog[3].methodName, 'endOfPaint');
});
}
// Expect a drawDRRect call to be registered in the mock call log, with the expectedArguments
void _expectDrawCall(
void _expectDrawDRRectCall(
MockEngineCanvas mock, Map<String, dynamic> expectedArguments) {
expect(mock.methodCallLog.length, equals(2));
MockCanvasCall mockCall = mock.methodCallLog[0];

View File

@ -23,7 +23,8 @@ void main() async {
double maxDiffRatePercent = 0.0}) async {
final EngineCanvas engineCanvas = BitmapCanvas(screenRect);
rc.apply(engineCanvas);
rc.endRecording();
rc.apply(engineCanvas, screenRect);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final html.Element sceneElement = html.Element.tag('flt-scene');

View File

@ -22,7 +22,8 @@ void main() async {
{Rect region = const Rect.fromLTWH(0, 0, 500, 500)}) async {
final engine.EngineCanvas engineCanvas = engine.BitmapCanvas(screenRect);
rc.apply(engineCanvas);
rc.endRecording();
rc.apply(engineCanvas, screenRect);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final html.Element sceneElement = html.Element.tag('flt-scene');

View File

@ -22,7 +22,8 @@ void main() async {
{Rect region = const Rect.fromLTWH(0, 0, 500, 500)}) async {
final engine.EngineCanvas engineCanvas = engine.BitmapCanvas(screenRect);
rc.apply(engineCanvas);
rc.endRecording();
rc.apply(engineCanvas, screenRect);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final html.Element sceneElement = html.Element.tag('flt-scene');

View File

@ -26,7 +26,8 @@ void main() async {
double maxDiffRatePercent = 0.0}) async {
final EngineCanvas engineCanvas = BitmapCanvas(screenRect);
rc.apply(engineCanvas);
rc.endRecording();
rc.apply(engineCanvas, screenRect);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final html.Element sceneElement = html.Element.tag('flt-scene');

View File

@ -39,7 +39,8 @@ void main() async {
..moveTo(3, 0)
..lineTo(100, 97);
rc.drawPath(path, testPaint);
rc.apply(engineCanvas);
rc.endRecording();
rc.apply(engineCanvas, screenRect);
engineCanvas.endOfPaint();
html.Element sceneElement = html.Element.tag('flt-scene');
@ -69,7 +70,8 @@ void main() async {
..quadraticBezierTo(100, 0, 100, 100);
rc2.drawImage(_createRealTestImage(), Offset(0, 0), Paint());
rc2.drawPath(path2, testPaint);
rc2.apply(engineCanvas);
rc2.endRecording();
rc2.apply(engineCanvas, screenRect);
sceneElement = html.Element.tag('flt-scene');
sceneElement.append(engineCanvas.rootElement);

View File

@ -31,9 +31,10 @@ void main() async {
..style = PaintingStyle.stroke;
canvas.drawPath(path, paint);
canvas.endRecording();
html.document.body.append(bitmapCanvas.rootElement);
canvas.apply(bitmapCanvas);
canvas.apply(bitmapCanvas, canvasBounds);
await matchGoldenFile('$scubaFileName.png', region: region);
bitmapCanvas.rootElement.remove();
}

View File

@ -22,7 +22,8 @@ void main() async {
{Rect region = const Rect.fromLTWH(0, 0, 500, 500),
bool write = false}) async {
final EngineCanvas engineCanvas = BitmapCanvas(screenRect);
rc.apply(engineCanvas);
rc.endRecording();
rc.apply(engineCanvas, screenRect);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final html.Element sceneElement = html.Element.tag('flt-scene');

View File

@ -21,7 +21,8 @@ void main() async {
{Rect region = const Rect.fromLTWH(0, 0, 500, 500),
bool write = false}) async {
final EngineCanvas engineCanvas = BitmapCanvas(screenRect);
rc.apply(engineCanvas);
rc.endRecording();
rc.apply(engineCanvas, screenRect);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final html.Element sceneElement = html.Element.tag('flt-scene');

View File

@ -21,10 +21,11 @@ void main() async {
setUpStableTestFonts();
void paintTest(EngineCanvas canvas, PaintTest painter) {
final RecordingCanvas recordingCanvas =
RecordingCanvas(const Rect.fromLTWH(0, 0, 600, 600));
final Rect screenRect = const Rect.fromLTWH(0, 0, 600, 600);
final RecordingCanvas recordingCanvas = RecordingCanvas(screenRect);
painter(recordingCanvas);
recordingCanvas.apply(canvas);
recordingCanvas.endRecording();
recordingCanvas.apply(canvas, screenRect);
}
testEachCanvas(

View File

@ -25,7 +25,8 @@ void main() async {
{Rect region = const Rect.fromLTWH(0, 0, 500, 500),
bool write = false}) async {
final EngineCanvas engineCanvas = BitmapCanvas(screenRect);
rc.apply(engineCanvas);
rc.endRecording();
rc.apply(engineCanvas, screenRect);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final html.Element sceneElement = html.Element.tag('flt-scene');

View File

@ -37,7 +37,8 @@ void main() async {
html.document.body.append(bitmapCanvas.rootElement);
html.document.body.append(svgElement);
canvas.apply(bitmapCanvas);
canvas.endRecording();
canvas.apply(bitmapCanvas, canvasBounds);
await matchGoldenFile('$scubaFileName.png', region: region);

View File

@ -22,7 +22,8 @@ void main() async {
{Rect region = const Rect.fromLTWH(0, 0, 500, 500),
bool write = false}) async {
final EngineCanvas engineCanvas = BitmapCanvas(screenRect);
rc.apply(engineCanvas);
rc.endRecording();
rc.apply(engineCanvas, screenRect);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final html.Element sceneElement = html.Element.tag('flt-scene');

View File

@ -21,7 +21,8 @@ void main() async {
{Rect region = const Rect.fromLTWH(0, 0, 500, 500),
bool write = false}) async {
final EngineCanvas engineCanvas = BitmapCanvas(screenRect);
rc.apply(engineCanvas);
rc.endRecording();
rc.apply(engineCanvas, screenRect);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final html.Element sceneElement = html.Element.tag('flt-scene');

View File

@ -30,7 +30,7 @@ void main() async {
engineCanvas
..save()
..drawRect(
rc.computePaintBounds(),
rc.pictureBounds,
SurfacePaintData()
..color = const Color.fromRGBO(0, 0, 255, 1.0)
..style = PaintingStyle.stroke
@ -38,7 +38,7 @@ void main() async {
)
..restore();
rc.apply(engineCanvas);
rc.apply(engineCanvas, screenRect);
// Wrap in <flt-scene> so that our CSS selectors kick in.
final html.Element sceneElement = html.Element.tag('flt-scene');
@ -63,15 +63,17 @@ void main() async {
test('Empty canvas reports correct paint bounds', () async {
final RecordingCanvas rc =
RecordingCanvas(const Rect.fromLTWH(1, 2, 300, 400));
expect(rc.computePaintBounds(), Rect.zero);
rc.endRecording();
expect(rc.pictureBounds, Rect.zero);
await _checkScreenshot(rc, 'empty_canvas');
});
test('Computes paint bounds for draw line', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawLine(const Offset(50, 100), const Offset(120, 140), testPaint);
rc.endRecording();
// The off by one is due to the minimum stroke width of 1.
expect(rc.computePaintBounds(), const Rect.fromLTRB(49, 99, 121, 141));
expect(rc.pictureBounds, const Rect.fromLTRB(49, 99, 121, 141));
await _checkScreenshot(rc, 'draw_line');
});
@ -81,8 +83,9 @@ void main() async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawLine(const Offset(50, 100), const Offset(screenWidth + 100.0, 140),
testPaint);
rc.endRecording();
// The off by one is due to the minimum stroke width of 1.
expect(rc.computePaintBounds(),
expect(rc.pictureBounds,
const Rect.fromLTRB(49.0, 99.0, screenWidth, 141.0));
await _checkScreenshot(rc, 'draw_line_exceeding_limits');
});
@ -90,7 +93,8 @@ void main() async {
test('Computes paint bounds for draw rect', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(10, 20, 30, 40));
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(10, 20, 30, 40));
await _checkScreenshot(rc, 'draw_rect');
});
@ -100,12 +104,14 @@ void main() async {
rc.drawRect(
const Rect.fromLTRB(10, 20, 30 + screenWidth, 40 + screenHeight),
testPaint);
expect(rc.computePaintBounds(),
rc.endRecording();
expect(rc.pictureBounds,
const Rect.fromLTRB(10, 20, screenWidth, screenHeight));
rc = RecordingCanvas(screenRect);
rc.drawRect(const Rect.fromLTRB(-200, -100, 30, 40), testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(0, 0, 30, 40));
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(0, 0, 30, 40));
await _checkScreenshot(rc, 'draw_rect_exceeding_limits');
});
@ -113,7 +119,8 @@ void main() async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.translate(5, 7);
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(15, 27, 35, 47));
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(15, 27, 35, 47));
await _checkScreenshot(rc, 'translate');
});
@ -121,7 +128,8 @@ void main() async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.scale(2, 2);
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(20, 40, 60, 80));
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(20, 40, 60, 80));
await _checkScreenshot(rc, 'scale');
});
@ -130,8 +138,9 @@ void main() async {
rc.rotate(math.pi / 4.0);
rc.drawLine(
const Offset(1, 0), Offset(50 * math.sqrt(2) - 1, 0), testPaint);
rc.endRecording();
// The extra 0.7 is due to stroke width of 1 rotated by 45 degrees.
expect(rc.computePaintBounds(),
expect(rc.pictureBounds,
within(distance: 0.1, from: const Rect.fromLTRB(0, 0, 50.7, 50.7)));
await _checkScreenshot(rc, 'rotate');
});
@ -140,8 +149,9 @@ void main() async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.skew(1.0, 0.0);
rc.drawRect(const Rect.fromLTRB(20, 20, 40, 40), testPaint);
rc.endRecording();
expect(
rc.computePaintBounds(),
rc.pictureBounds,
within(
distance: 0.1, from: const Rect.fromLTRB(40.0, 20.0, 80.0, 40.0)));
await _checkScreenshot(rc, 'skew_horizontally');
@ -151,8 +161,9 @@ void main() async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.skew(0.0, 1.0);
rc.drawRect(const Rect.fromLTRB(20, 20, 40, 40), testPaint);
rc.endRecording();
expect(
rc.computePaintBounds(),
rc.pictureBounds,
within(
distance: 0.1, from: const Rect.fromLTRB(20.0, 40.0, 40.0, 80.0)));
await _checkScreenshot(rc, 'skew_vertically');
@ -180,7 +191,8 @@ void main() async {
matrix[15] = 1.0;
rc.transform(matrix);
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
expect(rc.computePaintBounds(),
rc.endRecording();
expect(rc.pictureBounds,
const Rect.fromLTRB(168.0, 283.6, 224.0, 368.4));
await _checkScreenshot(rc, 'complex_transform');
});
@ -189,7 +201,8 @@ void main() async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawPaint(testPaint);
rc.drawRect(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
expect(rc.computePaintBounds(), screenRect);
rc.endRecording();
expect(rc.pictureBounds, screenRect);
await _checkScreenshot(rc, 'draw_paint');
});
@ -199,14 +212,16 @@ void main() async {
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);
rc.endRecording();
expect(rc.pictureBounds, screenRect);
await _checkScreenshot(rc, 'draw_color');
});
test('Computes paint bounds for draw oval', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawOval(const Rect.fromLTRB(10, 20, 30, 40), testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(10, 20, 30, 40));
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(10, 20, 30, 40));
await _checkScreenshot(rc, 'draw_oval');
});
@ -216,7 +231,8 @@ void main() async {
RRect.fromRectAndRadius(
const Rect.fromLTRB(10, 20, 30, 40), const Radius.circular(5.0)),
testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(10, 20, 30, 40));
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(10, 20, 30, 40));
await _checkScreenshot(rc, 'draw_round_rect');
});
@ -226,7 +242,8 @@ void main() async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawDRRect(RRect.fromRectAndCorners(const Rect.fromLTRB(10, 20, 30, 40)),
RRect.fromRectAndCorners(const Rect.fromLTRB(1, 2, 3, 4)), testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(0, 0, 0, 0));
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(0, 0, 0, 0));
await _checkScreenshot(rc, 'draw_drrect_empty');
});
@ -236,34 +253,44 @@ void main() async {
RRect.fromRectAndCorners(const Rect.fromLTRB(10, 20, 30, 40)),
RRect.fromRectAndCorners(const Rect.fromLTRB(12, 22, 28, 38)),
testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(10, 20, 30, 40));
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(10, 20, 30, 40));
await _checkScreenshot(rc, 'draw_drrect');
});
test('Computes paint bounds for draw circle', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
// Paint bounds of one circle.
RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawCircle(const Offset(20, 20), 10.0, testPaint);
rc.endRecording();
expect(
rc.computePaintBounds(), const Rect.fromLTRB(10.0, 10.0, 30.0, 30.0));
rc.pictureBounds, const Rect.fromLTRB(10.0, 10.0, 30.0, 30.0));
// Paint bounds of a union of two circles.
rc = RecordingCanvas(screenRect);
rc.drawCircle(const Offset(20, 20), 10.0, testPaint);
rc.drawCircle(const Offset(200, 300), 100.0, testPaint);
rc.endRecording();
expect(
rc.computePaintBounds(), const Rect.fromLTRB(10.0, 10.0, 300.0, 400.0));
rc.pictureBounds, const Rect.fromLTRB(10.0, 10.0, 300.0, 400.0));
await _checkScreenshot(rc, 'draw_circle');
});
test('Computes paint bounds for draw image', () {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawImage(TestImage(), const Offset(50, 100), Paint());
rc.endRecording();
expect(
rc.computePaintBounds(), const Rect.fromLTRB(50.0, 100.0, 70.0, 110.0));
rc.pictureBounds, const Rect.fromLTRB(50.0, 100.0, 70.0, 110.0));
});
test('Computes paint bounds for draw image rect', () {
final RecordingCanvas rc = RecordingCanvas(screenRect);
rc.drawImageRect(TestImage(), const Rect.fromLTRB(1, 1, 20, 10),
const Rect.fromLTRB(5, 6, 400, 500), Paint());
rc.endRecording();
expect(
rc.computePaintBounds(), const Rect.fromLTRB(5.0, 6.0, 400.0, 500.0));
rc.pictureBounds, const Rect.fromLTRB(5.0, 6.0, 400.0, 500.0));
});
test('Computes paint bounds for single-line draw paragraph', () async {
@ -274,8 +301,9 @@ void main() async {
const double widthConstraint = 300.0;
paragraph.layout(const ParagraphConstraints(width: widthConstraint));
rc.drawParagraph(paragraph, const Offset(textLeft, textTop));
rc.endRecording();
expect(
rc.computePaintBounds(),
rc.pictureBounds,
const Rect.fromLTRB(textLeft, textTop, textLeft + widthConstraint, 21.0),
);
await _checkScreenshot(rc, 'draw_paragraph');
@ -286,27 +314,35 @@ void main() async {
final Paragraph paragraph = createTestParagraph();
const double textLeft = 5.0;
const double textTop = 7.0;
const double widthConstraint =
130.0; // do not go lower than the shortest word.
// Do not go lower than the shortest word.
const double widthConstraint = 130.0;
paragraph.layout(const ParagraphConstraints(width: widthConstraint));
rc.drawParagraph(paragraph, const Offset(textLeft, textTop));
rc.endRecording();
expect(
rc.computePaintBounds(),
rc.pictureBounds,
const Rect.fromLTRB(textLeft, textTop, textLeft + widthConstraint, 35.0),
);
await _checkScreenshot(rc, 'draw_paragraph_multi_line');
});
test('Should exclude painting outside simple clipRect', () async {
final RecordingCanvas rc = RecordingCanvas(screenRect);
// One clipped line.
RecordingCanvas rc = RecordingCanvas(screenRect);
rc.clipRect(const Rect.fromLTRB(50, 50, 100, 100));
rc.drawLine(const Offset(10, 11), const Offset(20, 21), testPaint);
rc.endRecording();
expect(rc.pictureBounds, Rect.zero);
expect(rc.computePaintBounds(), Rect.zero);
// Two clipped lines.
rc = RecordingCanvas(screenRect);
rc.clipRect(const Rect.fromLTRB(50, 50, 100, 100));
rc.drawLine(const Offset(10, 11), const Offset(20, 21), testPaint);
rc.drawLine(const Offset(52, 53), const Offset(55, 56), testPaint);
rc.endRecording();
// Extra pixel due to default line length
expect(rc.computePaintBounds(), const Rect.fromLTRB(51, 52, 56, 57));
expect(rc.pictureBounds, const Rect.fromLTRB(51, 52, 56, 57));
await _checkScreenshot(rc, 'clip_rect_simple');
});
@ -314,13 +350,15 @@ void main() async {
RecordingCanvas rc = RecordingCanvas(screenRect);
rc.clipRect(const Rect.fromLTRB(50, 50, 100, 100));
rc.drawRect(const Rect.fromLTRB(20, 60, 120, 70), testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(50, 60, 100, 70));
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(50, 60, 100, 70));
await _checkScreenshot(rc, 'clip_rect_intersects_paint_left_to_right');
rc = RecordingCanvas(screenRect);
rc.clipRect(const Rect.fromLTRB(50, 50, 100, 100));
rc.drawRect(const Rect.fromLTRB(60, 20, 70, 200), testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(60, 50, 70, 100));
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(60, 50, 70, 100));
await _checkScreenshot(rc, 'clip_rect_intersects_paint_top_to_bottom');
});
@ -330,7 +368,9 @@ void main() async {
rc.scale(2.0, 2.0);
rc.clipRect(const Rect.fromLTRB(30, 30, 45, 45));
rc.drawRect(const Rect.fromLTRB(10, 30, 60, 35), testPaint);
expect(rc.computePaintBounds(), const Rect.fromLTRB(60, 60, 90, 70));
rc.endRecording();
expect(rc.pictureBounds, const Rect.fromLTRB(60, 60, 90, 70));
await _checkScreenshot(rc, 'clip_rects_intersect');
});
@ -340,8 +380,10 @@ void main() async {
final Path path = Path();
path.addRect(const Rect.fromLTRB(20, 30, 100, 110));
rc.drawShadow(path, const Color(0xFFFF0000), 2.0, true);
rc.endRecording();
expect(
rc.computePaintBounds(),
rc.pictureBounds,
within(distance: 0.05, from: const Rect.fromLTRB(17.9, 28.5, 103.5, 114.1)),
);
await _checkScreenshot(rc, 'path_with_shadow');
@ -361,8 +403,10 @@ void main() async {
..scale(1, -1)
..clipRect(const Rect.fromLTRB(0, 0, 100, 50))
..drawRect(const Rect.fromLTRB(0, 0, 100, 100), Paint());
rc.endRecording();
expect(
rc.computePaintBounds(), const Rect.fromLTRB(0.0, 50.0, 100.0, 100.0));
rc.pictureBounds, const Rect.fromLTRB(0.0, 50.0, 100.0, 100.0));
await _checkScreenshot(rc, 'scale_negative');
});
@ -374,8 +418,10 @@ void main() async {
..rotate(math.pi / 4.0)
..clipRect(const Rect.fromLTWH(-20, -20, 40, 40))
..drawRect(const Rect.fromLTWH(-80, -80, 160, 160), Paint());
rc.endRecording();
expect(
rc.computePaintBounds(),
rc.pictureBounds,
Rect.fromCircle(center: const Offset(50, 50), radius: 20 * math.sqrt(2)),
);
await _checkScreenshot(rc, 'clip_rect_rotated');
@ -388,8 +434,10 @@ void main() async {
..translate(50, 50)
..rotate(math.pi / 4.0)
..drawLine(const Offset(0, 0), const Offset(20, 20), Paint());
rc.endRecording();
expect(
rc.computePaintBounds(),
rc.pictureBounds,
within(distance: 0.1, from: const Rect.fromLTRB(34.4, 48.6, 65.6, 79.7)),
);
await _checkScreenshot(rc, 'line_rotated');
@ -418,6 +466,7 @@ void main() async {
..style = PaintingStyle.stroke
..strokeWidth = 2.0
..color = const Color(0xFF00FF00));
rc.endRecording();
await _checkScreenshot(rc, 'reuse_path');
});
@ -440,6 +489,7 @@ void main() async {
..strokeWidth = 2.0
..color = const Color(0xFF404000));
rc.restore();
rc.endRecording();
await _checkScreenshot(rc, 'path_with_line_and_roundrect');
});
@ -536,7 +586,7 @@ void main() async {
final SurfacePaint zeroSpreadPaint = SurfacePaint();
painter(canvas, zeroSpreadPaint);
sb.addPicture(Offset.zero, recorder.endRecording());
sb.addPicture(Offset.zero, drawBounds(canvas.computePaintBounds()));
sb.addPicture(Offset.zero, drawBounds(canvas.pictureBounds));
sb.pop();
}
@ -550,7 +600,7 @@ void main() async {
..strokeWidth = 5.0;
painter(canvas, thickStrokePaint);
sb.addPicture(Offset.zero, recorder.endRecording());
sb.addPicture(Offset.zero, drawBounds(canvas.computePaintBounds()));
sb.addPicture(Offset.zero, drawBounds(canvas.pictureBounds));
sb.pop();
}
@ -563,7 +613,7 @@ void main() async {
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 5.0);
painter(canvas, maskFilterBlurPaint);
sb.addPicture(Offset.zero, recorder.endRecording());
sb.addPicture(Offset.zero, drawBounds(canvas.computePaintBounds()));
sb.addPicture(Offset.zero, drawBounds(canvas.pictureBounds));
sb.pop();
}
@ -578,7 +628,7 @@ void main() async {
..maskFilter = const MaskFilter.blur(BlurStyle.normal, 5.0);
painter(canvas, thickStrokeAndBlurPaint);
sb.addPicture(Offset.zero, recorder.endRecording());
sb.addPicture(Offset.zero, drawBounds(canvas.computePaintBounds()));
sb.addPicture(Offset.zero, drawBounds(canvas.pictureBounds));
sb.pop();
}

View File

@ -137,7 +137,10 @@ class MockEngineCanvas implements EngineCanvas {
@override
void drawRect(Rect rect, SurfacePaintData paint) {
_called('drawRect', arguments: paint);
_called('drawRect', arguments: <String, dynamic>{
'rect': rect,
'paint': paint,
});
}
@override