diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index be32f3594ccf8..3d8aeadfcc928 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -387,6 +387,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/platform_views.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/plugins.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/pointer_binding.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/recording_canvas.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/rrect_renderer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/accessibility.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/checkable.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/semantics/image.dart diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart index a6f9acf329ac2..947bf1c6aac16 100644 --- a/lib/web_ui/lib/src/engine.dart +++ b/lib/web_ui/lib/src/engine.dart @@ -57,6 +57,7 @@ part 'engine/platform_views.dart'; part 'engine/plugins.dart'; part 'engine/pointer_binding.dart'; part 'engine/recording_canvas.dart'; +part 'engine/rrect_renderer.dart'; part 'engine/semantics/accessibility.dart'; part 'engine/semantics/checkable.dart'; part 'engine/semantics/image.dart'; diff --git a/lib/web_ui/lib/src/engine/bitmap_canvas.dart b/lib/web_ui/lib/src/engine/bitmap_canvas.dart index 381398b7396a6..6beddc120c50e 100644 --- a/lib/web_ui/lib/src/engine/bitmap_canvas.dart +++ b/lib/web_ui/lib/src/engine/bitmap_canvas.dart @@ -454,202 +454,16 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { @override void drawRRect(ui.RRect rrect, ui.PaintData paint) { _applyPaint(paint); - _drawRRectPath(rrect); + _RRectToCanvasRenderer(ctx).render(rrect); _strokeOrFill(paint); } - void _drawRRectPath(ui.RRect inputRRect, {bool startNewPath = true}) { - // TODO(mdebbar): Backport the overlapping corners fix to houdini_painter.js - // To draw the rounded rectangle, perform the following steps: - // 0. Ensure border radius don't overlap - // 1. Flip left,right top,bottom since web doesn't support flipped - // coordinates with negative radii. - // 2. draw the line for the top - // 3. draw the arc for the top-right corner - // 4. draw the line for the right side - // 5. draw the arc for the bottom-right corner - // 6. draw the line for the bottom of the rectangle - // 7. draw the arc for the bottom-left corner - // 8. draw the line for the left side - // 9. draw the arc for the top-left corner - // - // After drawing, the current point will be the left side of the top of the - // rounded rectangle (after the corner). - // TODO(het): Confirm that this is the end point in Flutter for RRect - - // Ensure border radius curves never overlap - final ui.RRect rrect = inputRRect.scaleRadii(); - - double left = rrect.left; - double right = rrect.right; - double top = rrect.top; - double bottom = rrect.bottom; - if (left > right) { - left = right; - right = rrect.left; - } - if (top > bottom) { - top = bottom; - bottom = rrect.top; - } - final double trRadiusX = rrect.trRadiusX.abs(); - final double tlRadiusX = rrect.tlRadiusX.abs(); - final double trRadiusY = rrect.trRadiusY.abs(); - final double tlRadiusY = rrect.tlRadiusY.abs(); - final double blRadiusX = rrect.blRadiusX.abs(); - final double brRadiusX = rrect.brRadiusX.abs(); - final double blRadiusY = rrect.blRadiusY.abs(); - final double brRadiusY = rrect.brRadiusY.abs(); - - if (startNewPath) { - ctx.beginPath(); - } - - ctx.moveTo(left + trRadiusX, top); - - // Top side and top-right corner - ctx.lineTo(right - trRadiusX, top); - ctx.ellipse( - right - trRadiusX, - top + trRadiusY, - trRadiusX, - trRadiusY, - 0, - 1.5 * math.pi, - 2.0 * math.pi, - false, - ); - - // Right side and bottom-right corner - ctx.lineTo(right, bottom - brRadiusY); - ctx.ellipse( - right - brRadiusX, - bottom - brRadiusY, - brRadiusX, - brRadiusY, - 0, - 0, - 0.5 * math.pi, - false, - ); - - // Bottom side and bottom-left corner - ctx.lineTo(left + blRadiusX, bottom); - ctx.ellipse( - left + blRadiusX, - bottom - blRadiusY, - blRadiusX, - blRadiusY, - 0, - 0.5 * math.pi, - math.pi, - false, - ); - - // Left side and top-left corner - ctx.lineTo(left, top + tlRadiusY); - ctx.ellipse( - left + tlRadiusX, - top + tlRadiusY, - tlRadiusX, - tlRadiusY, - 0, - math.pi, - 1.5 * math.pi, - false, - ); - } - - void _drawRRectPathReverse(ui.RRect inputRRect, {bool startNewPath = true}) { - // Ensure border radius curves never overlap - final ui.RRect rrect = inputRRect.scaleRadii(); - - double left = rrect.left; - double right = rrect.right; - double top = rrect.top; - double bottom = rrect.bottom; - final double trRadiusX = rrect.trRadiusX.abs(); - final double tlRadiusX = rrect.tlRadiusX.abs(); - final double trRadiusY = rrect.trRadiusY.abs(); - final double tlRadiusY = rrect.tlRadiusY.abs(); - final double blRadiusX = rrect.blRadiusX.abs(); - final double brRadiusX = rrect.brRadiusX.abs(); - final double blRadiusY = rrect.blRadiusY.abs(); - final double brRadiusY = rrect.brRadiusY.abs(); - - if (left > right) { - left = right; - right = rrect.left; - } - if (top > bottom) { - top = bottom; - bottom = rrect.top; - } - // Draw the rounded rectangle, counterclockwise. - ctx.moveTo(right - trRadiusX, top); - - if (startNewPath) { - ctx.beginPath(); - } - - // Top side and top-left corner - ctx.lineTo(left + tlRadiusX, top); - ctx.ellipse( - left + tlRadiusX, - top + tlRadiusY, - tlRadiusX, - tlRadiusY, - 0, - 1.5 * math.pi, - 1 * math.pi, - true, - ); - - // Left side and bottom-left corner - ctx.lineTo(left, bottom - blRadiusY); - ctx.ellipse( - left + blRadiusX, - bottom - blRadiusY, - blRadiusX, - blRadiusY, - 0, - 1 * math.pi, - 0.5 * math.pi, - true, - ); - - // Bottom side and bottom-right corner - ctx.lineTo(right - brRadiusX, bottom); - ctx.ellipse( - right - brRadiusX, - bottom - brRadiusY, - brRadiusX, - brRadiusY, - 0, - 0.5 * math.pi, - 0 * math.pi, - true, - ); - - // Right side and top-right corner - ctx.lineTo(right, top + trRadiusY); - ctx.ellipse( - right - trRadiusX, - top + trRadiusY, - trRadiusX, - trRadiusY, - 0, - 0 * math.pi, - 1.5 * math.pi, - true, - ); - } - @override void drawDRRect(ui.RRect outer, ui.RRect inner, ui.PaintData paint) { _applyPaint(paint); - _drawRRectPath(outer); - _drawRRectPathReverse(inner, startNewPath: false); + _RRectRenderer renderer = _RRectToCanvasRenderer(ctx); + renderer.render(outer); + renderer.render(inner, startNewPath: false, reverse: true); _strokeOrFill(paint); } @@ -886,7 +700,8 @@ class BitmapCanvas extends EngineCanvas with SaveStackTracking { break; case PathCommandTypes.rRect: final RRectCommand rrectCommand = command; - _drawRRectPath(rrectCommand.rrect, startNewPath: false); + _RRectToCanvasRenderer(ctx).render(rrectCommand.rrect, + startNewPath: false); break; case PathCommandTypes.rect: final RectCommand rectCommand = command; diff --git a/lib/web_ui/lib/src/engine/recording_canvas.dart b/lib/web_ui/lib/src/engine/recording_canvas.dart index 2608a6e30de62..5fcc806b10c6b 100644 --- a/lib/web_ui/lib/src/engine/recording_canvas.dart +++ b/lib/web_ui/lib/src/engine/recording_canvas.dart @@ -1128,6 +1128,14 @@ abstract class PathCommand { PathCommand shifted(ui.Offset offset); List serializeToCssPaint(); + + /// Transform the command and add to targetPath. + void transform(Float64List matrix4, ui.Path targetPath); + + /// Helper method for implementing transforms. + static ui.Offset _transformOffset(double x, double y, Float64List matrix4) => + ui.Offset((matrix4[0] * x) + (matrix4[4] * y) + matrix4[12], + (matrix4[1] * x) + (matrix4[5] * y) + matrix4[13]); } class MoveTo extends PathCommand { @@ -1146,6 +1154,12 @@ class MoveTo extends PathCommand { return [1, x, y]; } + @override + void transform(Float64List matrix4, ui.Path targetPath) { + final ui.Offset offset = PathCommand._transformOffset(x, y, matrix4); + targetPath.moveTo(offset.dx, offset.dy); + } + @override String toString() { if (assertionsEnabled) { @@ -1172,6 +1186,12 @@ class LineTo extends PathCommand { return [2, x, y]; } + @override + void transform(Float64List matrix4, ui.Path targetPath) { + final ui.Offset offset = PathCommand._transformOffset(x, y, matrix4); + targetPath.lineTo(offset.dx, offset.dy); + } + @override String toString() { if (assertionsEnabled) { @@ -1217,6 +1237,87 @@ class Ellipse extends PathCommand { ]; } + @override + void transform(Float64List matrix4, ui.Path targetPath) { + final ui.Path bezierPath = ui.Path(); + _drawArcWithBezier(x, y, radiusX, radiusY, rotation, + startAngle, + anticlockwise ? startAngle - endAngle : endAngle - startAngle, + matrix4, bezierPath); + targetPath.addPath(bezierPath, ui.Offset.zero, matrix4: matrix4); + } + + void _drawArcWithBezier(double centerX, double centerY, + double radiusX, double radiusY, double rotation, double startAngle, + double sweep, Float64List matrix4, ui.Path targetPath) { + double ratio = sweep.abs() / (math.pi / 2.0); + if ((1.0 - ratio).abs() < 0.0000001) { + ratio = 1.0; + } + final int segments = math.max(ratio.ceil(), 1); + final double anglePerSegment = sweep / segments; + double angle = startAngle; + for (int segment = 0; segment < segments; segment++) { + _drawArcSegment(targetPath, centerX, centerY, radiusX, radiusY, rotation, + angle, anglePerSegment, segment == 0, matrix4); + angle += anglePerSegment; + } + } + + void _drawArcSegment(ui.Path path, double centerX, double centerY, + double radiusX, double radiusY, double rotation, double startAngle, + double sweep, bool startPath, Float64List matrix4) { + final double s = 4 / 3 * math.tan(sweep / 4); + + // Rotate unit vector to startAngle and endAngle to use for computing start + // and end points of segment. + final double x1 = math.cos(startAngle); + final double y1 = math.sin(startAngle); + final double endAngle = startAngle + sweep; + final double x2 = math.cos(endAngle); + final double y2 = math.sin(endAngle); + + // Compute scaled curve control points. + final double cpx1 = (x1 - y1 * s) * radiusX; + final double cpy1 = (y1 + x1 * s) * radiusY; + final double cpx2 = (x2 + y2 * s) * radiusX; + final double cpy2 = (y2 - x2 * s) * radiusY; + + final double endPointX = centerX + x2 * radiusX; + final double endPointY = centerY + y2 * radiusY; + + final double rotationRad = rotation * math.pi / 180.0; + final double cosR = math.cos(rotationRad); + final double sinR = math.sin(rotationRad); + if (startPath) { + final double scaledX1 = x1 * radiusX; + final double scaledY1 = y1 * radiusY; + if (rotation == 0.0) { + path.moveTo(centerX + scaledX1, centerY + scaledY1); + } else { + final double rotatedStartX = (scaledX1 * cosR) + (scaledY1 * sinR); + final double rotatedStartY = (scaledY1 * cosR) - (scaledX1 * sinR); + path.moveTo(centerX + rotatedStartX, centerY + rotatedStartY); + } + } + if (rotation == 0.0) { + path.cubicTo(centerX + cpx1, centerY + cpy1, + centerX + cpx2, centerY + cpy2, + endPointX, endPointY); + } else { + final double rotatedCpx1 = centerX + (cpx1 * cosR) + (cpy1 * sinR); + final double rotatedCpy1 = centerY + (cpy1 * cosR) - (cpx1 * sinR); + final double rotatedCpx2 = centerX + (cpx2 * cosR) + (cpy2 * sinR); + final double rotatedCpy2 = centerY + (cpy2 * cosR) - (cpx2 * sinR); + final double rotatedEndX = centerX + ((endPointX - centerX) * cosR) + + ((endPointY - centerY) * sinR); + final double rotatedEndY = centerY + ((endPointY - centerY) * cosR) + - ((endPointX - centerX) * sinR); + path.cubicTo(rotatedCpx1, rotatedCpy1, rotatedCpx2, rotatedCpy2, + rotatedEndX, rotatedEndY); + } + } + @override String toString() { if (assertionsEnabled) { @@ -1247,6 +1348,22 @@ class QuadraticCurveTo extends PathCommand { return [4, x1, y1, x2, y2]; } + @override + void transform(Float64List matrix4, ui.Path targetPath) { + final double m0 = matrix4[0]; + final double m1 = matrix4[1]; + final double m4 = matrix4[4]; + final double m5 = matrix4[5]; + final double m12 = matrix4[12]; + final double m13 = matrix4[13]; + final double transformedX1 = (m0 * x1) + (m4 * y1) + m12; + final double transformedY1 = (m1 * x1) + (m5 * y1) + m13; + final double transformedX2 = (m0 * x2) + (m4 * y2) + m12; + final double transformedY2 = (m1 * x2) + (m5 * y2) + m13; + targetPath.quadraticBezierTo(transformedX1, transformedY1, + transformedX2, transformedY2); + } + @override String toString() { if (assertionsEnabled) { @@ -1279,6 +1396,24 @@ class BezierCurveTo extends PathCommand { return [5, x1, y1, x2, y2, x3, y3]; } + @override + void transform(Float64List matrix4, ui.Path targetPath) { + final double s0 = matrix4[0]; + final double s1 = matrix4[1]; + final double s4 = matrix4[4]; + final double s5 = matrix4[5]; + final double s12 = matrix4[12]; + final double s13 = matrix4[13]; + final double transformedX1 = (s0 * x1) + (s4 * y1) + s12; + final double transformedY1 = (s1 * x1) + (s5 * y1) + s13; + final double transformedX2 = (s0 * x2) + (s4 * y2) + s12; + final double transformedY2 = (s1 * x2) + (s5 * y2) + s13; + final double transformedX3 = (s0 * x3) + (s4 * y3) + s12; + final double transformedY3 = (s1 * x3) + (s5 * y3) + s13; + targetPath.cubicTo(transformedX1, transformedY1, + transformedX2, transformedY2, transformedX3, transformedY3); + } + @override String toString() { if (assertionsEnabled) { @@ -1303,6 +1438,38 @@ class RectCommand extends PathCommand { return RectCommand(x + offset.dx, y + offset.dy, width, height); } + @override + void transform(Float64List matrix4, ui.Path targetPath) { + final double s0 = matrix4[0]; + final double s1 = matrix4[1]; + final double s4 = matrix4[4]; + final double s5 = matrix4[5]; + final double s12 = matrix4[12]; + final double s13 = matrix4[13]; + final double transformedX1 = (s0 * x) + (s4 * y) + s12; + final double transformedY1 = (s1 * x) + (s5 * y) + s13; + final double x2 = x + width; + final double y2 = y + height; + final double transformedX2 = (s0 * x2) + (s4 * y) + s12; + final double transformedY2 = (s1 * x2) + (s5 * y) + s13; + final double transformedX3 = (s0 * x2) + (s4 * y2) + s12; + final double transformedY3 = (s1 * x2) + (s5 * y2) + s13; + final double transformedX4 = (s0 * x) + (s4 * y2) + s12; + final double transformedY4 = (s1 * x) + (s5 * y2) + s13; + if (transformedY1 == transformedY2 && transformedY3 == transformedY4 && + transformedX1 == transformedX4 && transformedX2 == transformedX3) { + // It is still a rectangle. + targetPath.addRect(ui.Rect.fromLTRB(transformedX1, transformedY1, + transformedX3, transformedY3)); + } else { + targetPath.moveTo(transformedX1, transformedY1); + targetPath.lineTo(transformedX2, transformedY2); + targetPath.lineTo(transformedX3, transformedY3); + targetPath.lineTo(transformedX4, transformedY4); + targetPath.close(); + } + } + @override List serializeToCssPaint() { return [6, x, y, width, height]; @@ -1334,6 +1501,13 @@ class RRectCommand extends PathCommand { } @override + void transform(Float64List matrix4, ui.Path targetPath) { + final ui.Path roundRectPath = ui.Path(); + _RRectToPathRenderer(roundRectPath).render(rrect); + targetPath.addPath(roundRectPath, ui.Offset.zero, matrix4: matrix4); + } + + @override String toString() { if (assertionsEnabled) { return '$rrect'; @@ -1356,6 +1530,11 @@ class CloseCommand extends PathCommand { return [8]; } + @override + void transform(Float64List matrix4, ui.Path targetPath) { + targetPath.close(); + } + @override String toString() { if (assertionsEnabled) { diff --git a/lib/web_ui/lib/src/engine/rrect_renderer.dart b/lib/web_ui/lib/src/engine/rrect_renderer.dart new file mode 100644 index 0000000000000..f463f2c5bad4f --- /dev/null +++ b/lib/web_ui/lib/src/engine/rrect_renderer.dart @@ -0,0 +1,224 @@ +// 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. + +part of engine; + +/// Renders an RRect using path primitives. +abstract class _RRectRenderer { + // TODO(mdebbar): Backport the overlapping corners fix to houdini_painter.js + // To draw the rounded rectangle, perform the following steps: + // 0. Ensure border radius don't overlap + // 1. Flip left,right top,bottom since web doesn't support flipped + // coordinates with negative radii. + // 2. draw the line for the top + // 3. draw the arc for the top-right corner + // 4. draw the line for the right side + // 5. draw the arc for the bottom-right corner + // 6. draw the line for the bottom of the rectangle + // 7. draw the arc for the bottom-left corner + // 8. draw the line for the left side + // 9. draw the arc for the top-left corner + // + // After drawing, the current point will be the left side of the top of the + // rounded rectangle (after the corner). + // TODO(het): Confirm that this is the end point in Flutter for RRect + + void render(ui.RRect inputRRect, + {bool startNewPath = true, bool reverse = false}) { + // Ensure border radius curves never overlap + final ui.RRect rrect = inputRRect.scaleRadii(); + + double left = rrect.left; + double right = rrect.right; + double top = rrect.top; + double bottom = rrect.bottom; + if (left > right) { + left = right; + right = rrect.left; + } + if (top > bottom) { + top = bottom; + bottom = rrect.top; + } + final double trRadiusX = rrect.trRadiusX.abs(); + final double tlRadiusX = rrect.tlRadiusX.abs(); + final double trRadiusY = rrect.trRadiusY.abs(); + final double tlRadiusY = rrect.tlRadiusY.abs(); + final double blRadiusX = rrect.blRadiusX.abs(); + final double brRadiusX = rrect.brRadiusX.abs(); + final double blRadiusY = rrect.blRadiusY.abs(); + final double brRadiusY = rrect.brRadiusY.abs(); + + if (!reverse) { + if (startNewPath) { + beginPath(); + } + + moveTo(left + trRadiusX, top); + + // Top side and top-right corner + lineTo(right - trRadiusX, top); + ellipse( + right - trRadiusX, + top + trRadiusY, + trRadiusX, + trRadiusY, + 0, + 1.5 * math.pi, + 2.0 * math.pi, + false, + ); + + // Right side and bottom-right corner + lineTo(right, bottom - brRadiusY); + ellipse( + right - brRadiusX, + bottom - brRadiusY, + brRadiusX, + brRadiusY, + 0, + 0, + 0.5 * math.pi, + false, + ); + + // Bottom side and bottom-left corner + lineTo(left + blRadiusX, bottom); + ellipse( + left + blRadiusX, + bottom - blRadiusY, + blRadiusX, + blRadiusY, + 0, + 0.5 * math.pi, + math.pi, + false, + ); + + // Left side and top-left corner + lineTo(left, top + tlRadiusY); + ellipse( + left + tlRadiusX, + top + tlRadiusY, + tlRadiusX, + tlRadiusY, + 0, + math.pi, + 1.5 * math.pi, + false, + ); + } else { + // Draw the rounded rectangle, counterclockwise. + moveTo(right - trRadiusX, top); + + if (startNewPath) { + beginPath(); + } + + // Top side and top-left corner + lineTo(left + tlRadiusX, top); + ellipse( + left + tlRadiusX, + top + tlRadiusY, + tlRadiusX, + tlRadiusY, + 0, + 1.5 * math.pi, + 1 * math.pi, + true, + ); + + // Left side and bottom-left corner + lineTo(left, bottom - blRadiusY); + ellipse( + left + blRadiusX, + bottom - blRadiusY, + blRadiusX, + blRadiusY, + 0, + 1 * math.pi, + 0.5 * math.pi, + true, + ); + + // Bottom side and bottom-right corner + lineTo(right - brRadiusX, bottom); + ellipse( + right - brRadiusX, + bottom - brRadiusY, + brRadiusX, + brRadiusY, + 0, + 0.5 * math.pi, + 0 * math.pi, + true, + ); + + // Right side and top-right corner + lineTo(right, top + trRadiusY); + ellipse( + right - trRadiusX, + top + trRadiusY, + trRadiusX, + trRadiusY, + 0, + 0 * math.pi, + 1.5 * math.pi, + true, + ); + } + } + + void beginPath(); + void moveTo(double x, double y); + void lineTo(double x, double y); + void ellipse(double centerX, double centerY, double radiusX, double radiusY, + double rotation, double startAngle, double endAngle, bool antiClockwise); +} + +/// Renders RRect to a 2d canvas. +class _RRectToCanvasRenderer extends _RRectRenderer { + final html.CanvasRenderingContext2D context; + _RRectToCanvasRenderer(this.context); + void beginPath() { + context.beginPath(); + } + + void moveTo(double x, double y) { + context.moveTo(x, y); + } + + void lineTo(double x, double y) { + context.lineTo(x, y); + } + + void ellipse(double centerX, double centerY, double radiusX, double radiusY, + double rotation, double startAngle, double endAngle, bool antiClockwise) { + context.ellipse(centerX, centerY, radiusX, radiusY, rotation, startAngle, + endAngle, antiClockwise); + } +} + +/// Renders RRect to a path. +class _RRectToPathRenderer extends _RRectRenderer { + final ui.Path path; + _RRectToPathRenderer(this.path); + void beginPath() {} + void moveTo(double x, double y) { + path.moveTo(x, y); + } + + void lineTo(double x, double y) { + path.lineTo(x, y); + } + + void ellipse(double centerX, double centerY, double radiusX, double radiusY, + double rotation, double startAngle, double endAngle, bool antiClockwise) { + path.addArc( + ui.Rect.fromLTRB(centerX - radiusX, centerY - radiusY, + centerX + radiusX, centerY + radiusY), + startAngle, + antiClockwise ? startAngle - endAngle : endAngle - startAngle); + } +} diff --git a/lib/web_ui/lib/src/ui/canvas.dart b/lib/web_ui/lib/src/ui/canvas.dart index e4d1693f2f3a3..6f0050973af93 100644 --- a/lib/web_ui/lib/src/ui/canvas.dart +++ b/lib/web_ui/lib/src/ui/canvas.dart @@ -1584,12 +1584,15 @@ class Path { if (dx == 0.0 && dy == 0.0) { subpaths.addAll(path.subpaths); } else { - throw UnimplementedError('Cannot add path with non-zero offset'); + subpaths.addAll(path.transform( + engine.Matrix4.translationValues(dx, dy, 0.0).storage).subpaths); } } void _addPathWithMatrix(Path path, double dx, double dy, Float64List matrix) { - throw UnimplementedError('Cannot add path with transform matrix'); + final engine.Matrix4 transform = engine.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 @@ -1742,18 +1745,24 @@ class Path { /// subpath translated by the given offset. Path shift(Offset offset) { assert(engine.offsetIsValid(offset)); - final List shiftedSubpaths = []; - for (final engine.Subpath subpath in subpaths) { - shiftedSubpaths.add(subpath.shift(offset)); + final List shiftedSubPaths = []; + for (final engine.Subpath subPath in subpaths) { + shiftedSubPaths.add(subPath.shift(offset)); } - return Path._clone(shiftedSubpaths, fillType); + return Path._clone(shiftedSubPaths, fillType); } /// Returns a copy of the path with all the segments of every - /// subpath transformed by the given matrix. + /// sub path transformed by the given matrix. Path transform(Float64List matrix4) { assert(engine.matrix4IsValid(matrix4)); - throw UnimplementedError(); + final Path transformedPath = Path(); + for (final engine.Subpath subPath in subpaths) { + for (final engine.PathCommand cmd in subPath.commands) { + cmd.transform(matrix4, transformedPath); + } + } + return transformedPath; } /// Computes the bounding rectangle for this path. diff --git a/lib/web_ui/test/golden_tests/engine/path_transform_test.dart b/lib/web_ui/test/golden_tests/engine/path_transform_test.dart new file mode 100644 index 0000000000000..6b87c0af7f8ed --- /dev/null +++ b/lib/web_ui/test/golden_tests/engine/path_transform_test.dart @@ -0,0 +1,226 @@ +// 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:math' as math; +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); + final Paint testPaint = Paint()..color = const Color(0xFFFF0000); + + // 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 draw transformed line.', () async { + final RecordingCanvas rc = + RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500)); + final Path path = Path(); + path.moveTo(0, 0); + path.lineTo(300, 200); + rc.drawPath( + path, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 + ..color = const Color(0xFF404000)); + final Path transformedPath = Path(); + final Matrix4 testMatrixTranslateRotate = + Matrix4.rotationZ(math.pi * 30.0 / 180.0)..translate(100, 20); + transformedPath.addPath(path, Offset.zero, + matrix4: testMatrixTranslateRotate.storage); + rc.drawPath( + transformedPath, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 + ..color = const Color.fromRGBO(0, 128, 255, 1.0)); + await _checkScreenshot(rc, 'path_transform_with_line'); + }); + + test('Should draw transformed line.', () async { + final RecordingCanvas rc = + RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500)); + final Path path = Path(); + path.addRect(Rect.fromLTRB(50, 40, 300, 100)); + rc.drawPath( + path, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 + ..color = const Color(0xFF404000)); + final Path transformedPath = Path(); + final Matrix4 testMatrixTranslateRotate = + Matrix4.rotationZ(math.pi * 30.0 / 180.0)..translate(100, 20); + transformedPath.addPath(path, Offset.zero, + matrix4: testMatrixTranslateRotate.storage); + rc.drawPath( + transformedPath, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 + ..color = const Color.fromRGBO(0, 128, 255, 1.0)); + await _checkScreenshot(rc, 'path_transform_with_rect'); + }); + + test('Should draw transformed quadratic curve.', () async { + final RecordingCanvas rc = + RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500)); + final Path path = Path(); + path.moveTo(100, 100); + path.quadraticBezierTo(100, 300, 400, 300); + rc.drawPath( + path, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 + ..color = const Color(0xFF404000)); + final Path transformedPath = Path(); + final Matrix4 testMatrixTranslateRotate = + Matrix4.rotationZ(math.pi * 30.0 / 180.0)..translate(100, -80); + transformedPath.addPath(path, Offset.zero, + matrix4: testMatrixTranslateRotate.storage); + rc.drawPath( + transformedPath, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 + ..color = const Color.fromRGBO(0, 128, 255, 1.0)); + await _checkScreenshot(rc, 'path_transform_with_quadratic_curve'); + }); + + test('Should draw transformed conic.', () async { + final RecordingCanvas rc = + RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500)); + const double yStart = 20; + + const Offset p0 = Offset(25, yStart + 25); + const Offset pc = Offset(60, yStart + 150); + const Offset p2 = Offset(100, yStart + 50); + + final Path path = Path(); + path.moveTo(p0.dx, p0.dy); + path.conicTo(pc.dx, pc.dy, p2.dx, p2.dy, 0.5); + path.close(); + path.moveTo(p0.dx, p0.dy + 100); + path.conicTo(pc.dx, pc.dy + 100, p2.dx, p2.dy + 100, 10); + path.close(); + + rc.drawPath( + path, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 + ..color = const Color(0xFF404000)); + final Path transformedPath = Path(); + final Matrix4 testMatrixTranslateRotate = + Matrix4.rotationZ(math.pi * 30.0 / 180.0)..translate(100, -80); + transformedPath.addPath(path, Offset.zero, + matrix4: testMatrixTranslateRotate.storage); + rc.drawPath( + transformedPath, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 + ..color = const Color.fromRGBO(0, 128, 255, 1.0)); + await _checkScreenshot(rc, 'path_transform_with_conic'); + }); + + test('Should draw transformed arc.', () async { + final RecordingCanvas rc = + RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500)); + const double yStart = 20; + + final Path path = Path(); + path.moveTo(350, 280); + path.arcToPoint(Offset(450, 90), + radius: Radius.elliptical(200, 50), + rotation: -math.pi / 6.0, + largeArc: true, + clockwise: true); + path.close(); + + rc.drawPath( + path, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 + ..color = const Color(0xFF404000)); + + final Path transformedPath = Path(); + final Matrix4 testMatrixTranslateRotate = + Matrix4.rotationZ(math.pi * 30.0 / 180.0)..translate(100, 10); + transformedPath.addPath(path, Offset.zero, + matrix4: testMatrixTranslateRotate.storage); + rc.drawPath( + transformedPath, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 + ..color = const Color.fromRGBO(0, 128, 255, 1.0)); + await _checkScreenshot(rc, 'path_transform_with_arc'); + }); + + test('Should draw transformed rrect.', () async { + final RecordingCanvas rc = + RecordingCanvas(const Rect.fromLTRB(0, 0, 500, 500)); + const double yStart = 20; + + final Path path = Path(); + path.addRRect(RRect.fromLTRBR(50, 50, 300, 200, Radius.elliptical(4, 8))); + + rc.drawPath( + path, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 + ..color = const Color(0xFF404000)); + + final Path transformedPath = Path(); + final Matrix4 testMatrixTranslateRotate = + Matrix4.rotationZ(math.pi * 30.0 / 180.0)..translate(100, -80); + transformedPath.addPath(path, Offset.zero, + matrix4: testMatrixTranslateRotate.storage); + rc.drawPath( + transformedPath, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2.0 + ..color = const Color.fromRGBO(0, 128, 255, 1.0)); + await _checkScreenshot(rc, 'path_transform_with_rrect'); + }); +}