| // 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; |
| |
| /// Enable this to print every command applied by a canvas. |
| const bool _debugDumpPaintCommands = false; |
| |
| // Returns the squared length of the x, y (of a border radius) |
| // It normalizes x, y values before working with them, by |
| // assuming anything < 0 to be 0, because flutter may pass |
| // negative radii (which Skia assumes to be 0), see: |
| // https://skia.org/user/api/SkRRect_Reference#SkRRect_inset |
| double _measureBorderRadius(double x, double y) { |
| double clampedX = x < 0 ? 0 : x; |
| double clampedY = y < 0 ? 0 : y; |
| return clampedX * clampedX + clampedY * clampedY; |
| } |
| |
| /// Records canvas commands to be applied to a [EngineCanvas]. |
| /// |
| /// See [Canvas] for docs for these methods. |
| class RecordingCanvas { |
| /// 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 |
| /// via DOM elements. |
| bool get hasArbitraryPaint => _hasArbitraryPaint; |
| bool _hasArbitraryPaint = false; |
| |
| /// Forces arbitrary paint even for simple pictures. |
| /// |
| /// This is useful for testing bitmap canvas when otherwise the compositor |
| /// would prefer a DOM canvas. |
| void debugEnforceArbitraryPaint() { |
| _hasArbitraryPaint = true; |
| } |
| |
| /// Whether this canvas contain drawing operations. |
| /// |
| /// Some pictures are created but only contain operations that do not result |
| /// in any pixels on the screen. For example, they will only contain saves, |
| /// restores, and translates. This happens when a parent [RenderObject] |
| /// prepares the canvas for its children to paint to, but the child ends up |
| /// not painting anything, such as when an empty [SizedBox] is used to add a |
| /// margin between two widgets. |
| bool get didDraw => _didDraw; |
| bool _didDraw = false; |
| |
| /// 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]. |
| /// |
| /// 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 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 { |
| 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... |
| // https://bugzilla.mozilla.org/show_bug.cgi?id=941146 |
| if (!_isNsErrorFailureException(e)) { |
| rethrow; |
| } |
| } |
| } |
| engineCanvas.endOfPaint(); |
| } |
| |
| /// Prints recorded commands. |
| String debugPrintCommands() { |
| if (assertionsEnabled) { |
| final StringBuffer debugBuf = StringBuffer(); |
| for (int i = 0; i < _commands.length; i++) { |
| final PaintCommand command = _commands[i]; |
| debugBuf.writeln('ctx.$command;'); |
| } |
| return debugBuf.toString(); |
| } |
| return null; |
| } |
| |
| 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()); |
| _paintBounds.saveTransformsAndClip(); |
| _saveCount++; |
| } |
| |
| void saveLayer(ui.Rect bounds, SurfacePaint paint) { |
| assert(!_debugRecordingEnded); |
| _hasArbitraryPaint = true; |
| // TODO(het): Implement this correctly using another canvas. |
| _commands.add(const PaintSave()); |
| _paintBounds.saveTransformsAndClip(); |
| _saveCount++; |
| } |
| |
| void restore() { |
| assert(!_debugRecordingEnded); |
| _paintBounds.restoreTransformsAndClip(); |
| if (_commands.isNotEmpty && _commands.last is PaintSave) { |
| // A restore followed a save without any drawing operations in between. |
| // This means that the save didn't have any effect on drawing operations |
| // and can be omitted. This makes our communication with the canvas less |
| // chatty. |
| _commands.removeLast(); |
| } else { |
| _commands.add(const PaintRestore()); |
| } |
| _saveCount--; |
| } |
| |
| 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(Float32List 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) { |
| assert(!_debugRecordingEnded); |
| final PaintClipRect command = PaintClipRect(rect); |
| _paintBounds.clipRect(rect, command); |
| _hasArbitraryPaint = true; |
| _commands.add(command); |
| } |
| |
| void clipRRect(ui.RRect rrect) { |
| assert(!_debugRecordingEnded); |
| final PaintClipRRect command = PaintClipRRect(rrect); |
| _paintBounds.clipRect(rrect.outerRect, command); |
| _hasArbitraryPaint = true; |
| _commands.add(command); |
| } |
| |
| void clipPath(ui.Path path, {bool doAntiAlias = true}) { |
| assert(!_debugRecordingEnded); |
| final PaintClipPath command = PaintClipPath(path); |
| _paintBounds.clipRect(path.getBounds(), command); |
| _hasArbitraryPaint = true; |
| _commands.add(command); |
| } |
| |
| void drawColor(ui.Color color, ui.BlendMode 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 |
| // 45-degree transform, the bounding box should be the length |
| // of the line long and stroke width wide, but our current |
| // algorithm produces a square with each side of the length |
| // matching the length of the line. |
| _paintBounds.growLTRB( |
| math.min(p1.dx, p2.dx) - paintSpread, |
| 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(command); |
| } |
| |
| void drawPaint(SurfacePaint paint) { |
| assert(!_debugRecordingEnded); |
| _hasArbitraryPaint = true; |
| _didDraw = true; |
| 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), command); |
| } else { |
| _paintBounds.grow(rect, command); |
| } |
| _commands.add(command); |
| } |
| |
| void drawRRect(ui.RRect rrect, SurfacePaint paint) { |
| assert(!_debugRecordingEnded); |
| if (paint.shader != null || !rrect.webOnlyUniformRadii) { |
| _hasArbitraryPaint = true; |
| } |
| _didDraw = true; |
| final double paintSpread = _getPaintSpread(paint); |
| final double left = math.min(rrect.left, rrect.right) - paintSpread; |
| 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; |
| 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; |
| ui.Rect outerRect = outer.outerRect; |
| if (outerRect == innerRect || outerRect.intersect(innerRect) != innerRect) { |
| return; // inner is not fully contained within outer |
| } |
| |
| // Compare radius "length" of the rectangles that are going to be actually drawn |
| final ui.RRect scaledOuter = outer.scaleRadii(); |
| final ui.RRect scaledInner = inner.scaleRadii(); |
| |
| final double outerTl = |
| _measureBorderRadius(scaledOuter.tlRadiusX, scaledOuter.tlRadiusY); |
| final double outerTr = |
| _measureBorderRadius(scaledOuter.trRadiusX, scaledOuter.trRadiusY); |
| final double outerBl = |
| _measureBorderRadius(scaledOuter.blRadiusX, scaledOuter.blRadiusY); |
| final double outerBr = |
| _measureBorderRadius(scaledOuter.brRadiusX, scaledOuter.brRadiusY); |
| |
| final double innerTl = |
| _measureBorderRadius(scaledInner.tlRadiusX, scaledInner.tlRadiusY); |
| final double innerTr = |
| _measureBorderRadius(scaledInner.trRadiusX, scaledInner.trRadiusY); |
| final double innerBl = |
| _measureBorderRadius(scaledInner.blRadiusX, scaledInner.blRadiusY); |
| final double innerBr = |
| _measureBorderRadius(scaledInner.brRadiusX, scaledInner.brRadiusY); |
| |
| if (innerTl > outerTl || |
| innerTr > outerTr || |
| innerBl > outerBl || |
| innerBr > outerBr) { |
| return; // Some inner radius is overlapping some outer radius |
| } |
| |
| _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(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), command); |
| } else { |
| _paintBounds.grow(rect, command); |
| } |
| _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 - distance, |
| c.dy - distance, |
| c.dx + distance, |
| c.dy + distance, |
| command, |
| ); |
| _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. |
| SurfacePath sPath = path; |
| final ui.Rect rect = sPath.webOnlyPathAsRect; |
| if (rect != null) { |
| drawRect(rect, paint); |
| return; |
| } |
| final ui.RRect rrect = sPath.webOnlyPathAsRoundedRect; |
| if (rrect != null) { |
| drawRRect(rrect, paint); |
| return; |
| } |
| } |
| SurfacePath sPath = path; |
| if (sPath.subpaths.isNotEmpty) { |
| _hasArbitraryPaint = true; |
| _didDraw = true; |
| ui.Rect pathBounds = sPath.getBounds(); |
| final double paintSpread = _getPaintSpread(paint); |
| if (paintSpread != 0.0) { |
| pathBounds = pathBounds.inflate(paintSpread); |
| } |
| // 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 = sPath.fillType; |
| _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; |
| 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; |
| 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. |
| return; |
| } |
| |
| _didDraw = true; |
| if (engineParagraph._geometricStyle.ellipsis != null) { |
| _hasArbitraryPaint = true; |
| } |
| 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, |
| 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); |
| 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; |
| 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; |
| final PaintDrawPoints command = PaintDrawPoints(pointMode, points, paint.paintData); |
| _growPaintBoundsByPoints(points, paint.strokeWidth, paint, command); |
| _commands.add(command); |
| } |
| |
| void _growPaintBoundsByPoints(Float32List points, double thickness, SurfacePaint paint, DrawCommand command) { |
| double minValueX, maxValueX, minValueY, maxValueY; |
| minValueX = maxValueX = points[0]; |
| minValueY = maxValueY = points[1]; |
| for (int i = 2, len = points.length; i < len; i += 2) { |
| final double x = points[i]; |
| final double y = points[i + 1]; |
| if (x.isNaN || y.isNaN) { |
| // Follows skia implementation that sets bounds to empty |
| // and aborts. |
| return; |
| } |
| minValueX = math.min(minValueX, x); |
| maxValueX = math.max(maxValueX, x); |
| minValueY = math.min(minValueY, y); |
| maxValueY = math.max(maxValueY, y); |
| } |
| final double distance = thickness / 2.0; |
| final double paintSpread = _getPaintSpread(paint); |
| _paintBounds.growLTRB( |
| minValueX - distance - paintSpread, |
| minValueY - distance - paintSpread, |
| maxValueX + distance + paintSpread, |
| maxValueY + distance + paintSpread, |
| command, |
| ); |
| } |
| |
| int _saveCount = 1; |
| int get saveCount => _saveCount; |
| |
| /// Prints the commands recorded by this canvas to the console. |
| void debugDumpCommands() { |
| print('/' * 40 + ' CANVAS COMMANDS ' + '/' * 40); |
| _commands.forEach(print); |
| print('/' * 37 + ' END OF CANVAS COMMANDS ' + '/' * 36); |
| } |
| } |
| |
| abstract class PaintCommand { |
| const PaintCommand(); |
| |
| void apply(EngineCanvas canvas); |
| |
| 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(); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.save(); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'save()'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(const <int>[1]); |
| } |
| } |
| |
| class PaintRestore extends PaintCommand { |
| const PaintRestore(); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.restore(); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'restore()'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(const <int>[2]); |
| } |
| } |
| |
| class PaintTranslate extends PaintCommand { |
| final double dx; |
| final double dy; |
| |
| PaintTranslate(this.dx, this.dy); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.translate(dx, dy); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'translate($dx, $dy)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(<num>[3, dx, dy]); |
| } |
| } |
| |
| class PaintScale extends PaintCommand { |
| final double sx; |
| final double sy; |
| |
| PaintScale(this.sx, this.sy); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.scale(sx, sy); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'scale($sx, $sy)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(<num>[4, sx, sy]); |
| } |
| } |
| |
| class PaintRotate extends PaintCommand { |
| final double radians; |
| |
| PaintRotate(this.radians); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.rotate(radians); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'rotate($radians)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(<num>[5, radians]); |
| } |
| } |
| |
| class PaintTransform extends PaintCommand { |
| final Float32List matrix4; |
| |
| PaintTransform(this.matrix4); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.transform(matrix4); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'transform(Matrix4.fromFloat32List(Float32List.fromList(<double>[${matrix4.join(', ')}])))'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(<dynamic>[6]..addAll(matrix4)); |
| } |
| } |
| |
| class PaintSkew extends PaintCommand { |
| final double sx; |
| final double sy; |
| |
| PaintSkew(this.sx, this.sy); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.skew(sx, sy); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'skew($sx, $sy)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(<num>[7, sx, sy]); |
| } |
| } |
| |
| class PaintClipRect extends DrawCommand { |
| final ui.Rect rect; |
| |
| PaintClipRect(this.rect); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.clipRect(rect); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'clipRect($rect)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(<dynamic>[8, _serializeRectToCssPaint(rect)]); |
| } |
| } |
| |
| class PaintClipRRect extends DrawCommand { |
| final ui.RRect rrect; |
| |
| PaintClipRRect(this.rrect); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.clipRRect(rrect); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'clipRRect($rrect)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(<dynamic>[ |
| 9, |
| _serializeRRectToCssPaint(rrect), |
| ]); |
| } |
| } |
| |
| class PaintClipPath extends DrawCommand { |
| final SurfacePath path; |
| |
| PaintClipPath(this.path); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.clipPath(path); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'clipPath($path)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(<dynamic>[10, path.webOnlySerializeToCssPaint()]); |
| } |
| } |
| |
| class PaintDrawColor extends DrawCommand { |
| final ui.Color color; |
| final ui.BlendMode blendMode; |
| |
| PaintDrawColor(this.color, this.blendMode); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.drawColor(color, blendMode); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'drawColor($color, $blendMode)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands |
| .add(<dynamic>[11, colorToCssString(color), blendMode.index]); |
| } |
| } |
| |
| class PaintDrawLine extends DrawCommand { |
| final ui.Offset p1; |
| final ui.Offset p2; |
| final SurfacePaintData paint; |
| |
| PaintDrawLine(this.p1, this.p2, this.paint); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.drawLine(p1, p2, paint); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'drawLine($p1, $p2, $paint)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(<dynamic>[ |
| 12, |
| p1.dx, |
| p1.dy, |
| p2.dx, |
| p2.dy, |
| _serializePaintToCssPaint(paint) |
| ]); |
| } |
| } |
| |
| class PaintDrawPaint extends DrawCommand { |
| final SurfacePaintData paint; |
| |
| PaintDrawPaint(this.paint); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.drawPaint(paint); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'drawPaint($paint)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(<dynamic>[13, _serializePaintToCssPaint(paint)]); |
| } |
| } |
| |
| class PaintDrawVertices extends DrawCommand { |
| final ui.Vertices vertices; |
| final ui.BlendMode blendMode; |
| final SurfacePaintData paint; |
| PaintDrawVertices(this.vertices, this.blendMode, this.paint); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.drawVertices(vertices, blendMode, paint); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'drawVertices($vertices, $blendMode, $paint)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| throw UnimplementedError(); |
| } |
| } |
| |
| class PaintDrawPoints extends DrawCommand { |
| final Float32List points; |
| final ui.PointMode pointMode; |
| final SurfacePaintData paint; |
| PaintDrawPoints(this.pointMode, this.points, this.paint); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.drawPoints(pointMode, points, paint); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'drawPoints($pointMode, $points, $paint)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| throw UnimplementedError(); |
| } |
| } |
| |
| class PaintDrawRect extends DrawCommand { |
| final ui.Rect rect; |
| final SurfacePaintData paint; |
| |
| PaintDrawRect(this.rect, this.paint); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.drawRect(rect, paint); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'drawRect($rect, $paint)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(<dynamic>[ |
| 14, |
| _serializeRectToCssPaint(rect), |
| _serializePaintToCssPaint(paint) |
| ]); |
| } |
| } |
| |
| class PaintDrawRRect extends DrawCommand { |
| final ui.RRect rrect; |
| final SurfacePaintData paint; |
| |
| PaintDrawRRect(this.rrect, this.paint); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.drawRRect(rrect, paint); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'drawRRect($rrect, $paint)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(<dynamic>[ |
| 15, |
| _serializeRRectToCssPaint(rrect), |
| _serializePaintToCssPaint(paint), |
| ]); |
| } |
| } |
| |
| class PaintDrawDRRect extends DrawCommand { |
| final ui.RRect outer; |
| final ui.RRect inner; |
| final SurfacePaintData paint; |
| |
| PaintDrawDRRect(this.outer, this.inner, this.paint); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.drawDRRect(outer, inner, paint); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'drawDRRect($outer, $inner, $paint)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(<dynamic>[ |
| 16, |
| _serializeRRectToCssPaint(outer), |
| _serializeRRectToCssPaint(inner), |
| _serializePaintToCssPaint(paint), |
| ]); |
| } |
| } |
| |
| class PaintDrawOval extends DrawCommand { |
| final ui.Rect rect; |
| final SurfacePaintData paint; |
| |
| PaintDrawOval(this.rect, this.paint); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.drawOval(rect, paint); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'drawOval($rect, $paint)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(<dynamic>[ |
| 17, |
| _serializeRectToCssPaint(rect), |
| _serializePaintToCssPaint(paint), |
| ]); |
| } |
| } |
| |
| class PaintDrawCircle extends DrawCommand { |
| final ui.Offset c; |
| final double radius; |
| final SurfacePaintData paint; |
| |
| PaintDrawCircle(this.c, this.radius, this.paint); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.drawCircle(c, radius, paint); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'drawCircle($c, $radius, $paint)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(<dynamic>[ |
| 18, |
| c.dx, |
| c.dy, |
| radius, |
| _serializePaintToCssPaint(paint), |
| ]); |
| } |
| } |
| |
| class PaintDrawPath extends DrawCommand { |
| final SurfacePath path; |
| final SurfacePaintData paint; |
| |
| PaintDrawPath(this.path, this.paint); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.drawPath(path, paint); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'drawPath($path, $paint)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(<dynamic>[ |
| 19, |
| path.webOnlySerializeToCssPaint(), |
| _serializePaintToCssPaint(paint), |
| ]); |
| } |
| } |
| |
| class PaintDrawShadow extends DrawCommand { |
| PaintDrawShadow( |
| this.path, this.color, this.elevation, this.transparentOccluder); |
| |
| final SurfacePath path; |
| final ui.Color color; |
| final double elevation; |
| final bool transparentOccluder; |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.drawShadow(path, color, elevation, transparentOccluder); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'drawShadow($path, $color, $elevation, $transparentOccluder)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| serializedCommands.add(<dynamic>[ |
| 20, |
| path.webOnlySerializeToCssPaint(), |
| <dynamic>[ |
| color.alpha, |
| color.red, |
| color.green, |
| color.blue, |
| ], |
| elevation, |
| transparentOccluder, |
| ]); |
| } |
| } |
| |
| class PaintDrawImage extends DrawCommand { |
| final ui.Image image; |
| final ui.Offset offset; |
| final SurfacePaintData paint; |
| |
| PaintDrawImage(this.image, this.offset, this.paint); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.drawImage(image, offset, paint); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'drawImage($image, $offset, $paint)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| if (assertionsEnabled) { |
| throw UnsupportedError('drawImage not serializable'); |
| } |
| } |
| } |
| |
| class PaintDrawImageRect extends DrawCommand { |
| final ui.Image image; |
| final ui.Rect src; |
| final ui.Rect dst; |
| final SurfacePaintData paint; |
| |
| PaintDrawImageRect(this.image, this.src, this.dst, this.paint); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.drawImageRect(image, src, dst, paint); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'drawImageRect($image, $src, $dst, $paint)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| if (assertionsEnabled) { |
| throw UnsupportedError('drawImageRect not serializable'); |
| } |
| } |
| } |
| |
| class PaintDrawParagraph extends DrawCommand { |
| final EngineParagraph paragraph; |
| final ui.Offset offset; |
| |
| PaintDrawParagraph(this.paragraph, this.offset); |
| |
| @override |
| void apply(EngineCanvas canvas) { |
| canvas.drawParagraph(paragraph, offset); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'DrawParagraph(${paragraph._plainText}, $offset)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| |
| @override |
| void serializeToCssPaint(List<List<dynamic>> serializedCommands) { |
| if (assertionsEnabled) { |
| throw UnsupportedError('drawParagraph not serializable'); |
| } |
| } |
| } |
| |
| List<dynamic> _serializePaintToCssPaint(SurfacePaintData paint) { |
| final EngineGradient engineShader = paint.shader; |
| return <dynamic>[ |
| paint.blendMode?.index, |
| paint.style?.index, |
| paint.strokeWidth, |
| paint.strokeCap?.index, |
| paint.isAntiAlias, |
| colorToCssString(paint.color), |
| engineShader?.webOnlySerializeToCssPaint(), |
| paint.maskFilter?.webOnlySerializeToCssPaint(), |
| paint.filterQuality?.index, |
| paint.colorFilter?.webOnlySerializeToCssPaint(), |
| ]; |
| } |
| |
| List<dynamic> _serializeRectToCssPaint(ui.Rect rect) { |
| return <dynamic>[ |
| rect.left, |
| rect.top, |
| rect.right, |
| rect.bottom, |
| ]; |
| } |
| |
| List<dynamic> _serializeRRectToCssPaint(ui.RRect rrect) { |
| return <dynamic>[ |
| rrect.left, |
| rrect.top, |
| rrect.right, |
| rrect.bottom, |
| rrect.tlRadiusX, |
| rrect.tlRadiusY, |
| rrect.trRadiusX, |
| rrect.trRadiusY, |
| rrect.brRadiusX, |
| rrect.brRadiusY, |
| rrect.blRadiusX, |
| rrect.blRadiusY, |
| ]; |
| } |
| |
| class Subpath { |
| double startX = 0.0; |
| double startY = 0.0; |
| double currentX = 0.0; |
| double currentY = 0.0; |
| |
| final List<PathCommand> commands; |
| |
| Subpath(this.startX, this.startY) : commands = <PathCommand>[]; |
| |
| Subpath shift(ui.Offset offset) { |
| final Subpath result = Subpath(startX + offset.dx, startY + offset.dy) |
| ..currentX = currentX + offset.dx |
| ..currentY = currentY + offset.dy; |
| |
| for (final PathCommand command in commands) { |
| result.commands.add(command.shifted(offset)); |
| } |
| |
| return result; |
| } |
| |
| List<dynamic> serializeToCssPaint() { |
| final List<dynamic> serialization = <dynamic>[]; |
| for (int i = 0; i < commands.length; i++) { |
| serialization.add(commands[i].serializeToCssPaint()); |
| } |
| return serialization; |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'Subpath(${commands.join(', ')})'; |
| } else { |
| return super.toString(); |
| } |
| } |
| } |
| |
| /// ! Houdini implementation relies on indices here. Keep in sync. |
| class PathCommandTypes { |
| static const int moveTo = 0; |
| static const int lineTo = 1; |
| static const int ellipse = 2; |
| static const int close = 3; |
| static const int quadraticCurveTo = 4; |
| static const int bezierCurveTo = 5; |
| static const int rect = 6; |
| static const int rRect = 7; |
| } |
| |
| abstract class PathCommand { |
| final int type; |
| const PathCommand(this.type); |
| |
| PathCommand shifted(ui.Offset offset); |
| |
| List<dynamic> serializeToCssPaint(); |
| |
| /// Transform the command and add to targetPath. |
| void transform(Float32List matrix4, SurfacePath targetPath); |
| |
| /// Helper method for implementing transforms. |
| static ui.Offset _transformOffset(double x, double y, Float32List matrix4) => |
| ui.Offset((matrix4[0] * x) + (matrix4[4] * y) + matrix4[12], |
| (matrix4[1] * x) + (matrix4[5] * y) + matrix4[13]); |
| } |
| |
| class MoveTo extends PathCommand { |
| final double x; |
| final double y; |
| |
| const MoveTo(this.x, this.y) : super(PathCommandTypes.moveTo); |
| |
| @override |
| MoveTo shifted(ui.Offset offset) { |
| return MoveTo(x + offset.dx, y + offset.dy); |
| } |
| |
| @override |
| List<dynamic> serializeToCssPaint() { |
| return <dynamic>[1, x, y]; |
| } |
| |
| @override |
| void transform(Float32List matrix4, ui.Path targetPath) { |
| final ui.Offset offset = PathCommand._transformOffset(x, y, matrix4); |
| targetPath.moveTo(offset.dx, offset.dy); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'MoveTo($x, $y)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| } |
| |
| class LineTo extends PathCommand { |
| final double x; |
| final double y; |
| |
| const LineTo(this.x, this.y) : super(PathCommandTypes.lineTo); |
| |
| @override |
| LineTo shifted(ui.Offset offset) { |
| return LineTo(x + offset.dx, y + offset.dy); |
| } |
| |
| @override |
| List<dynamic> serializeToCssPaint() { |
| return <dynamic>[2, x, y]; |
| } |
| |
| @override |
| void transform(Float32List matrix4, ui.Path targetPath) { |
| final ui.Offset offset = PathCommand._transformOffset(x, y, matrix4); |
| targetPath.lineTo(offset.dx, offset.dy); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'LineTo($x, $y)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| } |
| |
| class Ellipse extends PathCommand { |
| final double x; |
| final double y; |
| final double radiusX; |
| final double radiusY; |
| final double rotation; |
| final double startAngle; |
| final double endAngle; |
| final bool anticlockwise; |
| |
| const Ellipse(this.x, this.y, this.radiusX, this.radiusY, this.rotation, |
| this.startAngle, this.endAngle, this.anticlockwise) |
| : super(PathCommandTypes.ellipse); |
| |
| @override |
| Ellipse shifted(ui.Offset offset) { |
| return Ellipse(x + offset.dx, y + offset.dy, radiusX, radiusY, rotation, |
| startAngle, endAngle, anticlockwise); |
| } |
| |
| @override |
| List<dynamic> serializeToCssPaint() { |
| return <dynamic>[ |
| 3, |
| x, |
| y, |
| radiusX, |
| radiusY, |
| rotation, |
| startAngle, |
| endAngle, |
| anticlockwise, |
| ]; |
| } |
| |
| @override |
| void transform(Float32List matrix4, SurfacePath targetPath) { |
| final ui.Path bezierPath = ui.Path(); |
| _drawArcWithBezier( |
| x, |
| y, |
| radiusX, |
| radiusY, |
| rotation, |
| startAngle, |
| anticlockwise ? startAngle - endAngle : endAngle - startAngle, |
| matrix4, |
| bezierPath); |
| if (matrix4 != null) { |
| targetPath._addPathWithMatrix(bezierPath, 0, 0, matrix4); |
| } else { |
| targetPath._addPath(bezierPath, 0, 0); |
| } |
| } |
| |
| void _drawArcWithBezier( |
| double centerX, |
| double centerY, |
| double radiusX, |
| double radiusY, |
| double rotation, |
| double startAngle, |
| double sweep, |
| Float32List matrix4, |
| ui.Path targetPath) { |
| double ratio = sweep.abs() / (math.pi / 2.0); |
| if ((1.0 - ratio).abs() < 0.0000001) { |
| ratio = 1.0; |
| } |
| final int segments = math.max(ratio.ceil(), 1); |
| final double anglePerSegment = sweep / segments; |
| double angle = startAngle; |
| for (int segment = 0; segment < segments; segment++) { |
| _drawArcSegment(targetPath, centerX, centerY, radiusX, radiusY, rotation, |
| angle, anglePerSegment, segment == 0, matrix4); |
| angle += anglePerSegment; |
| } |
| } |
| |
| void _drawArcSegment( |
| ui.Path path, |
| double centerX, |
| double centerY, |
| double radiusX, |
| double radiusY, |
| double rotation, |
| double startAngle, |
| double sweep, |
| bool startPath, |
| Float32List matrix4) { |
| final double s = 4 / 3 * math.tan(sweep / 4); |
| |
| // Rotate unit vector to startAngle and endAngle to use for computing start |
| // and end points of segment. |
| final double x1 = math.cos(startAngle); |
| final double y1 = math.sin(startAngle); |
| final double endAngle = startAngle + sweep; |
| final double x2 = math.cos(endAngle); |
| final double y2 = math.sin(endAngle); |
| |
| // Compute scaled curve control points. |
| final double cpx1 = (x1 - y1 * s) * radiusX; |
| final double cpy1 = (y1 + x1 * s) * radiusY; |
| final double cpx2 = (x2 + y2 * s) * radiusX; |
| final double cpy2 = (y2 - x2 * s) * radiusY; |
| |
| final double endPointX = centerX + x2 * radiusX; |
| final double endPointY = centerY + y2 * radiusY; |
| |
| final double rotationRad = rotation * math.pi / 180.0; |
| final double cosR = math.cos(rotationRad); |
| final double sinR = math.sin(rotationRad); |
| if (startPath) { |
| final double scaledX1 = x1 * radiusX; |
| final double scaledY1 = y1 * radiusY; |
| if (rotation == 0.0) { |
| path.moveTo(centerX + scaledX1, centerY + scaledY1); |
| } else { |
| final double rotatedStartX = (scaledX1 * cosR) + (scaledY1 * sinR); |
| final double rotatedStartY = (scaledY1 * cosR) - (scaledX1 * sinR); |
| path.moveTo(centerX + rotatedStartX, centerY + rotatedStartY); |
| } |
| } |
| if (rotation == 0.0) { |
| path.cubicTo(centerX + cpx1, centerY + cpy1, centerX + cpx2, |
| centerY + cpy2, endPointX, endPointY); |
| } else { |
| final double rotatedCpx1 = centerX + (cpx1 * cosR) + (cpy1 * sinR); |
| final double rotatedCpy1 = centerY + (cpy1 * cosR) - (cpx1 * sinR); |
| final double rotatedCpx2 = centerX + (cpx2 * cosR) + (cpy2 * sinR); |
| final double rotatedCpy2 = centerY + (cpy2 * cosR) - (cpx2 * sinR); |
| final double rotatedEndX = centerX + |
| ((endPointX - centerX) * cosR) + |
| ((endPointY - centerY) * sinR); |
| final double rotatedEndY = centerY + |
| ((endPointY - centerY) * cosR) - |
| ((endPointX - centerX) * sinR); |
| path.cubicTo(rotatedCpx1, rotatedCpy1, rotatedCpx2, rotatedCpy2, |
| rotatedEndX, rotatedEndY); |
| } |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'Ellipse($x, $y, $radiusX, $radiusY)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| } |
| |
| class QuadraticCurveTo extends PathCommand { |
| final double x1; |
| final double y1; |
| final double x2; |
| final double y2; |
| |
| const QuadraticCurveTo(this.x1, this.y1, this.x2, this.y2) |
| : super(PathCommandTypes.quadraticCurveTo); |
| |
| @override |
| QuadraticCurveTo shifted(ui.Offset offset) { |
| return QuadraticCurveTo( |
| x1 + offset.dx, y1 + offset.dy, x2 + offset.dx, y2 + offset.dy); |
| } |
| |
| @override |
| List<dynamic> serializeToCssPaint() { |
| return <dynamic>[4, x1, y1, x2, y2]; |
| } |
| |
| @override |
| void transform(Float32List matrix4, ui.Path targetPath) { |
| final double m0 = matrix4[0]; |
| final double m1 = matrix4[1]; |
| final double m4 = matrix4[4]; |
| final double m5 = matrix4[5]; |
| final double m12 = matrix4[12]; |
| final double m13 = matrix4[13]; |
| final double transformedX1 = (m0 * x1) + (m4 * y1) + m12; |
| final double transformedY1 = (m1 * x1) + (m5 * y1) + m13; |
| final double transformedX2 = (m0 * x2) + (m4 * y2) + m12; |
| final double transformedY2 = (m1 * x2) + (m5 * y2) + m13; |
| targetPath.quadraticBezierTo( |
| transformedX1, transformedY1, transformedX2, transformedY2); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'QuadraticCurveTo($x1, $y1, $x2, $y2)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| } |
| |
| class BezierCurveTo extends PathCommand { |
| final double x1; |
| final double y1; |
| final double x2; |
| final double y2; |
| final double x3; |
| final double y3; |
| |
| const BezierCurveTo(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3) |
| : super(PathCommandTypes.bezierCurveTo); |
| |
| @override |
| BezierCurveTo shifted(ui.Offset offset) { |
| return BezierCurveTo(x1 + offset.dx, y1 + offset.dy, x2 + offset.dx, |
| y2 + offset.dy, x3 + offset.dx, y3 + offset.dy); |
| } |
| |
| @override |
| List<dynamic> serializeToCssPaint() { |
| return <dynamic>[5, x1, y1, x2, y2, x3, y3]; |
| } |
| |
| @override |
| void transform(Float32List matrix4, ui.Path targetPath) { |
| final double s0 = matrix4[0]; |
| final double s1 = matrix4[1]; |
| final double s4 = matrix4[4]; |
| final double s5 = matrix4[5]; |
| final double s12 = matrix4[12]; |
| final double s13 = matrix4[13]; |
| final double transformedX1 = (s0 * x1) + (s4 * y1) + s12; |
| final double transformedY1 = (s1 * x1) + (s5 * y1) + s13; |
| final double transformedX2 = (s0 * x2) + (s4 * y2) + s12; |
| final double transformedY2 = (s1 * x2) + (s5 * y2) + s13; |
| final double transformedX3 = (s0 * x3) + (s4 * y3) + s12; |
| final double transformedY3 = (s1 * x3) + (s5 * y3) + s13; |
| targetPath.cubicTo(transformedX1, transformedY1, transformedX2, |
| transformedY2, transformedX3, transformedY3); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'BezierCurveTo($x1, $y1, $x2, $y2, $x3, $y3)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| } |
| |
| class RectCommand extends PathCommand { |
| final double x; |
| final double y; |
| final double width; |
| final double height; |
| |
| const RectCommand(this.x, this.y, this.width, this.height) |
| : super(PathCommandTypes.rect); |
| |
| @override |
| RectCommand shifted(ui.Offset offset) { |
| return RectCommand(x + offset.dx, y + offset.dy, width, height); |
| } |
| |
| @override |
| void transform(Float32List matrix4, ui.Path targetPath) { |
| final double s0 = matrix4[0]; |
| final double s1 = matrix4[1]; |
| final double s4 = matrix4[4]; |
| final double s5 = matrix4[5]; |
| final double s12 = matrix4[12]; |
| final double s13 = matrix4[13]; |
| final double transformedX1 = (s0 * x) + (s4 * y) + s12; |
| final double transformedY1 = (s1 * x) + (s5 * y) + s13; |
| final double x2 = x + width; |
| final double y2 = y + height; |
| final double transformedX2 = (s0 * x2) + (s4 * y) + s12; |
| final double transformedY2 = (s1 * x2) + (s5 * y) + s13; |
| final double transformedX3 = (s0 * x2) + (s4 * y2) + s12; |
| final double transformedY3 = (s1 * x2) + (s5 * y2) + s13; |
| final double transformedX4 = (s0 * x) + (s4 * y2) + s12; |
| final double transformedY4 = (s1 * x) + (s5 * y2) + s13; |
| if (transformedY1 == transformedY2 && |
| transformedY3 == transformedY4 && |
| transformedX1 == transformedX4 && |
| transformedX2 == transformedX3) { |
| // It is still a rectangle. |
| targetPath.addRect(ui.Rect.fromLTRB( |
| transformedX1, transformedY1, transformedX3, transformedY3)); |
| } else { |
| targetPath.moveTo(transformedX1, transformedY1); |
| targetPath.lineTo(transformedX2, transformedY2); |
| targetPath.lineTo(transformedX3, transformedY3); |
| targetPath.lineTo(transformedX4, transformedY4); |
| targetPath.close(); |
| } |
| } |
| |
| @override |
| List<dynamic> serializeToCssPaint() { |
| return <dynamic>[6, x, y, width, height]; |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'Rect($x, $y, $width, $height)'; |
| } else { |
| return super.toString(); |
| } |
| } |
| } |
| |
| class RRectCommand extends PathCommand { |
| final ui.RRect rrect; |
| |
| const RRectCommand(this.rrect) : super(PathCommandTypes.rRect); |
| |
| @override |
| RRectCommand shifted(ui.Offset offset) { |
| return RRectCommand(rrect.shift(offset)); |
| } |
| |
| @override |
| List<dynamic> serializeToCssPaint() { |
| return <dynamic>[7, _serializeRRectToCssPaint(rrect)]; |
| } |
| |
| @override |
| void transform(Float32List matrix4, SurfacePath targetPath) { |
| final ui.Path roundRectPath = ui.Path(); |
| _RRectToPathRenderer(roundRectPath).render(rrect); |
| if (matrix4 != null) { |
| targetPath._addPathWithMatrix(roundRectPath, 0, 0, matrix4); |
| } else { |
| targetPath._addPath(roundRectPath, 0, 0); |
| } |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return '$rrect'; |
| } else { |
| return super.toString(); |
| } |
| } |
| } |
| |
| class CloseCommand extends PathCommand { |
| const CloseCommand() : super(PathCommandTypes.close); |
| |
| @override |
| CloseCommand shifted(ui.Offset offset) { |
| return this; |
| } |
| |
| @override |
| List<dynamic> serializeToCssPaint() { |
| return <dynamic>[8]; |
| } |
| |
| @override |
| void transform(Float32List matrix4, ui.Path targetPath) { |
| targetPath.close(); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| return 'Close()'; |
| } else { |
| return super.toString(); |
| } |
| } |
| } |
| |
| class _PaintBounds { |
| // Bounds of maximum area that is paintable by canvas ops. |
| final ui.Rect maxPaintBounds; |
| |
| bool _didPaintInsideClipArea = false; |
| // Bounds of actually painted area. If _left is not set, reported paintBounds |
| // should be empty since growLTRB calls were outside active clipping |
| // region. |
| double _left, _top, _right, _bottom; |
| // Stack of transforms. |
| List<Matrix4> _transforms; |
| // Stack of clip bounds. |
| List<ui.Rect> _clipStack; |
| bool _currentMatrixIsIdentity = true; |
| Matrix4 _currentMatrix = Matrix4.identity(); |
| bool _clipRectInitialized = false; |
| double _currentClipLeft = 0.0, |
| _currentClipTop = 0.0, |
| _currentClipRight = 0.0, |
| _currentClipBottom = 0.0; |
| |
| _PaintBounds(this.maxPaintBounds); |
| |
| void translate(double dx, double dy) { |
| if (dx != 0.0 || dy != 0.0) { |
| _currentMatrixIsIdentity = false; |
| } |
| _currentMatrix.translate(dx, dy); |
| } |
| |
| void scale(double sx, double sy) { |
| if (sx != 1.0 || sy != 1.0) { |
| _currentMatrixIsIdentity = false; |
| } |
| _currentMatrix.scale(sx, sy); |
| } |
| |
| void rotateZ(double radians) { |
| if (radians != 0.0) { |
| _currentMatrixIsIdentity = false; |
| } |
| _currentMatrix.rotateZ(radians); |
| } |
| |
| void transform(Float32List matrix4) { |
| final Matrix4 m4 = Matrix4.fromFloat32List(matrix4); |
| _currentMatrix.multiply(m4); |
| _currentMatrixIsIdentity = _currentMatrix.isIdentity(); |
| } |
| |
| void skew(double sx, double sy) { |
| _currentMatrixIsIdentity = false; |
| |
| // DO NOT USE Matrix4.skew(sx, sy)! It treats sx and sy values as radians, |
| // but in our case they are transform matrix values. |
| final Matrix4 skewMatrix = Matrix4.identity(); |
| final Float32List storage = skewMatrix.storage; |
| storage[1] = sy; |
| storage[4] = sx; |
| _currentMatrix.multiply(skewMatrix); |
| } |
| |
| static final Float32List _tempRectData = Float32List(4); |
| |
| void clipRect(final ui.Rect rect, DrawCommand command) { |
| double left = rect.left; |
| double top = rect.top; |
| double right = rect.right; |
| double bottom = rect.bottom; |
| |
| // If we have an active transform, calculate screen relative clipping |
| // rectangle and union with current clipping rectangle. |
| if (!_currentMatrixIsIdentity) { |
| _tempRectData[0] = left; |
| _tempRectData[1] = top; |
| _tempRectData[2] = right; |
| _tempRectData[3] = bottom; |
| |
| transformLTRB(_currentMatrix, _tempRectData); |
| left = _tempRectData[0]; |
| top = _tempRectData[1]; |
| right = _tempRectData[2]; |
| bottom = _tempRectData[3]; |
| } |
| |
| if (!_clipRectInitialized) { |
| _currentClipLeft = left; |
| _currentClipTop = top; |
| _currentClipRight = right; |
| _currentClipBottom = bottom; |
| _clipRectInitialized = true; |
| } else { |
| if (left > _currentClipLeft) { |
| _currentClipLeft = left; |
| } |
| if (top > _currentClipTop) { |
| _currentClipTop = top; |
| } |
| if (right < _currentClipRight) { |
| _currentClipRight = right; |
| } |
| if (bottom < _currentClipBottom) { |
| _currentClipBottom = 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, 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, DrawCommand command) { |
| if (left == right || top == bottom) { |
| command.isClippedOut = true; |
| return; |
| } |
| |
| double transformedPointLeft = left; |
| double transformedPointTop = top; |
| double transformedPointRight = right; |
| double transformedPointBottom = bottom; |
| |
| if (!_currentMatrixIsIdentity) { |
| _tempRectData[0] = left; |
| _tempRectData[1] = top; |
| _tempRectData[2] = right; |
| _tempRectData[3] = bottom; |
| |
| transformLTRB(_currentMatrix, _tempRectData); |
| transformedPointLeft = _tempRectData[0]; |
| transformedPointTop = _tempRectData[1]; |
| transformedPointRight = _tempRectData[2]; |
| transformedPointBottom = _tempRectData[3]; |
| } |
| |
| 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) { |
| transformedPointLeft = _currentClipLeft; |
| } |
| if (transformedPointRight > _currentClipRight) { |
| transformedPointRight = _currentClipRight; |
| } |
| if (transformedPointTop < _currentClipTop) { |
| transformedPointTop = _currentClipTop; |
| } |
| if (transformedPointBottom > _currentClipBottom) { |
| transformedPointBottom = _currentClipBottom; |
| } |
| } |
| |
| command.leftBound = transformedPointLeft; |
| command.topBound = transformedPointTop; |
| command.rightBound = transformedPointRight; |
| command.bottomBound = transformedPointBottom; |
| |
| if (_didPaintInsideClipArea) { |
| _left = math.min( |
| math.min(_left, transformedPointLeft), transformedPointRight); |
| _right = math.max( |
| math.max(_right, transformedPointLeft), transformedPointRight); |
| _top = |
| math.min(math.min(_top, transformedPointTop), transformedPointBottom); |
| _bottom = math.max( |
| math.max(_bottom, transformedPointTop), transformedPointBottom); |
| } else { |
| _left = math.min(transformedPointLeft, transformedPointRight); |
| _right = math.max(transformedPointLeft, transformedPointRight); |
| _top = math.min(transformedPointTop, transformedPointBottom); |
| _bottom = math.max(transformedPointTop, transformedPointBottom); |
| } |
| _didPaintInsideClipArea = true; |
| } |
| |
| void saveTransformsAndClip() { |
| _clipStack ??= <ui.Rect>[]; |
| _transforms ??= <Matrix4>[]; |
| _transforms.add(_currentMatrix?.clone()); |
| _clipStack.add(_clipRectInitialized |
| ? ui.Rect.fromLTRB(_currentClipLeft, _currentClipTop, _currentClipRight, |
| _currentClipBottom) |
| : null); |
| } |
| |
| void restoreTransformsAndClip() { |
| _currentMatrix = _transforms.removeLast(); |
| final ui.Rect clipRect = _clipStack.removeLast(); |
| if (clipRect != null) { |
| _currentClipLeft = clipRect.left; |
| _currentClipTop = clipRect.top; |
| _currentClipRight = clipRect.right; |
| _currentClipBottom = clipRect.bottom; |
| _clipRectInitialized = true; |
| } else if (_clipRectInitialized) { |
| _clipRectInitialized = false; |
| } |
| } |
| |
| ui.Rect computeBounds() { |
| if (!_didPaintInsideClipArea) { |
| return ui.Rect.zero; |
| } |
| |
| // The framework may send us NaNs in the case when it attempts to invert an |
| // infinitely size rect. |
| final double maxLeft = maxPaintBounds.left.isNaN |
| ? double.negativeInfinity |
| : maxPaintBounds.left; |
| final double maxRight = |
| maxPaintBounds.right.isNaN ? double.infinity : maxPaintBounds.right; |
| final double maxTop = |
| maxPaintBounds.top.isNaN ? double.negativeInfinity : maxPaintBounds.top; |
| final double maxBottom = |
| maxPaintBounds.bottom.isNaN ? double.infinity : maxPaintBounds.bottom; |
| |
| final double left = math.min(_left, _right); |
| final double right = math.max(_left, _right); |
| final double top = math.min(_top, _bottom); |
| final double bottom = math.max(_top, _bottom); |
| |
| if (right < maxLeft || bottom < maxTop) { |
| // Computed and max bounds do not intersect. |
| return ui.Rect.zero; |
| } |
| |
| return ui.Rect.fromLTRB( |
| math.max(left, maxLeft), |
| math.max(top, maxTop), |
| math.min(right, maxRight), |
| math.min(bottom, maxBottom), |
| ); |
| } |
| |
| @override |
| String toString() { |
| if (assertionsEnabled) { |
| final ui.Rect bounds = computeBounds(); |
| return '_PaintBounds($bounds of size ${bounds.size})'; |
| } else { |
| return super.toString(); |
| } |
| } |
| } |
| |
| /// Computes the length of the visual effect caused by paint parameters, such |
| /// as blur and stroke width. |
| /// |
| /// This paint spread should be taken into accound when estimating bounding |
| /// boxes for paint operations that apply the paint. |
| double _getPaintSpread(SurfacePaint paint) { |
| double spread = 0.0; |
| final ui.MaskFilter maskFilter = paint?.maskFilter; |
| if (maskFilter != null) { |
| // Multiply by 2 because the sigma is the standard deviation rather than |
| // the length of the blur. |
| // See also: https://developer.mozilla.org/en-US/docs/Web/CSS/filter-function/blur |
| spread += maskFilter.webOnlySigma * 2.0; |
| } |
| if (paint.strokeWidth != null && paint.strokeWidth != 0) { |
| // The multiplication by sqrt(2) is to account for line joints that |
| // meet at 90-degree angle. Division by 2 is because only half of the |
| // stroke is sticking out of the original shape. The other half is |
| // inside the shape. |
| const double sqrtOfTwoDivByTwo = 0.70710678118; |
| spread += paint.strokeWidth * sqrtOfTwoDivByTwo; |
| } |
| return spread; |
| } |