diff --git a/lib/web_ui/lib/src/engine/surface/path_metrics.dart b/lib/web_ui/lib/src/engine/surface/path_metrics.dart index 817f7243cfbbc..5f641c51c4fea 100644 --- a/lib/web_ui/lib/src/engine/surface/path_metrics.dart +++ b/lib/web_ui/lib/src/engine/surface/path_metrics.dart @@ -4,6 +4,8 @@ part of engine; +const double kEpsilon = 0.000000001; + /// An iterable collection of [PathMetric] objects describing a [Path]. /// /// A [PathMetrics] object is created by using the [Path.computeMetrics] method, @@ -18,9 +20,11 @@ part of engine; /// /// When iterating across a [PathMetrics]' contours, the [PathMetric] objects /// are only valid until the next one is obtained. -class SurfacePathMetrics extends IterableBase implements ui.PathMetrics { +class SurfacePathMetrics extends IterableBase + implements ui.PathMetrics { SurfacePathMetrics._(SurfacePath path, bool forceClosed) - : _iterator = SurfacePathMetricIterator._(SurfacePathMetric._(path, forceClosed)); + : _iterator = + SurfacePathMetricIterator._(_SurfacePathMeasure(path, forceClosed)); final SurfacePathMetricIterator _iterator; @@ -28,74 +32,33 @@ class SurfacePathMetrics extends IterableBase implements ui.PathM Iterator get iterator => _iterator; } -/// Tracks iteration from one segment of a path to the next for measurement. -class SurfacePathMetricIterator implements Iterator { - SurfacePathMetricIterator._(this._pathMetric); - - SurfacePathMetric _pathMetric; - bool _firstTime = true; - - @override - SurfacePathMetric get current => - _firstTime ? null : _pathMetric._segments.isEmpty ? null : _pathMetric; - - @override - bool moveNext() { - // PathMetric isn't a normal iterable - it's already initialized to its - // first Path. Should only call _moveNext when done with the first one. - if (_firstTime == true) { - _firstTime = false; - return _pathMetric._segments.isNotEmpty; - } else if (_pathMetric?._moveNext() == true) { - return true; - } - _pathMetric = null; - return false; +/// Maintains a single instance of computed segments for set of PathMetric +/// objects exposed through iterator. +class _SurfacePathMeasure { + _SurfacePathMeasure(this._path, this.forceClosed) { + // nextContour will increment this to the zero based index. + _currentContourIndex = -1; } -} - -// Maximum range value used in curve subdivision using Casteljau algorithm. -const int _kMaxTValue = 0x3FFFFFFF; -// Distance at which we stop subdividing cubic and quadratic curves. -const double _fTolerance = 0.5; -/// Utilities for measuring a [Path] and extracting subpaths. -/// -/// Iterate over the object returned by [Path.computeMetrics] to obtain -/// [PathMetric] objects. -/// -/// Once created, metrics will only be valid while the iterator is at the given -/// contour. When the next contour's [PathMetric] is obtained, this object -/// becomes invalid. -/// -/// Implementation is based on -/// https://github.com/google/skia/blob/master/src/core/SkContourMeasure.cpp -/// to maintain consistency with native platforms. -class SurfacePathMetric implements ui.PathMetric { final SurfacePath _path; - final bool _forceClosed; + final List<_PathContourMeasure> _contours = []; // If the contour ends with a call to [Path.close] (which may // have been implied when using [Path.addRect]) - bool _isClosed; - // Iterator index into [Path.subPaths] - int _subPathIndex = 0; - List<_PathSegment> _segments; - double _contourLength; + final bool forceClosed; - /// Create a new empty [Path] object. - SurfacePathMetric._(this._path, this._forceClosed) { - _buildSegments(); - } + int _currentContourIndex; + int get currentContourIndex => _currentContourIndex; - @override - int get contourIndex { - throw UnimplementedError('contourIndex is not implemented in the HTML backend'); - } + // Iterator index into [Path.subPaths] + int _subPathIndex = -1; + _PathContourMeasure _contourMeasure; - /// Return the total length of the current contour. - @override - double get length => _contourLength; + double length(int contourIndex) { + assert(contourIndex <= currentContourIndex, + 'Iterator must be advanced before index $contourIndex can be used.'); + return _contours[contourIndex].length; + } /// Computes the position of hte current contour at the given offset, and the /// angle of the path at that point. @@ -107,38 +70,22 @@ class SurfacePathMetric implements ui.PathMetric { /// Returns null if the contour has zero [length]. /// /// The distance is clamped to the [length] of the current contour. - @override - ui.Tangent getTangentForOffset(double distance) { - final Float32List posTan = _getPosTan(distance); - // first entry == 0 indicates that Skia returned false - if (posTan[0] == 0.0) { - return null; - } else { - return ui.Tangent( - ui.Offset(posTan[1], posTan[2]), ui.Offset(posTan[3], posTan[4])); - } + ui.Tangent getTangentForOffset(int contourIndex, double distance) { + return _contours[contourIndex].getTangentForOffset(distance); } - Float32List _getPosTan(double distance) => throw UnimplementedError(); - - /// Given a start and stop distance, return the intervening segment(s). - /// - /// `start` and `end` are pinned to legal values (0..[length]) - /// Returns null if the segment is 0 length or `start` > `stop`. - /// Begin the segment with a moveTo if `startWithMoveTo` is true. - @override - SurfacePath extractPath(double start, double end, {bool startWithMoveTo = true}) => - throw UnimplementedError(); + bool isClosed(int contourIndex) => _contours[contourIndex].isClosed; - /// Whether the contour is closed. - /// - /// Returns true if the contour ends with a call to [Path.close] (which may - /// have been implied when using [Path.addRect]) or if `forceClosed` was - /// specified as true in the call to [Path.computeMetrics]. Returns false - /// otherwise. - @override - bool get isClosed { - return _isClosed; + // Move to the next contour in the path. + // + // A path can have a next contour if [Path.moveTo] was called after drawing began. + // Return true if one exists, or false. + bool _nextContour() { + final bool next = _nativeNextContour(); + if (next) { + _currentContourIndex++; + } + return next; } // Move to the next contour in the path. @@ -151,27 +98,175 @@ class SurfacePathMetric implements ui.PathMetric { // [Iterator.current]. In this case, the [PathMetric] is valid before // calling `_moveNext` - `_moveNext` should be called after the first // iteration is done instead of before. - bool _moveNext() { + bool _nativeNextContour() { if (_subPathIndex == (_path.subpaths.length - 1)) { return false; } ++_subPathIndex; - _buildSegments(); + _contourMeasure = + _PathContourMeasure(_path.subpaths[_subPathIndex], forceClosed); + _contours.add(_contourMeasure); return true; } + ui.Path extractPath(int contourIndex, double start, double end, + {bool startWithMoveTo = true}) { + return _contours[contourIndex].extractPath(start, end, startWithMoveTo); + } +} + +/// Builds segments for a single contour to measure distance, compute tangent +/// and extract a sub path. +class _PathContourMeasure { + _PathContourMeasure(this.subPath, this.forceClosed) { + _buildSegments(); + } + + final List<_PathSegment> _segments = []; + // Allocate buffer large enough for returning cubic curve chop result. + // 2 floats for each coordinate x (start, end & control point 1 & 2). + static final Float32List _buffer = Float32List(8); + + final Subpath subPath; + final bool forceClosed; + double get length => _contourLength; + bool get isClosed => _isClosed; + + double _contourLength = 0.0; + bool _isClosed = false; + + ui.Tangent getTangentForOffset(double distance) { + final segmentIndex = _segmentIndexAtDistance(distance); + if (segmentIndex == -1) { + return null; + } + return _getPosTan(segmentIndex, distance); + } + + // Returns segment at [distance]. + int _segmentIndexAtDistance(double distance) { + if (distance.isNaN) { + return -1; + } + // Pin distance to legal range. + if (distance < 0.0) { + distance = 0.0; + } else if (distance > _contourLength) { + distance = _contourLength; + } + + // Binary search through segments to find segment at distance. + if (_segments.isEmpty) { + return -1; + } + int lo = 0; + int hi = _segments.length - 1; + while (lo < hi) { + int mid = (lo + hi) >> 1; + if (_segments[mid].distance < distance) { + lo = mid + 1; + } else { + hi = mid; + } + } + if (_segments[hi].distance < distance) { + hi++; + } + return hi; + } + + _SurfaceTangent _getPosTan(int segmentIndex, double distance) { + _PathSegment segment = _segments[segmentIndex]; + // Compute distance to segment. Since distance is cumulative to find + // t = 0..1 on the segment, we need to calculate start distance using prior + // segment. + final double startDistance = + segmentIndex == 0 ? 0 : _segments[segmentIndex - 1].distance; + final double totalDistance = segment.distance - startDistance; + final double t = totalDistance < kEpsilon + ? 0 + : (distance - startDistance) / totalDistance; + return segment.computeTangent(t); + } + + ui.Path extractPath( + double startDistance, double stopDistance, bool startWithMoveTo) { + if (startDistance < 0) { + startDistance = 0; + } + if (stopDistance > _contourLength) { + stopDistance = _contourLength; + } + if (startDistance > stopDistance || _segments.isEmpty) { + return null; + } + final ui.Path path = ui.Path(); + int startSegmentIndex = _segmentIndexAtDistance(startDistance); + int stopSegmentIndex = _segmentIndexAtDistance(stopDistance); + if (startSegmentIndex == -1 || stopSegmentIndex == -1) { + return null; + } + int currentSegmentIndex = startSegmentIndex; + _PathSegment seg = _segments[currentSegmentIndex]; + final _SurfaceTangent startTangent = + _getPosTan(startSegmentIndex, startDistance); + if (startWithMoveTo) { + final ui.Offset startPosition = startTangent.position; + path.moveTo(startPosition.dx, startPosition.dy); + } + final _SurfaceTangent stopTangent = + _getPosTan(stopSegmentIndex, stopDistance); + double startT = startTangent.t; + final double stopT = stopTangent.t; + if (startSegmentIndex == stopSegmentIndex) { + // We only have a single segment that covers the complete distance. + _outputSegmentTo(seg, startT, stopT, path); + } else { + do { + // Write this segment from startT to end (t = 1.0). + _outputSegmentTo(seg, startT, 1.0, path); + // Move to next segment until we hit stop segment. + ++currentSegmentIndex; + seg = _segments[currentSegmentIndex]; + startT = 0; + } while (currentSegmentIndex != stopSegmentIndex); + // Final write last segment from t=0.0 to t=stopT. + _outputSegmentTo(seg, 0.0, stopT, path); + } + return path; + } + + // Chops the segment at startT and endT and writes it to output [path]. + void _outputSegmentTo( + _PathSegment segment, double startT, double stopT, ui.Path path) { + final List points = segment.points; + switch (segment.segmentType) { + case PathCommandTypes.lineTo: + final double toX = (points[2] * stopT) + (points[0] * (1.0 - stopT)); + final double toY = (points[3] * stopT) + (points[1] * (1.0 - stopT)); + path.lineTo(toX, toY); + break; + case PathCommandTypes.bezierCurveTo: + _chopCubicAt(points, startT, stopT, _buffer); + path.cubicTo(_buffer[2], _buffer[3], _buffer[4], _buffer[5], _buffer[6], + _buffer[7]); + break; + case PathCommandTypes.quadraticCurveTo: + _chopQuadAt(points, startT, stopT, _buffer); + path.quadraticBezierTo(_buffer[2], _buffer[3], _buffer[4], _buffer[5]); + break; + default: + throw UnsupportedError('Invalid segment type'); + } + } + void _buildSegments() { - _segments = <_PathSegment>[]; + assert(_segments.isEmpty, '_buildSegments should be called once'); _isClosed = false; double distance = 0.0; bool haveSeenMoveTo = false; - if (_path.subpaths.isEmpty) { - _contourLength = 0; - return; - } - final Subpath subpath = _path.subpaths[_subPathIndex]; - final List commands = subpath.commands; + final List commands = subPath.commands; double currentX = 0.0, currentY = 0.0; final Function lineToHandler = (double x, double y) { final double dx = currentX - x; @@ -182,8 +277,8 @@ class SurfacePathMetric implements ui.PathMetric { // actually made it larger, since a very small delta might be > 0, but // still have no effect on distance (if distance >>> delta). if (distance > prevDistance) { - _segments.add(_PathSegment(PathCommandTypes.lineTo, distance, - [currentX, currentY, x, y])); + _segments.add(_PathSegment( + PathCommandTypes.lineTo, distance, [currentX, currentY, x, y])); } currentX = x; currentY = y; @@ -316,7 +411,7 @@ class SurfacePathMetric implements ui.PathMetric { throw UnimplementedError('Unknown path command $command'); } } - if (!_isClosed && _forceClosed && _segments.isNotEmpty) { + if (!_isClosed && forceClosed && _segments.isNotEmpty) { _PathSegment firstSegment = _segments.first; lineToHandler(firstSegment.points[0], firstSegment.points[1]); } @@ -383,10 +478,10 @@ class SurfacePathMetric implements ui.PathMetric { final double abcdX = (abcX + bcdX) / 2; final double abcdY = (abcY + bcdY) / 2; final int tHalf = (tMin + tMax) >> 1; - distance = _computeCubicSegments( - x0, y0, abX, abY, abcX, abcY, abcdX, abcdY, distance, tMin, tHalf, segments); - distance = _computeCubicSegments( - abcdX, abcdY, bcdX, bcdY, cdX, cdY, x3, y3, distance, tHalf, tMax, segments); + distance = _computeCubicSegments(x0, y0, abX, abY, abcX, abcY, abcdX, + abcdY, distance, tMin, tHalf, segments); + distance = _computeCubicSegments(abcdX, abcdY, bcdX, bcdY, cdX, cdY, x3, + y3, distance, tHalf, tMax, segments); } else { final double dx = x0 - x3; final double dy = y0 - y3; @@ -394,8 +489,8 @@ class SurfacePathMetric implements ui.PathMetric { final double prevDistance = distance; distance += startToEndDistance; if (distance > prevDistance) { - segments.add(_PathSegment(PathCommandTypes.bezierCurveTo, - distance, [x0, y0, x1, y1, x2, y2, x3, y3])); + segments.add(_PathSegment(PathCommandTypes.bezierCurveTo, distance, + [x0, y0, x1, y1, x2, y2, x3, y3])); } } return distance; @@ -436,8 +531,8 @@ class SurfacePathMetric implements ui.PathMetric { final double prevDistance = distance; distance += startToEndDistance; if (distance > prevDistance) { - _segments.add(_PathSegment(PathCommandTypes.quadraticCurveTo, - distance, [x0, y0, x1, y1, x2, y2])); + _segments.add(_PathSegment(PathCommandTypes.quadraticCurveTo, distance, + [x0, y0, x1, y1, x2, y2])); } } return distance; @@ -512,6 +607,111 @@ class SurfacePathMetric implements ui.PathMetric { } result.distance = distance; } +} + +/// Tracks iteration from one segment of a path to the next for measurement. +class SurfacePathMetricIterator implements Iterator { + SurfacePathMetricIterator._(this._pathMeasure) : assert(_pathMeasure != null); + + SurfacePathMetric _pathMetric; + _SurfacePathMeasure _pathMeasure; + bool _firstTime = true; + + @override + SurfacePathMetric get current => _pathMetric; + + @override + bool moveNext() { + if (_pathMeasure._nextContour()) { + _pathMetric = SurfacePathMetric._(_pathMeasure); + return true; + } + _pathMetric = null; + return false; + } +} + +// Maximum range value used in curve subdivision using Casteljau algorithm. +const int _kMaxTValue = 0x3FFFFFFF; +// Distance at which we stop subdividing cubic and quadratic curves. +const double _fTolerance = 0.5; + +/// Utilities for measuring a [Path] and extracting sub-paths. +/// +/// Iterate over the object returned by [Path.computeMetrics] to obtain +/// [PathMetric] objects. Callers that want to randomly access elements or +/// iterate multiple times should use `path.computeMetrics().toList()`, since +/// [PathMetrics] does not memoize. +/// +/// Once created, the metrics are only valid for the path as it was specified +/// when [Path.computeMetrics] was called. If additional contours are added or +/// any contours are updated, the metrics need to be recomputed. Previously +/// created metrics will still refer to a snapshot of the path at the time they +/// were computed, rather than to the actual metrics for the new mutations to +/// the path. +/// +/// Implementation is based on +/// https://github.com/google/skia/blob/master/src/core/SkContourMeasure.cpp +/// to maintain consistency with native platforms. +class SurfacePathMetric implements ui.PathMetric { + SurfacePathMetric._(this._measure) + : assert(_measure != null), + length = _measure.length(_measure.currentContourIndex), + isClosed = _measure.isClosed(_measure.currentContourIndex), + contourIndex = _measure.currentContourIndex; + + /// Return the total length of the current contour. + @override + final double length; + + /// Whether the contour is closed. + /// + /// Returns true if the contour ends with a call to [Path.close] (which may + /// have been implied when using methods like [Path.addRect]) or if + /// `forceClosed` was specified as true in the call to [Path.computeMetrics]. + /// Returns false otherwise. + final bool isClosed; + + /// The zero-based index of the contour. + /// + /// [Path] objects are made up of zero or more contours. The first contour is + /// created once a drawing command (e.g. [Path.lineTo]) is issued. A + /// [Path.moveTo] command after a drawing command may create a new contour, + /// although it may not if optimizations are applied that determine the move + /// command did not actually result in moving the pen. + /// + /// This property is only valid with reference to its original iterator and + /// the contours of the path at the time the path's metrics were computed. If + /// additional contours were added or existing contours updated, this metric + /// will be invalid for the current state of the path. + final int contourIndex; + + final _SurfacePathMeasure _measure; + + /// Computes the position of the current contour at the given offset, and the + /// angle of the path at that point. + /// + /// For example, calling this method with a distance of 1.41 for a line from + /// 0.0,0.0 to 2.0,2.0 would give a point 1.0,1.0 and the angle 45 degrees + /// (but in radians). + /// + /// Returns null if the contour has zero [length]. + /// + /// The distance is clamped to the [length] of the current contour. + @override + ui.Tangent getTangentForOffset(double distance) { + return _measure.getTangentForOffset(contourIndex, distance); + } + + /// Given a start and stop distance, return the intervening segment(s). + /// + /// `start` and `end` are pinned to legal values (0..[length]) + /// Returns null if the segment is 0 length or `start` > `stop`. + /// Begin the segment with a moveTo if `startWithMoveTo` is true. + ui.Path extractPath(double start, double end, {bool startWithMoveTo = true}) { + return _measure.extractPath(contourIndex, start, end, + startWithMoveTo: startWithMoveTo); + } @override String toString() => 'PathMetric'; @@ -524,10 +724,277 @@ class _EllipseSegmentResult { _EllipseSegmentResult(); } +// Given a vector dx, dy representing slope, normalize and return as [ui.Offset]. +ui.Offset _normalizeSlope(double dx, double dy) { + final double length = math.sqrt(dx * dx + dy * dy); + return length < kEpsilon + ? ui.Offset(0.0, 0.0) + : ui.Offset(dx / length, dy / length); +} + +class _SurfaceTangent extends ui.Tangent { + const _SurfaceTangent(ui.Offset position, ui.Offset vector, this.t) + : assert(position != null), + assert(vector != null), + assert(t != null), + super(position, vector); + + // Normalized distance of tangent point from start of a contour. + final double t; +} + class _PathSegment { _PathSegment(this.segmentType, this.distance, this.points); final int segmentType; final double distance; final List points; + + _SurfaceTangent computeTangent(double t) { + switch (segmentType) { + case PathCommandTypes.lineTo: + // Simple line. Position is simple interpolation from start to end point. + final double xAtDistance = (points[2] * t) + (points[0] * (1.0 - t)); + final double yAtDistance = (points[3] * t) + (points[1] * (1.0 - t)); + return _SurfaceTangent(ui.Offset(xAtDistance, yAtDistance), + _normalizeSlope(points[2] - points[0], points[3] - points[1]), t); + case PathCommandTypes.bezierCurveTo: + return tangentForCubicAt(t, points[0], points[1], points[2], points[3], + points[4], points[5], points[6], points[7]); + case PathCommandTypes.quadraticCurveTo: + return tangentForQuadAt(t, points[0], points[1], points[2], points[3], + points[4], points[5]); + default: + throw UnsupportedError('Invalid segment type'); + } + } + + _SurfaceTangent tangentForQuadAt(double t, double x0, double y0, double x1, + double y1, double x2, double y2) { + assert(t >= 0 && t <= 1); + final _SkQuadCoefficients _quadEval = + _SkQuadCoefficients(x0, y0, x1, y1, x2, y2); + final ui.Offset pos = ui.Offset(_quadEval.evalX(t), _quadEval.evalY(t)); + // Derivative of quad curve is 2(b - a + (a - 2b + c)t). + // If control point is at start or end point, this yields 0 for t = 0 and + // t = 1. In that case use the quad end points to compute tangent instead + // of derivative. + final ui.Offset tangentVector = ((t == 0 && x0 == x1 && y0 == y1) || + (t == 1 && x1 == x2 && y1 == y2)) + ? _normalizeSlope(x2 - x0, y2 - y0) + : _normalizeSlope( + 2 * ((x2 - x0) * t + (x1 - x0)), 2 * ((y2 - y0) * t + (y1 - y0))); + return _SurfaceTangent(pos, tangentVector, t); + } + + _SurfaceTangent tangentForCubicAt(double t, double x0, double y0, double x1, + double y1, double x2, double y2, double x3, double y3) { + assert(t >= 0 && t <= 1); + final _SkCubicCoefficients _cubicEval = + _SkCubicCoefficients(x0, y0, x1, y1, x2, y2, x3, y3); + final ui.Offset pos = ui.Offset(_cubicEval.evalX(t), _cubicEval.evalY(t)); + // Derivative of cubic is zero when t = 0 or 1 and adjacent control point + // is on the start or end point of curve. Use the other control point + // to compute the tangent or if both control points are on end points + // use end points for tangent. + final bool tAtZero = t == 0; + ui.Offset tangentVector; + if ((tAtZero && x0 == x1 && y0 == y1) || (t == 1 && x2 == x3 && y2 == y3)) { + double dx = tAtZero ? x2 - x0 : x3 - x1; + double dy = tAtZero ? y2 - y0 : y3 - y1; + if (dx == 0 && dy == 0) { + dx = x3 - x0; + dy = y3 - y0; + } + tangentVector = _normalizeSlope(dx, dy); + } else { + final double ax = x3 + (3 * (x1 - x2)) - x0; + final double ay = y3 + (3 * (y1 - y2)) - y0; + final double bx = 2 * (x2 - (2 * x1) + x0); + final double by = 2 * (y2 - (2 * y1) + y0); + final double cx = x1 - x0; + final double cy = y1 - y0; + final double tx = (ax * t + bx) * t + cx; + final double ty = (ay * t + by) * t + cy; + tangentVector = _normalizeSlope(tx, ty); + } + return _SurfaceTangent(pos, tangentVector, t); + } +} + +/// Evaluates A * t^2 + B * t + C = 0 for quadratic curve. +class _SkQuadCoefficients { + _SkQuadCoefficients( + double x0, double y0, double x1, double y1, double x2, double y2) + : cx = x0, + cy = y0, + bx = 2 * (x1 - x0), + by = 2 * (y1 - y0), + ax = x2 - (2 * x1) + x0, + ay = y2 - (2 * y1) + y0; + final double ax, ay, bx, by, cx, cy; + + double evalX(double t) => (ax * t + bx) * t + cx; + + double evalY(double t) => (ay * t + by) * t + cy; +} + +// Evaluates A * t^3 + B * t^2 + Ct + D = 0 for cubic curve. +class _SkCubicCoefficients { + final double ax, ay, bx, by, cx, cy, dx, dy; + _SkCubicCoefficients(double x0, double y0, double x1, double y1, double x2, + double y2, double x3, double y3) + : ax = x3 + (3 * (x1 - x2)) - x0, + ay = y3 + (3 * (y1 - y2)) - y0, + bx = 3 * (x2 - (2 * x1) + x0), + by = 3 * (y2 - (2 * y1) + y0), + cx = 3 * (x1 - x0), + cy = 3 * (y1 - y0), + dx = x0, + dy = y0; + + double evalX(double t) => (((ax * t + bx) * t) + cx) * t + dx; + + double evalY(double t) => (((ay * t + by) * t) + cy) * t + dy; +} + +/// Chops cubic spline at startT and stopT, writes result to buffer. +void _chopCubicAt( + List points, double startT, double stopT, Float32List buffer) { + assert(startT != 0 || stopT != 0); + final double p3y = points[7]; + final double p0x = points[0]; + final double p0y = points[1]; + final double p1x = points[2]; + final double p1y = points[3]; + final double p2x = points[4]; + final double p2y = points[5]; + final double p3x = points[6]; + // If startT == 0 chop at end point and return curve. + final bool chopStart = startT != 0; + final double t = chopStart ? startT : stopT; + + final double ab1x = _interpolate(p0x, p1x, t); + final double ab1y = _interpolate(p0y, p1y, t); + final double bc1x = _interpolate(p1x, p2x, t); + final double bc1y = _interpolate(p1y, p2y, t); + final double cd1x = _interpolate(p2x, p3x, t); + final double cd1y = _interpolate(p2y, p3y, t); + final double abc1x = _interpolate(ab1x, bc1x, t); + final double abc1y = _interpolate(ab1y, bc1y, t); + final double bcd1x = _interpolate(bc1x, cd1x, t); + final double bcd1y = _interpolate(bc1y, cd1y, t); + final double abcd1x = _interpolate(abc1x, bcd1x, t); + final double abcd1y = _interpolate(abc1y, bcd1y, t); + if (!chopStart) { + // Return left side of curve. + buffer[0] = p0x; + buffer[1] = p0y; + buffer[2] = ab1x; + buffer[3] = ab1y; + buffer[4] = abc1x; + buffer[5] = abc1y; + buffer[6] = abcd1x; + buffer[7] = abcd1y; + return; + } + if (stopT == 1) { + // Return right side of curve. + buffer[0] = abcd1x; + buffer[1] = abcd1y; + buffer[2] = bcd1x; + buffer[3] = bcd1y; + buffer[4] = cd1x; + buffer[5] = cd1y; + buffer[6] = p3x; + buffer[7] = p3y; + return; + } + // We chopped at startT, now the right hand side of curve is at + // abcd1, bcd1, cd1, p3x, p3y. Chop this part using endT; + final double endT = (stopT - startT) / (1 - startT); + final double ab2x = _interpolate(abcd1x, bcd1x, endT); + final double ab2y = _interpolate(abcd1y, bcd1y, endT); + final double bc2x = _interpolate(bcd1x, cd1x, endT); + final double bc2y = _interpolate(bcd1y, cd1y, endT); + final double cd2x = _interpolate(cd1x, p3x, endT); + final double cd2y = _interpolate(cd1y, p3y, endT); + final double abc2x = _interpolate(ab2x, bc2x, endT); + final double abc2y = _interpolate(ab2y, bc2y, endT); + final double bcd2x = _interpolate(bc2x, cd2x, endT); + final double bcd2y = _interpolate(bc2y, cd2y, endT); + final double abcd2x = _interpolate(abc2x, bcd2x, endT); + final double abcd2y = _interpolate(abc2y, bcd2y, endT); + buffer[0] = abcd1x; + buffer[1] = abcd1y; + buffer[2] = ab2x; + buffer[3] = ab2y; + buffer[4] = abc2x; + buffer[5] = abc2y; + buffer[6] = abcd2x; + buffer[7] = abcd2y; +} + +/// Chops quadratic curve at startT and stopT and writes result to buffer. +void _chopQuadAt( + List points, double startT, double stopT, Float32List buffer) { + assert(startT != 0 || stopT != 0); + final double p2y = points[5]; + final double p0x = points[0]; + final double p0y = points[1]; + final double p1x = points[2]; + final double p1y = points[3]; + final double p2x = points[4]; + + // If startT == 0 chop at end point and return curve. + final bool chopStart = startT != 0; + final double t = chopStart ? startT : stopT; + + final double ab1x = _interpolate(p0x, p1x, t); + final double ab1y = _interpolate(p0y, p1y, t); + final double bc1x = _interpolate(p1x, p2x, t); + final double bc1y = _interpolate(p1y, p2y, t); + final double abc1x = _interpolate(ab1x, bc1x, t); + final double abc1y = _interpolate(ab1y, bc1y, t); + if (!chopStart) { + // Return left side of curve. + buffer[0] = p0x; + buffer[1] = p0y; + buffer[2] = ab1x; + buffer[3] = ab1y; + buffer[4] = abc1x; + buffer[5] = abc1y; + return; + } + if (stopT == 1) { + // Return right side of curve. + buffer[0] = abc1x; + buffer[1] = abc1y; + buffer[2] = bc1x; + buffer[3] = bc1y; + buffer[4] = p2x; + buffer[5] = p2y; + return; + } + // We chopped at startT, now the right hand side of curve is at + // abc1x, abc1y, bc1x, bc1y, p2x, p2y + final double endT = (stopT - startT) / (1 - startT); + final double ab2x = _interpolate(abc1x, bc1x, endT); + final double ab2y = _interpolate(abc1y, bc1y, endT); + final double bc2x = _interpolate(bc1x, p2x, endT); + final double bc2y = _interpolate(bc1y, p2y, endT); + final double abc2x = _interpolate(ab2x, bc2x, endT); + final double abc2y = _interpolate(ab2y, bc2y, endT); + + buffer[0] = abc1x; + buffer[1] = abc1y; + buffer[2] = ab2x; + buffer[3] = ab2y; + buffer[4] = abc2x; + buffer[5] = abc2y; } + +// Interpolate between two doubles (Not using lerpDouble here since it null +// checks and treats values as 0). +double _interpolate(double startValue, double endValue, double t) + => (startValue * (1 - t)) + endValue * t; diff --git a/lib/web_ui/test/golden_tests/engine/path_metrics_test.dart b/lib/web_ui/test/golden_tests/engine/path_metrics_test.dart new file mode 100644 index 0000000000000..8bb7c5865d65a --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/path_metrics_test.dart @@ -0,0 +1,210 @@ +// 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. + +import 'dart:html' as html; +import 'dart:typed_data'; + +import 'package:ui/ui.dart' hide TextStyle; +import 'package:ui/src/engine.dart'; +import 'package:test/test.dart'; +import '../../matchers.dart'; + +import 'package:web_engine_tester/golden_tester.dart'; + +void main() async { + const double screenWidth = 600.0; + const double screenHeight = 800.0; + const Rect screenRect = Rect.fromLTWH(0, 0, screenWidth, screenHeight); + const Color black12Color = Color(0x1F000000); + const Color redAccentColor = Color(0xFFFF1744); + const double kDashLength = 5.0; + + // Commit a recording canvas to a bitmap, and compare with the expected + Future _checkScreenshot(RecordingCanvas rc, String fileName, + {Rect region = const Rect.fromLTWH(0, 0, 500, 500), + bool write = false}) async { + final EngineCanvas engineCanvas = BitmapCanvas(screenRect); + rc.apply(engineCanvas); + + // Wrap in so that our CSS selectors kick in. + final html.Element sceneElement = html.Element.tag('flt-scene'); + try { + sceneElement.append(engineCanvas.rootElement); + html.document.body.append(sceneElement); + await matchGoldenFile('$fileName.png', region: region); + } finally { + // The page is reused across tests, so remove the element after taking the + // Scuba screenshot. + sceneElement.remove(); + } + } + + setUp(() async { + debugEmulateFlutterTesterEnvironment = true; + await webOnlyInitializePlatform(); + webOnlyFontCollection.debugRegisterTestFonts(); + await webOnlyFontCollection.ensureFontsLoaded(); + }); + + test('Should calculate tangent on line', () async { + final Path path = Path(); + path.moveTo(50, 130); + path.lineTo(150, 20); + + PathMetric metric = path.computeMetrics().first; + Tangent t = metric.getTangentForOffset(50.0); + expect(t.position.dx, within(from: 83.633, distance: 0.01)); + expect(t.position.dy, within(from: 93.0, distance: 0.01)); + expect(t.vector.dx, within(from: 0.672, distance: 0.01)); + expect(t.vector.dy, within(from: -0.739, distance: 0.01)); + }); + + test('Should calculate tangent on cubic curve', () async { + final Path path = Path(); + double p0x = 150; + double p0y = 20; + double p1x = 240; + double p1y = 120; + double p2x = 320; + double p2y = 25; + path.moveTo(150, 20); + path.quadraticBezierTo(p1x, p1y, p2x, p2y); + PathMetric metric = path.computeMetrics().first; + Tangent t = metric.getTangentForOffset(50.0); + expect(t.position.dx, within(from: 187.25, distance: 0.01)); + expect(t.position.dy, within(from: 53.33, distance: 0.01)); + expect(t.vector.dx, within(from: 0.82, distance: 0.01)); + expect(t.vector.dy, within(from: 0.56, distance: 0.01)); + }); + + test('Should calculate tangent on quadratic curve', () async { + final Path path = Path(); + double p0x = 150; + double p0y = 20; + double p1x = 320; + double p1y = 25; + path.moveTo(150, 20); + path.quadraticBezierTo(p0x, p0y, p1x, p1y); + PathMetric metric = path.computeMetrics().first; + Tangent t = metric.getTangentForOffset(50.0); + expect(t.position.dx, within(from: 199.82, distance: 0.01)); + expect(t.position.dy, within(from: 21.46, distance: 0.01)); + expect(t.vector.dx, within(from: 0.99, distance: 0.01)); + expect(t.vector.dy, within(from: 0.02, distance: 0.01)); + }); + + // Test for extractPath to draw 5 pixel length dashed line using quad curve. + test('Should draw dashed line on quadratic curve.', () async { + final RecordingCanvas rc = + RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500)); + + final Paint paint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 3 + ..color = black12Color; + final Paint redPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1 + ..color = redAccentColor; + + final Path path = Path(); + path.moveTo(50, 130); + path.lineTo(150, 20); + double p0x = 150; + double p0y = 20; + double p1x = 240; + double p1y = 120; + double p2x = 320; + double p2y = 25; + path.quadraticBezierTo(p1x, p1y, p2x, p2y); + + rc.drawPath(path, paint); + + final Float32List buffer = Float32List(6); + List points = [p0x, p0y, p1x, p1y, p2x, p2y]; + double t0 = 0.2; + double t1 = 0.7; + + List metrics = path.computeMetrics().toList(); + double totalLength = 0; + for (PathMetric m in metrics) { + totalLength += m.length; + } + Path dashedPath = Path(); + for (final PathMetric measurePath in path.computeMetrics()) { + double distance = totalLength * t0; + bool draw = true; + while (distance < measurePath.length * t1) { + final double length = kDashLength; + if (draw) { + dashedPath.addPath( + measurePath.extractPath(distance, distance + length), + Offset.zero); + } + distance += length; + draw = !draw; + } + } + rc.drawPath(dashedPath, redPaint); + await _checkScreenshot(rc, 'path_dash_quadratic'); + }); + + // Test for extractPath to draw 5 pixel length dashed line using cubic curve. + test('Should draw dashed line on cubic curve.', () async { + final RecordingCanvas rc = + RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500)); + + final Paint paint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 3 + ..color = black12Color; + final Paint redPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1 + ..color = redAccentColor; + + final Path path = Path(); + path.moveTo(50, 130); + path.lineTo(150, 20); + double p0x = 150; + double p0y = 20; + double p1x = 40; + double p1y = 120; + double p2x = 300; + double p2y = 130; + double p3x = 320; + double p3y = 25; + path.cubicTo(p1x, p1y, p2x, p2y, p3x, p3y); + + rc.drawPath(path, paint); + + final Float32List buffer = Float32List(6); + List points = [p0x, p0y, p1x, p1y, p2x, p2y, p3x, p3y]; + double t0 = 0.2; + double t1 = 0.7; + + List metrics = path.computeMetrics().toList(); + double totalLength = 0; + for (PathMetric m in metrics) { + totalLength += m.length; + } + Path dashedPath = Path(); + for (final PathMetric measurePath in path.computeMetrics()) { + double distance = totalLength * t0; + bool draw = true; + while (distance < measurePath.length * t1) { + final double length = kDashLength; + if (draw) { + dashedPath.addPath( + measurePath.extractPath(distance, distance + length), + Offset.zero); + } + distance += length; + draw = !draw; + } + } + rc.drawPath(dashedPath, redPaint); + await _checkScreenshot(rc, 'path_dash_cubic'); + }); +}