diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 7340ce6a040..2317b1ea8b3 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -475,6 +475,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/image_filter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/offset.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/opacity.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/painting.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/path_metrics.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/picture.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/platform_view.dart @@ -482,6 +483,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/recording_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/scene.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/scene_builder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/surface.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/surface_stats.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/surface/transform.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/test_embedding.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/font_collection.dart diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine.dart b/engine/src/flutter/lib/web_ui/lib/src/engine.dart index 9af6c4f7887..142193378de 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine.dart @@ -103,6 +103,8 @@ part 'engine/surface/recording_canvas.dart'; part 'engine/surface/scene.dart'; part 'engine/surface/scene_builder.dart'; part 'engine/surface/surface.dart'; +part 'engine/surface/path.dart'; +part 'engine/surface/surface_stats.dart'; part 'engine/surface/transform.dart'; part 'engine/test_embedding.dart'; part 'engine/text/font_collection.dart'; @@ -187,8 +189,8 @@ void webOnlyInitializeEngine() { final int highResTimeMicroseconds = (1000 * highResTime).toInt(); if (window._onBeginFrame != null) { - window - .invokeOnBeginFrame(Duration(microseconds: highResTimeMicroseconds)); + window.invokeOnBeginFrame( + Duration(microseconds: highResTimeMicroseconds)); } if (window._onDrawFrame != null) { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/surface/painting.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/surface/painting.dart index 59388db3249..5a14d934efc 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/surface/painting.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/surface/painting.dart @@ -247,1164 +247,3 @@ class SurfacePaintData { ..strokeCap = strokeCap; } } - -/// A complex, one-dimensional subset of a plane. -/// -/// A path consists of a number of subpaths, and a _current point_. -/// -/// Subpaths consist of segments of various types, such as lines, -/// arcs, or beziers. Subpaths can be open or closed, and can -/// self-intersect. -/// -/// Closed subpaths enclose a (possibly discontiguous) region of the -/// plane based on the current [fillType]. -/// -/// The _current point_ is initially at the origin. After each -/// operation adding a segment to a subpath, the current point is -/// updated to the end of that segment. -/// -/// Paths can be drawn on canvases using [Canvas.drawPath], and can -/// used to create clip regions using [Canvas.clipPath]. -class SurfacePath implements ui.Path { - final List subpaths; - ui.PathFillType _fillType = ui.PathFillType.nonZero; - - Subpath get _currentSubpath => subpaths.isEmpty ? null : subpaths.last; - - List get _commands => _currentSubpath?.commands; - - /// The current x-coordinate for this path. - double get _currentX => _currentSubpath?.currentX ?? 0.0; - - /// The current y-coordinate for this path. - double get _currentY => _currentSubpath?.currentY ?? 0.0; - - /// Recorder used for hit testing paths. - static ui.RawRecordingCanvas _rawRecorder; - - SurfacePath() : subpaths = []; - - /// Creates a copy of another [Path]. - /// - /// This copy is fast and does not require additional memory unless either - /// the `source` path or the path returned by this constructor are modified. - SurfacePath.from(SurfacePath source) : subpaths = _deepCopy(source.subpaths); - - SurfacePath._shallowCopy(SurfacePath source) - : subpaths = List.from(source.subpaths); - - SurfacePath._clone(this.subpaths, this._fillType); - - static List _deepCopy(List source) { - // The last sub path can potentially still be mutated by calling ops. - // Copy all sub paths except the last active one which needs a deep copy. - final List paths = []; - int len = source.length; - if (len != 0) { - --len; - for (int i = 0; i < len; i++) { - paths.add(source[i]); - } - paths.add(source[len].shift(const ui.Offset(0, 0))); - } - return paths; - } - - /// Determines how the interior of this path is calculated. - /// - /// Defaults to the non-zero winding rule, [PathFillType.nonZero]. - @override - ui.PathFillType get fillType => _fillType; - @override - set fillType(ui.PathFillType value) { - _fillType = value; - } - - /// Opens a new subpath with starting point (x, y). - void _openNewSubpath(double x, double y) { - subpaths.add(Subpath(x, y)); - _setCurrentPoint(x, y); - } - - /// Sets the current point to (x, y). - void _setCurrentPoint(double x, double y) { - _currentSubpath.currentX = x; - _currentSubpath.currentY = y; - } - - /// Starts a new subpath at the given coordinate. - @override - void moveTo(double x, double y) { - _openNewSubpath(x, y); - _commands.add(MoveTo(x, y)); - } - - /// Starts a new subpath at the given offset from the current point. - @override - void relativeMoveTo(double dx, double dy) { - final double newX = _currentX + dx; - final double newY = _currentY + dy; - _openNewSubpath(newX, newY); - _commands.add(MoveTo(newX, newY)); - } - - /// Adds a straight line segment from the current point to the given - /// point. - @override - void lineTo(double x, double y) { - if (subpaths.isEmpty) { - moveTo(0.0, 0.0); - } - _commands.add(LineTo(x, y)); - _setCurrentPoint(x, y); - } - - /// Adds a straight line segment from the current point to the point - /// at the given offset from the current point. - @override - void relativeLineTo(double dx, double dy) { - final double newX = _currentX + dx; - final double newY = _currentY + dy; - if (subpaths.isEmpty) { - moveTo(0.0, 0.0); - } - _commands.add(LineTo(newX, newY)); - _setCurrentPoint(newX, newY); - } - - void _ensurePathStarted() { - if (subpaths.isEmpty) { - subpaths.add(Subpath(0.0, 0.0)); - } - } - - /// Adds a quadratic bezier segment that curves from the current - /// point to the given point (x2,y2), using the control point - /// (x1,y1). - @override - void quadraticBezierTo(double x1, double y1, double x2, double y2) { - _ensurePathStarted(); - _commands.add(QuadraticCurveTo(x1, y1, x2, y2)); - _setCurrentPoint(x2, y2); - } - - /// Adds a quadratic bezier segment that curves from the current - /// point to the point at the offset (x2,y2) from the current point, - /// using the control point at the offset (x1,y1) from the current - /// point. - @override - void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2) { - _ensurePathStarted(); - _commands.add(QuadraticCurveTo( - x1 + _currentX, y1 + _currentY, x2 + _currentX, y2 + _currentY)); - _setCurrentPoint(x2 + _currentX, y2 + _currentY); - } - - /// Adds a cubic bezier segment that curves from the current point - /// to the given point (x3,y3), using the control points (x1,y1) and - /// (x2,y2). - @override - void cubicTo( - double x1, double y1, double x2, double y2, double x3, double y3) { - _ensurePathStarted(); - _commands.add(BezierCurveTo(x1, y1, x2, y2, x3, y3)); - _setCurrentPoint(x3, y3); - } - - /// Adds a cubic bezier segment that curves from the current point - /// to the point at the offset (x3,y3) from the current point, using - /// the control points at the offsets (x1,y1) and (x2,y2) from the - /// current point. - @override - void relativeCubicTo( - double x1, double y1, double x2, double y2, double x3, double y3) { - _ensurePathStarted(); - _commands.add(BezierCurveTo(x1 + _currentX, y1 + _currentY, x2 + _currentX, - y2 + _currentY, x3 + _currentX, y3 + _currentY)); - _setCurrentPoint(x3 + _currentX, y3 + _currentY); - } - - /// Adds a bezier segment that curves from the current point to the - /// given point (x2,y2), using the control points (x1,y1) and the - /// weight w. If the weight is greater than 1, then the curve is a - /// hyperbola; if the weight equals 1, it's a parabola; and if it is - /// less than 1, it is an ellipse. - @override - void conicTo(double x1, double y1, double x2, double y2, double w) { - final List quads = - Conic(_currentX, _currentY, x1, y1, x2, y2, w).toQuads(); - final int len = quads.length; - for (int i = 1; i < len; i += 2) { - quadraticBezierTo( - quads[i].dx, quads[i].dy, quads[i + 1].dx, quads[i + 1].dy); - } - } - - /// Adds a bezier segment that curves from the current point to the - /// point at the offset (x2,y2) from the current point, using the - /// control point at the offset (x1,y1) from the current point and - /// the weight w. If the weight is greater than 1, then the curve is - /// a hyperbola; if the weight equals 1, it's a parabola; and if it - /// is less than 1, it is an ellipse. - @override - void relativeConicTo(double x1, double y1, double x2, double y2, double w) { - conicTo(_currentX + x1, _currentY + y1, _currentX + x2, _currentY + y2, w); - } - - /// If the `forceMoveTo` argument is false, adds a straight line - /// segment and an arc segment. - /// - /// If the `forceMoveTo` argument is true, starts a new subpath - /// consisting of an arc segment. - /// - /// In either case, the arc segment consists of the arc that follows - /// the edge of the oval bounded by the given rectangle, from - /// startAngle radians around the oval up to startAngle + sweepAngle - /// radians around the oval, with zero radians being the point on - /// the right hand side of the oval that crosses the horizontal line - /// that intersects the center of the rectangle and with positive - /// angles going clockwise around the oval. - /// - /// The line segment added if `forceMoveTo` is false starts at the - /// current point and ends at the start of the arc. - @override - void arcTo( - ui.Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) { - assert(rectIsValid(rect)); - final ui.Offset center = rect.center; - final double radiusX = rect.width / 2; - final double radiusY = rect.height / 2; - final double startX = radiusX * math.cos(startAngle) + center.dx; - final double startY = radiusY * math.sin(startAngle) + center.dy; - if (forceMoveTo) { - _openNewSubpath(startX, startY); - } else { - lineTo(startX, startY); - } - _commands.add(Ellipse(center.dx, center.dy, radiusX, radiusY, 0.0, - startAngle, startAngle + sweepAngle, sweepAngle.isNegative)); - - _setCurrentPoint(radiusX * math.cos(startAngle + sweepAngle) + center.dx, - radiusY * math.sin(startAngle + sweepAngle) + center.dy); - } - - /// Appends up to four conic curves weighted to describe an oval of `radius` - /// and rotated by `rotation`. - /// - /// The first curve begins from the last point in the path and the last ends - /// at `arcEnd`. The curves follow a path in a direction determined by - /// `clockwise` and `largeArc` in such a way that the sweep angle - /// is always less than 360 degrees. - /// - /// A simple line is appended if either either radii are zero or the last - /// point in the path is `arcEnd`. The radii are scaled to fit the last path - /// point if both are greater than zero but too small to describe an arc. - /// - /// See Conversion from endpoint to center parametrization described in - /// https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter - /// as reference for implementation. - @override - void arcToPoint( - ui.Offset arcEnd, { - ui.Radius radius = ui.Radius.zero, - double rotation = 0.0, - bool largeArc = false, - bool clockwise = true, - }) { - assert(offsetIsValid(arcEnd)); - assert(radiusIsValid(radius)); - // _currentX, _currentY are the coordinates of start point on path, - // arcEnd is final point of arc. - // rx,ry are the radii of the eclipse (semi-major/semi-minor axis) - // largeArc is false if arc is spanning less than or equal to 180 degrees. - // clockwise is false if arc sweeps through decreasing angles or true - // if sweeping through increasing angles. - // rotation is the angle from the x-axis of the current coordinate - // system to the x-axis of the eclipse. - - double rx = radius.x.abs(); - double ry = radius.y.abs(); - - // If the current point and target point for the arc are identical, it - // should be treated as a zero length path. This ensures continuity in - // animations. - final bool isSamePoint = _currentX == arcEnd.dx && _currentY == arcEnd.dy; - - // If rx = 0 or ry = 0 then this arc is treated as a straight line segment - // (a "lineto") joining the endpoints. - // http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters - if (isSamePoint || rx.toInt() == 0 || ry.toInt() == 0) { - _commands.add(LineTo(arcEnd.dx, arcEnd.dy)); - _setCurrentPoint(arcEnd.dx, arcEnd.dy); - return; - } - - // As an intermediate point to finding center parametrization, place the - // origin on the midpoint between start/end points and rotate to align - // coordinate axis with axes of the ellipse. - final double midPointX = (_currentX - arcEnd.dx) / 2.0; - final double midPointY = (_currentY - arcEnd.dy) / 2.0; - - // Convert rotation or radians. - final double xAxisRotation = math.pi * rotation / 180.0; - - // Cache cos/sin value. - final double cosXAxisRotation = math.cos(xAxisRotation); - final double sinXAxisRotation = math.sin(xAxisRotation); - - // Calculate rotate midpoint as x/yPrime. - final double xPrime = - (cosXAxisRotation * midPointX) + (sinXAxisRotation * midPointY); - final double yPrime = - (-sinXAxisRotation * midPointX) + (cosXAxisRotation * midPointY); - - // Check if the radii are big enough to draw the arc, scale radii if not. - // http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii - double rxSquare = rx * rx; - double rySquare = ry * ry; - final double xPrimeSquare = xPrime * xPrime; - final double yPrimeSquare = yPrime * yPrime; - - double radiiScale = (xPrimeSquare / rxSquare) + (yPrimeSquare / rySquare); - if (radiiScale > 1) { - radiiScale = math.sqrt(radiiScale); - rx *= radiiScale; - ry *= radiiScale; - rxSquare = rx * rx; - rySquare = ry * ry; - } - - // Compute transformed center. eq. 5.2 - final double distanceSquare = - (rxSquare * yPrimeSquare) + rySquare * xPrimeSquare; - final double cNumerator = (rxSquare * rySquare) - distanceSquare; - double scaleFactor = math.sqrt(math.max(cNumerator / distanceSquare, 0.0)); - if (largeArc == clockwise) { - scaleFactor = -scaleFactor; - } - // Ready to compute transformed center. - final double cxPrime = scaleFactor * ((rx * yPrime) / ry); - final double cyPrime = scaleFactor * (-(ry * xPrime) / rx); - - // Rotate to find actual center. - final double cx = cosXAxisRotation * cxPrime - - sinXAxisRotation * cyPrime + - ((_currentX + arcEnd.dx) / 2.0); - final double cy = sinXAxisRotation * cxPrime + - cosXAxisRotation * cyPrime + - ((_currentY + arcEnd.dy) / 2.0); - - // Calculate start angle and sweep. - // Start vector is from midpoint of start/end points to transformed center. - final double startVectorX = (xPrime - cxPrime) / rx; - final double startVectorY = (yPrime - cyPrime) / ry; - - final double startAngle = math.atan2(startVectorY, startVectorX); - final double endVectorX = (-xPrime - cxPrime) / rx; - final double endVectorY = (-yPrime - cyPrime) / ry; - double sweepAngle = math.atan2(endVectorY, endVectorX) - startAngle; - - if (clockwise && sweepAngle < 0) { - sweepAngle += math.pi * 2.0; - } else if (!clockwise && sweepAngle > 0) { - sweepAngle -= math.pi * 2.0; - } - - _commands.add(Ellipse(cx, cy, rx, ry, xAxisRotation, startAngle, - startAngle + sweepAngle, sweepAngle.isNegative)); - - _setCurrentPoint(arcEnd.dx, arcEnd.dy); - } - - /// Appends up to four conic curves weighted to describe an oval of `radius` - /// and rotated by `rotation`. - /// - /// The last path point is described by (px, py). - /// - /// The first curve begins from the last point in the path and the last ends - /// at `arcEndDelta.dx + px` and `arcEndDelta.dy + py`. The curves follow a - /// path in a direction determined by `clockwise` and `largeArc` - /// in such a way that the sweep angle is always less than 360 degrees. - /// - /// A simple line is appended if either either radii are zero, or, both - /// `arcEndDelta.dx` and `arcEndDelta.dy` are zero. The radii are scaled to - /// fit the last path point if both are greater than zero but too small to - /// describe an arc. - @override - void relativeArcToPoint( - ui.Offset arcEndDelta, { - ui.Radius radius = ui.Radius.zero, - double rotation = 0.0, - bool largeArc = false, - bool clockwise = true, - }) { - assert(offsetIsValid(arcEndDelta)); - assert(radiusIsValid(radius)); - arcToPoint( - ui.Offset(_currentX + arcEndDelta.dx, _currentY + arcEndDelta.dy), - radius: radius, - rotation: rotation, - largeArc: largeArc, - clockwise: clockwise); - } - - /// Adds a new subpath that consists of four lines that outline the - /// given rectangle. - @override - void addRect(ui.Rect rect) { - assert(rectIsValid(rect)); - _openNewSubpath(rect.left, rect.top); - _commands.add(RectCommand(rect.left, rect.top, rect.width, rect.height)); - } - - /// Adds a new subpath that consists of a curve that forms the - /// ellipse that fills the given rectangle. - /// - /// To add a circle, pass an appropriate rectangle as `oval`. - /// [Rect.fromCircle] can be used to easily describe the circle's center - /// [Offset] and radius. - @override - void addOval(ui.Rect oval) { - assert(rectIsValid(oval)); - final ui.Offset center = oval.center; - final double radiusX = oval.width / 2; - final double radiusY = oval.height / 2; - - /// At startAngle = 0, the path will begin at center + cos(0) * radius. - _openNewSubpath(center.dx + radiusX, center.dy); - _commands.add(Ellipse( - center.dx, center.dy, radiusX, radiusY, 0.0, 0.0, 2 * math.pi, false)); - } - - /// Adds a new subpath with one arc segment that consists of the arc - /// that follows the edge of the oval bounded by the given - /// rectangle, from startAngle radians around the oval up to - /// startAngle + sweepAngle radians around the oval, with zero - /// radians being the point on the right hand side of the oval that - /// crosses the horizontal line that intersects the center of the - /// rectangle and with positive angles going clockwise around the - /// oval. - @override - void addArc(ui.Rect oval, double startAngle, double sweepAngle) { - assert(rectIsValid(oval)); - final ui.Offset center = oval.center; - final double radiusX = oval.width / 2; - final double radiusY = oval.height / 2; - _openNewSubpath(radiusX * math.cos(startAngle) + center.dx, - radiusY * math.sin(startAngle) + center.dy); - _commands.add(Ellipse(center.dx, center.dy, radiusX, radiusY, 0.0, - startAngle, startAngle + sweepAngle, sweepAngle.isNegative)); - - _setCurrentPoint(radiusX * math.cos(startAngle + sweepAngle) + center.dx, - radiusY * math.sin(startAngle + sweepAngle) + center.dy); - } - - /// Adds a new subpath with a sequence of line segments that connect the given - /// points. - /// - /// If `close` is true, a final line segment will be added that connects the - /// last point to the first point. - /// - /// The `points` argument is interpreted as offsets from the origin. - @override - void addPolygon(List points, bool close) { - assert(points != null); - if (points.isEmpty) { - return; - } - - moveTo(points.first.dx, points.first.dy); - for (int i = 1; i < points.length; i++) { - final ui.Offset point = points[i]; - lineTo(point.dx, point.dy); - } - if (close) { - this.close(); - } else { - _setCurrentPoint(points.last.dx, points.last.dy); - } - } - - /// Adds a new subpath that consists of the straight lines and - /// curves needed to form the rounded rectangle described by the - /// argument. - @override - void addRRect(ui.RRect rrect) { - assert(rrectIsValid(rrect)); - - // Set the current point to the top left corner of the rectangle (the - // point on the top of the rectangle farthest to the left that isn't in - // the rounded corner). - // TODO(het): Is this the current point in Flutter? - _openNewSubpath(rrect.tallMiddleRect.left, rrect.top); - _commands.add(RRectCommand(rrect)); - } - - /// Adds a new subpath that consists of the given `path` offset by the given - /// `offset`. - /// - /// If `matrix4` is specified, the path will be transformed by this matrix - /// after the matrix is translated by the given offset. The matrix is a 4x4 - /// matrix stored in column major order. - @override - void addPath(ui.Path path, ui.Offset offset, {Float64List matrix4}) { - assert(path != null); // path is checked on the engine side - assert(offsetIsValid(offset)); - if (matrix4 != null) { - assert(matrix4IsValid(matrix4)); - _addPathWithMatrix(path, offset.dx, offset.dy, matrix4); - } else { - _addPath(path, offset.dx, offset.dy); - } - } - - void _addPath(SurfacePath path, double dx, double dy) { - if (dx == 0.0 && dy == 0.0) { - subpaths.addAll(path.subpaths); - } else { - subpaths.addAll(path - .transform(Matrix4.translationValues(dx, dy, 0.0).storage) - .subpaths); - } - } - - void _addPathWithMatrix( - SurfacePath path, double dx, double dy, Float64List matrix) { - final Matrix4 transform = Matrix4.fromFloat64List(matrix); - transform.translate(dx, dy); - subpaths.addAll(path.transform(transform.storage).subpaths); - } - - /// Adds the given path to this path by extending the current segment of this - /// path with the first segment of the given path. - /// - /// If `matrix4` is specified, the path will be transformed by this matrix - /// after the matrix is translated by the given `offset`. The matrix is a 4x4 - /// matrix stored in column major order. - @override - void extendWithPath(ui.Path path, ui.Offset offset, {Float64List matrix4}) { - assert(path != null); // path is checked on the engine side - assert(offsetIsValid(offset)); - if (matrix4 != null) { - assert(matrix4IsValid(matrix4)); - _extendWithPathAndMatrix(path, offset.dx, offset.dy, matrix4); - } else { - _extendWithPath(path, offset.dx, offset.dy); - } - } - - void _extendWithPath(SurfacePath path, double dx, double dy) { - if (dx == 0.0 && dy == 0.0) { - assert(path.subpaths.length == 1); - _ensurePathStarted(); - _commands.addAll(path.subpaths.single.commands); - _setCurrentPoint( - path.subpaths.single.currentX, path.subpaths.single.currentY); - } else { - throw UnimplementedError('Cannot extend path with non-zero offset'); - } - } - - void _extendWithPathAndMatrix( - SurfacePath path, double dx, double dy, Float64List matrix) { - throw UnimplementedError('Cannot extend path with transform matrix'); - } - - /// Closes the last subpath, as if a straight line had been drawn - /// from the current point to the first point of the subpath. - @override - void close() { - _ensurePathStarted(); - _commands.add(const CloseCommand()); - _setCurrentPoint(_currentSubpath.startX, _currentSubpath.startY); - } - - /// Clears the [Path] object of all subpaths, returning it to the - /// same state it had when it was created. The _current point_ is - /// reset to the origin. - @override - void reset() { - subpaths.clear(); - } - - /// Tests to see if the given point is within the path. (That is, whether the - /// point would be in the visible portion of the path if the path was used - /// with [Canvas.clipPath].) - /// - /// The `point` argument is interpreted as an offset from the origin. - /// - /// Returns true if the point is in the path, and false otherwise. - /// - /// Note: Not very efficient, it creates a canvas, plays path and calls - /// Context2D isPointInPath. If performance becomes issue, retaining - /// RawRecordingCanvas can remove create/remove rootElement cost. - @override - bool contains(ui.Offset point) { - assert(offsetIsValid(point)); - final int subPathCount = subpaths.length; - if (subPathCount == 0) { - return false; - } - final double pointX = point.dx; - final double pointY = point.dy; - if (subPathCount == 1) { - // Optimize for rect/roundrect checks. - final Subpath subPath = subpaths[0]; - if (subPath.commands.length == 1) { - final PathCommand cmd = subPath.commands[0]; - if (cmd is RectCommand) { - if (pointY < cmd.y || pointY > (cmd.y + cmd.height)) { - return false; - } - if (pointX < cmd.x || pointX > (cmd.x + cmd.width)) { - return false; - } - return true; - } else if (cmd is RRectCommand) { - final ui.RRect rRect = cmd.rrect; - if (pointY < rRect.top || pointY > rRect.bottom) { - return false; - } - if (pointX < rRect.left || pointX > rRect.right) { - return false; - } - final double rRectWidth = rRect.width; - final double rRectHeight = rRect.height; - final double tlRadiusX = math.min(rRect.tlRadiusX, rRectWidth / 2.0); - final double tlRadiusY = math.min(rRect.tlRadiusY, rRectHeight / 2.0); - if (pointX < (rRect.left + tlRadiusX) && - pointY < (rRect.top + tlRadiusY)) { - // Top left corner - return _ellipseContains( - pointX, - pointY, - rRect.left + tlRadiusX, - rRect.top + tlRadiusY, - tlRadiusX, - tlRadiusY); - } - final double trRadiusX = math.min(rRect.trRadiusX, rRectWidth / 2.0); - final double trRadiusY = math.min(rRect.trRadiusY, rRectHeight / 2.0); - if (pointX >= (rRect.right - trRadiusX) && - pointY < (rRect.top + trRadiusY)) { - // Top right corner - return _ellipseContains( - pointX, - pointY, - rRect.right - trRadiusX, - rRect.top + trRadiusY, - trRadiusX, - trRadiusY); - } - final double brRadiusX = math.min(rRect.brRadiusX, rRectWidth / 2.0); - final double brRadiusY = math.min(rRect.brRadiusY, rRectHeight / 2.0); - if (pointX >= (rRect.right - brRadiusX) && - pointY >= (rRect.bottom - brRadiusY)) { - // Bottom right corner - return _ellipseContains( - pointX, - pointY, - rRect.right - brRadiusX, - rRect.bottom - brRadiusY, - trRadiusX, - trRadiusY); - } - final double blRadiusX = math.min(rRect.blRadiusX, rRectWidth / 2.0); - final double blRadiusY = math.min(rRect.blRadiusY, rRectHeight / 2.0); - if (pointX < (rRect.left + blRadiusX) && - pointY >= (rRect.bottom - blRadiusY)) { - // Bottom left corner - return _ellipseContains( - pointX, - pointY, - rRect.left + blRadiusX, - rRect.bottom - blRadiusY, - trRadiusX, - trRadiusY); - } - return true; - } - // TODO: For improved performance, handle Ellipse special case. - } - } - final ui.Size size = window.physicalSize; - // If device pixel ratio has changed we can't reuse prior raw recorder. - if (_rawRecorder != null && - _rawRecorder._devicePixelRatio != - EngineWindow.browserDevicePixelRatio) { - _rawRecorder = null; - } - final double dpr = window.devicePixelRatio; - _rawRecorder ??= ui.RawRecordingCanvas(ui.Size(size.width / dpr, - size.height / dpr)); - // Account for the shift due to padding. - _rawRecorder.translate(-BitmapCanvas.kPaddingPixels.toDouble(), - -BitmapCanvas.kPaddingPixels.toDouble()); - _rawRecorder.drawPath( - this, (SurfacePaint()..color = const ui.Color(0xFF000000)).paintData); - final double recorderDevicePixelRatio = _rawRecorder._devicePixelRatio; - final bool result = _rawRecorder._canvasPool.context.isPointInPath( - pointX * recorderDevicePixelRatio, pointY * recorderDevicePixelRatio); - _rawRecorder.dispose(); - return result; - } - - /// Returns a copy of the path with all the segments of every - /// subpath translated by the given offset. - @override - SurfacePath shift(ui.Offset offset) { - assert(offsetIsValid(offset)); - final List shiftedSubPaths = []; - for (final Subpath subPath in subpaths) { - shiftedSubPaths.add(subPath.shift(offset)); - } - return SurfacePath._clone(shiftedSubPaths, fillType); - } - - /// Returns a copy of the path with all the segments of every - /// sub path transformed by the given matrix. - @override - SurfacePath transform(Float64List matrix4) { - assert(matrix4IsValid(matrix4)); - final SurfacePath transformedPath = SurfacePath(); - for (final Subpath subPath in subpaths) { - for (final PathCommand cmd in subPath.commands) { - cmd.transform(matrix4, transformedPath); - } - } - return transformedPath; - } - - /// Computes the bounding rectangle for this path. - /// - /// A path containing only axis-aligned points on the same straight line will - /// have no area, and therefore `Rect.isEmpty` will return true for such a - /// path. Consider checking `rect.width + rect.height > 0.0` instead, or - /// using the [computeMetrics] API to check the path length. - /// - /// For many more elaborate paths, the bounds may be inaccurate. For example, - /// when a path contains a circle, the points used to compute the bounds are - /// the circle's implied control points, which form a square around the - /// circle; if the circle has a transformation applied using [transform] then - /// that square is rotated, and the (axis-aligned, non-rotated) bounding box - /// therefore ends up grossly overestimating the actual area covered by the - /// circle. - // see https://skia.org/user/api/SkPath_Reference#SkPath_getBounds - @override - ui.Rect getBounds() { - // Sufficiently small number for curve eq. - const double epsilon = 0.000000001; - bool ltrbInitialized = false; - double left = 0.0, top = 0.0, right = 0.0, bottom = 0.0; - double curX = 0.0; - double curY = 0.0; - double minX = 0.0, maxX = 0.0, minY = 0.0, maxY = 0.0; - for (Subpath subpath in subpaths) { - for (PathCommand op in subpath.commands) { - bool skipBounds = false; - switch (op.type) { - case PathCommandTypes.moveTo: - final MoveTo cmd = op; - curX = minX = maxX = cmd.x; - curY = minY = maxY = cmd.y; - break; - case PathCommandTypes.lineTo: - final LineTo cmd = op; - curX = minX = maxX = cmd.x; - curY = minY = maxY = cmd.y; - break; - case PathCommandTypes.ellipse: - final Ellipse cmd = op; - // Rotate 4 corners of bounding box. - final double rx = cmd.radiusX; - final double ry = cmd.radiusY; - final double cosVal = math.cos(cmd.rotation); - final double sinVal = math.sin(cmd.rotation); - final double rxCos = rx * cosVal; - final double ryCos = ry * cosVal; - final double rxSin = rx * sinVal; - final double rySin = ry * sinVal; - - final double leftDeltaX = rxCos - rySin; - final double rightDeltaX = -rxCos - rySin; - final double topDeltaY = ryCos + rxSin; - final double bottomDeltaY = ryCos - rxSin; - - final double centerX = cmd.x; - final double centerY = cmd.y; - - double rotatedX = centerX + leftDeltaX; - double rotatedY = centerY + topDeltaY; - minX = maxX = rotatedX; - minY = maxY = rotatedY; - - rotatedX = centerX + rightDeltaX; - rotatedY = centerY + bottomDeltaY; - minX = math.min(minX, rotatedX); - maxX = math.max(maxX, rotatedX); - minY = math.min(minY, rotatedY); - maxY = math.max(maxY, rotatedY); - - rotatedX = centerX - leftDeltaX; - rotatedY = centerY - topDeltaY; - minX = math.min(minX, rotatedX); - maxX = math.max(maxX, rotatedX); - minY = math.min(minY, rotatedY); - maxY = math.max(maxY, rotatedY); - - rotatedX = centerX - rightDeltaX; - rotatedY = centerY - bottomDeltaY; - minX = math.min(minX, rotatedX); - maxX = math.max(maxX, rotatedX); - minY = math.min(minY, rotatedY); - maxY = math.max(maxY, rotatedY); - - curX = centerX + cmd.radiusX; - curY = centerY; - break; - case PathCommandTypes.quadraticCurveTo: - final QuadraticCurveTo cmd = op; - final double x1 = curX; - final double y1 = curY; - final double cpX = cmd.x1; - final double cpY = cmd.y1; - final double x2 = cmd.x2; - final double y2 = cmd.y2; - - minX = math.min(x1, x2); - minY = math.min(y1, y2); - maxX = math.max(x1, x2); - maxY = math.max(y1, y2); - - // Curve equation : (1-t)(1-t)P1 + 2t(1-t)CP + t*t*P2. - // At extrema's derivative = 0. - // Solve for - // -2x1+2tx1 + 2cpX + 4tcpX + 2tx2 = 0 - // -2x1 + 2cpX +2t(x1 + 2cpX + x2) = 0 - // t = (x1 - cpX) / (x1 - 2cpX + x2) - - double denom = x1 - (2 * cpX) + x2; - if (denom.abs() > epsilon) { - final num t1 = (x1 - cpX) / denom; - if ((t1 >= 0) && (t1 <= 1.0)) { - // Solve (x,y) for curve at t = tx to find extrema - final num tprime = 1.0 - t1; - final num extremaX = (tprime * tprime * x1) + - (2 * t1 * tprime * cpX) + - (t1 * t1 * x2); - final num extremaY = (tprime * tprime * y1) + - (2 * t1 * tprime * cpY) + - (t1 * t1 * y2); - // Expand bounds. - minX = math.min(minX, extremaX); - maxX = math.max(maxX, extremaX); - minY = math.min(minY, extremaY); - maxY = math.max(maxY, extremaY); - } - } - // Now calculate dy/dt = 0 - denom = y1 - (2 * cpY) + y2; - if (denom.abs() > epsilon) { - final num t2 = (y1 - cpY) / denom; - if ((t2 >= 0) && (t2 <= 1.0)) { - final num tprime2 = 1.0 - t2; - final num extrema2X = (tprime2 * tprime2 * x1) + - (2 * t2 * tprime2 * cpX) + - (t2 * t2 * x2); - final num extrema2Y = (tprime2 * tprime2 * y1) + - (2 * t2 * tprime2 * cpY) + - (t2 * t2 * y2); - // Expand bounds. - minX = math.min(minX, extrema2X); - maxX = math.max(maxX, extrema2X); - minY = math.min(minY, extrema2Y); - maxY = math.max(maxY, extrema2Y); - } - } - curX = x2; - curY = y2; - break; - case PathCommandTypes.bezierCurveTo: - final BezierCurveTo cmd = op; - final double startX = curX; - final double startY = curY; - final double cpX1 = cmd.x1; - final double cpY1 = cmd.y1; - final double cpX2 = cmd.x2; - final double cpY2 = cmd.y2; - final double endX = cmd.x3; - final double endY = cmd.y3; - // Bounding box is defined by all points on the curve where - // monotonicity changes. - minX = math.min(startX, endX); - minY = math.min(startY, endY); - maxX = math.max(startX, endX); - maxY = math.max(startY, endY); - - double extremaX; - double extremaY; - double a, b, c; - - // Check for simple case of strong ordering before calculating - // extrema - if (!(((startX < cpX1) && (cpX1 < cpX2) && (cpX2 < endX)) || - ((startX > cpX1) && (cpX1 > cpX2) && (cpX2 > endX)))) { - // The extrema point is dx/dt B(t) = 0 - // The derivative of B(t) for cubic bezier is a quadratic equation - // with multiple roots - // B'(t) = a*t*t + b*t + c*t - a = -startX + (3 * (cpX1 - cpX2)) + endX; - b = 2 * (startX - (2 * cpX1) + cpX2); - c = -startX + cpX1; - - // Now find roots for quadratic equation with known coefficients - // a,b,c - // The roots are (-b+-sqrt(b*b-4*a*c)) / 2a - num s = (b * b) - (4 * a * c); - // If s is negative, we have no real roots - if ((s >= 0.0) && (a.abs() > epsilon)) { - if (s == 0.0) { - // we have only 1 root - final num t = -b / (2 * a); - final num tprime = 1.0 - t; - if ((t >= 0.0) && (t <= 1.0)) { - extremaX = ((tprime * tprime * tprime) * startX) + - ((3 * tprime * tprime * t) * cpX1) + - ((3 * tprime * t * t) * cpX2) + - (t * t * t * endX); - minX = math.min(extremaX, minX); - maxX = math.max(extremaX, maxX); - } - } else { - // we have 2 roots - s = math.sqrt(s); - num t = (-b - s) / (2 * a); - num tprime = 1.0 - t; - if ((t >= 0.0) && (t <= 1.0)) { - extremaX = ((tprime * tprime * tprime) * startX) + - ((3 * tprime * tprime * t) * cpX1) + - ((3 * tprime * t * t) * cpX2) + - (t * t * t * endX); - minX = math.min(extremaX, minX); - maxX = math.max(extremaX, maxX); - } - // check 2nd root - t = (-b + s) / (2 * a); - tprime = 1.0 - t; - if ((t >= 0.0) && (t <= 1.0)) { - extremaX = ((tprime * tprime * tprime) * startX) + - ((3 * tprime * tprime * t) * cpX1) + - ((3 * tprime * t * t) * cpX2) + - (t * t * t * endX); - - minX = math.min(extremaX, minX); - maxX = math.max(extremaX, maxX); - } - } - } - } - - // Now calc extremes for dy/dt = 0 just like above - if (!(((startY < cpY1) && (cpY1 < cpY2) && (cpY2 < endY)) || - ((startY > cpY1) && (cpY1 > cpY2) && (cpY2 > endY)))) { - // The extrema point is dy/dt B(t) = 0 - // The derivative of B(t) for cubic bezier is a quadratic equation - // with multiple roots - // B'(t) = a*t*t + b*t + c*t - a = -startY + (3 * (cpY1 - cpY2)) + endY; - b = 2 * (startY - (2 * cpY1) + cpY2); - c = -startY + cpY1; - - // Now find roots for quadratic equation with known coefficients - // a,b,c - // The roots are (-b+-sqrt(b*b-4*a*c)) / 2a - num s = (b * b) - (4 * a * c); - // If s is negative, we have no real roots - if ((s >= 0.0) && (a.abs() > epsilon)) { - if (s == 0.0) { - // we have only 1 root - final num t = -b / (2 * a); - final num tprime = 1.0 - t; - if ((t >= 0.0) && (t <= 1.0)) { - extremaY = ((tprime * tprime * tprime) * startY) + - ((3 * tprime * tprime * t) * cpY1) + - ((3 * tprime * t * t) * cpY2) + - (t * t * t * endY); - minY = math.min(extremaY, minY); - maxY = math.max(extremaY, maxY); - } - } else { - // we have 2 roots - s = math.sqrt(s); - final num t = (-b - s) / (2 * a); - final num tprime = 1.0 - t; - if ((t >= 0.0) && (t <= 1.0)) { - extremaY = ((tprime * tprime * tprime) * startY) + - ((3 * tprime * tprime * t) * cpY1) + - ((3 * tprime * t * t) * cpY2) + - (t * t * t * endY); - minY = math.min(extremaY, minY); - maxY = math.max(extremaY, maxY); - } - // check 2nd root - final num t2 = (-b + s) / (2 * a); - final num tprime2 = 1.0 - t2; - if ((t2 >= 0.0) && (t2 <= 1.0)) { - extremaY = ((tprime2 * tprime2 * tprime2) * startY) + - ((3 * tprime2 * tprime2 * t2) * cpY1) + - ((3 * tprime2 * t2 * t2) * cpY2) + - (t2 * t2 * t2 * endY); - minY = math.min(extremaY, minY); - maxY = math.max(extremaY, maxY); - } - } - } - } - break; - case PathCommandTypes.rect: - final RectCommand cmd = op; - left = cmd.x; - double width = cmd.width; - if (cmd.width < 0) { - left -= width; - width = -width; - } - double top = cmd.y; - double height = cmd.height; - if (cmd.height < 0) { - top -= height; - height = -height; - } - curX = minX = left; - maxX = left + width; - curY = minY = top; - maxY = top + height; - break; - case PathCommandTypes.rRect: - final RRectCommand cmd = op; - final ui.RRect rRect = cmd.rrect; - curX = minX = rRect.left; - maxX = rRect.left + rRect.width; - curY = minY = rRect.top; - maxY = rRect.top + rRect.height; - break; - case PathCommandTypes.close: - default: - skipBounds = false; - break; - } - if (!skipBounds) { - if (!ltrbInitialized) { - left = minX; - right = maxX; - top = minY; - bottom = maxY; - ltrbInitialized = true; - } else { - left = math.min(left, minX); - right = math.max(right, maxX); - top = math.min(top, minY); - bottom = math.max(bottom, maxY); - } - } - } - } - return ltrbInitialized - ? ui.Rect.fromLTRB(left, top, right, bottom) - : ui.Rect.zero; - } - - /// Creates a [PathMetrics] object for this path. - /// - /// If `forceClosed` is set to true, the contours of the path will be measured - /// as if they had been closed, even if they were not explicitly closed. - @override - SurfacePathMetrics computeMetrics({bool forceClosed = false}) { - return SurfacePathMetrics._(this, forceClosed); - } - - /// Detects if path is rounded rectangle and returns rounded rectangle or - /// null. - /// - /// Used for web optimization of physical shape represented as - /// a persistent div. - ui.RRect get webOnlyPathAsRoundedRect { - if (subpaths.length != 1) { - return null; - } - final Subpath subPath = subpaths[0]; - if (subPath.commands.length != 1) { - return null; - } - final PathCommand command = subPath.commands[0]; - return (command is RRectCommand) ? command.rrect : null; - } - - /// Detects if path is simple rectangle and returns rectangle or null. - /// - /// Used for web optimization of physical shape represented as - /// a persistent div. - ui.Rect get webOnlyPathAsRect { - if (subpaths.length != 1) { - return null; - } - final Subpath subPath = subpaths[0]; - if (subPath.commands.length != 1) { - return null; - } - final PathCommand command = subPath.commands[0]; - return (command is RectCommand) - ? ui.Rect.fromLTWH(command.x, command.y, command.width, command.height) - : null; - } - - /// Detects if path is simple oval and returns [Ellipse] or null. - /// - /// Used for web optimization of physical shape represented as - /// a persistent div. - Ellipse get webOnlyPathAsCircle { - if (subpaths.length != 1) { - return null; - } - final Subpath subPath = subpaths[0]; - if (subPath.commands.length != 1) { - return null; - } - final PathCommand command = subPath.commands[0]; - if (command is Ellipse) { - final Ellipse ellipse = command; - if ((ellipse.endAngle - ellipse.startAngle) % (2 * math.pi) == 0.0) { - return ellipse; - } - } - return null; - } - - /// Serializes this path to a value that's sent to a CSS custom painter for - /// painting. - List webOnlySerializeToCssPaint() { - final List serializedSubpaths = []; - for (int i = 0; i < subpaths.length; i++) { - serializedSubpaths.add(subpaths[i].serializeToCssPaint()); - } - return serializedSubpaths; - } - - @override - String toString() { - if (assertionsEnabled) { - return 'Path(${subpaths.join(', ')})'; - } else { - return super.toString(); - } - } -} - -// Returns true if point is inside ellipse. -bool _ellipseContains(double px, double py, double centerX, double centerY, - double radiusX, double radiusY) { - final double dx = px - centerX; - final double dy = py - centerY; - return ((dx * dx) / (radiusX * radiusX)) + ((dy * dy) / (radiusY * radiusY)) < - 1.0; -} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/surface/path.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/surface/path.dart new file mode 100644 index 00000000000..1cbd03f2b6c --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/surface/path.dart @@ -0,0 +1,1147 @@ +// 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; + +/// A complex, one-dimensional subset of a plane. +/// +/// A path consists of a number of subpaths, and a _current point_. +/// +/// Subpaths consist of segments of various types, such as lines, +/// arcs, or beziers. Subpaths can be open or closed, and can +/// self-intersect. +/// +/// Closed subpaths enclose a (possibly discontiguous) region of the +/// plane based on the current [fillType]. +/// +/// The _current point_ is initially at the origin. After each +/// operation adding a segment to a subpath, the current point is +/// updated to the end of that segment. +/// +/// Paths can be drawn on canvases using [Canvas.drawPath], and can +/// used to create clip regions using [Canvas.clipPath]. +class SurfacePath implements ui.Path { + final List subpaths; + ui.PathFillType _fillType = ui.PathFillType.nonZero; + + Subpath get _currentSubpath => subpaths.isEmpty ? null : subpaths.last; + + List get _commands => _currentSubpath?.commands; + + /// The current x-coordinate for this path. + double get _currentX => _currentSubpath?.currentX ?? 0.0; + + /// The current y-coordinate for this path. + double get _currentY => _currentSubpath?.currentY ?? 0.0; + + /// Recorder used for hit testing paths. + static ui.RawRecordingCanvas _rawRecorder; + + SurfacePath() : subpaths = []; + + /// Creates a copy of another [Path]. + /// + /// This copy is fast and does not require additional memory unless either + /// the `source` path or the path returned by this constructor are modified. + SurfacePath.from(SurfacePath source) : subpaths = _deepCopy(source.subpaths); + + SurfacePath._shallowCopy(SurfacePath source) + : subpaths = List.from(source.subpaths); + + SurfacePath._clone(this.subpaths, this._fillType); + + static List _deepCopy(List source) { + // The last sub path can potentially still be mutated by calling ops. + // Copy all sub paths except the last active one which needs a deep copy. + final List paths = []; + int len = source.length; + if (len != 0) { + --len; + for (int i = 0; i < len; i++) { + paths.add(source[i]); + } + paths.add(source[len].shift(const ui.Offset(0, 0))); + } + return paths; + } + + /// Determines how the interior of this path is calculated. + /// + /// Defaults to the non-zero winding rule, [PathFillType.nonZero]. + @override + ui.PathFillType get fillType => _fillType; + @override + set fillType(ui.PathFillType value) { + _fillType = value; + } + + /// Opens a new subpath with starting point (x, y). + void _openNewSubpath(double x, double y) { + subpaths.add(Subpath(x, y)); + _setCurrentPoint(x, y); + } + + /// Sets the current point to (x, y). + void _setCurrentPoint(double x, double y) { + _currentSubpath.currentX = x; + _currentSubpath.currentY = y; + } + + /// Starts a new subpath at the given coordinate. + @override + void moveTo(double x, double y) { + _openNewSubpath(x, y); + _commands.add(MoveTo(x, y)); + } + + /// Starts a new subpath at the given offset from the current point. + @override + void relativeMoveTo(double dx, double dy) { + final double newX = _currentX + dx; + final double newY = _currentY + dy; + _openNewSubpath(newX, newY); + _commands.add(MoveTo(newX, newY)); + } + + /// Adds a straight line segment from the current point to the given + /// point. + @override + void lineTo(double x, double y) { + if (subpaths.isEmpty) { + moveTo(0.0, 0.0); + } + _commands.add(LineTo(x, y)); + _setCurrentPoint(x, y); + } + + /// Adds a straight line segment from the current point to the point + /// at the given offset from the current point. + @override + void relativeLineTo(double dx, double dy) { + final double newX = _currentX + dx; + final double newY = _currentY + dy; + if (subpaths.isEmpty) { + moveTo(0.0, 0.0); + } + _commands.add(LineTo(newX, newY)); + _setCurrentPoint(newX, newY); + } + + void _ensurePathStarted() { + if (subpaths.isEmpty) { + subpaths.add(Subpath(0.0, 0.0)); + } + } + + /// Adds a quadratic bezier segment that curves from the current + /// point to the given point (x2,y2), using the control point + /// (x1,y1). + @override + void quadraticBezierTo(double x1, double y1, double x2, double y2) { + _ensurePathStarted(); + _commands.add(QuadraticCurveTo(x1, y1, x2, y2)); + _setCurrentPoint(x2, y2); + } + + /// Adds a quadratic bezier segment that curves from the current + /// point to the point at the offset (x2,y2) from the current point, + /// using the control point at the offset (x1,y1) from the current + /// point. + @override + void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2) { + _ensurePathStarted(); + _commands.add(QuadraticCurveTo( + x1 + _currentX, y1 + _currentY, x2 + _currentX, y2 + _currentY)); + _setCurrentPoint(x2 + _currentX, y2 + _currentY); + } + + /// Adds a cubic bezier segment that curves from the current point + /// to the given point (x3,y3), using the control points (x1,y1) and + /// (x2,y2). + @override + void cubicTo( + double x1, double y1, double x2, double y2, double x3, double y3) { + _ensurePathStarted(); + _commands.add(BezierCurveTo(x1, y1, x2, y2, x3, y3)); + _setCurrentPoint(x3, y3); + } + + /// Adds a cubic bezier segment that curves from the current point + /// to the point at the offset (x3,y3) from the current point, using + /// the control points at the offsets (x1,y1) and (x2,y2) from the + /// current point. + @override + void relativeCubicTo( + double x1, double y1, double x2, double y2, double x3, double y3) { + _ensurePathStarted(); + _commands.add(BezierCurveTo(x1 + _currentX, y1 + _currentY, x2 + _currentX, + y2 + _currentY, x3 + _currentX, y3 + _currentY)); + _setCurrentPoint(x3 + _currentX, y3 + _currentY); + } + + /// Adds a bezier segment that curves from the current point to the + /// given point (x2,y2), using the control points (x1,y1) and the + /// weight w. If the weight is greater than 1, then the curve is a + /// hyperbola; if the weight equals 1, it's a parabola; and if it is + /// less than 1, it is an ellipse. + @override + void conicTo(double x1, double y1, double x2, double y2, double w) { + final List quads = + Conic(_currentX, _currentY, x1, y1, x2, y2, w).toQuads(); + final int len = quads.length; + for (int i = 1; i < len; i += 2) { + quadraticBezierTo( + quads[i].dx, quads[i].dy, quads[i + 1].dx, quads[i + 1].dy); + } + } + + /// Adds a bezier segment that curves from the current point to the + /// point at the offset (x2,y2) from the current point, using the + /// control point at the offset (x1,y1) from the current point and + /// the weight w. If the weight is greater than 1, then the curve is + /// a hyperbola; if the weight equals 1, it's a parabola; and if it + /// is less than 1, it is an ellipse. + @override + void relativeConicTo(double x1, double y1, double x2, double y2, double w) { + conicTo(_currentX + x1, _currentY + y1, _currentX + x2, _currentY + y2, w); + } + + /// If the `forceMoveTo` argument is false, adds a straight line + /// segment and an arc segment. + /// + /// If the `forceMoveTo` argument is true, starts a new subpath + /// consisting of an arc segment. + /// + /// In either case, the arc segment consists of the arc that follows + /// the edge of the oval bounded by the given rectangle, from + /// startAngle radians around the oval up to startAngle + sweepAngle + /// radians around the oval, with zero radians being the point on + /// the right hand side of the oval that crosses the horizontal line + /// that intersects the center of the rectangle and with positive + /// angles going clockwise around the oval. + /// + /// The line segment added if `forceMoveTo` is false starts at the + /// current point and ends at the start of the arc. + @override + void arcTo( + ui.Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) { + assert(rectIsValid(rect)); + final ui.Offset center = rect.center; + final double radiusX = rect.width / 2; + final double radiusY = rect.height / 2; + final double startX = radiusX * math.cos(startAngle) + center.dx; + final double startY = radiusY * math.sin(startAngle) + center.dy; + if (forceMoveTo) { + _openNewSubpath(startX, startY); + } else { + lineTo(startX, startY); + } + _commands.add(Ellipse(center.dx, center.dy, radiusX, radiusY, 0.0, + startAngle, startAngle + sweepAngle, sweepAngle.isNegative)); + + _setCurrentPoint(radiusX * math.cos(startAngle + sweepAngle) + center.dx, + radiusY * math.sin(startAngle + sweepAngle) + center.dy); + } + + /// Appends up to four conic curves weighted to describe an oval of `radius` + /// and rotated by `rotation`. + /// + /// The first curve begins from the last point in the path and the last ends + /// at `arcEnd`. The curves follow a path in a direction determined by + /// `clockwise` and `largeArc` in such a way that the sweep angle + /// is always less than 360 degrees. + /// + /// A simple line is appended if either either radii are zero or the last + /// point in the path is `arcEnd`. The radii are scaled to fit the last path + /// point if both are greater than zero but too small to describe an arc. + /// + /// See Conversion from endpoint to center parametrization described in + /// https://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter + /// as reference for implementation. + @override + void arcToPoint( + ui.Offset arcEnd, { + ui.Radius radius = ui.Radius.zero, + double rotation = 0.0, + bool largeArc = false, + bool clockwise = true, + }) { + assert(offsetIsValid(arcEnd)); + assert(radiusIsValid(radius)); + // _currentX, _currentY are the coordinates of start point on path, + // arcEnd is final point of arc. + // rx,ry are the radii of the eclipse (semi-major/semi-minor axis) + // largeArc is false if arc is spanning less than or equal to 180 degrees. + // clockwise is false if arc sweeps through decreasing angles or true + // if sweeping through increasing angles. + // rotation is the angle from the x-axis of the current coordinate + // system to the x-axis of the eclipse. + + double rx = radius.x.abs(); + double ry = radius.y.abs(); + + // If the current point and target point for the arc are identical, it + // should be treated as a zero length path. This ensures continuity in + // animations. + final bool isSamePoint = _currentX == arcEnd.dx && _currentY == arcEnd.dy; + + // If rx = 0 or ry = 0 then this arc is treated as a straight line segment + // (a "lineto") joining the endpoints. + // http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters + if (isSamePoint || rx.toInt() == 0 || ry.toInt() == 0) { + _commands.add(LineTo(arcEnd.dx, arcEnd.dy)); + _setCurrentPoint(arcEnd.dx, arcEnd.dy); + return; + } + + // As an intermediate point to finding center parametrization, place the + // origin on the midpoint between start/end points and rotate to align + // coordinate axis with axes of the ellipse. + final double midPointX = (_currentX - arcEnd.dx) / 2.0; + final double midPointY = (_currentY - arcEnd.dy) / 2.0; + + // Convert rotation or radians. + final double xAxisRotation = math.pi * rotation / 180.0; + + // Cache cos/sin value. + final double cosXAxisRotation = math.cos(xAxisRotation); + final double sinXAxisRotation = math.sin(xAxisRotation); + + // Calculate rotate midpoint as x/yPrime. + final double xPrime = + (cosXAxisRotation * midPointX) + (sinXAxisRotation * midPointY); + final double yPrime = + (-sinXAxisRotation * midPointX) + (cosXAxisRotation * midPointY); + + // Check if the radii are big enough to draw the arc, scale radii if not. + // http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii + double rxSquare = rx * rx; + double rySquare = ry * ry; + final double xPrimeSquare = xPrime * xPrime; + final double yPrimeSquare = yPrime * yPrime; + + double radiiScale = (xPrimeSquare / rxSquare) + (yPrimeSquare / rySquare); + if (radiiScale > 1) { + radiiScale = math.sqrt(radiiScale); + rx *= radiiScale; + ry *= radiiScale; + rxSquare = rx * rx; + rySquare = ry * ry; + } + + // Compute transformed center. eq. 5.2 + final double distanceSquare = + (rxSquare * yPrimeSquare) + rySquare * xPrimeSquare; + final double cNumerator = (rxSquare * rySquare) - distanceSquare; + double scaleFactor = math.sqrt(math.max(cNumerator / distanceSquare, 0.0)); + if (largeArc == clockwise) { + scaleFactor = -scaleFactor; + } + // Ready to compute transformed center. + final double cxPrime = scaleFactor * ((rx * yPrime) / ry); + final double cyPrime = scaleFactor * (-(ry * xPrime) / rx); + + // Rotate to find actual center. + final double cx = cosXAxisRotation * cxPrime - + sinXAxisRotation * cyPrime + + ((_currentX + arcEnd.dx) / 2.0); + final double cy = sinXAxisRotation * cxPrime + + cosXAxisRotation * cyPrime + + ((_currentY + arcEnd.dy) / 2.0); + + // Calculate start angle and sweep. + // Start vector is from midpoint of start/end points to transformed center. + final double startVectorX = (xPrime - cxPrime) / rx; + final double startVectorY = (yPrime - cyPrime) / ry; + + final double startAngle = math.atan2(startVectorY, startVectorX); + final double endVectorX = (-xPrime - cxPrime) / rx; + final double endVectorY = (-yPrime - cyPrime) / ry; + double sweepAngle = math.atan2(endVectorY, endVectorX) - startAngle; + + if (clockwise && sweepAngle < 0) { + sweepAngle += math.pi * 2.0; + } else if (!clockwise && sweepAngle > 0) { + sweepAngle -= math.pi * 2.0; + } + + _commands.add(Ellipse(cx, cy, rx, ry, xAxisRotation, startAngle, + startAngle + sweepAngle, sweepAngle.isNegative)); + + _setCurrentPoint(arcEnd.dx, arcEnd.dy); + } + + /// Appends up to four conic curves weighted to describe an oval of `radius` + /// and rotated by `rotation`. + /// + /// The last path point is described by (px, py). + /// + /// The first curve begins from the last point in the path and the last ends + /// at `arcEndDelta.dx + px` and `arcEndDelta.dy + py`. The curves follow a + /// path in a direction determined by `clockwise` and `largeArc` + /// in such a way that the sweep angle is always less than 360 degrees. + /// + /// A simple line is appended if either either radii are zero, or, both + /// `arcEndDelta.dx` and `arcEndDelta.dy` are zero. The radii are scaled to + /// fit the last path point if both are greater than zero but too small to + /// describe an arc. + @override + void relativeArcToPoint( + ui.Offset arcEndDelta, { + ui.Radius radius = ui.Radius.zero, + double rotation = 0.0, + bool largeArc = false, + bool clockwise = true, + }) { + assert(offsetIsValid(arcEndDelta)); + assert(radiusIsValid(radius)); + arcToPoint( + ui.Offset(_currentX + arcEndDelta.dx, _currentY + arcEndDelta.dy), + radius: radius, + rotation: rotation, + largeArc: largeArc, + clockwise: clockwise); + } + + /// Adds a new subpath that consists of four lines that outline the + /// given rectangle. + @override + void addRect(ui.Rect rect) { + assert(rectIsValid(rect)); + _openNewSubpath(rect.left, rect.top); + _commands.add(RectCommand(rect.left, rect.top, rect.width, rect.height)); + } + + /// Adds a new subpath that consists of a curve that forms the + /// ellipse that fills the given rectangle. + /// + /// To add a circle, pass an appropriate rectangle as `oval`. + /// [Rect.fromCircle] can be used to easily describe the circle's center + /// [Offset] and radius. + @override + void addOval(ui.Rect oval) { + assert(rectIsValid(oval)); + final ui.Offset center = oval.center; + final double radiusX = oval.width / 2; + final double radiusY = oval.height / 2; + + /// At startAngle = 0, the path will begin at center + cos(0) * radius. + _openNewSubpath(center.dx + radiusX, center.dy); + _commands.add(Ellipse( + center.dx, center.dy, radiusX, radiusY, 0.0, 0.0, 2 * math.pi, false)); + } + + /// Adds a new subpath with one arc segment that consists of the arc + /// that follows the edge of the oval bounded by the given + /// rectangle, from startAngle radians around the oval up to + /// startAngle + sweepAngle radians around the oval, with zero + /// radians being the point on the right hand side of the oval that + /// crosses the horizontal line that intersects the center of the + /// rectangle and with positive angles going clockwise around the + /// oval. + @override + void addArc(ui.Rect oval, double startAngle, double sweepAngle) { + assert(rectIsValid(oval)); + final ui.Offset center = oval.center; + final double radiusX = oval.width / 2; + final double radiusY = oval.height / 2; + _openNewSubpath(radiusX * math.cos(startAngle) + center.dx, + radiusY * math.sin(startAngle) + center.dy); + _commands.add(Ellipse(center.dx, center.dy, radiusX, radiusY, 0.0, + startAngle, startAngle + sweepAngle, sweepAngle.isNegative)); + + _setCurrentPoint(radiusX * math.cos(startAngle + sweepAngle) + center.dx, + radiusY * math.sin(startAngle + sweepAngle) + center.dy); + } + + /// Adds a new subpath with a sequence of line segments that connect the given + /// points. + /// + /// If `close` is true, a final line segment will be added that connects the + /// last point to the first point. + /// + /// The `points` argument is interpreted as offsets from the origin. + @override + void addPolygon(List points, bool close) { + assert(points != null); + if (points.isEmpty) { + return; + } + + moveTo(points.first.dx, points.first.dy); + for (int i = 1; i < points.length; i++) { + final ui.Offset point = points[i]; + lineTo(point.dx, point.dy); + } + if (close) { + this.close(); + } else { + _setCurrentPoint(points.last.dx, points.last.dy); + } + } + + /// Adds a new subpath that consists of the straight lines and + /// curves needed to form the rounded rectangle described by the + /// argument. + @override + void addRRect(ui.RRect rrect) { + assert(rrectIsValid(rrect)); + + // Set the current point to the top left corner of the rectangle (the + // point on the top of the rectangle farthest to the left that isn't in + // the rounded corner). + // TODO(het): Is this the current point in Flutter? + _openNewSubpath(rrect.tallMiddleRect.left, rrect.top); + _commands.add(RRectCommand(rrect)); + } + + /// Adds a new subpath that consists of the given `path` offset by the given + /// `offset`. + /// + /// If `matrix4` is specified, the path will be transformed by this matrix + /// after the matrix is translated by the given offset. The matrix is a 4x4 + /// matrix stored in column major order. + @override + void addPath(ui.Path path, ui.Offset offset, {Float64List matrix4}) { + assert(path != null); // path is checked on the engine side + assert(offsetIsValid(offset)); + if (matrix4 != null) { + assert(matrix4IsValid(matrix4)); + _addPathWithMatrix(path, offset.dx, offset.dy, matrix4); + } else { + _addPath(path, offset.dx, offset.dy); + } + } + + void _addPath(SurfacePath path, double dx, double dy) { + if (dx == 0.0 && dy == 0.0) { + subpaths.addAll(path.subpaths); + } else { + subpaths.addAll(path + .transform(Matrix4.translationValues(dx, dy, 0.0).storage) + .subpaths); + } + } + + void _addPathWithMatrix( + SurfacePath path, double dx, double dy, Float64List matrix) { + final Matrix4 transform = Matrix4.fromFloat64List(matrix); + transform.translate(dx, dy); + subpaths.addAll(path.transform(transform.storage).subpaths); + } + + /// Adds the given path to this path by extending the current segment of this + /// path with the first segment of the given path. + /// + /// If `matrix4` is specified, the path will be transformed by this matrix + /// after the matrix is translated by the given `offset`. The matrix is a 4x4 + /// matrix stored in column major order. + @override + void extendWithPath(ui.Path path, ui.Offset offset, {Float64List matrix4}) { + assert(path != null); // path is checked on the engine side + assert(offsetIsValid(offset)); + if (matrix4 != null) { + assert(matrix4IsValid(matrix4)); + _extendWithPathAndMatrix(path, offset.dx, offset.dy, matrix4); + } else { + _extendWithPath(path, offset.dx, offset.dy); + } + } + + void _extendWithPath(SurfacePath path, double dx, double dy) { + if (dx == 0.0 && dy == 0.0) { + assert(path.subpaths.length == 1); + _ensurePathStarted(); + _commands.addAll(path.subpaths.single.commands); + _setCurrentPoint( + path.subpaths.single.currentX, path.subpaths.single.currentY); + } else { + throw UnimplementedError('Cannot extend path with non-zero offset'); + } + } + + void _extendWithPathAndMatrix( + SurfacePath path, double dx, double dy, Float64List matrix) { + throw UnimplementedError('Cannot extend path with transform matrix'); + } + + /// Closes the last subpath, as if a straight line had been drawn + /// from the current point to the first point of the subpath. + @override + void close() { + _ensurePathStarted(); + _commands.add(const CloseCommand()); + _setCurrentPoint(_currentSubpath.startX, _currentSubpath.startY); + } + + /// Clears the [Path] object of all subpaths, returning it to the + /// same state it had when it was created. The _current point_ is + /// reset to the origin. + @override + void reset() { + subpaths.clear(); + } + + /// Tests to see if the given point is within the path. (That is, whether the + /// point would be in the visible portion of the path if the path was used + /// with [Canvas.clipPath].) + /// + /// The `point` argument is interpreted as an offset from the origin. + /// + /// Returns true if the point is in the path, and false otherwise. + /// + /// Note: Not very efficient, it creates a canvas, plays path and calls + /// Context2D isPointInPath. If performance becomes issue, retaining + /// RawRecordingCanvas can remove create/remove rootElement cost. + @override + bool contains(ui.Offset point) { + assert(offsetIsValid(point)); + final int subPathCount = subpaths.length; + if (subPathCount == 0) { + return false; + } + final double pointX = point.dx; + final double pointY = point.dy; + if (subPathCount == 1) { + // Optimize for rect/roundrect checks. + final Subpath subPath = subpaths[0]; + if (subPath.commands.length == 1) { + final PathCommand cmd = subPath.commands[0]; + if (cmd is RectCommand) { + if (pointY < cmd.y || pointY > (cmd.y + cmd.height)) { + return false; + } + if (pointX < cmd.x || pointX > (cmd.x + cmd.width)) { + return false; + } + return true; + } else if (cmd is RRectCommand) { + final ui.RRect rRect = cmd.rrect; + if (pointY < rRect.top || pointY > rRect.bottom) { + return false; + } + if (pointX < rRect.left || pointX > rRect.right) { + return false; + } + final double rRectWidth = rRect.width; + final double rRectHeight = rRect.height; + final double tlRadiusX = math.min(rRect.tlRadiusX, rRectWidth / 2.0); + final double tlRadiusY = math.min(rRect.tlRadiusY, rRectHeight / 2.0); + if (pointX < (rRect.left + tlRadiusX) && + pointY < (rRect.top + tlRadiusY)) { + // Top left corner + return _ellipseContains(pointX, pointY, rRect.left + tlRadiusX, + rRect.top + tlRadiusY, tlRadiusX, tlRadiusY); + } + final double trRadiusX = math.min(rRect.trRadiusX, rRectWidth / 2.0); + final double trRadiusY = math.min(rRect.trRadiusY, rRectHeight / 2.0); + if (pointX >= (rRect.right - trRadiusX) && + pointY < (rRect.top + trRadiusY)) { + // Top right corner + return _ellipseContains(pointX, pointY, rRect.right - trRadiusX, + rRect.top + trRadiusY, trRadiusX, trRadiusY); + } + final double brRadiusX = math.min(rRect.brRadiusX, rRectWidth / 2.0); + final double brRadiusY = math.min(rRect.brRadiusY, rRectHeight / 2.0); + if (pointX >= (rRect.right - brRadiusX) && + pointY >= (rRect.bottom - brRadiusY)) { + // Bottom right corner + return _ellipseContains(pointX, pointY, rRect.right - brRadiusX, + rRect.bottom - brRadiusY, trRadiusX, trRadiusY); + } + final double blRadiusX = math.min(rRect.blRadiusX, rRectWidth / 2.0); + final double blRadiusY = math.min(rRect.blRadiusY, rRectHeight / 2.0); + if (pointX < (rRect.left + blRadiusX) && + pointY >= (rRect.bottom - blRadiusY)) { + // Bottom left corner + return _ellipseContains(pointX, pointY, rRect.left + blRadiusX, + rRect.bottom - blRadiusY, trRadiusX, trRadiusY); + } + return true; + } + // TODO: For improved performance, handle Ellipse special case. + } + } + final ui.Size size = window.physicalSize; + // If device pixel ratio has changed we can't reuse prior raw recorder. + if (_rawRecorder != null && + _rawRecorder._devicePixelRatio != + EngineWindow.browserDevicePixelRatio) { + _rawRecorder = null; + } + final double dpr = window.devicePixelRatio; + _rawRecorder ??= + ui.RawRecordingCanvas(ui.Size(size.width / dpr, size.height / dpr)); + // Account for the shift due to padding. + _rawRecorder.translate(-BitmapCanvas.kPaddingPixels.toDouble(), + -BitmapCanvas.kPaddingPixels.toDouble()); + _rawRecorder.drawPath( + this, (SurfacePaint()..color = const ui.Color(0xFF000000)).paintData); + final double recorderDevicePixelRatio = _rawRecorder._devicePixelRatio; + final bool result = _rawRecorder._canvasPool.context.isPointInPath( + pointX * recorderDevicePixelRatio, pointY * recorderDevicePixelRatio); + _rawRecorder.dispose(); + return result; + } + + /// Returns a copy of the path with all the segments of every + /// subpath translated by the given offset. + @override + SurfacePath shift(ui.Offset offset) { + assert(offsetIsValid(offset)); + final List shiftedSubPaths = []; + for (final Subpath subPath in subpaths) { + shiftedSubPaths.add(subPath.shift(offset)); + } + return SurfacePath._clone(shiftedSubPaths, fillType); + } + + /// Returns a copy of the path with all the segments of every + /// sub path transformed by the given matrix. + @override + SurfacePath transform(Float64List matrix4) { + assert(matrix4IsValid(matrix4)); + final SurfacePath transformedPath = SurfacePath(); + for (final Subpath subPath in subpaths) { + for (final PathCommand cmd in subPath.commands) { + cmd.transform(matrix4, transformedPath); + } + } + return transformedPath; + } + + /// Computes the bounding rectangle for this path. + /// + /// A path containing only axis-aligned points on the same straight line will + /// have no area, and therefore `Rect.isEmpty` will return true for such a + /// path. Consider checking `rect.width + rect.height > 0.0` instead, or + /// using the [computeMetrics] API to check the path length. + /// + /// For many more elaborate paths, the bounds may be inaccurate. For example, + /// when a path contains a circle, the points used to compute the bounds are + /// the circle's implied control points, which form a square around the + /// circle; if the circle has a transformation applied using [transform] then + /// that square is rotated, and the (axis-aligned, non-rotated) bounding box + /// therefore ends up grossly overestimating the actual area covered by the + /// circle. + // see https://skia.org/user/api/SkPath_Reference#SkPath_getBounds + @override + ui.Rect getBounds() { + // Sufficiently small number for curve eq. + const double epsilon = 0.000000001; + bool ltrbInitialized = false; + double left = 0.0, top = 0.0, right = 0.0, bottom = 0.0; + double curX = 0.0; + double curY = 0.0; + double minX = 0.0, maxX = 0.0, minY = 0.0, maxY = 0.0; + for (Subpath subpath in subpaths) { + for (PathCommand op in subpath.commands) { + bool skipBounds = false; + switch (op.type) { + case PathCommandTypes.moveTo: + final MoveTo cmd = op; + curX = minX = maxX = cmd.x; + curY = minY = maxY = cmd.y; + break; + case PathCommandTypes.lineTo: + final LineTo cmd = op; + curX = minX = maxX = cmd.x; + curY = minY = maxY = cmd.y; + break; + case PathCommandTypes.ellipse: + final Ellipse cmd = op; + // Rotate 4 corners of bounding box. + final double rx = cmd.radiusX; + final double ry = cmd.radiusY; + final double cosVal = math.cos(cmd.rotation); + final double sinVal = math.sin(cmd.rotation); + final double rxCos = rx * cosVal; + final double ryCos = ry * cosVal; + final double rxSin = rx * sinVal; + final double rySin = ry * sinVal; + + final double leftDeltaX = rxCos - rySin; + final double rightDeltaX = -rxCos - rySin; + final double topDeltaY = ryCos + rxSin; + final double bottomDeltaY = ryCos - rxSin; + + final double centerX = cmd.x; + final double centerY = cmd.y; + + double rotatedX = centerX + leftDeltaX; + double rotatedY = centerY + topDeltaY; + minX = maxX = rotatedX; + minY = maxY = rotatedY; + + rotatedX = centerX + rightDeltaX; + rotatedY = centerY + bottomDeltaY; + minX = math.min(minX, rotatedX); + maxX = math.max(maxX, rotatedX); + minY = math.min(minY, rotatedY); + maxY = math.max(maxY, rotatedY); + + rotatedX = centerX - leftDeltaX; + rotatedY = centerY - topDeltaY; + minX = math.min(minX, rotatedX); + maxX = math.max(maxX, rotatedX); + minY = math.min(minY, rotatedY); + maxY = math.max(maxY, rotatedY); + + rotatedX = centerX - rightDeltaX; + rotatedY = centerY - bottomDeltaY; + minX = math.min(minX, rotatedX); + maxX = math.max(maxX, rotatedX); + minY = math.min(minY, rotatedY); + maxY = math.max(maxY, rotatedY); + + curX = centerX + cmd.radiusX; + curY = centerY; + break; + case PathCommandTypes.quadraticCurveTo: + final QuadraticCurveTo cmd = op; + final double x1 = curX; + final double y1 = curY; + final double cpX = cmd.x1; + final double cpY = cmd.y1; + final double x2 = cmd.x2; + final double y2 = cmd.y2; + + minX = math.min(x1, x2); + minY = math.min(y1, y2); + maxX = math.max(x1, x2); + maxY = math.max(y1, y2); + + // Curve equation : (1-t)(1-t)P1 + 2t(1-t)CP + t*t*P2. + // At extrema's derivative = 0. + // Solve for + // -2x1+2tx1 + 2cpX + 4tcpX + 2tx2 = 0 + // -2x1 + 2cpX +2t(x1 + 2cpX + x2) = 0 + // t = (x1 - cpX) / (x1 - 2cpX + x2) + + double denom = x1 - (2 * cpX) + x2; + if (denom.abs() > epsilon) { + final num t1 = (x1 - cpX) / denom; + if ((t1 >= 0) && (t1 <= 1.0)) { + // Solve (x,y) for curve at t = tx to find extrema + final num tprime = 1.0 - t1; + final num extremaX = (tprime * tprime * x1) + + (2 * t1 * tprime * cpX) + + (t1 * t1 * x2); + final num extremaY = (tprime * tprime * y1) + + (2 * t1 * tprime * cpY) + + (t1 * t1 * y2); + // Expand bounds. + minX = math.min(minX, extremaX); + maxX = math.max(maxX, extremaX); + minY = math.min(minY, extremaY); + maxY = math.max(maxY, extremaY); + } + } + // Now calculate dy/dt = 0 + denom = y1 - (2 * cpY) + y2; + if (denom.abs() > epsilon) { + final num t2 = (y1 - cpY) / denom; + if ((t2 >= 0) && (t2 <= 1.0)) { + final num tprime2 = 1.0 - t2; + final num extrema2X = (tprime2 * tprime2 * x1) + + (2 * t2 * tprime2 * cpX) + + (t2 * t2 * x2); + final num extrema2Y = (tprime2 * tprime2 * y1) + + (2 * t2 * tprime2 * cpY) + + (t2 * t2 * y2); + // Expand bounds. + minX = math.min(minX, extrema2X); + maxX = math.max(maxX, extrema2X); + minY = math.min(minY, extrema2Y); + maxY = math.max(maxY, extrema2Y); + } + } + curX = x2; + curY = y2; + break; + case PathCommandTypes.bezierCurveTo: + final BezierCurveTo cmd = op; + final double startX = curX; + final double startY = curY; + final double cpX1 = cmd.x1; + final double cpY1 = cmd.y1; + final double cpX2 = cmd.x2; + final double cpY2 = cmd.y2; + final double endX = cmd.x3; + final double endY = cmd.y3; + // Bounding box is defined by all points on the curve where + // monotonicity changes. + minX = math.min(startX, endX); + minY = math.min(startY, endY); + maxX = math.max(startX, endX); + maxY = math.max(startY, endY); + + double extremaX; + double extremaY; + double a, b, c; + + // Check for simple case of strong ordering before calculating + // extrema + if (!(((startX < cpX1) && (cpX1 < cpX2) && (cpX2 < endX)) || + ((startX > cpX1) && (cpX1 > cpX2) && (cpX2 > endX)))) { + // The extrema point is dx/dt B(t) = 0 + // The derivative of B(t) for cubic bezier is a quadratic equation + // with multiple roots + // B'(t) = a*t*t + b*t + c*t + a = -startX + (3 * (cpX1 - cpX2)) + endX; + b = 2 * (startX - (2 * cpX1) + cpX2); + c = -startX + cpX1; + + // Now find roots for quadratic equation with known coefficients + // a,b,c + // The roots are (-b+-sqrt(b*b-4*a*c)) / 2a + num s = (b * b) - (4 * a * c); + // If s is negative, we have no real roots + if ((s >= 0.0) && (a.abs() > epsilon)) { + if (s == 0.0) { + // we have only 1 root + final num t = -b / (2 * a); + final num tprime = 1.0 - t; + if ((t >= 0.0) && (t <= 1.0)) { + extremaX = ((tprime * tprime * tprime) * startX) + + ((3 * tprime * tprime * t) * cpX1) + + ((3 * tprime * t * t) * cpX2) + + (t * t * t * endX); + minX = math.min(extremaX, minX); + maxX = math.max(extremaX, maxX); + } + } else { + // we have 2 roots + s = math.sqrt(s); + num t = (-b - s) / (2 * a); + num tprime = 1.0 - t; + if ((t >= 0.0) && (t <= 1.0)) { + extremaX = ((tprime * tprime * tprime) * startX) + + ((3 * tprime * tprime * t) * cpX1) + + ((3 * tprime * t * t) * cpX2) + + (t * t * t * endX); + minX = math.min(extremaX, minX); + maxX = math.max(extremaX, maxX); + } + // check 2nd root + t = (-b + s) / (2 * a); + tprime = 1.0 - t; + if ((t >= 0.0) && (t <= 1.0)) { + extremaX = ((tprime * tprime * tprime) * startX) + + ((3 * tprime * tprime * t) * cpX1) + + ((3 * tprime * t * t) * cpX2) + + (t * t * t * endX); + + minX = math.min(extremaX, minX); + maxX = math.max(extremaX, maxX); + } + } + } + } + + // Now calc extremes for dy/dt = 0 just like above + if (!(((startY < cpY1) && (cpY1 < cpY2) && (cpY2 < endY)) || + ((startY > cpY1) && (cpY1 > cpY2) && (cpY2 > endY)))) { + // The extrema point is dy/dt B(t) = 0 + // The derivative of B(t) for cubic bezier is a quadratic equation + // with multiple roots + // B'(t) = a*t*t + b*t + c*t + a = -startY + (3 * (cpY1 - cpY2)) + endY; + b = 2 * (startY - (2 * cpY1) + cpY2); + c = -startY + cpY1; + + // Now find roots for quadratic equation with known coefficients + // a,b,c + // The roots are (-b+-sqrt(b*b-4*a*c)) / 2a + num s = (b * b) - (4 * a * c); + // If s is negative, we have no real roots + if ((s >= 0.0) && (a.abs() > epsilon)) { + if (s == 0.0) { + // we have only 1 root + final num t = -b / (2 * a); + final num tprime = 1.0 - t; + if ((t >= 0.0) && (t <= 1.0)) { + extremaY = ((tprime * tprime * tprime) * startY) + + ((3 * tprime * tprime * t) * cpY1) + + ((3 * tprime * t * t) * cpY2) + + (t * t * t * endY); + minY = math.min(extremaY, minY); + maxY = math.max(extremaY, maxY); + } + } else { + // we have 2 roots + s = math.sqrt(s); + final num t = (-b - s) / (2 * a); + final num tprime = 1.0 - t; + if ((t >= 0.0) && (t <= 1.0)) { + extremaY = ((tprime * tprime * tprime) * startY) + + ((3 * tprime * tprime * t) * cpY1) + + ((3 * tprime * t * t) * cpY2) + + (t * t * t * endY); + minY = math.min(extremaY, minY); + maxY = math.max(extremaY, maxY); + } + // check 2nd root + final num t2 = (-b + s) / (2 * a); + final num tprime2 = 1.0 - t2; + if ((t2 >= 0.0) && (t2 <= 1.0)) { + extremaY = ((tprime2 * tprime2 * tprime2) * startY) + + ((3 * tprime2 * tprime2 * t2) * cpY1) + + ((3 * tprime2 * t2 * t2) * cpY2) + + (t2 * t2 * t2 * endY); + minY = math.min(extremaY, minY); + maxY = math.max(extremaY, maxY); + } + } + } + } + break; + case PathCommandTypes.rect: + final RectCommand cmd = op; + left = cmd.x; + double width = cmd.width; + if (cmd.width < 0) { + left -= width; + width = -width; + } + double top = cmd.y; + double height = cmd.height; + if (cmd.height < 0) { + top -= height; + height = -height; + } + curX = minX = left; + maxX = left + width; + curY = minY = top; + maxY = top + height; + break; + case PathCommandTypes.rRect: + final RRectCommand cmd = op; + final ui.RRect rRect = cmd.rrect; + curX = minX = rRect.left; + maxX = rRect.left + rRect.width; + curY = minY = rRect.top; + maxY = rRect.top + rRect.height; + break; + case PathCommandTypes.close: + default: + skipBounds = false; + break; + } + if (!skipBounds) { + if (!ltrbInitialized) { + left = minX; + right = maxX; + top = minY; + bottom = maxY; + ltrbInitialized = true; + } else { + left = math.min(left, minX); + right = math.max(right, maxX); + top = math.min(top, minY); + bottom = math.max(bottom, maxY); + } + } + } + } + return ltrbInitialized + ? ui.Rect.fromLTRB(left, top, right, bottom) + : ui.Rect.zero; + } + + /// Creates a [PathMetrics] object for this path. + /// + /// If `forceClosed` is set to true, the contours of the path will be measured + /// as if they had been closed, even if they were not explicitly closed. + @override + SurfacePathMetrics computeMetrics({bool forceClosed = false}) { + return SurfacePathMetrics._(this, forceClosed); + } + + /// Detects if path is rounded rectangle and returns rounded rectangle or + /// null. + /// + /// Used for web optimization of physical shape represented as + /// a persistent div. + ui.RRect get webOnlyPathAsRoundedRect { + if (subpaths.length != 1) { + return null; + } + final Subpath subPath = subpaths[0]; + if (subPath.commands.length != 1) { + return null; + } + final PathCommand command = subPath.commands[0]; + return (command is RRectCommand) ? command.rrect : null; + } + + /// Detects if path is simple rectangle and returns rectangle or null. + /// + /// Used for web optimization of physical shape represented as + /// a persistent div. + ui.Rect get webOnlyPathAsRect { + if (subpaths.length != 1) { + return null; + } + final Subpath subPath = subpaths[0]; + if (subPath.commands.length != 1) { + return null; + } + final PathCommand command = subPath.commands[0]; + return (command is RectCommand) + ? ui.Rect.fromLTWH(command.x, command.y, command.width, command.height) + : null; + } + + /// Detects if path is simple oval and returns [Ellipse] or null. + /// + /// Used for web optimization of physical shape represented as + /// a persistent div. + Ellipse get webOnlyPathAsCircle { + if (subpaths.length != 1) { + return null; + } + final Subpath subPath = subpaths[0]; + if (subPath.commands.length != 1) { + return null; + } + final PathCommand command = subPath.commands[0]; + if (command is Ellipse) { + final Ellipse ellipse = command; + if ((ellipse.endAngle - ellipse.startAngle) % (2 * math.pi) == 0.0) { + return ellipse; + } + } + return null; + } + + /// Serializes this path to a value that's sent to a CSS custom painter for + /// painting. + List webOnlySerializeToCssPaint() { + final List serializedSubpaths = []; + for (int i = 0; i < subpaths.length; i++) { + serializedSubpaths.add(subpaths[i].serializeToCssPaint()); + } + return serializedSubpaths; + } + + @override + String toString() { + if (assertionsEnabled) { + return 'Path(${subpaths.join(', ')})'; + } else { + return super.toString(); + } + } +} + +// Returns true if point is inside ellipse. +bool _ellipseContains(double px, double py, double centerX, double centerY, + double radiusX, double radiusY) { + final double dx = px - centerX; + final double dy = py - centerY; + return ((dx * dx) / (radiusX * radiusX)) + ((dy * dy) / (radiusY * radiusY)) < + 1.0; +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/surface/surface.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/surface/surface.dart index 98fe25673b8..b8ea68f6cb1 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/surface/surface.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/surface/surface.dart @@ -78,7 +78,8 @@ void commitScene(PersistedScene scene) { if (_retainedSurfaces.isNotEmpty) { for (int i = 0; i < _retainedSurfaces.length; i++) { final PersistedSurface retainedSurface = _retainedSurfaces[i]; - assert(debugAssertSurfaceState(retainedSurface, PersistedSurfaceState.pendingRetention)); + assert(debugAssertSurfaceState( + retainedSurface, PersistedSurfaceState.pendingRetention)); retainedSurface.state = PersistedSurfaceState.active; } _retainedSurfaces = []; @@ -112,322 +113,6 @@ void commitScene(PersistedScene scene) { }()); } -/// Surfaces that were retained this frame. -/// -/// Surfaces should be added to this list directly. Instead, if a surface needs -/// to be retained call [_retainSurface]. -List get debugRetainedSurfaces => _retainedSurfaces; -List _retainedSurfaces = []; - -/// Maps every surface currently active on the screen to debug statistics. -Map _surfaceStats = - {}; - -List> _surfaceStatsTimeline = - >[]; - -/// Returns debug statistics for the given [surface]. -_DebugSurfaceStats _surfaceStatsFor(PersistedSurface surface) { - if (!_debugExplainSurfaceStats) { - throw Exception( - '_surfaceStatsFor is only available when _debugExplainSurfaceStats is set to true.'); - } - return _surfaceStats.putIfAbsent(surface, () => _DebugSurfaceStats(surface)); -} - -/// Compositor information collected for one frame useful for assessing the -/// efficiency of constructing the frame. -/// -/// This information is only available in debug mode. -/// -/// For stats pertaining to a single surface the numeric counter fields are -/// typically either 0 or 1. For aggregated stats, the numbers can be >1. -class _DebugSurfaceStats { - _DebugSurfaceStats(this.surface); - - /// The surface these stats are for, or `null` if these are aggregated stats. - final PersistedSurface surface; - - /// How many times a surface was retained from a previously rendered frame. - int retainSurfaceCount = 0; - - /// How many times a surface reused an HTML element from a previously rendered - /// surface. - int reuseElementCount = 0; - - /// If a surface is a [PersistedPicture], how many times it painted. - int paintCount = 0; - - /// If a surface is a [PersistedPicture], how many pixels it painted. - int paintPixelCount = 0; - - /// If a surface is a [PersistedPicture], how many times it reused a - /// previously allocated `` element when it painted. - int reuseCanvasCount = 0; - - /// If a surface is a [PersistedPicture], how many times it allocated a new - /// bitmap canvas. - int allocateBitmapCanvasCount = 0; - - /// If a surface is a [PersistedPicture], how many pixels it allocated for - /// the bitmap. - /// - /// For aggregated stats, this is the total sum of all pixels across all - /// canvases. - int allocatedBitmapSizeInPixels = 0; - - /// The number of HTML DOM nodes a surface allocated. - /// - /// For aggregated stats, this is the total sum of all DOM nodes across all - /// surfaces. - int allocatedDomNodeCount = 0; - - /// Adds all counters of [oneSurfaceStats] into this object. - void aggregate(_DebugSurfaceStats oneSurfaceStats) { - retainSurfaceCount += oneSurfaceStats.retainSurfaceCount; - reuseElementCount += oneSurfaceStats.reuseElementCount; - paintCount += oneSurfaceStats.paintCount; - paintPixelCount += oneSurfaceStats.paintPixelCount; - reuseCanvasCount += oneSurfaceStats.reuseCanvasCount; - allocateBitmapCanvasCount += oneSurfaceStats.allocateBitmapCanvasCount; - allocatedBitmapSizeInPixels += oneSurfaceStats.allocatedBitmapSizeInPixels; - allocatedDomNodeCount += oneSurfaceStats.allocatedDomNodeCount; - } -} - -html.CanvasRenderingContext2D _debugSurfaceStatsOverlayCtx; - -void _debugRepaintSurfaceStatsOverlay(PersistedScene scene) { - final int overlayWidth = html.window.innerWidth; - const int rowHeight = 30; - const int rowCount = 4; - const int overlayHeight = rowHeight * rowCount; - const int strokeWidth = 2; - - _surfaceStatsTimeline.add(_surfaceStats); - - while (_surfaceStatsTimeline.length > (overlayWidth / strokeWidth)) { - _surfaceStatsTimeline.removeAt(0); - } - - if (_debugSurfaceStatsOverlayCtx == null) { - final html.CanvasElement _debugSurfaceStatsOverlay = html.CanvasElement( - width: overlayWidth, - height: overlayHeight, - ); - _debugSurfaceStatsOverlay.style - ..position = 'fixed' - ..left = '0' - ..top = '0' - ..zIndex = '1000' - ..opacity = '0.8'; - _debugSurfaceStatsOverlayCtx = _debugSurfaceStatsOverlay.context2D; - html.document.body.append(_debugSurfaceStatsOverlay); - } - - _debugSurfaceStatsOverlayCtx - ..fillStyle = 'black' - ..beginPath() - ..rect(0, 0, overlayWidth, overlayHeight) - ..fill(); - - final double physicalScreenWidth = - html.window.innerWidth * EngineWindow.browserDevicePixelRatio; - final double physicalScreenHeight = - html.window.innerHeight * EngineWindow.browserDevicePixelRatio; - final double physicsScreenPixelCount = - physicalScreenWidth * physicalScreenHeight; - - final int totalDomNodeCount = scene.rootElement.querySelectorAll('*').length; - - for (int i = 0; i < _surfaceStatsTimeline.length; i++) { - final Map statsMap = - _surfaceStatsTimeline[i]; - final _DebugSurfaceStats totals = _DebugSurfaceStats(null); - int pixelCount = 0; - for (_DebugSurfaceStats oneSurfaceStats in statsMap.values) { - totals.aggregate(oneSurfaceStats); - if (oneSurfaceStats.surface is PersistedPicture) { - final PersistedPicture picture = oneSurfaceStats.surface; - pixelCount += picture.bitmapPixelCount; - } - } - - final double repaintRate = totals.paintPixelCount / pixelCount; - final double domAllocationRate = - totals.allocatedDomNodeCount / totalDomNodeCount; - final double bitmapAllocationRate = - totals.allocatedBitmapSizeInPixels / physicsScreenPixelCount; - final double surfaceRetainRate = - totals.retainSurfaceCount / _surfaceStatsTimeline[i].length; - - // Repaints - _debugSurfaceStatsOverlayCtx - ..lineWidth = strokeWidth - ..strokeStyle = 'red' - ..beginPath() - ..moveTo(strokeWidth * i, rowHeight) - ..lineTo(strokeWidth * i, rowHeight * (1 - repaintRate)) - ..stroke(); - - // DOM allocations - _debugSurfaceStatsOverlayCtx - ..lineWidth = strokeWidth - ..strokeStyle = 'red' - ..beginPath() - ..moveTo(strokeWidth * i, 2 * rowHeight) - ..lineTo(strokeWidth * i, rowHeight * (2 - domAllocationRate)) - ..stroke(); - - // Bitmap allocations - _debugSurfaceStatsOverlayCtx - ..lineWidth = strokeWidth - ..strokeStyle = 'red' - ..beginPath() - ..moveTo(strokeWidth * i, 3 * rowHeight) - ..lineTo(strokeWidth * i, rowHeight * (3 - bitmapAllocationRate)) - ..stroke(); - - // Surface retentions - _debugSurfaceStatsOverlayCtx - ..lineWidth = strokeWidth - ..strokeStyle = 'green' - ..beginPath() - ..moveTo(strokeWidth * i, 4 * rowHeight) - ..lineTo(strokeWidth * i, rowHeight * (4 - surfaceRetainRate)) - ..stroke(); - } - - _debugSurfaceStatsOverlayCtx - ..font = 'normal normal 14px sans-serif' - ..fillStyle = 'white' - ..fillText('Repaint rate', 5, rowHeight - 5) - ..fillText('DOM alloc rate', 5, 2 * rowHeight - 5) - ..fillText('Bitmap alloc rate', 5, 3 * rowHeight - 5) - ..fillText('Retain rate', 5, 4 * rowHeight - 5); - - for (int i = 1; i <= rowCount; i++) { - _debugSurfaceStatsOverlayCtx - ..lineWidth = 1 - ..strokeStyle = 'blue' - ..beginPath() - ..moveTo(0, overlayHeight - rowHeight * i) - ..lineTo(overlayWidth, overlayHeight - rowHeight * i) - ..stroke(); - } -} - -/// Prints debug statistics for the current frame to the console. -void _debugPrintSurfaceStats(PersistedScene scene, int frameNumber) { - int pictureCount = 0; - int paintCount = 0; - - int bitmapCanvasCount = 0; - int bitmapReuseCount = 0; - int bitmapAllocationCount = 0; - int bitmapPaintCount = 0; - int bitmapPixelsAllocated = 0; - - int domCanvasCount = 0; - int domPaintCount = 0; - - int surfaceRetainCount = 0; - int elementReuseCount = 0; - - int totalAllocatedDomNodeCount = 0; - - void countReusesRecursively(PersistedSurface surface) { - final _DebugSurfaceStats stats = _surfaceStatsFor(surface); - assert(stats != null); - - surfaceRetainCount += stats.retainSurfaceCount; - elementReuseCount += stats.reuseElementCount; - totalAllocatedDomNodeCount += stats.allocatedDomNodeCount; - - if (surface is PersistedStandardPicture) { - pictureCount += 1; - paintCount += stats.paintCount; - - if (surface._canvas is DomCanvas) { - domCanvasCount++; - domPaintCount += stats.paintCount; - } - - if (surface._canvas is BitmapCanvas) { - bitmapCanvasCount++; - bitmapPaintCount += stats.paintCount; - } - - bitmapReuseCount += stats.reuseCanvasCount; - bitmapAllocationCount += stats.allocateBitmapCanvasCount; - bitmapPixelsAllocated += stats.allocatedBitmapSizeInPixels; - } - - surface.visitChildren(countReusesRecursively); - } - - scene.visitChildren(countReusesRecursively); - - final StringBuffer buf = StringBuffer(); - buf - ..writeln( - '---------------------- FRAME #$frameNumber -------------------------') - ..writeln('Surfaces retained: $surfaceRetainCount') - ..writeln('Elements reused: $elementReuseCount') - ..writeln('Elements allocated: $totalAllocatedDomNodeCount') - ..writeln('Pictures: $pictureCount') - ..writeln(' Painted: $paintCount') - ..writeln(' Skipped painting: ${pictureCount - paintCount}') - ..writeln('DOM canvases:') - ..writeln(' Painted: $domPaintCount') - ..writeln(' Skipped painting: ${domCanvasCount - domPaintCount}') - ..writeln('Bitmap canvases: $bitmapCanvasCount') - ..writeln(' Painted: $bitmapPaintCount') - ..writeln(' Skipped painting: ${bitmapCanvasCount - bitmapPaintCount}') - ..writeln(' Reused: $bitmapReuseCount') - ..writeln(' Allocated: $bitmapAllocationCount') - ..writeln(' Allocated pixels: $bitmapPixelsAllocated') - ..writeln(' Available for reuse: ${_recycledCanvases.length}'); - - // A microtask will fire after the DOM is flushed, letting us probe into - // actual tags. - scheduleMicrotask(() { - final List canvasElements = - html.document.querySelectorAll('canvas'); - final StringBuffer canvasInfo = StringBuffer(); - final int pixelCount = canvasElements - .cast() - .map((html.CanvasElement e) { - final int pixels = e.width * e.height; - canvasInfo.writeln(' - ${e.width} x ${e.height} = $pixels pixels'); - return pixels; - }).fold(0, (int total, int pixels) => total + pixels); - final double physicalScreenWidth = - html.window.innerWidth * EngineWindow.browserDevicePixelRatio; - final double physicalScreenHeight = - html.window.innerHeight * EngineWindow.browserDevicePixelRatio; - final double physicsScreenPixelCount = - physicalScreenWidth * physicalScreenHeight; - final double screenPixelRatio = pixelCount / physicsScreenPixelCount; - final String screenDescription = - '1 screen is $physicalScreenWidth x $physicalScreenHeight = $physicsScreenPixelCount pixels'; - final String canvasPixelDescription = - '$pixelCount (${screenPixelRatio.toStringAsFixed(2)} x screens'; - buf - ..writeln(' Elements: ${canvasElements.length}') - ..writeln(canvasInfo) - ..writeln(' Pixels: $canvasPixelDescription; $screenDescription)') - ..writeln('-----------------------------------------------------------'); - final bool screenPixelRatioTooHigh = - screenPixelRatio > _kScreenPixelRatioWarningThreshold; - if (screenPixelRatioTooHigh) { - print( - 'WARNING: pixel/screen ratio too high (${screenPixelRatio.toStringAsFixed(2)}x)'); - } - print(buf); - }); -} - /// Signature of a function that receives a [PersistedSurface]. /// /// This is used to traverse surfaces using [PersistedSurface.visitChildren]. @@ -502,8 +187,10 @@ class PersistedSurfaceException implements Exception { /// Verifies that the [surface] is in one of the valid states. /// /// This function should be used inside an assertion expression. -bool debugAssertSurfaceState(PersistedSurface surface, PersistedSurfaceState state1, [PersistedSurfaceState state2, PersistedSurfaceState state3]) { - final List validStates = [ state1, state2, state3 ]; +bool debugAssertSurfaceState( + PersistedSurface surface, PersistedSurfaceState state1, + [PersistedSurfaceState state2, PersistedSurfaceState state3]) { + final List validStates = [state1, state2, state3]; if (validStates.contains(surface.state)) { return true; @@ -571,7 +258,8 @@ abstract class PersistedSurface implements ui.EngineLayer { /// reused before the request to retain it came in. In this case, the surface /// is [revive]d and rebuilt from scratch. void tryRetain() { - assert(debugAssertSurfaceState(this, PersistedSurfaceState.active, PersistedSurfaceState.released)); + assert(debugAssertSurfaceState( + this, PersistedSurfaceState.active, PersistedSurfaceState.released)); // Request that the layer is retained, but only if it's still active. It // could have been released. if (isActive) { @@ -664,8 +352,9 @@ abstract class PersistedSurface implements ui.EngineLayer { if (rootElement != null) { try { throw null; - } catch(_, stack) { - print('Attempted to build a $runtimeType, but it already has an HTML element ${rootElement.tagName}.'); + } catch (_, stack) { + print( + 'Attempted to build a $runtimeType, but it already has an HTML element ${rootElement.tagName}.'); print(stack.toString().split('\n').take(20).join('\n')); } } @@ -690,7 +379,8 @@ abstract class PersistedSurface implements ui.EngineLayer { @mustCallSuper void adoptElements(covariant PersistedSurface oldSurface) { assert(oldSurface.rootElement != null); - assert(debugAssertSurfaceState(oldSurface, PersistedSurfaceState.active, PersistedSurfaceState.pendingUpdate)); + assert(debugAssertSurfaceState(oldSurface, PersistedSurfaceState.active, + PersistedSurfaceState.pendingUpdate)); assert(() { if (oldSurface.isPendingUpdate) { final PersistedContainerSurface self = this; @@ -719,7 +409,8 @@ abstract class PersistedSurface implements ui.EngineLayer { assert(oldSurface != null); assert(!identical(oldSurface, this)); assert(debugAssertSurfaceState(this, PersistedSurfaceState.created)); - assert(debugAssertSurfaceState(oldSurface, PersistedSurfaceState.active, PersistedSurfaceState.pendingUpdate)); + assert(debugAssertSurfaceState(oldSurface, PersistedSurfaceState.active, + PersistedSurfaceState.pendingUpdate)); adoptElements(oldSurface); @@ -954,7 +645,11 @@ abstract class PersistedContainerSurface extends PersistedSurface { /// Adds a child to this container. void appendChild(PersistedSurface child) { - assert(debugAssertSurfaceState(child, PersistedSurfaceState.created, PersistedSurfaceState.pendingRetention, PersistedSurfaceState.pendingUpdate)); + assert(debugAssertSurfaceState( + child, + PersistedSurfaceState.created, + PersistedSurfaceState.pendingRetention, + PersistedSurfaceState.pendingUpdate)); _children.add(child); child.parent = this; } @@ -991,7 +686,8 @@ abstract class PersistedContainerSurface extends PersistedSurface { } else if (child is PersistedContainerSurface && child.oldLayer != null) { final PersistedSurface oldLayer = child.oldLayer; assert(oldLayer.rootElement != null); - assert(debugAssertSurfaceState(oldLayer, PersistedSurfaceState.pendingUpdate)); + assert(debugAssertSurfaceState( + oldLayer, PersistedSurfaceState.pendingUpdate)); child.update(child.oldLayer); } else { assert(debugAssertSurfaceState(child, PersistedSurfaceState.created)); @@ -1018,7 +714,8 @@ abstract class PersistedContainerSurface extends PersistedSurface { @override void update(PersistedContainerSurface oldSurface) { - assert(debugAssertSurfaceState(oldSurface, PersistedSurfaceState.active, PersistedSurfaceState.pendingUpdate)); + assert(debugAssertSurfaceState(oldSurface, PersistedSurfaceState.active, + PersistedSurfaceState.pendingUpdate)); assert(runtimeType == oldSurface.runtimeType); super.update(oldSurface); assert(debugAssertSurfaceState(oldSurface, PersistedSurfaceState.released)); @@ -1053,7 +750,8 @@ abstract class PersistedContainerSurface extends PersistedSurface { } for (int i = 0; i < _children.length; i++) { final PersistedSurface newChild = _children[i]; - assert(debugAssertSurfaceState(newChild, PersistedSurfaceState.active, PersistedSurfaceState.pendingRetention)); + assert(debugAssertSurfaceState(newChild, PersistedSurfaceState.active, + PersistedSurfaceState.pendingRetention)); assert(newChild.rootElement != null); assert(newChild.rootElement.parent == childContainer); } @@ -1076,13 +774,16 @@ abstract class PersistedContainerSurface extends PersistedSurface { final PersistedSurface newChild = _children[i]; if (newChild.isPendingRetention) { newChild.retain(); - assert(debugAssertSurfaceState(newChild, PersistedSurfaceState.pendingRetention)); + assert(debugAssertSurfaceState( + newChild, PersistedSurfaceState.pendingRetention)); } else if (newChild is PersistedContainerSurface && newChild.oldLayer != null) { final PersistedContainerSurface oldLayer = newChild.oldLayer; - assert(debugAssertSurfaceState(oldLayer, PersistedSurfaceState.pendingUpdate)); + assert(debugAssertSurfaceState( + oldLayer, PersistedSurfaceState.pendingUpdate)); newChild.update(oldLayer); - assert(debugAssertSurfaceState(oldLayer, PersistedSurfaceState.released)); + assert( + debugAssertSurfaceState(oldLayer, PersistedSurfaceState.released)); assert(debugAssertSurfaceState(newChild, PersistedSurfaceState.active)); } else { newChild.build(); @@ -1126,14 +827,16 @@ abstract class PersistedContainerSurface extends PersistedSurface { newChild.retain(); _discardActiveChildren(oldSurface); - assert(debugAssertSurfaceState(newChild, PersistedSurfaceState.pendingRetention)); + assert(debugAssertSurfaceState( + newChild, PersistedSurfaceState.pendingRetention)); return; } // Updated child is moved to the correct location in the tree; all others // are released. if (newChild is PersistedContainerSurface && newChild.oldLayer != null) { - assert(debugAssertSurfaceState(newChild.oldLayer, PersistedSurfaceState.pendingUpdate)); + assert(debugAssertSurfaceState( + newChild.oldLayer, PersistedSurfaceState.pendingUpdate)); assert(newChild.rootElement == null); assert(newChild.oldLayer.rootElement != null); @@ -1176,7 +879,8 @@ abstract class PersistedContainerSurface extends PersistedSurface { childContainer.append(newChild.rootElement); } - assert(debugAssertSurfaceState(bestMatch, PersistedSurfaceState.released)); + assert( + debugAssertSurfaceState(bestMatch, PersistedSurfaceState.released)); } else { newChild.build(); childContainer.append(newChild.rootElement); @@ -1235,29 +939,37 @@ abstract class PersistedContainerSurface extends PersistedSurface { final PersistedSurface newChild = _children[bottomInNew]; if (newChild.isPendingRetention) { newChild.retain(); - assert(debugAssertSurfaceState(newChild, PersistedSurfaceState.pendingRetention)); + assert(debugAssertSurfaceState( + newChild, PersistedSurfaceState.pendingRetention)); } else if (newChild is PersistedContainerSurface && newChild.oldLayer != null) { final PersistedContainerSurface oldLayer = newChild.oldLayer; - assert(debugAssertSurfaceState(oldLayer, PersistedSurfaceState.pendingUpdate)); + assert(debugAssertSurfaceState( + oldLayer, PersistedSurfaceState.pendingUpdate)); newChild.update(oldLayer); - assert(debugAssertSurfaceState(oldLayer, PersistedSurfaceState.released)); + assert( + debugAssertSurfaceState(oldLayer, PersistedSurfaceState.released)); assert(debugAssertSurfaceState(newChild, PersistedSurfaceState.active)); } else { final PersistedSurface matchedOldChild = matches[newChild]; if (matchedOldChild != null) { - assert(debugAssertSurfaceState(matchedOldChild, PersistedSurfaceState.active)); + assert(debugAssertSurfaceState( + matchedOldChild, PersistedSurfaceState.active)); newChild.update(matchedOldChild); - assert(debugAssertSurfaceState(matchedOldChild, PersistedSurfaceState.released)); - assert(debugAssertSurfaceState(newChild, PersistedSurfaceState.active)); + assert(debugAssertSurfaceState( + matchedOldChild, PersistedSurfaceState.released)); + assert( + debugAssertSurfaceState(newChild, PersistedSurfaceState.active)); } else { newChild.build(); - assert(debugAssertSurfaceState(newChild, PersistedSurfaceState.active)); + assert( + debugAssertSurfaceState(newChild, PersistedSurfaceState.active)); } } insertDomNodeIfMoved(newChild); assert(newChild.rootElement != null); - assert(debugAssertSurfaceState(newChild, PersistedSurfaceState.active, PersistedSurfaceState.pendingRetention)); + assert(debugAssertSurfaceState(newChild, PersistedSurfaceState.active, + PersistedSurfaceState.pendingRetention)); nextSibling = newChild; } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/surface/surface_stats.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/surface/surface_stats.dart new file mode 100644 index 00000000000..9bb6cab18a3 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/surface/surface_stats.dart @@ -0,0 +1,322 @@ +// 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; + +/// Surfaces that were retained this frame. +/// +/// Surfaces should be added to this list directly. Instead, if a surface needs +/// to be retained call [_retainSurface]. +List get debugRetainedSurfaces => _retainedSurfaces; +List _retainedSurfaces = []; + +/// Maps every surface currently active on the screen to debug statistics. +Map _surfaceStats = + {}; + +List> _surfaceStatsTimeline = + >[]; + +/// Returns debug statistics for the given [surface]. +_DebugSurfaceStats _surfaceStatsFor(PersistedSurface surface) { + if (!_debugExplainSurfaceStats) { + throw Exception( + '_surfaceStatsFor is only available when _debugExplainSurfaceStats is set to true.'); + } + return _surfaceStats.putIfAbsent(surface, () => _DebugSurfaceStats(surface)); +} + +/// Compositor information collected for one frame useful for assessing the +/// efficiency of constructing the frame. +/// +/// This information is only available in debug mode. +/// +/// For stats pertaining to a single surface the numeric counter fields are +/// typically either 0 or 1. For aggregated stats, the numbers can be >1. +class _DebugSurfaceStats { + _DebugSurfaceStats(this.surface); + + /// The surface these stats are for, or `null` if these are aggregated stats. + final PersistedSurface surface; + + /// How many times a surface was retained from a previously rendered frame. + int retainSurfaceCount = 0; + + /// How many times a surface reused an HTML element from a previously rendered + /// surface. + int reuseElementCount = 0; + + /// If a surface is a [PersistedPicture], how many times it painted. + int paintCount = 0; + + /// If a surface is a [PersistedPicture], how many pixels it painted. + int paintPixelCount = 0; + + /// If a surface is a [PersistedPicture], how many times it reused a + /// previously allocated `` element when it painted. + int reuseCanvasCount = 0; + + /// If a surface is a [PersistedPicture], how many times it allocated a new + /// bitmap canvas. + int allocateBitmapCanvasCount = 0; + + /// If a surface is a [PersistedPicture], how many pixels it allocated for + /// the bitmap. + /// + /// For aggregated stats, this is the total sum of all pixels across all + /// canvases. + int allocatedBitmapSizeInPixels = 0; + + /// The number of HTML DOM nodes a surface allocated. + /// + /// For aggregated stats, this is the total sum of all DOM nodes across all + /// surfaces. + int allocatedDomNodeCount = 0; + + /// Adds all counters of [oneSurfaceStats] into this object. + void aggregate(_DebugSurfaceStats oneSurfaceStats) { + retainSurfaceCount += oneSurfaceStats.retainSurfaceCount; + reuseElementCount += oneSurfaceStats.reuseElementCount; + paintCount += oneSurfaceStats.paintCount; + paintPixelCount += oneSurfaceStats.paintPixelCount; + reuseCanvasCount += oneSurfaceStats.reuseCanvasCount; + allocateBitmapCanvasCount += oneSurfaceStats.allocateBitmapCanvasCount; + allocatedBitmapSizeInPixels += oneSurfaceStats.allocatedBitmapSizeInPixels; + allocatedDomNodeCount += oneSurfaceStats.allocatedDomNodeCount; + } +} + +html.CanvasRenderingContext2D _debugSurfaceStatsOverlayCtx; + +void _debugRepaintSurfaceStatsOverlay(PersistedScene scene) { + final int overlayWidth = html.window.innerWidth; + const int rowHeight = 30; + const int rowCount = 4; + const int overlayHeight = rowHeight * rowCount; + const int strokeWidth = 2; + + _surfaceStatsTimeline.add(_surfaceStats); + + while (_surfaceStatsTimeline.length > (overlayWidth / strokeWidth)) { + _surfaceStatsTimeline.removeAt(0); + } + + if (_debugSurfaceStatsOverlayCtx == null) { + final html.CanvasElement _debugSurfaceStatsOverlay = html.CanvasElement( + width: overlayWidth, + height: overlayHeight, + ); + _debugSurfaceStatsOverlay.style + ..position = 'fixed' + ..left = '0' + ..top = '0' + ..zIndex = '1000' + ..opacity = '0.8'; + _debugSurfaceStatsOverlayCtx = _debugSurfaceStatsOverlay.context2D; + html.document.body.append(_debugSurfaceStatsOverlay); + } + + _debugSurfaceStatsOverlayCtx + ..fillStyle = 'black' + ..beginPath() + ..rect(0, 0, overlayWidth, overlayHeight) + ..fill(); + + final double physicalScreenWidth = + html.window.innerWidth * EngineWindow.browserDevicePixelRatio; + final double physicalScreenHeight = + html.window.innerHeight * EngineWindow.browserDevicePixelRatio; + final double physicsScreenPixelCount = + physicalScreenWidth * physicalScreenHeight; + + final int totalDomNodeCount = scene.rootElement.querySelectorAll('*').length; + + for (int i = 0; i < _surfaceStatsTimeline.length; i++) { + final Map statsMap = + _surfaceStatsTimeline[i]; + final _DebugSurfaceStats totals = _DebugSurfaceStats(null); + int pixelCount = 0; + for (_DebugSurfaceStats oneSurfaceStats in statsMap.values) { + totals.aggregate(oneSurfaceStats); + if (oneSurfaceStats.surface is PersistedPicture) { + final PersistedPicture picture = oneSurfaceStats.surface; + pixelCount += picture.bitmapPixelCount; + } + } + + final double repaintRate = totals.paintPixelCount / pixelCount; + final double domAllocationRate = + totals.allocatedDomNodeCount / totalDomNodeCount; + final double bitmapAllocationRate = + totals.allocatedBitmapSizeInPixels / physicsScreenPixelCount; + final double surfaceRetainRate = + totals.retainSurfaceCount / _surfaceStatsTimeline[i].length; + + // Repaints + _debugSurfaceStatsOverlayCtx + ..lineWidth = strokeWidth + ..strokeStyle = 'red' + ..beginPath() + ..moveTo(strokeWidth * i, rowHeight) + ..lineTo(strokeWidth * i, rowHeight * (1 - repaintRate)) + ..stroke(); + + // DOM allocations + _debugSurfaceStatsOverlayCtx + ..lineWidth = strokeWidth + ..strokeStyle = 'red' + ..beginPath() + ..moveTo(strokeWidth * i, 2 * rowHeight) + ..lineTo(strokeWidth * i, rowHeight * (2 - domAllocationRate)) + ..stroke(); + + // Bitmap allocations + _debugSurfaceStatsOverlayCtx + ..lineWidth = strokeWidth + ..strokeStyle = 'red' + ..beginPath() + ..moveTo(strokeWidth * i, 3 * rowHeight) + ..lineTo(strokeWidth * i, rowHeight * (3 - bitmapAllocationRate)) + ..stroke(); + + // Surface retentions + _debugSurfaceStatsOverlayCtx + ..lineWidth = strokeWidth + ..strokeStyle = 'green' + ..beginPath() + ..moveTo(strokeWidth * i, 4 * rowHeight) + ..lineTo(strokeWidth * i, rowHeight * (4 - surfaceRetainRate)) + ..stroke(); + } + + _debugSurfaceStatsOverlayCtx + ..font = 'normal normal 14px sans-serif' + ..fillStyle = 'white' + ..fillText('Repaint rate', 5, rowHeight - 5) + ..fillText('DOM alloc rate', 5, 2 * rowHeight - 5) + ..fillText('Bitmap alloc rate', 5, 3 * rowHeight - 5) + ..fillText('Retain rate', 5, 4 * rowHeight - 5); + + for (int i = 1; i <= rowCount; i++) { + _debugSurfaceStatsOverlayCtx + ..lineWidth = 1 + ..strokeStyle = 'blue' + ..beginPath() + ..moveTo(0, overlayHeight - rowHeight * i) + ..lineTo(overlayWidth, overlayHeight - rowHeight * i) + ..stroke(); + } +} + +/// Prints debug statistics for the current frame to the console. +void _debugPrintSurfaceStats(PersistedScene scene, int frameNumber) { + int pictureCount = 0; + int paintCount = 0; + + int bitmapCanvasCount = 0; + int bitmapReuseCount = 0; + int bitmapAllocationCount = 0; + int bitmapPaintCount = 0; + int bitmapPixelsAllocated = 0; + + int domCanvasCount = 0; + int domPaintCount = 0; + + int surfaceRetainCount = 0; + int elementReuseCount = 0; + + int totalAllocatedDomNodeCount = 0; + + void countReusesRecursively(PersistedSurface surface) { + final _DebugSurfaceStats stats = _surfaceStatsFor(surface); + assert(stats != null); + + surfaceRetainCount += stats.retainSurfaceCount; + elementReuseCount += stats.reuseElementCount; + totalAllocatedDomNodeCount += stats.allocatedDomNodeCount; + + if (surface is PersistedStandardPicture) { + pictureCount += 1; + paintCount += stats.paintCount; + + if (surface._canvas is DomCanvas) { + domCanvasCount++; + domPaintCount += stats.paintCount; + } + + if (surface._canvas is BitmapCanvas) { + bitmapCanvasCount++; + bitmapPaintCount += stats.paintCount; + } + + bitmapReuseCount += stats.reuseCanvasCount; + bitmapAllocationCount += stats.allocateBitmapCanvasCount; + bitmapPixelsAllocated += stats.allocatedBitmapSizeInPixels; + } + + surface.visitChildren(countReusesRecursively); + } + + scene.visitChildren(countReusesRecursively); + + final StringBuffer buf = StringBuffer(); + buf + ..writeln( + '---------------------- FRAME #$frameNumber -------------------------') + ..writeln('Surfaces retained: $surfaceRetainCount') + ..writeln('Elements reused: $elementReuseCount') + ..writeln('Elements allocated: $totalAllocatedDomNodeCount') + ..writeln('Pictures: $pictureCount') + ..writeln(' Painted: $paintCount') + ..writeln(' Skipped painting: ${pictureCount - paintCount}') + ..writeln('DOM canvases:') + ..writeln(' Painted: $domPaintCount') + ..writeln(' Skipped painting: ${domCanvasCount - domPaintCount}') + ..writeln('Bitmap canvases: $bitmapCanvasCount') + ..writeln(' Painted: $bitmapPaintCount') + ..writeln(' Skipped painting: ${bitmapCanvasCount - bitmapPaintCount}') + ..writeln(' Reused: $bitmapReuseCount') + ..writeln(' Allocated: $bitmapAllocationCount') + ..writeln(' Allocated pixels: $bitmapPixelsAllocated') + ..writeln(' Available for reuse: ${_recycledCanvases.length}'); + + // A microtask will fire after the DOM is flushed, letting us probe into + // actual tags. + scheduleMicrotask(() { + final List canvasElements = + html.document.querySelectorAll('canvas'); + final StringBuffer canvasInfo = StringBuffer(); + final int pixelCount = canvasElements + .cast() + .map((html.CanvasElement e) { + final int pixels = e.width * e.height; + canvasInfo.writeln(' - ${e.width} x ${e.height} = $pixels pixels'); + return pixels; + }).fold(0, (int total, int pixels) => total + pixels); + final double physicalScreenWidth = + html.window.innerWidth * EngineWindow.browserDevicePixelRatio; + final double physicalScreenHeight = + html.window.innerHeight * EngineWindow.browserDevicePixelRatio; + final double physicsScreenPixelCount = + physicalScreenWidth * physicalScreenHeight; + final double screenPixelRatio = pixelCount / physicsScreenPixelCount; + final String screenDescription = + '1 screen is $physicalScreenWidth x $physicalScreenHeight = $physicsScreenPixelCount pixels'; + final String canvasPixelDescription = + '$pixelCount (${screenPixelRatio.toStringAsFixed(2)} x screens'; + buf + ..writeln(' Elements: ${canvasElements.length}') + ..writeln(canvasInfo) + ..writeln(' Pixels: $canvasPixelDescription; $screenDescription)') + ..writeln('-----------------------------------------------------------'); + final bool screenPixelRatioTooHigh = + screenPixelRatio > _kScreenPixelRatioWarningThreshold; + if (screenPixelRatioTooHigh) { + print( + 'WARNING: pixel/screen ratio too high (${screenPixelRatio.toStringAsFixed(2)}x)'); + } + print(buf); + }); +}