Add RSuperellipse support to Web (global cache) (#171489)
This PR adds `RSuperellipse` support to Web, so that these ops will
actually draw `RSuperellipse`s instead of falling back to `RRect`s,
except for platform views.
* (I was told in some earlier comments that RSuperellipses should fall
back to RRects for platform views but I can't remember where for now. If
reviewers think otherwise I can implement them right away or make this a
TODO in the future.)
The `RSuperellipse`s are drawn after being converted to paths. The
algorithm is nothing new, but already used in
`round_superellipse_param.cc`.
For performance optimization, `RSuperellipse`s that have uniform radii
will have their paths cached in a global cache after offset
normalization.
This PR does not add any new public APIs. (Although, I'm planning to
also implement this to the main `dart:ui` in the future to support
non-Impeller-nor-Web platforms, which will need
`RSuperellipse.toPathOffset` public.)
Fixes https://github.com/flutter/flutter/issues/163718
## Pre-launch Checklist
- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.
If you need help, consider asking for advice on the #hackers-new channel
on [Discord].
<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
diff --git a/dev/benchmarks/macrobenchmarks/lib/src/web/bench_draw_rrect_rsuperellipse.dart b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_draw_rrect_rsuperellipse.dart
new file mode 100644
index 0000000..8080ca9
--- /dev/null
+++ b/dev/benchmarks/macrobenchmarks/lib/src/web/bench_draw_rrect_rsuperellipse.dart
@@ -0,0 +1,86 @@
+// Copyright 2014 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:ui';
+
+import 'recorder.dart';
+
+typedef _Draw = void Function(Canvas canvas, int key, Rect rect, Radius radius, Paint paint);
+
+/// Repeatedly paints a grid of rounded rectangles or rounded superellipses.
+///
+/// Measures the performance of the `drawRSuperellipse` operation compared with `drawRRect`.
+class BenchDrawRRectRSuperellipse extends SceneBuilderRecorder {
+ /// A variant of the benchmark that draws rounded rectangles.
+ ///
+ /// This variant is used as a comparison benchmark for `drawRSuperellipse`.
+ BenchDrawRRectRSuperellipse.drawRRect() : _draw = _drawRRect, super(name: drawRRectName);
+
+ /// A variant of the benchmark that draws rounded superellipses.
+ BenchDrawRRectRSuperellipse.drawRSuperellipse() : super(name: drawRSuperellipseName) {
+ _draw = _drawRSuperellipse;
+ }
+
+ static const String drawRRectName = 'draw_rrect';
+ static const String drawRSuperellipseName = 'draw_rsuperellipse';
+
+ /// Number of rows in the grid.
+ static const int kRows = 25;
+
+ /// Number of columns in the grid.
+ static const int kColumns = 40;
+
+ late final _Draw _draw;
+
+ static void _drawRRect(Canvas canvas, int key, Rect rect, Radius radius, Paint paint) {
+ canvas.drawRRect(RRect.fromRectAndRadius(rect, radius), paint);
+ }
+
+ static void _drawRSuperellipse(Canvas canvas, int key, Rect rect, Radius radius, Paint paint) {
+ canvas.drawRSuperellipse(RSuperellipse.fromRectAndRadius(rect, radius), paint);
+ }
+
+ /// Counter used to offset the rendered rsuperellipse to make them wobble.
+ ///
+ /// The wobbling is there so a human could visually verify that the benchmark
+ /// is correctly pumping frames.
+ double wobbleCounter = 0;
+
+ static final Paint _paint = Paint()..color = const Color.fromARGB(255, 255, 0, 0);
+
+ @override
+ void onDrawFrame(SceneBuilder sceneBuilder) {
+ final PictureRecorder pictureRecorder = PictureRecorder();
+ final Canvas canvas = Canvas(pictureRecorder);
+ final Size viewSize = view.physicalSize;
+
+ final Size cellSize = Size(viewSize.width / kColumns, viewSize.height / kRows);
+ final Size rectSize = cellSize * 0.8;
+ final double maxRadius = rectSize.shortestSide / 2;
+
+ for (int row = 0; row < kRows; row++) {
+ canvas.save();
+ for (int col = 0; col < kColumns; col++) {
+ final double radius = maxRadius / kColumns * col;
+ _draw(
+ canvas,
+ row * kColumns + col,
+ Offset((wobbleCounter - 5).abs(), 0) & rectSize,
+ Radius.circular(radius),
+ _paint,
+ );
+ canvas.translate(cellSize.width, 0);
+ }
+ canvas.restore();
+ canvas.translate(0, cellSize.height);
+ }
+
+ wobbleCounter += 1;
+ wobbleCounter = wobbleCounter % 10;
+ final Picture picture = pictureRecorder.endRecording();
+ sceneBuilder.pushOffset(0.0, 0.0);
+ sceneBuilder.addPicture(Offset.zero, picture);
+ sceneBuilder.pop();
+ }
+}
diff --git a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart
index f85960f..03e5209 100644
--- a/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart
+++ b/dev/benchmarks/macrobenchmarks/lib/web_benchmarks.dart
@@ -17,6 +17,7 @@
import 'src/web/bench_clipped_out_pictures.dart';
import 'src/web/bench_default_target_platform.dart';
import 'src/web/bench_draw_rect.dart';
+import 'src/web/bench_draw_rrect_rsuperellipse.dart';
import 'src/web/bench_dynamic_clip_on_static_picture.dart';
import 'src/web/bench_harness.dart';
import 'src/web/bench_image_decoding.dart';
@@ -56,6 +57,9 @@
BenchClippedOutPictures.benchmarkName: () => BenchClippedOutPictures(),
BenchDrawRect.benchmarkName: () => BenchDrawRect.staticPaint(),
BenchDrawRect.variablePaintBenchmarkName: () => BenchDrawRect.variablePaint(),
+ BenchDrawRRectRSuperellipse.drawRRectName: () => BenchDrawRRectRSuperellipse.drawRRect(),
+ BenchDrawRRectRSuperellipse.drawRSuperellipseName: () =>
+ BenchDrawRRectRSuperellipse.drawRSuperellipse(),
BenchPathRecording.benchmarkName: () => BenchPathRecording(),
BenchTextOutOfPictureBounds.benchmarkName: () => BenchTextOutOfPictureBounds(),
BenchSimpleLazyTextScroll.benchmarkName: () => BenchSimpleLazyTextScroll(),
diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter
index 0e9526d..be8fc56 100644
--- a/engine/src/flutter/ci/licenses_golden/licenses_flutter
+++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter
@@ -52630,6 +52630,7 @@
ORIGIN: ../../../flutter/lib/web_ui/lib/platform_dispatcher.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/platform_isolate.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/pointer.dart + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/lib/rsuperellipse_param.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/semantics.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/alarm_clock.dart + ../../../flutter/LICENSE
@@ -55704,6 +55705,7 @@
FILE: ../../../flutter/lib/web_ui/lib/platform_dispatcher.dart
FILE: ../../../flutter/lib/web_ui/lib/platform_isolate.dart
FILE: ../../../flutter/lib/web_ui/lib/pointer.dart
+FILE: ../../../flutter/lib/web_ui/lib/rsuperellipse_param.dart
FILE: ../../../flutter/lib/web_ui/lib/semantics.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/alarm_clock.dart
diff --git a/engine/src/flutter/impeller/geometry/round_superellipse_unittests.cc b/engine/src/flutter/impeller/geometry/round_superellipse_unittests.cc
index 274a450..3752bc5 100644
--- a/engine/src/flutter/impeller/geometry/round_superellipse_unittests.cc
+++ b/engine/src/flutter/impeller/geometry/round_superellipse_unittests.cc
@@ -698,9 +698,9 @@
#undef CHECK_POINT_AND_MIRRORS
}
-TEST(RoundSuperellipseTest, SlimDiagnalContains) {
- // This shape has large radii on one diagnal and tiny radii on the other,
- // resulting in a almond-like shape placed diagnally (NW to SE).
+TEST(RoundSuperellipseTest, SlimDiagonalContains) {
+ // This shape has large radii on one diagonal and tiny radii on the other,
+ // resulting in a almond-like shape placed diagonally (NW to SE).
Rect bounds = Rect::MakeLTRB(-50.0f, -50.0f, 50.0f, 50.0f);
auto rr = RoundSuperellipse::MakeRectRadii(
bounds, {
@@ -721,17 +721,17 @@
CHECK_POINT_WITH_OFFSET(rr, Point(49.70, 49.70), Point(0.02, 0.02));
// Checks two points symmetrical to the origin.
-#define CHECK_DIAGNAL_POINTS(p) \
+#define CHECK_DIAGONAL_POINTS(p) \
CHECK_POINT_WITH_OFFSET(rr, (p), Point(0.02, -0.02)); \
CHECK_POINT_WITH_OFFSET(rr, (p) * Point(-1, -1), Point(-0.02, 0.02));
// A few other points along the edge
- CHECK_DIAGNAL_POINTS(Point(-40.0, -49.59));
- CHECK_DIAGNAL_POINTS(Point(-20.0, -45.64));
- CHECK_DIAGNAL_POINTS(Point(0.0, -37.01));
- CHECK_DIAGNAL_POINTS(Point(20.0, -21.96));
- CHECK_DIAGNAL_POINTS(Point(21.05, -20.92));
- CHECK_DIAGNAL_POINTS(Point(40.0, 5.68));
+ CHECK_DIAGONAL_POINTS(Point(-40.0, -49.59));
+ CHECK_DIAGONAL_POINTS(Point(-20.0, -45.64));
+ CHECK_DIAGONAL_POINTS(Point(0.0, -37.01));
+ CHECK_DIAGONAL_POINTS(Point(20.0, -21.96));
+ CHECK_DIAGONAL_POINTS(Point(21.05, -20.92));
+ CHECK_DIAGONAL_POINTS(Point(40.0, 5.68));
#undef CHECK_POINT_AND_MIRRORS
}
diff --git a/engine/src/flutter/lib/web_ui/lib/geometry.dart b/engine/src/flutter/lib/web_ui/lib/geometry.dart
index 9f5e694..ea4140e 100644
--- a/engine/src/flutter/lib/web_ui/lib/geometry.dart
+++ b/engine/src/flutter/lib/web_ui/lib/geometry.dart
@@ -441,6 +441,7 @@
required double brRadiusY,
required double blRadiusX,
required double blRadiusY,
+ required bool uniformRadii,
});
final double left;
@@ -460,6 +461,19 @@
final double blRadiusY;
Radius get blRadius => Radius.elliptical(blRadiusX, blRadiusY);
+ // Whether the radii of the four corners are guaranteed to be the same.
+ //
+ // This is a Web-only flag for optimization.
+ //
+ // Returns `true` only if the object was constructed in a way that guarantees
+ // all radii are equal (e.g., from `.fromRectAndRadius`), saving the cost of
+ // checking the corner radii fields individually.
+ //
+ // A `false` return value does not mean the radii are necessarily different.
+ // It simply means there's no guarantee, and they must be compared manually
+ // if uniformity needs to be determined.
+ bool get _uniformRadii => false;
+
T shift(Offset offset) {
return _create(
left: left + offset.dx,
@@ -474,6 +488,7 @@
blRadiusY: blRadiusY,
brRadiusX: brRadiusX,
brRadiusY: brRadiusY,
+ uniformRadii: _uniformRadii,
);
}
@@ -491,6 +506,7 @@
blRadiusY: math.max(0, blRadiusY + delta),
brRadiusX: math.max(0, brRadiusX + delta),
brRadiusY: math.max(0, brRadiusY + delta),
+ uniformRadii: _uniformRadii,
);
}
@@ -614,6 +630,7 @@
blRadiusY: blRadiusY * scale,
brRadiusX: brRadiusX * scale,
brRadiusY: brRadiusY * scale,
+ uniformRadii: _uniformRadii,
);
}
@@ -630,6 +647,7 @@
blRadiusY: blRadiusY,
brRadiusX: brRadiusX,
brRadiusY: brRadiusY,
+ uniformRadii: _uniformRadii,
);
}
@@ -651,6 +669,7 @@
brRadiusY: math.max(0, brRadiusY * k),
blRadiusX: math.max(0, blRadiusX * k),
blRadiusY: math.max(0, blRadiusY * k),
+ uniformRadii: _uniformRadii,
);
} else {
return _create(
@@ -666,6 +685,7 @@
brRadiusY: math.max(0, _lerpDouble(brRadiusY, b.brRadiusY, t)),
blRadiusX: math.max(0, _lerpDouble(blRadiusX, b.blRadiusX, t)),
blRadiusY: math.max(0, _lerpDouble(blRadiusY, b.blRadiusY, t)),
+ uniformRadii: _uniformRadii,
);
}
}
@@ -876,6 +896,7 @@
required double brRadiusY,
required double blRadiusX,
required double blRadiusY,
+ required bool uniformRadii, // Not used. See `get _uniformRadii`.
}) => RRect._raw(
top: top,
left: left,
@@ -891,6 +912,10 @@
brRadiusY: brRadiusY,
);
+ // RRect doesn't need uniformRadii for optimization for now.
+ @override
+ bool get _uniformRadii => false;
+
static const RRect zero = RRect._raw();
bool contains(Offset point) {
@@ -976,6 +1001,7 @@
blRadiusY: radiusY,
brRadiusX: radiusX,
brRadiusY: radiusY,
+ uniformRadii: true,
);
RSuperellipse.fromLTRBR(double left, double top, double right, double bottom, Radius radius)
@@ -992,6 +1018,7 @@
blRadiusY: radius.y,
brRadiusX: radius.x,
brRadiusY: radius.y,
+ uniformRadii: true,
);
RSuperellipse.fromRectXY(Rect rect, double radiusX, double radiusY)
@@ -1008,6 +1035,7 @@
blRadiusY: radiusY,
brRadiusX: radiusX,
brRadiusY: radiusY,
+ uniformRadii: true,
);
RSuperellipse.fromRectAndRadius(Rect rect, Radius radius)
@@ -1024,6 +1052,7 @@
blRadiusY: radius.y,
brRadiusX: radius.x,
brRadiusY: radius.y,
+ uniformRadii: true,
);
RSuperellipse.fromLTRBAndCorners(
@@ -1048,6 +1077,13 @@
blRadiusY: bottomLeft.y,
brRadiusX: bottomRight.x,
brRadiusY: bottomRight.y,
+ uniformRadii:
+ topLeft.x == topRight.x &&
+ topLeft.y == topRight.y &&
+ topLeft.x == bottomLeft.x &&
+ topLeft.y == bottomLeft.y &&
+ topLeft.x == bottomRight.x &&
+ topLeft.y == bottomRight.y,
);
RSuperellipse.fromRectAndCorners(
@@ -1069,6 +1105,13 @@
blRadiusY: bottomLeft.y,
brRadiusX: bottomRight.x,
brRadiusY: bottomRight.y,
+ uniformRadii:
+ topLeft.x == topRight.x &&
+ topLeft.y == topRight.y &&
+ topLeft.x == bottomLeft.x &&
+ topLeft.y == bottomLeft.y &&
+ topLeft.x == bottomRight.x &&
+ topLeft.y == bottomRight.y,
);
const RSuperellipse._raw({
@@ -1084,7 +1127,8 @@
super.brRadiusY = 0.0,
super.blRadiusX = 0.0,
super.blRadiusY = 0.0,
- });
+ bool uniformRadii = false,
+ }) : _uniformRadii = uniformRadii;
@override
RSuperellipse _create({
@@ -1100,6 +1144,7 @@
required double brRadiusY,
required double blRadiusX,
required double blRadiusY,
+ required bool uniformRadii,
}) => RSuperellipse._raw(
top: top,
left: left,
@@ -1113,8 +1158,34 @@
blRadiusY: blRadiusY,
brRadiusX: brRadiusX,
brRadiusY: brRadiusY,
+ uniformRadii: uniformRadii,
);
+ @override
+ final bool _uniformRadii;
+
+ /// (Web only) Returns a [Path] for this shape and an [Offset] for its
+ /// placement.
+ ///
+ /// To correctly position the shape, the caller is required to apply the
+ /// returned `offset` to the `path`.
+ ///
+ /// The returned path's coordinate system is relative to the `offset`. The
+ /// caller should not make any assumptions about the path's origin or whether
+ /// the offset is zero, as the implementation may use different strategies for
+ /// efficiency.
+ ///
+ /// For example, to draw the shape, first translate the canvas by the `offset`
+ /// and then draw the `path`. To add it to another path, provide both the
+ /// `path` and the `offset` to the `addPath` method.
+ (Path, Offset) toPathOffset() {
+ if (_uniformRadii) {
+ return (_RSuperellipseCache.instance.get(width, height, tlRadius), center);
+ } else {
+ return (_RSuperellipsePathBuilder.exact(this).path, Offset.zero);
+ }
+ }
+
// Approximates a rounded superellipse with a round rectangle to the
// best practical accuracy.
//
@@ -1143,12 +1214,8 @@
static const RSuperellipse zero = RSuperellipse._raw();
bool contains(Offset point) {
- // Web doesn't support RSuperellipse, but falls back to RRect in all use
- // cases. Therefore this `contains` is implemented as RRect. Once Web
- // supports RSuperellipse this method should be changed to the correct shape.
- // TODO(dkwingsmt): Properly implement the shape on Web instead of
- // falling back to RRect. https://github.com/flutter/flutter/issues/163718
- return toApproximateRRect().contains(point);
+ final (Path path, Offset offset) = toPathOffset();
+ return path.contains(point - offset);
}
static RSuperellipse? lerp(RSuperellipse? a, RSuperellipse? b, double t) {
@@ -1166,6 +1233,7 @@
return _toString(className: 'RSuperellipse');
}
}
+
// Modeled after Skia's SkRSXform.
class RSTransform {
diff --git a/engine/src/flutter/lib/web_ui/lib/rsuperellipse_param.dart b/engine/src/flutter/lib/web_ui/lib/rsuperellipse_param.dart
new file mode 100644
index 0000000..d376e95
--- /dev/null
+++ b/engine/src/flutter/lib/web_ui/lib/rsuperellipse_param.dart
@@ -0,0 +1,561 @@
+// 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 ui;
+
+// The logic in this file mirrors the implementation in
+// `round_superellipse_param.cc`, which has detailed comments.
+
+// A helper class for composing affine transformations.
+//
+// This is used to build and combine transforms because `dart:ui` does not
+// provide a direct API for matrix composition.
+extension type _Transform(Offset Function(Offset) apply) {
+ static _Transform makeComposite(_Transform second, _Transform first) {
+ return _Transform((Offset p) => second.apply(first.apply(p)));
+ }
+
+ static _Transform makeTranslate(Offset offset) {
+ return _Transform((Offset p) => Offset(p.dx + offset.dx, p.dy + offset.dy));
+ }
+
+ static _Transform makeScale(Offset scale) {
+ return _Transform((Offset p) => Offset(p.dx * scale.dx, p.dy * scale.dy));
+ }
+
+ static final _Transform kFlip = _Transform((Offset p) {
+ return Offset(p.dy, p.dx);
+ });
+}
+
+// The Path class extended with a few utility methods.
+extension type _RSuperellipsePath(Path path) {
+ void cubicToPoints(Offset p2, Offset p3, Offset p4) {
+ path.cubicTo(p2.dx, p2.dy, p3.dx, p3.dy, p4.dx, p4.dy);
+ }
+
+ void lineToPoint(Offset p) {
+ path.lineTo(p.dx, p.dy);
+ }
+}
+
+// An octant of an RSuperellipse, used in _RSuperellipseQuadrant.
+class _RSuperellipseOctant {
+ factory _RSuperellipseOctant(Offset center, double a, double radius) {
+ if (radius <= 0) {
+ return _RSuperellipseOctant.square(offset: center, seA: a);
+ }
+
+ final double ratio = a * 2 / radius;
+ final double g = kGapFactor * radius;
+
+ final (double n, double xJOverA) = _computeNAndXj(ratio);
+ final double xJ = xJOverA * a;
+ final double yJ = math.pow(1 - math.pow(xJOverA, n), 1 / n) * a;
+ final double maxTheta = math.asin(math.pow(xJOverA, n / 2));
+
+ final double tanPhiJ = math.pow(xJ / yJ, n - 1) as double;
+ final double d = (xJ - tanPhiJ * yJ) / (1 - tanPhiJ);
+ final double R = (a - d - g) * math.sqrt(2);
+
+ final Offset pointM = Offset(a - g, a - g);
+ final Offset pointJ = Offset(xJ, yJ);
+ final Offset circleCenter = radius == 0 ? pointM : _findCircleCenter(pointJ, pointM, R);
+ final double circleMaxAngle = radius == 0
+ ? 0
+ : _angleTo(pointM - circleCenter, pointJ - circleCenter);
+
+ return _RSuperellipseOctant._raw(
+ offset: center,
+ seA: a,
+ seN: n,
+ seMaxTheta: maxTheta,
+ circleStart: pointJ,
+ circleCenter: circleCenter,
+ circleMaxAngle: circleMaxAngle,
+ );
+ }
+
+ const _RSuperellipseOctant.square({required Offset offset, required double seA})
+ : this._raw(
+ offset: offset,
+ seA: seA,
+ seN: 0,
+ seMaxTheta: 0,
+ circleStart: Offset.zero,
+ circleCenter: Offset.zero,
+ circleMaxAngle: 0,
+ );
+
+ const _RSuperellipseOctant._raw({
+ required this.offset,
+ required this.seA,
+ required this.seN,
+ required this.seMaxTheta,
+ required this.circleStart,
+ required this.circleCenter,
+ required this.circleMaxAngle,
+ });
+
+ final Offset offset;
+ final double seA;
+ final double seN;
+ final double seMaxTheta;
+ final Offset circleStart;
+ final Offset circleCenter;
+ final double circleMaxAngle;
+
+ void addToPath(
+ _RSuperellipsePath path,
+ _Transform externalTransform, {
+ required bool reverse,
+ required bool flip,
+ }) {
+ _Transform transform = _Transform.makeComposite(
+ externalTransform,
+ _Transform.makeTranslate(offset),
+ );
+ if (flip) {
+ transform = _Transform.makeComposite(transform, _Transform.kFlip);
+ }
+
+ final List<Offset> circlePoints = _circularArcPoints();
+ final List<Offset> sePoints = _superellipseArcPoints();
+
+ if (!reverse) {
+ path.cubicToPoints(
+ transform.apply(sePoints[1]),
+ transform.apply(sePoints[2]),
+ transform.apply(sePoints[3]),
+ );
+ path.cubicToPoints(
+ transform.apply(circlePoints[1]),
+ transform.apply(circlePoints[2]),
+ transform.apply(circlePoints[3]),
+ );
+ } else {
+ path.cubicToPoints(
+ transform.apply(circlePoints[2]),
+ transform.apply(circlePoints[1]),
+ transform.apply(circlePoints[0]),
+ );
+ path.cubicToPoints(
+ transform.apply(sePoints[2]),
+ transform.apply(sePoints[1]),
+ transform.apply(sePoints[0]),
+ );
+ }
+ }
+
+ List<Offset> _superellipseArcPoints() {
+ final Offset start = Offset(0, seA);
+ final Offset end = circleStart;
+ const Offset startTangent = Offset(1, 0);
+ final Offset circleStartVector = circleStart - circleCenter;
+ final Offset endTangent =
+ Offset(-circleStartVector.dy, circleStartVector.dx) / circleStartVector.distance;
+
+ final (double startFactor, double endFactor) = _superellipseBezierFactors(seN);
+ return <Offset>[
+ start,
+ start + startTangent * startFactor * seA,
+ end + endTangent * endFactor * seA,
+ end,
+ ];
+ }
+
+ List<Offset> _circularArcPoints() {
+ final Offset startVector = circleStart - circleCenter;
+ final Offset endVector = _rotate(startVector, -circleMaxAngle);
+ final Offset circleEnd = circleCenter + endVector;
+ final Offset startTangent = Offset(startVector.dy, -startVector.dx) / startVector.distance;
+ final Offset endTangent = Offset(-endVector.dy, endVector.dx) / endVector.distance;
+ final double bezierFactor = math.tan(circleMaxAngle / 4) * 4 / 3;
+ final double radius = startVector.distance;
+
+ return <Offset>[
+ circleStart,
+ circleStart + startTangent * bezierFactor * radius,
+ circleEnd + endTangent * bezierFactor * radius,
+ circleEnd,
+ ];
+ }
+
+ static Offset _rotate(Offset p, double radians) {
+ final double cosine = math.cos(radians);
+ final double sine = math.sin(radians);
+ return Offset(p.dx * cosine - p.dy * sine, p.dx * sine + p.dy * cosine);
+ }
+
+ static (double, double) _superellipseBezierFactors(double n) {
+ const List<(double, double)> kPrecomputedVariables = [
+ /*n= 2.0*/ (0.01339448, 0.05994973),
+ /*n= 3.0*/ (0.13664115, 0.13592082),
+ /*n= 4.0*/ (0.24545546, 0.14099516),
+ /*n= 5.0*/ (0.32353151, 0.12808021),
+ /*n= 6.0*/ (0.39093068, 0.11726264),
+ /*n= 7.0*/ (0.44847800, 0.10808278),
+ /*n= 8.0*/ (0.49817452, 0.10026175),
+ /*n= 9.0*/ (0.54105583, 0.09344429),
+ /*n=10.0*/ (0.57812578, 0.08748984),
+ /*n=11.0*/ (0.61050961, 0.08224722),
+ /*n=12.0*/ (0.63903989, 0.07759639),
+ /*n=13.0*/ (0.66416338, 0.07346530),
+ /*n=14.0*/ (0.68675338, 0.06974996),
+ /*n=15.0*/ (0.70678034, 0.06529512),
+ ];
+ final int kNumRecords = kPrecomputedVariables.length;
+ const double kStep = 1.00;
+ const double kMinN = 2.00;
+ final double kMaxN = kMinN + (kNumRecords - 1) * kStep;
+
+ if (n >= kMaxN) {
+ return (
+ 1.07 - math.exp(1.307649835) * math.pow(n, -0.8568516731),
+ -0.01 + math.exp(-0.9287690322) * math.pow(n, -0.6120901398),
+ );
+ }
+
+ final double steps = ((n - kMinN) / kStep).clamp(0, kNumRecords - 1);
+ final int left = steps.floor().clamp(0, kNumRecords - 2);
+ final double frac = steps - left;
+
+ return (
+ (1 - frac) * kPrecomputedVariables[left].$1 + frac * kPrecomputedVariables[left + 1].$1,
+ (1 - frac) * kPrecomputedVariables[left].$2 + frac * kPrecomputedVariables[left + 1].$2,
+ );
+ }
+
+ static Offset _findCircleCenter(Offset a, Offset b, double r) {
+ final Offset aToB = b - a;
+ final Offset m = (a + b) / 2;
+ final Offset cToM = Offset(-aToB.dy, aToB.dx);
+ final double distanceAm = aToB.distance / 2;
+ final double distanceCm = math.sqrt(r * r - distanceAm * distanceAm);
+ return m - cToM / cToM.distance * distanceCm;
+ }
+
+ static (double, double) _computeNAndXj(double ratio) {
+ const List<List<double>> kPrecomputedVariables = [
+ /*ratio=2.00*/ [2.00000000, 1.13276676],
+ /*ratio=2.10*/ [2.18349805, 1.20311921],
+ /*ratio=2.20*/ [2.33888662, 1.28698796],
+ /*ratio=2.30*/ [2.48660575, 1.36351941],
+ /*ratio=2.40*/ [2.62226596, 1.44717976],
+ /*ratio=2.50*/ [2.75148990, 1.53385819],
+ /*ratio=3.00*/ [3.36298265, 1.98288283],
+ /*ratio=3.50*/ [4.08649929, 2.23811846],
+ /*ratio=4.00*/ [4.85481134, 2.47563463],
+ /*ratio=4.50*/ [5.62945551, 2.72948597],
+ /*ratio=5.00*/ [6.43023796, 2.98020421],
+ ];
+
+ const double kMinRatio = 2.00;
+ const double kFirstStepInverse = 10; // = 1 / 0.10
+ const double kFirstMaxRatio = 2.50;
+ const double kFirstNumRecords = 6;
+ const double kSecondStepInverse = 2; // = 1 / 0.50
+ const double kSecondMaxRatio = 5.00;
+ const double kThirdNSlope = 1.559599389;
+ const double kThirdFactorXjSlope = 0.522807185;
+ final int kNumRecords = kPrecomputedVariables.length;
+
+ if (ratio > kSecondMaxRatio) {
+ final double n =
+ kThirdNSlope * (ratio - kSecondMaxRatio) + kPrecomputedVariables[kNumRecords - 1][0];
+ final double factorXj =
+ kThirdFactorXjSlope * (ratio - kSecondMaxRatio) +
+ kPrecomputedVariables[kNumRecords - 1][1];
+ return (n, 1 - 1 / factorXj);
+ }
+ ratio = ratio.clamp(kMinRatio, kSecondMaxRatio);
+ final double steps;
+ if (ratio < kFirstMaxRatio) {
+ steps = (ratio - kMinRatio) * kFirstStepInverse;
+ } else {
+ steps = (ratio - kFirstMaxRatio) * kSecondStepInverse + kFirstNumRecords - 1;
+ }
+
+ final int left = steps.floor().clamp(0, kNumRecords - 2);
+ final double frac = steps - left;
+
+ final double n =
+ (1 - frac) * kPrecomputedVariables[left][0] + frac * kPrecomputedVariables[left + 1][0];
+ final double factorXj =
+ (1 - frac) * kPrecomputedVariables[left][1] + frac * kPrecomputedVariables[left + 1][1];
+ return (n, 1 - 1 / factorXj);
+ }
+
+ static const double kGapFactor = 0.29289321881; // 1-cos(pi/4)
+
+ static double _angleTo(Offset a, Offset b) {
+ return math.atan2(a.dx * b.dy - a.dy * b.dx, a.dx * b.dx + a.dy * b.dy);
+ }
+}
+
+// A quadrant of an RSuperellipse, used in _RSuperellipsePathBuilder.
+class _RSuperellipseQuadrant {
+ factory _RSuperellipseQuadrant(Offset center, Offset corner, Radius inRadii, Size sign) {
+ final Offset cornerVector = corner - center;
+ final Size radii = Size(inRadii.x.abs(), inRadii.y.abs());
+
+ final double normRadius = radii.shortestSide;
+ final Size forwardScale = normRadius == 0 ? const Size(1, 1) : radii / normRadius;
+ final Size normHalfSize = Size(
+ cornerVector.dx.abs() / forwardScale.width,
+ cornerVector.dy.abs() / forwardScale.height,
+ );
+ final Offset signedScale = _replaceNaNWith(
+ Offset(cornerVector.dx / normHalfSize.width, cornerVector.dy / normHalfSize.height),
+ sign,
+ );
+
+ final double c = normHalfSize.width - normHalfSize.height;
+
+ return _RSuperellipseQuadrant._raw(
+ offset: center,
+ signedScale: signedScale,
+ sign: sign,
+ top: _RSuperellipseOctant(Offset(0, -c), normHalfSize.width, normRadius),
+ right: _RSuperellipseOctant(Offset(c, 0), normHalfSize.height, normRadius),
+ );
+ }
+
+ const _RSuperellipseQuadrant._raw({
+ required this.offset,
+ required this.signedScale,
+ required this.sign,
+ required this.top,
+ required this.right,
+ });
+
+ final Offset offset;
+ final Offset signedScale;
+ final Size sign;
+ final _RSuperellipseOctant top;
+ final _RSuperellipseOctant right;
+
+ void addToPath(
+ _RSuperellipsePath path, {
+ required bool reverse,
+ Size extraScale = const Size(1, 1),
+ }) {
+ final _Transform transform = _Transform.makeComposite(
+ _Transform.makeTranslate(offset),
+ _Transform.makeScale(signedScale.scale(extraScale.width, extraScale.height)),
+ );
+ if (top.seN < 2 || right.seN < 2) {
+ if (!reverse) {
+ final _Transform transformOctant = _Transform.makeComposite(
+ transform,
+ _Transform.makeTranslate(right.offset),
+ );
+ path.lineToPoint(transformOctant.apply(Offset(right.seA, right.seA)));
+ path.lineToPoint(transformOctant.apply(Offset(right.seA, 0)));
+ } else {
+ final _Transform transformOctant = _Transform.makeComposite(
+ transform,
+ _Transform.makeTranslate(top.offset),
+ );
+ path.lineToPoint(transformOctant.apply(Offset(top.seA, top.seA)));
+ path.lineToPoint(transformOctant.apply(Offset(0, top.seA)));
+ }
+ return;
+ }
+ if (!reverse) {
+ top.addToPath(path, transform, reverse: false, flip: false);
+ right.addToPath(path, transform, reverse: true, flip: true);
+ } else {
+ right.addToPath(path, transform, reverse: false, flip: true);
+ top.addToPath(path, transform, reverse: true, flip: false);
+ }
+ }
+
+ static Offset _replaceNaNWith(Offset p, Size sign) {
+ return Offset(p.dx.isFinite ? p.dx : sign.width, p.dy.isFinite ? p.dy : sign.height);
+ }
+}
+
+// A class that can build a path for a `RSuperellipse`.
+//
+// Used in `_RSuperellipsePathCache`.
+class _RSuperellipsePathBuilder {
+ // Build a path for a translated version of the provided RSuperellipse, so
+ // that the center of the bound is placed at the origin. The target RSuperellipse
+ // must also have a uniform radius.
+ _RSuperellipsePathBuilder.normalized(double width, double height, double radiusX, double radiusY)
+ : path = Path() {
+ final _RSuperellipsePath p = _RSuperellipsePath(path);
+ final _RSuperellipseQuadrant bottomRight = _RSuperellipseQuadrant(
+ Offset.zero,
+ Offset(width / 2, height / 2),
+ Radius.elliptical(radiusX, radiusY),
+ const Size(1, 1),
+ );
+ final Offset start = Offset(0, height / 2);
+ path.moveTo(start.dx, start.dy);
+ bottomRight.addToPath(p, reverse: false);
+ bottomRight.addToPath(p, reverse: true, extraScale: const Size(1, -1));
+ bottomRight.addToPath(p, reverse: false, extraScale: const Size(-1, -1));
+ bottomRight.addToPath(p, reverse: true, extraScale: const Size(-1, 1));
+ path.lineTo(start.dx, start.dy);
+ path.close();
+ }
+
+ // Build a path for an RSuperellipse with arbitrary position and radii.
+ _RSuperellipsePathBuilder.exact(RSuperellipse r) : path = Path() {
+ final _RSuperellipsePath p = _RSuperellipsePath(path);
+ final Offset start = Offset((r.left + r.right) / 2, r.top);
+ path.moveTo(start.dx, start.dy);
+
+ final double topSplit = _split(r.left, r.right, r.tlRadiusX, r.trRadiusX);
+ final double rightSplit = _split(r.top, r.bottom, r.trRadiusY, r.brRadiusY);
+ final double bottomSplit = _split(r.left, r.right, r.blRadiusX, r.brRadiusX);
+ final double leftSplit = _split(r.top, r.bottom, r.tlRadiusY, r.blRadiusY);
+ _RSuperellipseQuadrant(
+ Offset(topSplit, rightSplit),
+ Offset(r.right, r.top),
+ r.trRadius,
+ const Size(1, -1),
+ ).addToPath(p, reverse: false);
+ _RSuperellipseQuadrant(
+ Offset(bottomSplit, rightSplit),
+ Offset(r.right, r.bottom),
+ r.brRadius,
+ const Size(1, 1),
+ ).addToPath(p, reverse: true);
+ _RSuperellipseQuadrant(
+ Offset(bottomSplit, leftSplit),
+ Offset(r.left, r.bottom),
+ r.blRadius,
+ const Size(-1, 1),
+ ).addToPath(p, reverse: false);
+ _RSuperellipseQuadrant(
+ Offset(topSplit, leftSplit),
+ Offset(r.left, r.top),
+ r.tlRadius,
+ const Size(-1, -1),
+ ).addToPath(p, reverse: true);
+
+ path.lineTo(start.dx, start.dy);
+ path.close();
+ }
+
+ final Path path;
+
+ static double _split(double left, double right, double ratioLeft, double ratioRight) {
+ if (ratioLeft == 0 && ratioRight == 0) {
+ return (left + right) / 2;
+ }
+ return (left * ratioRight + right * ratioLeft) / (ratioLeft + ratioRight);
+ }
+}
+
+// The cache key for a `RSuperellipse` with uniform radii to be used in
+// `_RSuperellipseCache`.
+//
+// To handle floating-point precision issues and make it usable as a Map key,
+// we multiply each float by a factor and round it to an integer.
+class _RSuperellipseCacheKey {
+ _RSuperellipseCacheKey(double width, double height, double radiusX, double radiusY)
+ : _widthInt = (width * kPrecisionFactor).round(),
+ _heightInt = (height * kPrecisionFactor).round(),
+ _radiusXInt = (radiusX * kPrecisionFactor).round(),
+ _radiusYInt = (radiusY * kPrecisionFactor).round();
+
+ final int _widthInt;
+ final int _heightInt;
+ final int _radiusXInt;
+ final int _radiusYInt;
+
+ double get width => _widthInt / kPrecisionFactor;
+ double get height => _heightInt / kPrecisionFactor;
+ double get radiusX => _radiusXInt / kPrecisionFactor;
+ double get radiusY => _radiusYInt / kPrecisionFactor;
+
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) {
+ return true;
+ }
+ return other is _RSuperellipseCacheKey &&
+ _widthInt == other._widthInt &&
+ _heightInt == other._heightInt &&
+ _radiusXInt == other._radiusXInt &&
+ _radiusYInt == other._radiusYInt;
+ }
+
+ @override
+ int get hashCode => Object.hash(_widthInt, _heightInt, _radiusXInt, _radiusYInt);
+
+ @override
+ String toString() {
+ return '_RSuperellipseCacheKey('
+ 'width: ${_widthInt / kPrecisionFactor},'
+ 'height: ${_heightInt / kPrecisionFactor},'
+ 'radiusX: ${_radiusXInt / kPrecisionFactor},'
+ 'radiusY: ${_radiusYInt / kPrecisionFactor})';
+ }
+
+ static const double kPrecisionFactor = 100;
+}
+
+/// An LRU (Least Recently Used) cache that maps from normalized RSuperellipse
+/// to its path.
+class _RSuperellipseCache {
+ _RSuperellipseCache._({required this.capacity}) : assert(capacity > 0);
+
+ static final _RSuperellipseCache instance = _RSuperellipseCache._(capacity: kCapacity);
+
+ // A rough estimate by that a typical screen should hardly contain more than
+ // 20 RSuperellipses.
+ static const int kCapacity = 50;
+
+ final int capacity;
+
+ final Map<_RSuperellipseCacheKey, Path> _cache = <_RSuperellipseCacheKey, Path>{};
+
+ /// Retrieves a Path from the cache.
+ ///
+ /// If the path is found, it is moved to the most-recently-used position.
+ /// Otherwise, a new path is built and inserted.
+ Path get(double width, double height, Radius radius) {
+ final key = _RSuperellipseCacheKey(width, height, radius.x, radius.y);
+
+ // Remove the key and re-insert it to mark it as most recently used.
+ final Path? path = _cache.remove(key);
+ if (path != null) {
+ _cache[key] = path;
+ return path;
+ } else {
+ final Path newPath = _buildPath(key);
+ _cache[key] = newPath;
+ _checkCacheSize();
+ return newPath;
+ }
+ }
+
+ Path _buildPath(_RSuperellipseCacheKey key) {
+ return _RSuperellipsePathBuilder.normalized(
+ key.width,
+ key.height,
+ key.radiusX,
+ key.radiusY,
+ ).path;
+ }
+
+ /// Evicts all entries from the cache.
+ void clear() {
+ _cache.clear();
+ }
+
+ /// Remove entries from the cache until it is at or below the desired size.
+ void _checkCacheSize() {
+ while (_cache.length > capacity) {
+ // .keys.first gives the least recently used key in a `LinkedHashMap`.
+ _cache.remove(_cache.keys.first);
+ }
+ assert(_cache.length <= capacity);
+ }
+}
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart
index 7b3a99c..b52283e 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart
@@ -5,6 +5,7 @@
import 'dart:math' as math;
import 'dart:typed_data';
+import 'package:ui/src/engine.dart' show LazyPath;
import 'package:ui/ui.dart' as ui;
import '../color_filter.dart';
@@ -53,13 +54,14 @@
}
void clipRSuperellipse(ui.RSuperellipse rsuperellipse, bool doAntiAlias) {
- // TODO(dkwingsmt): Properly implement RSuperellipse on Web instead of falling
- // back to RRect. https://github.com/flutter/flutter/issues/163718
- skCanvas.clipRRect(
- toSkRRect(rsuperellipse.toApproximateRRect()),
+ final (ui.Path path, ui.Offset offset) = rsuperellipse.toPathOffset();
+ translate(offset.dx, offset.dy);
+ skCanvas.clipPath(
+ ((path as LazyPath).builtPath as CkPath).skiaObject,
_clipOpIntersect,
doAntiAlias,
);
+ translate(-offset.dx, -offset.dy);
}
void clipRect(ui.Rect rect, ui.ClipOp clipOp, bool doAntiAlias) {
@@ -228,6 +230,15 @@
skPaint.delete();
}
+ void drawRSuperellipse(ui.RSuperellipse rsuperellipse, CkPaint paint) {
+ final skPaint = paint.toSkPaint();
+ final (ui.Path path, ui.Offset offset) = rsuperellipse.toPathOffset();
+ translate(offset.dx, offset.dy);
+ skCanvas.drawPath(((path as LazyPath).builtPath as CkPath).skiaObject, skPaint);
+ translate(-offset.dx, -offset.dy);
+ skPaint.delete();
+ }
+
void drawRect(ui.Rect rect, CkPaint paint) {
final skPaint = paint.toSkPaint();
skCanvas.drawRect(toSkRect(rect), skPaint);
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart
index e78dcf8..8ca9e39 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart
@@ -121,10 +121,7 @@
@override
void clipRSuperellipse(ui.RSuperellipse rsuperellipse, {bool doAntiAlias = true}) {
- assert(rsuperellipseIsValid(rsuperellipse));
- // TODO(dkwingsmt): Properly implement RSuperellipse on Web instead of falling
- // back to RRect. https://github.com/flutter/flutter/issues/163718
- _clipRRect(rsuperellipse.toApproximateRRect(), doAntiAlias);
+ _canvas.clipRSuperellipse(rsuperellipse, doAntiAlias);
}
@override
@@ -198,10 +195,7 @@
@override
void drawRSuperellipse(ui.RSuperellipse rsuperellipse, ui.Paint paint) {
- assert(rsuperellipseIsValid(rsuperellipse));
- // TODO(dkwingsmt): Properly implement RSuperellipse on Web instead of falling
- // back to RRect. https://github.com/flutter/flutter/issues/163718
- _drawRRect(rsuperellipse.toApproximateRRect(), paint);
+ _canvas.drawRSuperellipse(rsuperellipse, paint as CkPaint);
}
@override
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart
index 7fbcc26..2d3c551 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart
@@ -815,6 +815,12 @@
_mutators.add(Mutator.clipRRect(rrect));
}
+ void pushClipRSuperellipse(ui.RSuperellipse rsuperellipse) {
+ // RSuperellipse ops in PlatformView are approximated by RRect because they
+ // are expensive.
+ pushClipRRect(rsuperellipse.toApproximateRRect());
+ }
+
void pushClipPath(ui.Path path) {
_mutators.add(Mutator.clipPath(path));
}
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart
index c593657..f5d8c5e 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer.dart
@@ -112,7 +112,7 @@
}
}
-/// A layer that clips its child layers by a given [RRect].
+/// A layer that clips its child layers by a given [RSuperellipse].
class ClipRSuperellipseEngineLayer extends ContainerLayer
implements ui.ClipRSuperellipseEngineLayer {
ClipRSuperellipseEngineLayer(this.clipRSuperellipse, this.clipBehavior)
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart
index f1e14b2..e12f536 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_visitor.dart
@@ -112,9 +112,7 @@
@override
void visitClipRSuperellipse(ClipRSuperellipseEngineLayer clipRSuperellipse) {
- // TODO(dkwingsmt): Properly implement RSuperellipse on Web instead of falling
- // back to RRect. https://github.com/flutter/flutter/issues/163718
- mutatorsStack.pushClipRRect(clipRSuperellipse.clipRSuperellipse.toApproximateRRect());
+ mutatorsStack.pushClipRSuperellipse(clipRSuperellipse.clipRSuperellipse);
final ui.Rect childPaintBounds = prerollChildren(clipRSuperellipse);
if (childPaintBounds.overlaps(clipRSuperellipse.clipRSuperellipse.outerRect)) {
clipRSuperellipse.paintBounds = childPaintBounds.intersect(
@@ -575,12 +573,9 @@
@override
void visitClipRSuperellipse(ClipRSuperellipseEngineLayer clipRSuperellipse) {
assert(clipRSuperellipse.needsPainting);
-
nWayCanvas.save();
- // TODO(dkwingsmt): Properly implement RSuperellipse on Web instead of falling
- // back to RRect. https://github.com/flutter/flutter/issues/163718
- nWayCanvas.clipRRect(
- clipRSuperellipse.clipRSuperellipse.toApproximateRRect(),
+ nWayCanvas.clipRSuperellipse(
+ clipRSuperellipse.clipRSuperellipse,
clipRSuperellipse.clipBehavior != ui.Clip.hardEdge,
);
if (clipRSuperellipse.clipBehavior == ui.Clip.antiAliasWithSaveLayer) {
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart
index d494ba1..7488601 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart
@@ -96,4 +96,11 @@
_canvases[i].clipRRect(rrect, doAntiAlias);
}
}
+
+ /// Calls [clipRSuperellipse] on all canvases.
+ void clipRSuperellipse(ui.RSuperellipse rsuperellipse, bool doAntiAlias) {
+ // RSuperellipse ops in PlatformView are approximated by RRect because they
+ // are expensive.
+ return clipRRect(rsuperellipse.toApproximateRRect(), doAntiAlias);
+ }
}
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart
index 014126a..e5fbc5d 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart
@@ -114,9 +114,8 @@
@override
void addRSuperellipse(ui.RSuperellipse rsuperellipse) {
- // TODO(dkwingsmt): Properly implement RSuperellipse on Web instead of falling
- // back to RRect. https://github.com/flutter/flutter/issues/163718
- addRRect(rsuperellipse.toApproximateRRect());
+ final (ui.Path path, ui.Offset offset) = rsuperellipse.toPathOffset();
+ addPath((path as LazyPath).builtPath, offset);
}
@override
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart
index e83a642..d940772 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/layers.dart
@@ -324,8 +324,8 @@
@override
PlatformViewStyling createPlatformViewStyling() {
- // TODO(dkwingsmt): Properly implement RSuperellipse on Web instead of falling
- // back to RRect. https://github.com/flutter/flutter/issues/163718
+ // RSuperellipse ops in PlatformView are approximated by RRect because they
+ // are expensive.
return PlatformViewStyling(clip: PlatformViewRRectClip(rsuperellipse.toApproximateRRect()));
}
@@ -338,7 +338,7 @@
@override
Map<String, Object> get debugJsonDescription {
return <String, Object>{
- 'type': 'clipRSuperEllipse',
+ 'type': 'clipRSuperellipse',
'rsuperellipse': {
'left': rsuperellipse.left,
'top': rsuperellipse.top,
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart
index 8e494e0..9da4ca6 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart
@@ -136,9 +136,10 @@
@override
void clipRSuperellipse(ui.RSuperellipse rsuperellipse, {bool doAntiAlias = true}) {
- // TODO(dkwingsmt): Properly implement RSuperellipse on Web instead of falling
- // back to RRect. https://github.com/flutter/flutter/issues/163718
- clipRRect(rsuperellipse.toApproximateRRect(), doAntiAlias: doAntiAlias);
+ final (ui.Path path, ui.Offset offset) = rsuperellipse.toPathOffset();
+ translate(offset.dx, offset.dy);
+ clipPath(path, doAntiAlias: doAntiAlias);
+ translate(-offset.dx, -offset.dy);
}
@override
@@ -184,9 +185,10 @@
@override
void drawRSuperellipse(ui.RSuperellipse rsuperellipse, ui.Paint paint) {
- // TODO(dkwingsmt): Properly implement RSuperellipse on Web instead of falling
- // back to RRect. https://github.com/flutter/flutter/issues/163718
- drawRRect(rsuperellipse.toApproximateRRect(), paint);
+ final (ui.Path path, ui.Offset offset) = rsuperellipse.toPathOffset();
+ translate(offset.dx, offset.dy);
+ drawPath(path, paint);
+ translate(-offset.dx, -offset.dy);
}
@override
diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart
index 5323e5d..0486f45 100644
--- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart
+++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart
@@ -174,9 +174,8 @@
@override
void addRSuperellipse(ui.RSuperellipse rsuperellipse) {
- // TODO(dkwingsmt): Properly implement RSuperellipse on Web instead of falling
- // back to RRect. https://github.com/flutter/flutter/issues/163718
- addRRect(rsuperellipse.toApproximateRRect());
+ final (ui.Path path, ui.Offset offset) = rsuperellipse.toPathOffset();
+ addPath((path as LazyPath).builtPath, offset);
}
@override
diff --git a/engine/src/flutter/lib/web_ui/lib/ui.dart b/engine/src/flutter/lib/web_ui/lib/ui.dart
index bd53947..3d4addf 100644
--- a/engine/src/flutter/lib/web_ui/lib/ui.dart
+++ b/engine/src/flutter/lib/web_ui/lib/ui.dart
@@ -30,6 +30,7 @@
part 'platform_dispatcher.dart';
part 'platform_isolate.dart';
part 'pointer.dart';
+part 'rsuperellipse_param.dart';
part 'semantics.dart';
part 'text.dart';
part 'tile_mode.dart';
diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/rsuperellipse_contains_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/rsuperellipse_contains_test.dart
new file mode 100644
index 0000000..27cfd6a
--- /dev/null
+++ b/engine/src/flutter/lib/web_ui/test/canvaskit/rsuperellipse_contains_test.dart
@@ -0,0 +1,168 @@
+// 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 'package:test/bootstrap/browser.dart';
+import 'package:test/test.dart';
+
+import 'package:ui/src/engine.dart';
+import 'package:ui/ui.dart';
+
+import 'common.dart';
+
+// Tests for `RSuperellipse.contains` are placed in the canvaskit folder instead
+// of the engine folder because `RSuperellipse.contains` is implemented with
+// paths, which needs a renderer.
+//
+// Some of the numbers slightly deviate from round_superellipse_unittests
+// because it uses Bezier path approximation.
+
+void main() {
+ internalBootstrapBrowserTest(() => testMain);
+}
+
+void testMain() {
+ group('RSuperellipse.contains', () {
+ setUpCanvasKitTest();
+
+ tearDown(() {
+ CanvasKitRenderer.instance.debugClear();
+ });
+
+ test('RSuperellipse.contains is correct with no corners', () {
+ // RSuperellipse of bounds with no corners contains corners just barely.
+ const RSuperellipse rse = RSuperellipse.fromLTRBXY(-50, -50, 50, 50, 0, 0);
+
+ expect(rse.contains(const Offset(-50, -50)), isTrue);
+ // Rectangles have half-in, half-out containment so we need
+ // to be careful about testing containment of right/bottom corners.
+ expect(rse.contains(const Offset(-50, 49.99)), isTrue);
+ expect(rse.contains(const Offset(49.99, -50)), isTrue);
+ expect(rse.contains(const Offset(49.99, 49.99)), isTrue);
+ expect(rse.contains(const Offset(-50.01, -50)), isFalse);
+ expect(rse.contains(const Offset(-50, -50.01)), isFalse);
+ expect(rse.contains(const Offset(-50.01, 50)), isFalse);
+ expect(rse.contains(const Offset(-50, 50.01)), isFalse);
+ expect(rse.contains(const Offset(50.01, -50)), isFalse);
+ expect(rse.contains(const Offset(50, -50.01)), isFalse);
+ expect(rse.contains(const Offset(50.01, 50)), isFalse);
+ expect(rse.contains(const Offset(50, 50.01)), isFalse);
+ });
+
+ test('RSuperellipse.contains is correct with tiny corners', () {
+ // RSuperellipse of bounds with even the tiniest corners does not contain corners.
+ const RSuperellipse rse = RSuperellipse.fromLTRBXY(-50, -50, 50, 50, 0.01, 0.01);
+
+ expect(rse.contains(const Offset(-50, -50)), isFalse);
+ expect(rse.contains(const Offset(-50, 50)), isFalse);
+ expect(rse.contains(const Offset(50, -50)), isFalse);
+ expect(rse.contains(const Offset(50, 50)), isFalse);
+ });
+
+ test('RSuperellipse.contains is correct with uniform corners', () {
+ const RSuperellipse rse = RSuperellipse.fromLTRBXY(-50, -50, 50, 50, 5.0, 5.0);
+
+ void checkPointAndMirrors(Offset p) {
+ checkPointWithOffset(rse, Offset(p.dx, p.dy), const Offset(0.02, 0.02));
+ checkPointWithOffset(rse, Offset(p.dx, -p.dy), const Offset(0.02, -0.02));
+ checkPointWithOffset(rse, Offset(-p.dx, p.dy), const Offset(-0.02, 0.02));
+ checkPointWithOffset(rse, Offset(-p.dx, -p.dy), const Offset(-0.02, -0.02));
+ }
+
+ checkPointAndMirrors(const Offset(0, 49.995)); // Top
+ checkPointAndMirrors(const Offset(44.15, 49.95)); // Top curve start
+ checkPointAndMirrors(const Offset(45.72, 49.87)); // Top joint
+ checkPointAndMirrors(const Offset(48.53, 48.53)); // Circular arc mid
+ checkPointAndMirrors(const Offset(49.87, 45.72)); // Right joint
+ checkPointAndMirrors(const Offset(49.95, 44.15)); // Right curve start
+ checkPointAndMirrors(const Offset(49.995, 0)); // Right
+ });
+
+ test('RSuperellipse.contains is correct with uniform elliptical corners', () {
+ const RSuperellipse rse = RSuperellipse.fromLTRBXY(-50, -50, 50, 50, 5.0, 10.0);
+
+ void checkPointAndMirrors(Offset p) {
+ checkPointWithOffset(rse, Offset(p.dx, p.dy), const Offset(0.02, 0.02));
+ checkPointWithOffset(rse, Offset(p.dx, -p.dy), const Offset(0.02, -0.02));
+ checkPointWithOffset(rse, Offset(-p.dx, p.dy), const Offset(-0.02, 0.02));
+ checkPointWithOffset(rse, Offset(-p.dx, -p.dy), const Offset(-0.02, -0.02));
+ }
+
+ checkPointAndMirrors(const Offset(0, 49.995)); // Top
+ checkPointAndMirrors(const Offset(43.90, 49.911)); // Top curve start
+ checkPointAndMirrors(const Offset(45.69, 49.75)); // Top joint
+ checkPointAndMirrors(const Offset(48.51, 47.07)); // Circular arc mid
+ checkPointAndMirrors(const Offset(49.87, 41.44)); // Right joint
+ checkPointAndMirrors(const Offset(49.95, 38.49)); // Right curve start
+ checkPointAndMirrors(const Offset(49.995, 0)); // Right
+ });
+
+ test('RSuperellipse.contains is correct with uniform corners and unequal height and width', () {
+ // The bounds is not centered at the origin and has unequal height and width.
+ const RSuperellipse rse = RSuperellipse.fromLTRBXY(0, 0, 50, 100, 23.0, 30.0);
+
+ final Offset center = rse.outerRect.center;
+ void checkPointAndMirrors(Offset globalPoint) {
+ final Offset p = globalPoint - center;
+ checkPointWithOffset(rse, Offset(p.dx, p.dy) + center, const Offset(0.02, 0.02));
+ checkPointWithOffset(rse, Offset(p.dx, -p.dy) + center, const Offset(0.02, -0.02));
+ checkPointWithOffset(rse, Offset(-p.dx, p.dy) + center, const Offset(-0.02, 0.02));
+ checkPointWithOffset(rse, Offset(-p.dx, -p.dy) + center, const Offset(-0.02, -0.02));
+ }
+
+ checkPointAndMirrors(const Offset(24.99, 99.99)); // Bottom mid-edge
+ checkPointAndMirrors(const Offset(29.99, 99.64));
+ checkPointAndMirrors(const Offset(34.99, 98.06));
+ checkPointAndMirrors(const Offset(39.99, 94.73));
+ checkPointAndMirrors(const Offset(44.13, 89.99));
+ checkPointAndMirrors(const Offset(48.46, 79.99));
+ checkPointAndMirrors(const Offset(49.68, 69.99));
+ checkPointAndMirrors(const Offset(49.97, 59.99));
+ checkPointAndMirrors(const Offset(49.99, 49.99)); // Right mid-edge
+ });
+
+ test('RSuperellipse.contains is correct for a slim diagonal shape', () {
+ // This shape has large radii on one diagonal and tiny radii on the other,
+ // resulting in a almond-like shape placed diagonally (NW to SE).
+ final RSuperellipse rse = RSuperellipse.fromLTRBAndCorners(
+ -50,
+ -50,
+ 50,
+ 50,
+ topLeft: const Radius.circular(1.0),
+ topRight: const Radius.circular(99.0),
+ bottomLeft: const Radius.circular(99.0),
+ bottomRight: const Radius.circular(1.0),
+ );
+
+ expect(rse.contains(Offset.zero), isTrue);
+ expect(rse.contains(const Offset(-49.999, -49.999)), isFalse);
+ expect(rse.contains(const Offset(-49.999, 49.999)), isFalse);
+ expect(rse.contains(const Offset(49.999, 49.999)), isFalse);
+ expect(rse.contains(const Offset(49.999, -49.999)), isFalse);
+
+ // The pointy ends at the NE and SW corners
+ checkPointWithOffset(rse, const Offset(-49.70, -49.70), const Offset(-0.02, -0.02));
+ checkPointWithOffset(rse, const Offset(49.70, 49.70), const Offset(0.02, 0.02));
+
+ // Checks two points symmetrical to the origin.
+ void checkDiagonalPoints(Offset p) {
+ checkPointWithOffset(rse, p, const Offset(0.02, -0.02));
+ checkPointWithOffset(rse, Offset(-p.dx, -p.dy), const Offset(-0.02, 0.02));
+ }
+
+ // A few other points along the edge
+ checkDiagonalPoints(const Offset(-40.0, -49.59));
+ checkDiagonalPoints(const Offset(-20.0, -45.64));
+ checkDiagonalPoints(const Offset(0.0, -37.01));
+ checkDiagonalPoints(const Offset(20.0, -21.96));
+ checkDiagonalPoints(const Offset(21.05, -20.92));
+ checkDiagonalPoints(const Offset(40.0, 5.68));
+ });
+ });
+}
+
+void checkPointWithOffset(RSuperellipse rse, Offset inPoint, Offset outwardOffset) {
+ expect(rse.contains(inPoint), isTrue);
+ expect(rse.contains(inPoint + outwardOffset), isFalse);
+}
diff --git a/engine/src/flutter/testing/dart/geometry_test.dart b/engine/src/flutter/testing/dart/geometry_test.dart
index e9c7943..2c0cf50 100644
--- a/engine/src/flutter/testing/dart/geometry_test.dart
+++ b/engine/src/flutter/testing/dart/geometry_test.dart
@@ -664,9 +664,9 @@
checkPointAndMirrors(const Offset(49.99, 49.99)); // Right mid-edge
});
- test('RSuperellipse.contains is correct for a slim diagnal shape', () {
- // This shape has large radii on one diagnal and tiny radii on the other,
- // resulting in a almond-like shape placed diagnally (NW to SE).
+ test('RSuperellipse.contains is correct for a slim diagonal shape', () {
+ // This shape has large radii on one diagonal and tiny radii on the other,
+ // resulting in a almond-like shape placed diagonally (NW to SE).
final RSuperellipse rse = RSuperellipse.fromLTRBAndCorners(
-50,
-50,
@@ -689,18 +689,18 @@
checkPointWithOffset(rse, const Offset(49.70, 49.70), const Offset(0.02, 0.02));
// Checks two points symmetrical to the origin.
- void checkDiagnalPoints(Offset p) {
+ void checkDiagonalPoints(Offset p) {
checkPointWithOffset(rse, p, const Offset(0.02, -0.02));
checkPointWithOffset(rse, Offset(-p.dx, -p.dy), const Offset(-0.02, 0.02));
}
// A few other points along the edge
- checkDiagnalPoints(const Offset(-40.0, -49.59));
- checkDiagnalPoints(const Offset(-20.0, -45.64));
- checkDiagnalPoints(const Offset(0.0, -37.01));
- checkDiagnalPoints(const Offset(20.0, -21.96));
- checkDiagnalPoints(const Offset(21.05, -20.92));
- checkDiagnalPoints(const Offset(40.0, 5.68));
+ checkDiagonalPoints(const Offset(-40.0, -49.59));
+ checkDiagonalPoints(const Offset(-20.0, -45.64));
+ checkDiagonalPoints(const Offset(0.0, -37.01));
+ checkDiagonalPoints(const Offset(20.0, -21.96));
+ checkDiagonalPoints(const Offset(21.05, -20.92));
+ checkDiagonalPoints(const Offset(40.0, 5.68));
});
test('RSuperellipse.contains is correct for points outside of a sharp corner', () {
diff --git a/engine/src/flutter/testing/dart/path_test.dart b/engine/src/flutter/testing/dart/path_test.dart
index 8ae3c83..299ccd3 100644
--- a/engine/src/flutter/testing/dart/path_test.dart
+++ b/engine/src/flutter/testing/dart/path_test.dart
@@ -256,7 +256,7 @@
);
});
- test('RSuperellipse path is correct for a slim diagnal shape', () {
+ test('RSuperellipse path is correct for a slim diagonal shape', () {
// This test mirrors a similar test from "geometry_test.dart" and serves as
// a smoke test.
final RSuperellipse rsuperellipse = RSuperellipse.fromLTRBAndCorners(
@@ -284,18 +284,18 @@
checkPointWithOffset(path, const Offset(49.70, 49.70), const Offset(0.02, 0.02));
// Checks two points symmetrical to the origin.
- void checkDiagnalPoints(Offset p) {
+ void checkDiagonalPoints(Offset p) {
checkPointWithOffset(path, p, const Offset(0.02, -0.02));
checkPointWithOffset(path, Offset(-p.dx, -p.dy), const Offset(-0.02, 0.02));
}
// A few other points along the edge
- checkDiagnalPoints(const Offset(-40.0, -49.59));
- checkDiagnalPoints(const Offset(-20.0, -45.64));
- checkDiagnalPoints(const Offset(0.0, -37.01));
- checkDiagnalPoints(const Offset(20.0, -21.96));
- checkDiagnalPoints(const Offset(21.05, -20.92));
- checkDiagnalPoints(const Offset(40.0, 5.68));
+ checkDiagonalPoints(const Offset(-40.0, -49.59));
+ checkDiagonalPoints(const Offset(-20.0, -45.64));
+ checkDiagonalPoints(const Offset(0.0, -37.01));
+ checkDiagonalPoints(const Offset(20.0, -21.96));
+ checkDiagonalPoints(const Offset(21.05, -20.92));
+ checkDiagonalPoints(const Offset(40.0, 5.68));
});
}