Add `Radius.clamp` and `Radius.clampValues` (#36106) (#36302)

* Add `Radius.clamp` and `Radius.clampValues` (#36106)

* Build CanvasKit in the Flutter Engine (#32510)

* Clamp `RRect` radii when deflating, assert on negative radii (#36062)

Co-authored-by: Greg Spencer <gspencergoog@users.noreply.github.com>
Co-authored-by: Harry Terkelsen <hterkelsen@users.noreply.github.com>
diff --git a/DEPS b/DEPS
index fa4e448..5eb34d1 100644
--- a/DEPS
+++ b/DEPS
@@ -109,7 +109,7 @@
 ]
 
 deps = {
-  'src': 'https://github.com/flutter/buildroot.git' + '@' + '337fdd987f500ca48902aef9abbcde98be2803c7',
+  'src': 'https://github.com/flutter/buildroot.git' + '@' + 'af893d511e89f93194f86dae8a4ef39e3b3fe59b',
 
    # Fuchsia compatibility
    #
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 643fd39..b8c5efd 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -1005,6 +1005,7 @@
 FILE: ../../../flutter/lib/ui/isolate_name_server/isolate_name_server_natives.h
 FILE: ../../../flutter/lib/ui/key.dart
 FILE: ../../../flutter/lib/ui/lerp.dart
+FILE: ../../../flutter/lib/ui/math.dart
 FILE: ../../../flutter/lib/ui/natives.dart
 FILE: ../../../flutter/lib/ui/painting.dart
 FILE: ../../../flutter/lib/ui/painting/canvas.cc
@@ -1149,6 +1150,7 @@
 FILE: ../../../flutter/lib/web_ui/lib/initialization.dart
 FILE: ../../../flutter/lib/web_ui/lib/key.dart
 FILE: ../../../flutter/lib/web_ui/lib/lerp.dart
+FILE: ../../../flutter/lib/web_ui/lib/math.dart
 FILE: ../../../flutter/lib/web_ui/lib/natives.dart
 FILE: ../../../flutter/lib/web_ui/lib/painting.dart
 FILE: ../../../flutter/lib/web_ui/lib/path.dart
diff --git a/lib/ui/dart_ui.gni b/lib/ui/dart_ui.gni
index 96a7b06..7059f7a 100644
--- a/lib/ui/dart_ui.gni
+++ b/lib/ui/dart_ui.gni
@@ -12,6 +12,7 @@
   "//flutter/lib/ui/isolate_name_server.dart",
   "//flutter/lib/ui/key.dart",
   "//flutter/lib/ui/lerp.dart",
+  "//flutter/lib/ui/math.dart",
   "//flutter/lib/ui/natives.dart",
   "//flutter/lib/ui/painting.dart",
   "//flutter/lib/ui/platform_dispatcher.dart",
diff --git a/lib/ui/geometry.dart b/lib/ui/geometry.dart
index 8a1fe6f..7a39525 100644
--- a/lib/ui/geometry.dart
+++ b/lib/ui/geometry.dart
@@ -939,6 +939,37 @@
   /// You can use [Radius.zero] with [RRect] to have right-angle corners.
   static const Radius zero = Radius.circular(0.0);
 
+  /// Returns this [Radius], with values clamped to the given min and max
+  /// [Radius] values.
+  ///
+  /// The `min` value defaults to `Radius.circular(-double.infinity)`, and
+  /// the `max` value defaults to `Radius.circular(double.infinity)`.
+  Radius clamp({Radius? minimum, Radius? maximum}) {
+    minimum ??= const Radius.circular(-double.infinity);
+    maximum ??= const Radius.circular(double.infinity);
+    return Radius.elliptical(
+      clampDouble(x, minimum.x, maximum.x),
+      clampDouble(y, minimum.y, maximum.y),
+    );
+  }
+
+  /// Returns this [Radius], with values clamped to the given min and max
+  /// values in each dimension
+  ///
+  /// The `minimumX` and `minimumY` values default to `-double.infinity`, and
+  /// the `maximumX` and `maximumY` values default to `double.infinity`.
+  Radius clampValues({
+    double? minimumX,
+    double? minimumY,
+    double? maximumX,
+    double? maximumY,
+  }) {
+    return Radius.elliptical(
+      clampDouble(x, minimumX ?? -double.infinity, maximumX ?? double.infinity),
+      clampDouble(y, minimumY ?? -double.infinity, maximumY ?? double.infinity),
+    );
+  }
+
   /// Unary negation operator.
   ///
   /// Returns a Radius with the distances negated.
@@ -1056,8 +1087,16 @@
 class RRect {
   /// Construct a rounded rectangle from its left, top, right, and bottom edges,
   /// and the same radii along its horizontal axis and its vertical axis.
-  const RRect.fromLTRBXY(double left, double top, double right, double bottom,
-                   double radiusX, double radiusY) : this._raw(
+  ///
+  /// Will assert in debug mode if `radiusX` or `radiusY` are negative.
+  const RRect.fromLTRBXY(
+    double left,
+    double top,
+    double right,
+    double bottom,
+    double radiusX,
+    double radiusY,
+  ) : this._raw(
     top: top,
     left: left,
     right: right,
@@ -1074,8 +1113,15 @@
 
   /// Construct a rounded rectangle from its left, top, right, and bottom edges,
   /// and the same radius in each corner.
-  RRect.fromLTRBR(double left, double top, double right, double bottom,
-                  Radius radius)
+  ///
+  /// Will assert in debug mode if the `radius` is negative in either x or y.
+  RRect.fromLTRBR(
+    double left,
+    double top,
+    double right,
+    double bottom,
+    Radius radius,
+  )
     : this._raw(
         top: top,
         left: left,
@@ -1093,6 +1139,8 @@
 
   /// Construct a rounded rectangle from its bounding box and the same radii
   /// along its horizontal axis and its vertical axis.
+  ///
+  /// Will assert in debug mode if `radiusX` or `radiusY` are negative.
   RRect.fromRectXY(Rect rect, double radiusX, double radiusY)
     : this._raw(
         top: rect.top,
@@ -1111,6 +1159,8 @@
 
   /// Construct a rounded rectangle from its bounding box and a radius that is
   /// the same in each corner.
+  ///
+  /// Will assert in debug mode if the `radius` is negative in either x or y.
   RRect.fromRectAndRadius(Rect rect, Radius radius)
     : this._raw(
         top: rect.top,
@@ -1130,7 +1180,8 @@
   /// Construct a rounded rectangle from its left, top, right, and bottom edges,
   /// and topLeft, topRight, bottomRight, and bottomLeft radii.
   ///
-  /// The corner radii default to [Radius.zero], i.e. right-angled corners.
+  /// The corner radii default to [Radius.zero], i.e. right-angled corners. Will
+  /// assert in debug mode if any of the radii are negative in either x or y.
   RRect.fromLTRBAndCorners(
     double left,
     double top,
@@ -1158,7 +1209,8 @@
   /// Construct a rounded rectangle from its bounding box and and topLeft,
   /// topRight, bottomRight, and bottomLeft radii.
   ///
-  /// The corner radii default to [Radius.zero], i.e. right-angled corners
+  /// The corner radii default to [Radius.zero], i.e. right-angled corners. Will
+  /// assert in debug mode if any of the radii are negative in either x or y.
   RRect.fromRectAndCorners(
     Rect rect,
     {
@@ -1206,7 +1258,15 @@
        assert(brRadiusX != null),
        assert(brRadiusY != null),
        assert(blRadiusX != null),
-       assert(blRadiusY != null);
+       assert(blRadiusY != null),
+       assert(tlRadiusX >= 0),
+       assert(tlRadiusY >= 0),
+       assert(trRadiusX >= 0),
+       assert(trRadiusY >= 0),
+       assert(brRadiusX >= 0),
+       assert(brRadiusY >= 0),
+       assert(blRadiusX >= 0),
+       assert(blRadiusY >= 0);
 
   Float32List _getValue32()  {
     final Float32List result = Float32List(12);
@@ -1302,14 +1362,14 @@
       top: top - delta,
       right: right + delta,
       bottom: bottom + delta,
-      tlRadiusX: tlRadiusX + delta,
-      tlRadiusY: tlRadiusY + delta,
-      trRadiusX: trRadiusX + delta,
-      trRadiusY: trRadiusY + delta,
-      blRadiusX: blRadiusX + delta,
-      blRadiusY: blRadiusY + delta,
-      brRadiusX: brRadiusX + delta,
-      brRadiusY: brRadiusY + delta,
+      tlRadiusX: math.max(0, tlRadiusX + delta),
+      tlRadiusY: math.max(0, tlRadiusY + delta),
+      trRadiusX: math.max(0, trRadiusX + delta),
+      trRadiusY: math.max(0, trRadiusY + delta),
+      blRadiusX: math.max(0, blRadiusX + delta),
+      blRadiusY: math.max(0, blRadiusY + delta),
+      brRadiusX: math.max(0, brRadiusX + delta),
+      brRadiusY: math.max(0, brRadiusY + delta),
     );
   }
 
@@ -1472,6 +1532,7 @@
     scale = _getMin(scale, tlRadiusX, trRadiusX, width);
     scale = _getMin(scale, trRadiusY, brRadiusY, height);
     scale = _getMin(scale, brRadiusX, blRadiusX, width);
+    assert(scale >= 0);
 
     if (scale < 1.0) {
       return RRect._raw(
@@ -1590,14 +1651,14 @@
           top: a.top * k,
           right: a.right * k,
           bottom: a.bottom * k,
-          tlRadiusX: a.tlRadiusX * k,
-          tlRadiusY: a.tlRadiusY * k,
-          trRadiusX: a.trRadiusX * k,
-          trRadiusY: a.trRadiusY * k,
-          brRadiusX: a.brRadiusX * k,
-          brRadiusY: a.brRadiusY * k,
-          blRadiusX: a.blRadiusX * k,
-          blRadiusY: a.blRadiusY * k,
+          tlRadiusX: math.max(0, a.tlRadiusX * k),
+          tlRadiusY: math.max(0, a.tlRadiusY * k),
+          trRadiusX: math.max(0, a.trRadiusX * k),
+          trRadiusY: math.max(0, a.trRadiusY * k),
+          brRadiusX: math.max(0, a.brRadiusX * k),
+          brRadiusY: math.max(0, a.brRadiusY * k),
+          blRadiusX: math.max(0, a.blRadiusX * k),
+          blRadiusY: math.max(0, a.blRadiusY * k),
         );
       }
     } else {
@@ -1607,14 +1668,14 @@
           top: b.top * t,
           right: b.right * t,
           bottom: b.bottom * t,
-          tlRadiusX: b.tlRadiusX * t,
-          tlRadiusY: b.tlRadiusY * t,
-          trRadiusX: b.trRadiusX * t,
-          trRadiusY: b.trRadiusY * t,
-          brRadiusX: b.brRadiusX * t,
-          brRadiusY: b.brRadiusY * t,
-          blRadiusX: b.blRadiusX * t,
-          blRadiusY: b.blRadiusY * t,
+          tlRadiusX: math.max(0, b.tlRadiusX * t),
+          tlRadiusY: math.max(0, b.tlRadiusY * t),
+          trRadiusX: math.max(0, b.trRadiusX * t),
+          trRadiusY: math.max(0, b.trRadiusY * t),
+          brRadiusX: math.max(0, b.brRadiusX * t),
+          brRadiusY: math.max(0, b.brRadiusY * t),
+          blRadiusX: math.max(0, b.blRadiusX * t),
+          blRadiusY: math.max(0, b.blRadiusY * t),
         );
       } else {
         return RRect._raw(
@@ -1622,14 +1683,14 @@
           top: _lerpDouble(a.top, b.top, t),
           right: _lerpDouble(a.right, b.right, t),
           bottom: _lerpDouble(a.bottom, b.bottom, t),
-          tlRadiusX: _lerpDouble(a.tlRadiusX, b.tlRadiusX, t),
-          tlRadiusY: _lerpDouble(a.tlRadiusY, b.tlRadiusY, t),
-          trRadiusX: _lerpDouble(a.trRadiusX, b.trRadiusX, t),
-          trRadiusY: _lerpDouble(a.trRadiusY, b.trRadiusY, t),
-          brRadiusX: _lerpDouble(a.brRadiusX, b.brRadiusX, t),
-          brRadiusY: _lerpDouble(a.brRadiusY, b.brRadiusY, t),
-          blRadiusX: _lerpDouble(a.blRadiusX, b.blRadiusX, t),
-          blRadiusY: _lerpDouble(a.blRadiusY, b.blRadiusY, t),
+          tlRadiusX: math.max(0, _lerpDouble(a.tlRadiusX, b.tlRadiusX, t)),
+          tlRadiusY: math.max(0, _lerpDouble(a.tlRadiusY, b.tlRadiusY, t)),
+          trRadiusX: math.max(0, _lerpDouble(a.trRadiusX, b.trRadiusX, t)),
+          trRadiusY: math.max(0, _lerpDouble(a.trRadiusY, b.trRadiusY, t)),
+          brRadiusX: math.max(0, _lerpDouble(a.brRadiusX, b.brRadiusX, t)),
+          brRadiusY: math.max(0, _lerpDouble(a.brRadiusY, b.brRadiusY, t)),
+          blRadiusX: math.max(0, _lerpDouble(a.blRadiusX, b.blRadiusX, t)),
+          blRadiusY: math.max(0, _lerpDouble(a.blRadiusY, b.blRadiusY, t)),
         );
       }
     }
diff --git a/lib/ui/math.dart b/lib/ui/math.dart
new file mode 100644
index 0000000..aeb835a
--- /dev/null
+++ b/lib/ui/math.dart
@@ -0,0 +1,25 @@
+// 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 dart.ui;
+
+/// Same as [num.clamp] but optimized for a non-null [double].
+///
+/// This is faster because it avoids polymorphism, boxing, and special cases for
+/// floating point numbers.
+//
+// See also: //dev/benchmarks/microbenchmarks/lib/foundation/clamp.dart
+double clampDouble(double x, double min, double max) {
+  assert(min <= max && !max.isNaN && !min.isNaN);
+  if (x < min) {
+    return min;
+  }
+  if (x > max) {
+    return max;
+  }
+  if (x.isNaN) {
+    return max;
+  }
+  return x;
+}
diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart
index 3c0a7ad..316de56 100644
--- a/lib/ui/painting.dart
+++ b/lib/ui/painting.dart
@@ -320,7 +320,7 @@
   /// The [opacity] value may not be null.
   static int getAlphaFromOpacity(double opacity) {
     assert(opacity != null);
-    return (opacity.clamp(0.0, 1.0) * 255).round();
+    return (clampDouble(opacity, 0.0, 1.0) * 255).round();
   }
 
   @override
diff --git a/lib/ui/ui.dart b/lib/ui/ui.dart
index 2695a80..36818d3 100644
--- a/lib/ui/ui.dart
+++ b/lib/ui/ui.dart
@@ -31,6 +31,7 @@
 part 'isolate_name_server.dart';
 part 'key.dart';
 part 'lerp.dart';
+part 'math.dart';
 part 'natives.dart';
 part 'painting.dart';
 part 'platform_dispatcher.dart';
diff --git a/lib/web_ui/dev/build.dart b/lib/web_ui/dev/build.dart
index bf5b55d..087e4fa 100644
--- a/lib/web_ui/dev/build.dart
+++ b/lib/web_ui/dev/build.dart
@@ -48,7 +48,7 @@
     if (buildCanvasKit) {
       steps.addAll(<PipelineStep>[
         GnPipelineStep(target: 'canvaskit'),
-        NinjaPipelineStep(target: environment.canvasKitOutDir),
+        NinjaPipelineStep(target: environment.wasmReleaseOutDir),
       ]);
     }
     final Pipeline buildPipeline = Pipeline(steps: steps);
@@ -74,7 +74,7 @@
 /// state. GN is pretty quick though, so it's OK to not support interruption.
 class GnPipelineStep extends ProcessStep {
   GnPipelineStep({this.target = 'engine'})
-      : assert(target == 'engine' || target == 'sdk');
+      : assert(target == 'engine' || target == 'canvaskit');
 
   @override
   String get description => 'gn';
@@ -89,7 +89,7 @@
 
   @override
   Future<ProcessManager> createProcess() {
-    print('Running gn...');
+    print('Running gn for $target...');
     final List<String> gnArgs = <String>[];
     if (target == 'engine') {
       gnArgs.addAll(<String>[
@@ -98,7 +98,10 @@
         '--full-dart-sdk',
       ]);
     } else if (target == 'canvaskit') {
-      gnArgs.add('--wasm');
+      gnArgs.addAll(<String>[
+        '--wasm',
+        '--runtime-mode=release',
+      ]);
     } else {
       throw StateError('Target was not engine or canvaskit: $target');
     }
diff --git a/lib/web_ui/dev/environment.dart b/lib/web_ui/dev/environment.dart
index 1c5530b..35df6c0 100644
--- a/lib/web_ui/dev/environment.dart
+++ b/lib/web_ui/dev/environment.dart
@@ -25,8 +25,8 @@
         io.Directory(pathlib.join(engineSrcDir.path, 'out'));
     final io.Directory hostDebugUnoptDir =
         io.Directory(pathlib.join(outDir.path, 'host_debug_unopt'));
-    final io.Directory canvasKitOutDir =
-        io.Directory(pathlib.join(outDir.path, 'wasm_debug'));
+    final io.Directory wasmReleaseOutDir =
+        io.Directory(pathlib.join(outDir.path, 'wasm_release'));
     final io.Directory dartSdkDir =
         io.Directory(pathlib.join(hostDebugUnoptDir.path, 'dart-sdk'));
     final io.Directory webUiRootDir = io.Directory(
@@ -44,6 +44,7 @@
       }
     }
 
+
     return Environment._(
       self: self,
       webUiRootDir: webUiRootDir,
@@ -51,7 +52,7 @@
       engineToolsDir: engineToolsDir,
       outDir: outDir,
       hostDebugUnoptDir: hostDebugUnoptDir,
-      canvasKitOutDir: canvasKitOutDir,
+      wasmReleaseOutDir: wasmReleaseOutDir,
       dartSdkDir: dartSdkDir,
     );
   }
@@ -63,7 +64,7 @@
     required this.engineToolsDir,
     required this.outDir,
     required this.hostDebugUnoptDir,
-    required this.canvasKitOutDir,
+    required this.wasmReleaseOutDir,
     required this.dartSdkDir,
   });
 
@@ -84,11 +85,13 @@
   /// This is where you'll find the ninja output, such as the Dart SDK.
   final io.Directory outDir;
 
-  /// The "host_debug_unopt" build of the Dart SDK.
+  /// The output directory for the host_debug_unopt build.
   final io.Directory hostDebugUnoptDir;
 
-  /// The output directory for the build of CanvasKit.
-  final io.Directory canvasKitOutDir;
+  /// The output directory for the wasm_release build.
+  ///
+  /// We build CanvasKit in release mode to reduce code size.
+  final io.Directory wasmReleaseOutDir;
 
   /// The root of the Dart SDK.
   final io.Directory dartSdkDir;
diff --git a/lib/web_ui/dev/steps/compile_tests_step.dart b/lib/web_ui/dev/steps/compile_tests_step.dart
index e9931b7..2cc2d22 100644
--- a/lib/web_ui/dev/steps/compile_tests_step.dart
+++ b/lib/web_ui/dev/steps/compile_tests_step.dart
@@ -23,10 +23,12 @@
 ///  * test/        - compiled test code
 ///  * test_images/ - test images copied from Skis sources.
 class CompileTestsStep implements PipelineStep {
-  CompileTestsStep({this.testFiles});
+  CompileTestsStep({this.testFiles, this.useLocalCanvasKit = false});
 
   final List<FilePath>? testFiles;
 
+  final bool useLocalCanvasKit;
+
   @override
   String get description => 'compile_tests';
 
@@ -41,7 +43,7 @@
   @override
   Future<void> run() async {
     await environment.webUiBuildDir.create();
-    await copyCanvasKitFiles();
+    await copyCanvasKitFiles(useLocalCanvasKit: useLocalCanvasKit);
     await buildHostPage();
     await copyTestFonts();
     await copySkiaTestImages();
@@ -122,11 +124,13 @@
   }
 }
 
-Future<void> copyCanvasKitFiles() async {
+Future<void> copyCanvasKitFiles({bool useLocalCanvasKit = false}) async {
   // If CanvasKit has been built locally, use that instead of the CIPD version.
-  final io.File localCanvasKitWasm =
-      io.File(pathlib.join(environment.canvasKitOutDir.path, 'canvaskit.wasm'));
-  final bool builtLocalCanvasKit = localCanvasKitWasm.existsSync();
+  final io.File localCanvasKitWasm = io.File(pathlib.join(
+    environment.wasmReleaseOutDir.path,
+    'canvaskit.wasm',
+  ));
+  final bool builtLocalCanvasKit = localCanvasKitWasm.existsSync() && useLocalCanvasKit;
 
   final io.Directory targetDir = io.Directory(pathlib.join(
     environment.webUiBuildDir.path,
@@ -136,7 +140,10 @@
   if (builtLocalCanvasKit) {
     final List<io.File> canvasKitFiles = <io.File>[
       localCanvasKitWasm,
-      io.File(pathlib.join(environment.canvasKitOutDir.path, 'canvaskit.js')),
+      io.File(pathlib.join(
+        environment.wasmReleaseOutDir.path,
+        'canvaskit.js',
+      )),
     ];
     for (final io.File file in canvasKitFiles) {
       final io.File normalTargetFile = io.File(pathlib.join(
diff --git a/lib/web_ui/dev/test_runner.dart b/lib/web_ui/dev/test_runner.dart
index cb4628f..8984d2f 100644
--- a/lib/web_ui/dev/test_runner.dart
+++ b/lib/web_ui/dev/test_runner.dart
@@ -73,6 +73,11 @@
         help: 'Optional. The path to a local build of CanvasKit to use in '
               'tests. If omitted, the test runner uses the default CanvasKit '
               'build.',
+      )
+      ..addFlag(
+        'use-local-canvaskit',
+        help: 'Optional. Whether or not to use the locally built version of '
+              'CanvasKit in the tests.',
       );
   }
 
@@ -115,6 +120,9 @@
   /// Path to a CanvasKit build. Overrides the default CanvasKit.
   String? get overridePathToCanvasKit => argResults!['canvaskit-path'] as String?;
 
+  /// Whether or not to use the locally built version of CanvasKit.
+  bool get useLocalCanvasKit => boolArg('use-local-canvaskit');
+
   @override
   Future<bool> run() async {
     final List<FilePath> testFiles = runAllTests
@@ -123,7 +131,7 @@
 
     final Pipeline testPipeline = Pipeline(steps: <PipelineStep>[
       if (isWatchMode) ClearTerminalScreenStep(),
-      CompileTestsStep(testFiles: testFiles),
+      CompileTestsStep(testFiles: testFiles, useLocalCanvasKit: useLocalCanvasKit),
       RunTestsStep(
         browserName: browserName,
         testFiles: testFiles,
diff --git a/lib/web_ui/lib/geometry.dart b/lib/web_ui/lib/geometry.dart
index 3d60b13..7d17c73 100644
--- a/lib/web_ui/lib/geometry.dart
+++ b/lib/web_ui/lib/geometry.dart
@@ -341,6 +341,25 @@
   final double x;
   final double y;
   static const Radius zero = Radius.circular(0.0);
+  Radius clamp({Radius? minimum, Radius? maximum}) {
+    minimum ??= const Radius.circular(-double.infinity);
+    maximum ??= const Radius.circular(double.infinity);
+    return Radius.elliptical(
+      clampDouble(x, minimum.x, maximum.x),
+      clampDouble(y, minimum.y, maximum.y),
+    );
+  }
+  Radius clampValues({
+    double? minimumX,
+    double? minimumY,
+    double? maximumX,
+    double? maximumY,
+  }) {
+    return Radius.elliptical(
+      clampDouble(x, minimumX ?? -double.infinity, maximumX ?? double.infinity),
+      clampDouble(y, minimumY ?? -double.infinity, maximumY ?? double.infinity),
+    );
+  }
   Radius operator -() => Radius.elliptical(-x, -y);
   Radius operator -(Radius other) => Radius.elliptical(x - other.x, y - other.y);
   Radius operator +(Radius other) => Radius.elliptical(x + other.x, y + other.y);
@@ -559,6 +578,14 @@
         assert(brRadiusY != null),
         assert(blRadiusX != null),
         assert(blRadiusY != null),
+        assert(tlRadiusX >= 0),
+        assert(tlRadiusY >= 0),
+        assert(trRadiusX >= 0),
+        assert(trRadiusY >= 0),
+        assert(brRadiusX >= 0),
+        assert(brRadiusY >= 0),
+        assert(blRadiusX >= 0),
+        assert(blRadiusY >= 0),
         webOnlyUniformRadii = uniformRadii;
 
   final double left;
@@ -604,14 +631,14 @@
       top: top - delta,
       right: right + delta,
       bottom: bottom + delta,
-      tlRadiusX: tlRadiusX + delta,
-      tlRadiusY: tlRadiusY + delta,
-      trRadiusX: trRadiusX + delta,
-      trRadiusY: trRadiusY + delta,
-      blRadiusX: blRadiusX + delta,
-      blRadiusY: blRadiusY + delta,
-      brRadiusX: brRadiusX + delta,
-      brRadiusY: brRadiusY + delta,
+      tlRadiusX: math.max(0, tlRadiusX + delta),
+      tlRadiusY: math.max(0, tlRadiusY + delta),
+      trRadiusX: math.max(0, trRadiusX + delta),
+      trRadiusY: math.max(0, trRadiusY + delta),
+      blRadiusX: math.max(0, blRadiusX + delta),
+      blRadiusY: math.max(0, blRadiusY + delta),
+      brRadiusX: math.max(0, brRadiusX + delta),
+      brRadiusY: math.max(0, brRadiusY + delta),
     );
   }
 
@@ -816,14 +843,14 @@
           top: a.top * k,
           right: a.right * k,
           bottom: a.bottom * k,
-          tlRadiusX: a.tlRadiusX * k,
-          tlRadiusY: a.tlRadiusY * k,
-          trRadiusX: a.trRadiusX * k,
-          trRadiusY: a.trRadiusY * k,
-          brRadiusX: a.brRadiusX * k,
-          brRadiusY: a.brRadiusY * k,
-          blRadiusX: a.blRadiusX * k,
-          blRadiusY: a.blRadiusY * k,
+          tlRadiusX: math.max(0, a.tlRadiusX * k),
+          tlRadiusY: math.max(0, a.tlRadiusY * k),
+          trRadiusX: math.max(0, a.trRadiusX * k),
+          trRadiusY: math.max(0, a.trRadiusY * k),
+          brRadiusX: math.max(0, a.brRadiusX * k),
+          brRadiusY: math.max(0, a.brRadiusY * k),
+          blRadiusX: math.max(0, a.blRadiusX * k),
+          blRadiusY: math.max(0, a.blRadiusY * k),
         );
       }
     } else {
@@ -833,14 +860,14 @@
           top: b.top * t,
           right: b.right * t,
           bottom: b.bottom * t,
-          tlRadiusX: b.tlRadiusX * t,
-          tlRadiusY: b.tlRadiusY * t,
-          trRadiusX: b.trRadiusX * t,
-          trRadiusY: b.trRadiusY * t,
-          brRadiusX: b.brRadiusX * t,
-          brRadiusY: b.brRadiusY * t,
-          blRadiusX: b.blRadiusX * t,
-          blRadiusY: b.blRadiusY * t,
+          tlRadiusX: math.max(0, b.tlRadiusX * t),
+          tlRadiusY: math.max(0, b.tlRadiusY * t),
+          trRadiusX: math.max(0, b.trRadiusX * t),
+          trRadiusY: math.max(0, b.trRadiusY * t),
+          brRadiusX: math.max(0, b.brRadiusX * t),
+          brRadiusY: math.max(0, b.brRadiusY * t),
+          blRadiusX: math.max(0, b.blRadiusX * t),
+          blRadiusY: math.max(0, b.blRadiusY * t),
         );
       } else {
         return RRect._raw(
@@ -848,14 +875,14 @@
           top: _lerpDouble(a.top, b.top, t),
           right: _lerpDouble(a.right, b.right, t),
           bottom: _lerpDouble(a.bottom, b.bottom, t),
-          tlRadiusX: _lerpDouble(a.tlRadiusX, b.tlRadiusX, t),
-          tlRadiusY: _lerpDouble(a.tlRadiusY, b.tlRadiusY, t),
-          trRadiusX: _lerpDouble(a.trRadiusX, b.trRadiusX, t),
-          trRadiusY: _lerpDouble(a.trRadiusY, b.trRadiusY, t),
-          brRadiusX: _lerpDouble(a.brRadiusX, b.brRadiusX, t),
-          brRadiusY: _lerpDouble(a.brRadiusY, b.brRadiusY, t),
-          blRadiusX: _lerpDouble(a.blRadiusX, b.blRadiusX, t),
-          blRadiusY: _lerpDouble(a.blRadiusY, b.blRadiusY, t),
+          tlRadiusX: math.max(0, _lerpDouble(a.tlRadiusX, b.tlRadiusX, t)),
+          tlRadiusY: math.max(0, _lerpDouble(a.tlRadiusY, b.tlRadiusY, t)),
+          trRadiusX: math.max(0, _lerpDouble(a.trRadiusX, b.trRadiusX, t)),
+          trRadiusY: math.max(0, _lerpDouble(a.trRadiusY, b.trRadiusY, t)),
+          brRadiusX: math.max(0, _lerpDouble(a.brRadiusX, b.brRadiusX, t)),
+          brRadiusY: math.max(0, _lerpDouble(a.brRadiusY, b.brRadiusY, t)),
+          blRadiusX: math.max(0, _lerpDouble(a.blRadiusX, b.blRadiusX, t)),
+          blRadiusY: math.max(0, _lerpDouble(a.blRadiusY, b.blRadiusY, t)),
         );
       }
     }
diff --git a/lib/web_ui/lib/math.dart b/lib/web_ui/lib/math.dart
new file mode 100644
index 0000000..adea127
--- /dev/null
+++ b/lib/web_ui/lib/math.dart
@@ -0,0 +1,19 @@
+// 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;
+
+double clampDouble(double x, double min, double max) {
+  assert(min <= max && !max.isNaN && !min.isNaN);
+  if (x < min) {
+    return min;
+  }
+  if (x > max) {
+    return max;
+  }
+  if (x.isNaN) {
+    return max;
+  }
+  return x;
+}
diff --git a/lib/web_ui/lib/painting.dart b/lib/web_ui/lib/painting.dart
index 3ab2ba0..03e44ec 100644
--- a/lib/web_ui/lib/painting.dart
+++ b/lib/web_ui/lib/painting.dart
@@ -146,7 +146,7 @@
 
   static int getAlphaFromOpacity(double opacity) {
     assert(opacity != null);
-    return (opacity.clamp(0.0, 1.0) * 255).round();
+    return (clampDouble(opacity, 0.0, 1.0) * 255).round();
   }
 
   @override
diff --git a/lib/web_ui/lib/src/engine/html/recording_canvas.dart b/lib/web_ui/lib/src/engine/html/recording_canvas.dart
index b1f86da..a7638d1 100644
--- a/lib/web_ui/lib/src/engine/html/recording_canvas.dart
+++ b/lib/web_ui/lib/src/engine/html/recording_canvas.dart
@@ -23,16 +23,8 @@
 /// Enable this to print every command applied by a canvas.
 const bool _debugDumpPaintCommands = false;
 
-// Returns the squared length of the x, y (of a border radius)
-// It normalizes x, y values before working with them, by
-// assuming anything < 0 to be 0, because flutter may pass
-// negative radii (which Skia assumes to be 0), see:
-// https://skia.org/user/api/SkRRect_Reference#SkRRect_inset
-double _measureBorderRadius(double x, double y) {
-  final double clampedX = x < 0 ? 0 : x;
-  final double clampedY = y < 0 ? 0 : y;
-  return clampedX * clampedX + clampedY * clampedY;
-}
+// Returns the squared length of the x, y (of a border radius).
+double _measureBorderRadius(double x, double y) => x*x + y*y;
 
 /// Records canvas commands to be applied to a [EngineCanvas].
 ///
diff --git a/lib/web_ui/lib/ui.dart b/lib/web_ui/lib/ui.dart
index a01d539..1878d4f 100644
--- a/lib/web_ui/lib/ui.dart
+++ b/lib/web_ui/lib/ui.dart
@@ -24,6 +24,7 @@
 part 'initialization.dart';
 part 'key.dart';
 part 'lerp.dart';
+part 'math.dart';
 part 'natives.dart';
 part 'painting.dart';
 part 'path.dart';
diff --git a/lib/web_ui/test/engine/recording_canvas_test.dart b/lib/web_ui/test/engine/recording_canvas_test.dart
index b3f80d1..1e4666e 100644
--- a/lib/web_ui/test/engine/recording_canvas_test.dart
+++ b/lib/web_ui/test/engine/recording_canvas_test.dart
@@ -121,7 +121,7 @@
       expect(mockCanvas.methodCallLog.single.methodName, 'endOfPaint');
     });
 
-    test('negative corners in inner RRect get passed through to draw', () {
+    test('deflated corners in inner RRect get passed through to draw', () {
       // This comes from github issue #40728
       final RRect outer = RRect.fromRectAndCorners(
           const Rect.fromLTWH(0, 0, 88, 48),
@@ -129,9 +129,8 @@
           bottomLeft: const Radius.circular(6));
       final RRect inner = outer.deflate(1);
 
-      // If these assertions fail, check [_measureBorderRadius] in recording_canvas.dart
-      expect(inner.brRadius, equals(const Radius.circular(-1)));
-      expect(inner.trRadius, equals(const Radius.circular(-1)));
+      expect(inner.brRadius, equals(Radius.zero));
+      expect(inner.trRadius, equals(Radius.zero));
 
       underTest.drawDRRect(outer, inner, somePaint);
       underTest.endRecording();
diff --git a/lib/web_ui/test/geometry_test.dart b/lib/web_ui/test/geometry_test.dart
index 62b7f52..24150d8 100644
--- a/lib/web_ui/test/geometry_test.dart
+++ b/lib/web_ui/test/geometry_test.dart
@@ -75,4 +75,220 @@
     expect(const Size(-1.0, -1.0).aspectRatio, 1.0);
     expect(const Size(3.0, 4.0).aspectRatio, 3.0 / 4.0);
   });
+  test('Radius.clamp() operates as expected', () {
+    final RRect rrectMin = RRect.fromLTRBR(1, 3, 5, 7,
+      const Radius.circular(-100).clamp(minimum: Radius.zero));
+
+    expect(rrectMin.left, 1);
+    expect(rrectMin.top, 3);
+    expect(rrectMin.right, 5);
+    expect(rrectMin.bottom, 7);
+    expect(rrectMin.trRadius, equals(const Radius.circular(0)));
+    expect(rrectMin.blRadius, equals(const Radius.circular(0)));
+
+    final RRect rrectMax = RRect.fromLTRBR(1, 3, 5, 7,
+      const Radius.circular(100).clamp(maximum: const Radius.circular(10)));
+
+    expect(rrectMax.left, 1);
+    expect(rrectMax.top, 3);
+    expect(rrectMax.right, 5);
+    expect(rrectMax.bottom, 7);
+    expect(rrectMax.trRadius, equals(const Radius.circular(10)));
+    expect(rrectMax.blRadius, equals(const Radius.circular(10)));
+
+    final RRect rrectMix = RRect.fromLTRBR(1, 3, 5, 7,
+      const Radius.elliptical(-100, 100).clamp(minimum: Radius.zero, maximum: const Radius.circular(10)));
+
+    expect(rrectMix.left, 1);
+    expect(rrectMix.top, 3);
+    expect(rrectMix.right, 5);
+    expect(rrectMix.bottom, 7);
+    expect(rrectMix.trRadius, equals(const Radius.elliptical(0, 10)));
+    expect(rrectMix.blRadius, equals(const Radius.elliptical(0, 10)));
+
+    final RRect rrectMix1 = RRect.fromLTRBR(1, 3, 5, 7,
+      const Radius.elliptical(100, -100).clamp(minimum: Radius.zero, maximum: const Radius.circular(10)));
+
+    expect(rrectMix1.left, 1);
+    expect(rrectMix1.top, 3);
+    expect(rrectMix1.right, 5);
+    expect(rrectMix1.bottom, 7);
+    expect(rrectMix1.trRadius, equals(const Radius.elliptical(10, 0)));
+    expect(rrectMix1.blRadius, equals(const Radius.elliptical(10, 0)));
+  });
+  test('Radius.clampValues() operates as expected', () {
+    final RRect rrectMin = RRect.fromLTRBR(1, 3, 5, 7,
+      const Radius.circular(-100).clampValues(minimumX: 0, minimumY: 0));
+
+    expect(rrectMin.left, 1);
+    expect(rrectMin.top, 3);
+    expect(rrectMin.right, 5);
+    expect(rrectMin.bottom, 7);
+    expect(rrectMin.trRadius, equals(const Radius.circular(0)));
+    expect(rrectMin.blRadius, equals(const Radius.circular(0)));
+
+    final RRect rrectMax = RRect.fromLTRBR(1, 3, 5, 7,
+      const Radius.circular(100).clampValues(maximumX: 10, maximumY: 20));
+
+    expect(rrectMax.left, 1);
+    expect(rrectMax.top, 3);
+    expect(rrectMax.right, 5);
+    expect(rrectMax.bottom, 7);
+    expect(rrectMax.trRadius, equals(const Radius.elliptical(10, 20)));
+    expect(rrectMax.blRadius, equals(const Radius.elliptical(10, 20)));
+
+    final RRect rrectMix = RRect.fromLTRBR(1, 3, 5, 7,
+      const Radius.elliptical(-100, 100).clampValues(minimumX: 5, minimumY: 6, maximumX: 10, maximumY: 20));
+
+    expect(rrectMix.left, 1);
+    expect(rrectMix.top, 3);
+    expect(rrectMix.right, 5);
+    expect(rrectMix.bottom, 7);
+    expect(rrectMix.trRadius, equals(const Radius.elliptical(5, 20)));
+    expect(rrectMix.blRadius, equals(const Radius.elliptical(5, 20)));
+
+    final RRect rrectMix2 = RRect.fromLTRBR(1, 3, 5, 7,
+      const Radius.elliptical(100, -100).clampValues(minimumX: 5, minimumY: 6, maximumX: 10, maximumY: 20));
+
+    expect(rrectMix2.left, 1);
+    expect(rrectMix2.top, 3);
+    expect(rrectMix2.right, 5);
+    expect(rrectMix2.bottom, 7);
+    expect(rrectMix2.trRadius, equals(const Radius.elliptical(10, 6)));
+    expect(rrectMix2.blRadius, equals(const Radius.elliptical(10, 6)));
+  });
+  test('RRect asserts when corner radii are negative', () {
+    bool assertsEnabled = false;
+    assert(() {
+      assertsEnabled = true;
+      return true;
+    }());
+    if (!assertsEnabled) {
+      return;
+    }
+
+    expect(() {
+      RRect.fromRectAndCorners(
+        const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+        topLeft: const Radius.circular(-1),
+      );
+    }, throwsA(isA<AssertionError>()));
+
+    expect(() {
+      RRect.fromRectAndCorners(
+        const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+        topRight: const Radius.circular(-2),
+      );
+    }, throwsA(isA<AssertionError>()));
+
+    expect(() {
+      RRect.fromRectAndCorners(
+        const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+        bottomLeft: const Radius.circular(-3),
+      );
+    }, throwsA(isA<AssertionError>()));
+
+    expect(() {
+      RRect.fromRectAndCorners(
+        const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+        bottomRight: const Radius.circular(-4),
+      );
+    }, throwsA(isA<AssertionError>()));
+  });
+  test('RRect.inflate clamps when deflating past zero', () {
+    RRect rrect = RRect.fromRectAndCorners(
+      const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+      topLeft: const Radius.circular(1),
+      topRight: const Radius.circular(2),
+      bottomLeft: const Radius.circular(3),
+      bottomRight: const Radius.circular(4),
+    ).inflate(-1);
+
+    expect(rrect.tlRadiusX, 0);
+    expect(rrect.tlRadiusY, 0);
+    expect(rrect.trRadiusX, 1);
+    expect(rrect.trRadiusY, 1);
+    expect(rrect.blRadiusX, 2);
+    expect(rrect.blRadiusY, 2);
+    expect(rrect.brRadiusX, 3);
+    expect(rrect.brRadiusY, 3);
+
+    rrect = rrect.inflate(-1);
+    expect(rrect.tlRadiusX, 0);
+    expect(rrect.tlRadiusY, 0);
+    expect(rrect.trRadiusX, 0);
+    expect(rrect.trRadiusY, 0);
+    expect(rrect.blRadiusX, 1);
+    expect(rrect.blRadiusY, 1);
+    expect(rrect.brRadiusX, 2);
+    expect(rrect.brRadiusY, 2);
+
+    rrect = rrect.inflate(-1);
+    expect(rrect.tlRadiusX, 0);
+    expect(rrect.tlRadiusY, 0);
+    expect(rrect.trRadiusX, 0);
+    expect(rrect.trRadiusY, 0);
+    expect(rrect.blRadiusX, 0);
+    expect(rrect.blRadiusY, 0);
+    expect(rrect.brRadiusX, 1);
+    expect(rrect.brRadiusY, 1);
+
+    rrect = rrect.inflate(-1);
+    expect(rrect.tlRadiusX, 0);
+    expect(rrect.tlRadiusY, 0);
+    expect(rrect.trRadiusX, 0);
+    expect(rrect.trRadiusY, 0);
+    expect(rrect.blRadiusX, 0);
+    expect(rrect.blRadiusY, 0);
+    expect(rrect.brRadiusX, 0);
+    expect(rrect.brRadiusY, 0);
+  });
+  test('RRect.deflate clamps when deflating past zero', () {
+    RRect rrect = RRect.fromRectAndCorners(
+      const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+      topLeft: const Radius.circular(1),
+      topRight: const Radius.circular(2),
+      bottomLeft: const Radius.circular(3),
+      bottomRight: const Radius.circular(4),
+    ).deflate(1);
+
+    expect(rrect.tlRadiusX, 0);
+    expect(rrect.tlRadiusY, 0);
+    expect(rrect.trRadiusX, 1);
+    expect(rrect.trRadiusY, 1);
+    expect(rrect.blRadiusX, 2);
+    expect(rrect.blRadiusY, 2);
+    expect(rrect.brRadiusX, 3);
+    expect(rrect.brRadiusY, 3);
+
+    rrect = rrect.deflate(1);
+    expect(rrect.tlRadiusX, 0);
+    expect(rrect.tlRadiusY, 0);
+    expect(rrect.trRadiusX, 0);
+    expect(rrect.trRadiusY, 0);
+    expect(rrect.blRadiusX, 1);
+    expect(rrect.blRadiusY, 1);
+    expect(rrect.brRadiusX, 2);
+    expect(rrect.brRadiusY, 2);
+
+    rrect = rrect.deflate(1);
+    expect(rrect.tlRadiusX, 0);
+    expect(rrect.tlRadiusY, 0);
+    expect(rrect.trRadiusX, 0);
+    expect(rrect.trRadiusY, 0);
+    expect(rrect.blRadiusX, 0);
+    expect(rrect.blRadiusY, 0);
+    expect(rrect.brRadiusX, 1);
+    expect(rrect.brRadiusY, 1);
+
+    rrect = rrect.deflate(1);
+    expect(rrect.tlRadiusX, 0);
+    expect(rrect.tlRadiusY, 0);
+    expect(rrect.trRadiusX, 0);
+    expect(rrect.trRadiusY, 0);
+    expect(rrect.blRadiusX, 0);
+    expect(rrect.blRadiusY, 0);
+    expect(rrect.brRadiusX, 0);
+    expect(rrect.brRadiusY, 0);
+  });
 }
diff --git a/testing/dart/geometry_test.dart b/testing/dart/geometry_test.dart
index 8e0101d..8dc7f37 100644
--- a/testing/dart/geometry_test.dart
+++ b/testing/dart/geometry_test.dart
@@ -292,4 +292,225 @@
     expect(rrect.brRadiusX, 0.25);
     expect(rrect.brRadiusY, 0.75);
   });
+
+  test('Radius.clamp() operates as expected', () {
+    final RRect rrectMin = RRect.fromLTRBR(1, 3, 5, 7,
+      const Radius.circular(-100).clamp(minimum: Radius.zero));
+
+    expect(rrectMin.left, 1);
+    expect(rrectMin.top, 3);
+    expect(rrectMin.right, 5);
+    expect(rrectMin.bottom, 7);
+    expect(rrectMin.trRadius, equals(const Radius.circular(0)));
+    expect(rrectMin.blRadius, equals(const Radius.circular(0)));
+
+    final RRect rrectMax = RRect.fromLTRBR(1, 3, 5, 7,
+      const Radius.circular(100).clamp(maximum: const Radius.circular(10)));
+
+    expect(rrectMax.left, 1);
+    expect(rrectMax.top, 3);
+    expect(rrectMax.right, 5);
+    expect(rrectMax.bottom, 7);
+    expect(rrectMax.trRadius, equals(const Radius.circular(10)));
+    expect(rrectMax.blRadius, equals(const Radius.circular(10)));
+
+    final RRect rrectMix = RRect.fromLTRBR(1, 3, 5, 7,
+      const Radius.elliptical(-100, 100).clamp(minimum: Radius.zero, maximum: const Radius.circular(10)));
+
+    expect(rrectMix.left, 1);
+    expect(rrectMix.top, 3);
+    expect(rrectMix.right, 5);
+    expect(rrectMix.bottom, 7);
+    expect(rrectMix.trRadius, equals(const Radius.elliptical(0, 10)));
+    expect(rrectMix.blRadius, equals(const Radius.elliptical(0, 10)));
+
+    final RRect rrectMix1 = RRect.fromLTRBR(1, 3, 5, 7,
+      const Radius.elliptical(100, -100).clamp(minimum: Radius.zero, maximum: const Radius.circular(10)));
+
+    expect(rrectMix1.left, 1);
+    expect(rrectMix1.top, 3);
+    expect(rrectMix1.right, 5);
+    expect(rrectMix1.bottom, 7);
+    expect(rrectMix1.trRadius, equals(const Radius.elliptical(10, 0)));
+    expect(rrectMix1.blRadius, equals(const Radius.elliptical(10, 0)));
+  });
+
+  test('Radius.clampValues() operates as expected', () {
+    final RRect rrectMin = RRect.fromLTRBR(1, 3, 5, 7,
+      const Radius.circular(-100).clampValues(minimumX: 0, minimumY: 0));
+
+    expect(rrectMin.left, 1);
+    expect(rrectMin.top, 3);
+    expect(rrectMin.right, 5);
+    expect(rrectMin.bottom, 7);
+    expect(rrectMin.trRadius, equals(const Radius.circular(0)));
+    expect(rrectMin.blRadius, equals(const Radius.circular(0)));
+
+    final RRect rrectMax = RRect.fromLTRBR(1, 3, 5, 7,
+      const Radius.circular(100).clampValues(maximumX: 10, maximumY: 20));
+
+    expect(rrectMax.left, 1);
+    expect(rrectMax.top, 3);
+    expect(rrectMax.right, 5);
+    expect(rrectMax.bottom, 7);
+    expect(rrectMax.trRadius, equals(const Radius.elliptical(10, 20)));
+    expect(rrectMax.blRadius, equals(const Radius.elliptical(10, 20)));
+
+    final RRect rrectMix = RRect.fromLTRBR(1, 3, 5, 7,
+      const Radius.elliptical(-100, 100).clampValues(minimumX: 5, minimumY: 6, maximumX: 10, maximumY: 20));
+
+    expect(rrectMix.left, 1);
+    expect(rrectMix.top, 3);
+    expect(rrectMix.right, 5);
+    expect(rrectMix.bottom, 7);
+    expect(rrectMix.trRadius, equals(const Radius.elliptical(5, 20)));
+    expect(rrectMix.blRadius, equals(const Radius.elliptical(5, 20)));
+
+    final RRect rrectMix2 = RRect.fromLTRBR(1, 3, 5, 7,
+      const Radius.elliptical(100, -100).clampValues(minimumX: 5, minimumY: 6, maximumX: 10, maximumY: 20));
+
+    expect(rrectMix2.left, 1);
+    expect(rrectMix2.top, 3);
+    expect(rrectMix2.right, 5);
+    expect(rrectMix2.bottom, 7);
+    expect(rrectMix2.trRadius, equals(const Radius.elliptical(10, 6)));
+    expect(rrectMix2.blRadius, equals(const Radius.elliptical(10, 6)));
+  });
+
+  test('RRect asserts when corner radii are negative', () {
+    bool assertsEnabled = false;
+    assert(() {
+      assertsEnabled = true;
+      return true;
+    }());
+    if (!assertsEnabled) {
+      return;
+    }
+
+    expect(() {
+      RRect.fromRectAndCorners(
+        const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+        topLeft: const Radius.circular(-1),
+      );
+    }, throwsA(isInstanceOf<AssertionError>()));
+
+    expect(() {
+      RRect.fromRectAndCorners(
+        const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+        topRight: const Radius.circular(-2),
+      );
+    }, throwsA(isInstanceOf<AssertionError>()));
+
+    expect(() {
+      RRect.fromRectAndCorners(
+        const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+        bottomLeft: const Radius.circular(-3),
+      );
+    }, throwsA(isInstanceOf<AssertionError>()));
+
+    expect(() {
+      RRect.fromRectAndCorners(
+        const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+        bottomRight: const Radius.circular(-4),
+      );
+    }, throwsA(isInstanceOf<AssertionError>()));
+  });
+
+  test('RRect.inflate clamps when deflating past zero', () {
+    RRect rrect = RRect.fromRectAndCorners(
+      const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+      topLeft: const Radius.circular(1),
+      topRight: const Radius.circular(2),
+      bottomLeft: const Radius.circular(3),
+      bottomRight: const Radius.circular(4),
+    ).inflate(-1);
+
+    expect(rrect.tlRadiusX, 0);
+    expect(rrect.tlRadiusY, 0);
+    expect(rrect.trRadiusX, 1);
+    expect(rrect.trRadiusY, 1);
+    expect(rrect.blRadiusX, 2);
+    expect(rrect.blRadiusY, 2);
+    expect(rrect.brRadiusX, 3);
+    expect(rrect.brRadiusY, 3);
+
+    rrect = rrect.inflate(-1);
+    expect(rrect.tlRadiusX, 0);
+    expect(rrect.tlRadiusY, 0);
+    expect(rrect.trRadiusX, 0);
+    expect(rrect.trRadiusY, 0);
+    expect(rrect.blRadiusX, 1);
+    expect(rrect.blRadiusY, 1);
+    expect(rrect.brRadiusX, 2);
+    expect(rrect.brRadiusY, 2);
+
+    rrect = rrect.inflate(-1);
+    expect(rrect.tlRadiusX, 0);
+    expect(rrect.tlRadiusY, 0);
+    expect(rrect.trRadiusX, 0);
+    expect(rrect.trRadiusY, 0);
+    expect(rrect.blRadiusX, 0);
+    expect(rrect.blRadiusY, 0);
+    expect(rrect.brRadiusX, 1);
+    expect(rrect.brRadiusY, 1);
+
+    rrect = rrect.inflate(-1);
+    expect(rrect.tlRadiusX, 0);
+    expect(rrect.tlRadiusY, 0);
+    expect(rrect.trRadiusX, 0);
+    expect(rrect.trRadiusY, 0);
+    expect(rrect.blRadiusX, 0);
+    expect(rrect.blRadiusY, 0);
+    expect(rrect.brRadiusX, 0);
+    expect(rrect.brRadiusY, 0);
+  });
+
+  test('RRect.deflate clamps when deflating past zero', () {
+    RRect rrect = RRect.fromRectAndCorners(
+      const Rect.fromLTRB(10.0, 20.0, 30.0, 40.0),
+      topLeft: const Radius.circular(1),
+      topRight: const Radius.circular(2),
+      bottomLeft: const Radius.circular(3),
+      bottomRight: const Radius.circular(4),
+    ).deflate(1);
+
+    expect(rrect.tlRadiusX, 0);
+    expect(rrect.tlRadiusY, 0);
+    expect(rrect.trRadiusX, 1);
+    expect(rrect.trRadiusY, 1);
+    expect(rrect.blRadiusX, 2);
+    expect(rrect.blRadiusY, 2);
+    expect(rrect.brRadiusX, 3);
+    expect(rrect.brRadiusY, 3);
+
+    rrect = rrect.deflate(1);
+    expect(rrect.tlRadiusX, 0);
+    expect(rrect.tlRadiusY, 0);
+    expect(rrect.trRadiusX, 0);
+    expect(rrect.trRadiusY, 0);
+    expect(rrect.blRadiusX, 1);
+    expect(rrect.blRadiusY, 1);
+    expect(rrect.brRadiusX, 2);
+    expect(rrect.brRadiusY, 2);
+
+    rrect = rrect.deflate(1);
+    expect(rrect.tlRadiusX, 0);
+    expect(rrect.tlRadiusY, 0);
+    expect(rrect.trRadiusX, 0);
+    expect(rrect.trRadiusY, 0);
+    expect(rrect.blRadiusX, 0);
+    expect(rrect.blRadiusY, 0);
+    expect(rrect.brRadiusX, 1);
+    expect(rrect.brRadiusY, 1);
+
+    rrect = rrect.deflate(1);
+    expect(rrect.tlRadiusX, 0);
+    expect(rrect.tlRadiusY, 0);
+    expect(rrect.trRadiusX, 0);
+    expect(rrect.trRadiusY, 0);
+    expect(rrect.blRadiusX, 0);
+    expect(rrect.blRadiusY, 0);
+    expect(rrect.brRadiusX, 0);
+    expect(rrect.brRadiusY, 0);
+  });
 }
diff --git a/tools/gn b/tools/gn
index 7b218ff..f49a3c2 100755
--- a/tools/gn
+++ b/tools/gn
@@ -211,6 +211,14 @@
 
   gn_args = {}
 
+  gn_args['is_debug'] = args.unoptimized
+
+  # If building for WASM, set the GN args using 'to_gn_wasm_args' as most
+  # of the Flutter SDK specific arguments are unused.
+  if args.target_os == 'wasm':
+    to_gn_wasm_args(args, gn_args)
+    return gn_args
+
   if args.bitcode:
     if args.target_os != 'ios':
       raise Exception('Bitcode is only supported for iOS')
@@ -237,13 +245,8 @@
 
   if args.enable_skshaper:
     gn_args['skia_use_icu'] = True
-    if args.target_os != 'wasm':
-      gn_args['flutter_always_use_skshaper'] = args.always_use_skshaper
-    else:
-      gn_args['skia_use_harfbuzz'] = True
-      gn_args['icu_use_data_file'] = False
+    gn_args['flutter_always_use_skshaper'] = args.always_use_skshaper
   gn_args['is_official_build'] = True  # Disable Skia test utilities.
-  gn_args['is_debug'] = args.unoptimized
   gn_args['android_full_debug'
          ] = args.target_os == 'android' and args.unoptimized
   if args.clang is None:
@@ -253,8 +256,6 @@
 
   if args.target_os == 'android' or args.target_os == 'ios':
     gn_args['skia_gl_standard'] = 'gles'
-  elif args.target_os == 'wasm':
-    gn_args['skia_gl_standard'] = 'webgl'
   else:
     # We explicitly don't want to pick GL because we run GLES tests using SwiftShader.
     gn_args['skia_gl_standard'] = ''
@@ -318,36 +319,34 @@
     gn_args['dart_debug'] = True
     gn_args['dart_debug_optimization_level'] = '0'
 
-  # Flutter-specific arguments which don't apply for a CanvasKit build.
-  if args.target_os != 'wasm':
-    gn_args['flutter_use_fontconfig'] = args.enable_fontconfig
-    gn_args['flutter_enable_skshaper'] = args.enable_skshaper
-    gn_args['dart_component_kind'
-           ] = 'static_library'  # Always link Dart in statically.
-    gn_args['embedder_for_target'] = args.embedder_for_target
-    gn_args['dart_lib_export_symbols'] = False
-    gn_args['flutter_runtime_mode'] = runtime_mode
-    gn_args['full_dart_sdk'] = args.full_dart_sdk
-    gn_args['dart_version_git_info'] = not args.no_dart_version_git_info
+  gn_args['flutter_use_fontconfig'] = args.enable_fontconfig
+  gn_args['flutter_enable_skshaper'] = args.enable_skshaper
+  gn_args['dart_component_kind'
+         ] = 'static_library'  # Always link Dart in statically.
+  gn_args['embedder_for_target'] = args.embedder_for_target
+  gn_args['dart_lib_export_symbols'] = False
+  gn_args['flutter_runtime_mode'] = runtime_mode
+  gn_args['full_dart_sdk'] = args.full_dart_sdk
+  gn_args['dart_version_git_info'] = not args.no_dart_version_git_info
 
-    gn_args['dart_lib_export_symbols'] = False
-    if runtime_mode == 'debug':
-      gn_args['dart_runtime_mode'] = 'develop'
-    elif runtime_mode == 'jit_release':
-      gn_args['dart_runtime_mode'] = 'release'
-    else:
-      gn_args['dart_runtime_mode'] = runtime_mode
+  gn_args['dart_lib_export_symbols'] = False
+  if runtime_mode == 'debug':
+    gn_args['dart_runtime_mode'] = 'develop'
+  elif runtime_mode == 'jit_release':
+    gn_args['dart_runtime_mode'] = 'release'
+  else:
+    gn_args['dart_runtime_mode'] = runtime_mode
 
-    # Desktop embeddings can have more dependencies than the engine library,
-    # which can be problematic in some build environments (e.g., building on
-    # Linux will bring in pkg-config dependencies at generation time). These
-    # flags allow preventing those those targets from being part of the build
-    # tree.
-    gn_args['enable_desktop_embeddings'] = not args.disable_desktop_embeddings
+  # Desktop embeddings can have more dependencies than the engine library,
+  # which can be problematic in some build environments (e.g., building on
+  # Linux will bring in pkg-config dependencies at generation time). These
+  # flags allow preventing those those targets from being part of the build
+  # tree.
+  gn_args['enable_desktop_embeddings'] = not args.disable_desktop_embeddings
 
-    # Overrides whether Boring SSL is compiled with system as. Only meaningful
-    # on Android.
-    gn_args['bssl_use_clang_integrated_as'] = True
+  # Overrides whether Boring SSL is compiled with system as. Only meaningful
+  # on Android.
+  gn_args['bssl_use_clang_integrated_as'] = True
 
   if args.allow_deprecated_api_calls:
     gn_args['allow_deprecated_api_calls'] = args.allow_deprecated_api_calls
@@ -417,8 +416,7 @@
   else:
     gn_args['skia_use_gl'] = args.target_os != 'fuchsia'
 
-  if sys.platform == 'darwin' and args.target_os not in ['android', 'fuchsia',
-                                                         'wasm']:
+  if sys.platform == 'darwin' and args.target_os not in ['android', 'fuchsia']:
     # OpenGL is deprecated on macOS > 10.11.
     # This is not necessarily needed but enabling this until we have a way to
     # build a macOS metal only shell and a gl only shell.
@@ -429,7 +427,7 @@
   # Enable Vulkan on all platforms except for Android and iOS. This is just
   # to save on mobile binary size, as there's no reason the Vulkan embedder
   # features can't work on these platforms.
-  if args.target_os not in ['android', 'ios', 'wasm']:
+  if args.target_os not in ['android', 'ios']:
     gn_args['skia_use_vulkan'] = True
     gn_args['skia_vulkan_memory_allocator_dir'
            ] = '//third_party/vulkan_memory_allocator'
@@ -569,6 +567,67 @@
   return gn_args
 
 
+# When building for WASM, almost all GN args used in the Flutter SDK
+# build are unused. This method is used instead.
+def to_gn_wasm_args(args, gn_args):
+  gn_args['is_official_build'] = True
+  gn_args['skia_enable_flutter_defines'] = True
+  gn_args['is_component_build'] = False
+  gn_args['use_clang_static_analyzer'] = False
+  gn_args['is_clang'] = True
+  gn_args['target_os'] = 'wasm'
+  gn_args['target_cpu'] = 'wasm'
+  gn_args['skia_use_angle'] = False
+  gn_args['skia_use_dng_sdk'] = False
+  gn_args['skia_use_expat'] = False
+  gn_args['skia_use_vulkan'] = False
+  gn_args['skia_use_webgpu'] = False
+  gn_args['skia_use_libheif'] = False
+  gn_args['skia_use_libjpeg_turbo_decode'] = True
+  gn_args['skia_use_libjpeg_turbo_encode'] = False
+  gn_args['skia_use_libpng_decode'] = True
+  gn_args['skia_use_libpng_encode'] = True
+  gn_args['skia_use_libwebp_decode'] = True
+  gn_args['skia_use_libwebp_encode'] = False
+  gn_args['skia_use_lua'] = False
+  gn_args['skia_use_wuffs'] = True
+  gn_args['skia_use_zlib'] = True
+  gn_args['skia_gl_standard'] = 'webgl'
+  gn_args['skia_enable_gpu'] = True
+  gn_args['skia_enable_sksl_tracing'] = False
+  gn_args['skia_use_icu'] = True
+  gn_args['icu_use_data_file'] = False
+  gn_args['skia_use_freetype'] = True
+  gn_args['skia_use_harfbuzz'] = True
+  gn_args['skia_use_fontconfig'] = False
+  gn_args['skia_use_libheif'] = False
+  gn_args['skia_enable_fontmgr_custom_directory'] = False
+  gn_args['skia_enable_fontmgr_custom_embedded'] = True
+  gn_args['skia_enable_fontmgr_custom_empty'] = False
+  gn_args['skia_enable_skshaper'] = True
+  gn_args['skia_enable_skparagraph'] = True
+  gn_args['skia_canvaskit_force_tracing'] = False
+  gn_args['skia_canvaskit_enable_skp_serialization'] = True
+  gn_args['skia_canvaskit_enable_effects_deserialization'] = False
+  gn_args['skia_canvaskit_enable_skottie'] = False
+  gn_args['skia_canvaskit_include_viewer'] = False
+  gn_args['skia_canvaskit_enable_particles'] = False
+  gn_args['skia_canvaskit_enable_pathops'] = True
+  gn_args['skia_canvaskit_enable_rt_shader'] = True
+  gn_args['skia_canvaskit_enable_matrix_helper'] = False
+  gn_args['skia_canvaskit_enable_canvas_bindings'] = False
+  gn_args['skia_canvaskit_enable_font'] = True
+  gn_args['skia_canvaskit_enable_embedded_font'] = True
+  gn_args['skia_canvaskit_enable_alias_font'] = True
+  gn_args['skia_canvaskit_legacy_draw_vertices_blend_mode'] = False
+  gn_args['skia_canvaskit_enable_debugger'] = False
+  gn_args['skia_canvaskit_enable_paragraph'] = True
+  gn_args['skia_canvaskit_enable_webgl'] = True
+  gn_args['skia_canvaskit_enable_webgpu'] = False
+  is_profile_build = args.runtime_mode == 'profile' or args.runtime_mode == 'debug'
+  gn_args['skia_canvaskit_profile_build'] = is_profile_build
+
+
 def parse_args(args):
   args = args[1:]
   parser = argparse.ArgumentParser(description='A script to run `gn gen`.')
diff --git a/wasm/BUILD.gn b/wasm/BUILD.gn
new file mode 100644
index 0000000..7d799fe
--- /dev/null
+++ b/wasm/BUILD.gn
@@ -0,0 +1,12 @@
+# Copyright 2022 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.
+
+# This BUILD.gn is kept separate from //flutter/BUILD.gn because
+# //flutter/BUILD.gn pulls in Flutter SDK dependencies which will crash
+# when the target CPU is WASM.
+
+# This is the default target when building when the target CPU is WASM.
+group("wasm") {
+  deps = [ "//third_party/skia/modules/canvaskit" ]
+}