mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Add support for path transform (flutter/engine#12794)
* Implement path.transform
This commit is contained in:
parent
4a39184fec
commit
16bb890bbf
@ -390,6 +390,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
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -1128,6 +1128,14 @@ abstract class PathCommand {
|
||||
PathCommand shifted(ui.Offset offset);
|
||||
|
||||
List<dynamic> 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 <dynamic>[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 <dynamic>[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 <dynamic>[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 <dynamic>[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<dynamic> serializeToCssPaint() {
|
||||
return <dynamic>[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 <dynamic>[8];
|
||||
}
|
||||
|
||||
@override
|
||||
void transform(Float64List matrix4, ui.Path targetPath) {
|
||||
targetPath.close();
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (assertionsEnabled) {
|
||||
|
||||
224
engine/src/flutter/lib/web_ui/lib/src/engine/rrect_renderer.dart
Normal file
224
engine/src/flutter/lib/web_ui/lib/src/engine/rrect_renderer.dart
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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<engine.Subpath> shiftedSubpaths = <engine.Subpath>[];
|
||||
for (final engine.Subpath subpath in subpaths) {
|
||||
shiftedSubpaths.add(subpath.shift(offset));
|
||||
final List<engine.Subpath> shiftedSubPaths = <engine.Subpath>[];
|
||||
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.
|
||||
|
||||
@ -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<void> _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 <flt-scene> 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');
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user