Add StarBorder and StarBorder.polygon, with example. (#108489)

diff --git a/dev/bots/analyze.dart b/dev/bots/analyze.dart
index f3543fb..b7ff8b6 100644
--- a/dev/bots/analyze.dart
+++ b/dev/bots/analyze.dart
@@ -412,7 +412,7 @@
     if (needsTag) {
       if (!hasTagNotation) {
         errors.add('${file.path}: Files containing golden tests must be tagged using '
-            '`@Tags(...)` at the top of the file before import statements.');
+            "@Tags(<String>['reduced-test-set']) at the top of the file before import statements.");
       } else if (!hasReducedTag) {
         errors.add('${file.path}: Files containing golden tests must be tagged with '
             "'reduced-test-set'.");
diff --git a/dev/bots/test/analyze_test.dart b/dev/bots/test/analyze_test.dart
index b38609d..b5c747f 100644
--- a/dev/bots/test/analyze_test.dart
+++ b/dev/bots/test/analyze_test.dart
@@ -74,10 +74,9 @@
 
   test('analyze.dart - verifyGoldenTags', () async {
     final String result = await capture(() => verifyGoldenTags(testRootPath, minimumMatches: 6), exitCode: 1);
-    const String noTag = 'Files containing golden tests must be '
-        'tagged using `@Tags(...)` at the top of the file before import statements.';
-    const String missingTag = 'Files containing golden tests must be '
-        "tagged with 'reduced-test-set'.";
+    const String noTag = "Files containing golden tests must be tagged using @Tags(<String>['reduced-test-set']) "
+        'at the top of the file before import statements.';
+    const String missingTag = "Files containing golden tests must be tagged with 'reduced-test-set'.";
     String lines = <String>[
         'test/analyze-test-input/root/packages/foo/golden_missing_tag.dart: $missingTag',
         'test/analyze-test-input/root/packages/foo/golden_no_tag.dart: $noTag',
diff --git a/dev/manual_tests/lib/star_border.dart b/dev/manual_tests/lib/star_border.dart
new file mode 100644
index 0000000..6286af6
--- /dev/null
+++ b/dev/manual_tests/lib/star_border.dart
@@ -0,0 +1,555 @@
+// 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 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+void main() => runApp(const MyApp());
+
+enum LerpTarget {
+  circle,
+  roundedRect,
+  rect,
+  stadium,
+  polygon,
+  star,
+}
+
+class MyApp extends StatelessWidget {
+  const MyApp({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return const MaterialApp(
+      home: MyHomePage(),
+    );
+  }
+}
+
+class MyHomePage extends StatefulWidget {
+  const MyHomePage({super.key});
+
+  @override
+  State<MyHomePage> createState() => _MyHomePageState();
+}
+
+class _MyHomePageState extends State<MyHomePage> {
+  static final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
+  final OptionModel _model = OptionModel();
+  final TextEditingController textController = TextEditingController();
+
+  @override
+  void initState() {
+    super.initState();
+    _model.addListener(_modelChanged);
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    _model.removeListener(_modelChanged);
+  }
+
+  void _modelChanged() {
+    setState(() {});
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return SafeArea(
+      child: Scaffold(
+        key: scaffoldKey,
+        appBar: AppBar(
+          title: const Text('Star Border'),
+          backgroundColor: const Color(0xff323232),
+        ),
+        body: Column(
+          children: <Widget>[
+            Container(color: Colors.grey.shade200, child: Options(_model)),
+            Expanded(
+              child: Row(
+                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+                children: <Widget>[
+                  Container(
+                    key: UniqueKey(),
+                    alignment: Alignment.center,
+                    width: 300,
+                    height: 200,
+                    decoration: ShapeDecoration(
+                      color: Colors.blue.shade100,
+                      shape: lerpBorder(
+                        StarBorder.polygon(
+                          side: const BorderSide(strokeAlign: StrokeAlign.center, width: 2),
+                          sides: _model.points,
+                          pointRounding: _model.pointRounding,
+                          rotation: _model.rotation,
+                          squash: _model.squash,
+                        ),
+                        _model._lerpTarget,
+                        _model._lerpAmount,
+                        to: _model.lerpTo,
+                      )!,
+                    ),
+                    child: const Text('Polygon'),
+                  ),
+                  Container(
+                    key: UniqueKey(),
+                    alignment: Alignment.center,
+                    width: 300,
+                    height: 200,
+                    decoration: ShapeDecoration(
+                      color: Colors.blue.shade100,
+                      shape: lerpBorder(
+                        StarBorder(
+                          side: const BorderSide(strokeAlign: StrokeAlign.center, width: 2),
+                          points: _model.points,
+                          innerRadiusRatio: _model.innerRadiusRatio,
+                          pointRounding: _model.pointRounding,
+                          valleyRounding: _model.valleyRounding,
+                          rotation: _model.rotation,
+                          squash: _model.squash,
+                        ),
+                        _model._lerpTarget,
+                        _model._lerpAmount,
+                        to: _model.lerpTo,
+                      )!,
+                    ),
+                    child: const Text('Star'),
+                  ),
+                ],
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}
+
+class OptionModel extends ChangeNotifier {
+  double get pointRounding => _pointRounding;
+  double _pointRounding = 0.0;
+  set pointRounding(double value) {
+    if (value != _pointRounding) {
+      _pointRounding = value;
+      if (_valleyRounding + _pointRounding > 1) {
+        _valleyRounding = 1.0 - _pointRounding;
+      }
+      notifyListeners();
+    }
+  }
+
+  double get valleyRounding => _valleyRounding;
+  double _valleyRounding = 0.0;
+  set valleyRounding(double value) {
+    if (value != _valleyRounding) {
+      _valleyRounding = value;
+      if (_valleyRounding + _pointRounding > 1) {
+        _pointRounding = 1.0 - _valleyRounding;
+      }
+      notifyListeners();
+    }
+  }
+
+  double get squash => _squash;
+  double _squash = 0.0;
+  set squash(double value) {
+    if (value != _squash) {
+      _squash = value;
+      notifyListeners();
+    }
+  }
+
+  double get rotation => _rotation;
+  double _rotation = 0.0;
+  set rotation(double value) {
+    if (value != _rotation) {
+      _rotation = value;
+      notifyListeners();
+    }
+  }
+
+  double get innerRadiusRatio => _innerRadiusRatio;
+  double _innerRadiusRatio = 0.4;
+  set innerRadiusRatio(double value) {
+    if (value != _innerRadiusRatio) {
+      _innerRadiusRatio = clampDouble(value, 0.0001, double.infinity);
+      notifyListeners();
+    }
+  }
+
+  double get points => _points;
+  double _points = 5;
+  set points(double value) {
+    if (value != _points) {
+      _points = value;
+      notifyListeners();
+    }
+  }
+
+  double get lerpAmount => _lerpAmount;
+  double _lerpAmount = 0.0;
+  set lerpAmount(double value) {
+    if (value != _lerpAmount) {
+      _lerpAmount = value;
+      notifyListeners();
+    }
+  }
+
+  bool get lerpTo => _lerpTo;
+  bool _lerpTo = true;
+  set lerpTo(bool value) {
+    if (_lerpTo != value) {
+      _lerpTo = value;
+      notifyListeners();
+    }
+  }
+
+  LerpTarget get lerpTarget => _lerpTarget;
+  LerpTarget _lerpTarget = LerpTarget.circle;
+  set lerpTarget(LerpTarget value) {
+    if (value != _lerpTarget) {
+      _lerpTarget = value;
+      notifyListeners();
+    }
+  }
+
+  void reset() {
+    final OptionModel defaultModel = OptionModel();
+    _pointRounding = defaultModel.pointRounding;
+    _valleyRounding = defaultModel.valleyRounding;
+    _rotation = defaultModel.rotation;
+    _squash = defaultModel.squash;
+    _lerpAmount = defaultModel.lerpAmount;
+    _lerpTo = defaultModel.lerpTo;
+    _lerpTarget = defaultModel.lerpTarget;
+    _innerRadiusRatio = defaultModel._innerRadiusRatio;
+    _points = defaultModel.points;
+    notifyListeners();
+  }
+}
+
+class LabeledCheckbox extends StatelessWidget {
+  const LabeledCheckbox({super.key, required this.label, this.onChanged, this.value});
+
+  final String label;
+  final ValueChanged<bool?>? onChanged;
+  final bool? value;
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      mainAxisSize: MainAxisSize.min,
+      children: <Widget>[
+        Checkbox(
+          onChanged: onChanged,
+          value: value,
+        ),
+        Text(label),
+      ],
+    );
+  }
+}
+
+class Options extends StatefulWidget {
+  const Options(this.model, {super.key});
+
+  final OptionModel model;
+
+  @override
+  State<Options> createState() => _OptionsState();
+}
+
+class _OptionsState extends State<Options> {
+  @override
+  void initState() {
+    super.initState();
+    widget.model.addListener(_modelChanged);
+  }
+
+  @override
+  void didUpdateWidget(Options oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (widget.model != oldWidget.model) {
+      oldWidget.model.removeListener(_modelChanged);
+      widget.model.addListener(_modelChanged);
+    }
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    widget.model.removeListener(_modelChanged);
+  }
+
+  void _modelChanged() {
+    setState(() {});
+  }
+
+  double sliderValue = 0.0;
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.fromLTRB(5.0, 0.0, 5.0, 10.0),
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        children: <Widget>[
+          Row(
+            children: <Widget>[
+              Expanded(
+                child: ControlSlider(
+                  label: 'Point Rounding',
+                  value: widget.model.pointRounding,
+                  onChanged: (double value) {
+                    widget.model.pointRounding = value;
+                  },
+                ),
+              ),
+              Expanded(
+                child: ControlSlider(
+                  label: 'Valley Rounding',
+                  value: widget.model.valleyRounding,
+                  onChanged: (double value) {
+                    widget.model.valleyRounding = value;
+                  },
+                ),
+              ),
+            ],
+          ),
+          Row(
+            children: <Widget>[
+              Expanded(
+                child: ControlSlider(
+                  label: 'Squash',
+                  value: widget.model.squash,
+                  onChanged: (double value) {
+                    widget.model.squash = value;
+                  },
+                ),
+              ),
+              Expanded(
+                child: ControlSlider(
+                  label: 'Rotation',
+                  value: widget.model.rotation,
+                  max: 360,
+                  onChanged: (double value) {
+                    widget.model.rotation = value;
+                  },
+                ),
+              ),
+            ],
+          ),
+          Row(
+            children: <Widget>[
+              Expanded(
+                child: Row(
+                  children: <Widget>[
+                    Expanded(
+                      child: ControlSlider(
+                        label: 'Points',
+                        value: widget.model.points,
+                        min: 2,
+                        max: 20,
+                        onChanged: (double value) {
+                          widget.model.points = value;
+                        },
+                      ),
+                    ),
+                    OutlinedButton(
+                        child: const Text('Nearest'),
+                        onPressed: () {
+                          widget.model.points = widget.model.points.roundToDouble();
+                        }),
+                  ],
+                ),
+              ),
+              Expanded(
+                child: ControlSlider(
+                  label: 'Inner Radius',
+                  value: widget.model.innerRadiusRatio,
+                  onChanged: (double value) {
+                    widget.model.innerRadiusRatio = value;
+                  },
+                ),
+              ),
+            ],
+          ),
+          Row(
+            children: <Widget>[
+              Expanded(
+                flex: 2,
+                child: Padding(
+                  padding: const EdgeInsetsDirectional.only(end: 8.0),
+                  child: ControlSlider(
+                    label: 'Lerp',
+                    value: widget.model.lerpAmount,
+                    onChanged: (double value) {
+                      widget.model.lerpAmount = value;
+                    },
+                  ),
+                ),
+              ),
+              Padding(
+                padding: const EdgeInsetsDirectional.only(start: 8.0, end: 20.0),
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: <Widget>[
+                    Row(children: <Widget>[
+                      Radio<bool>(
+                          value: true,
+                          groupValue: widget.model.lerpTo,
+                          onChanged: (bool? value) {
+                            widget.model.lerpTo = value!;
+                          }),
+                      const Text('To'),
+                    ]),
+                    Row(children: <Widget>[
+                      Radio<bool>(
+                          value: false,
+                          groupValue: widget.model.lerpTo,
+                          onChanged: (bool? value) {
+                            widget.model.lerpTo = value!;
+                          }),
+                      const Text('From'),
+                    ])
+                  ],
+                ),
+              ),
+              Expanded(
+                child: Row(
+                  children: <Widget>[
+                    Expanded(
+                      child: DropdownButton<LerpTarget>(
+                        items: LerpTarget.values.map<DropdownMenuItem<LerpTarget>>((LerpTarget target) {
+                          return DropdownMenuItem<LerpTarget>(value: target, child: Text(target.name));
+                        }).toList(),
+                        value: widget.model.lerpTarget,
+                        onChanged: (LerpTarget? value) {
+                          if (value == null) {
+                            return;
+                          }
+                          widget.model.lerpTarget = value;
+                        },
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            ],
+          ),
+          ElevatedButton(
+            onPressed: () {
+              widget.model.reset();
+              sliderValue = 0.0;
+            },
+            child: const Text('Reset'),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class ControlSlider extends StatelessWidget {
+  const ControlSlider({
+    super.key,
+    required this.label,
+    required this.value,
+    required this.onChanged,
+    this.min = 0.0,
+    this.max = 1.0,
+  });
+
+  final String label;
+  final double value;
+  final void Function(double value) onChanged;
+  final double min;
+  final double max;
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.all(4.0),
+      child: Row(
+        mainAxisSize: MainAxisSize.min,
+        children: <Widget>[
+          Text(label),
+          Expanded(
+            child: Slider(
+              label: value.toStringAsFixed(1),
+              onChanged: onChanged,
+              min: min,
+              max: max,
+              value: value,
+            ),
+          ),
+          Text(
+            value.toStringAsFixed(3),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+const Color lerpToColor = Colors.red;
+const BorderSide lerpToBorder = BorderSide(width: 5, color: lerpToColor);
+
+ShapeBorder? lerpBorder(StarBorder border, LerpTarget target, double t, {bool to = true}) {
+  switch (target) {
+    case LerpTarget.circle:
+      if (to) {
+        return border.lerpTo(const CircleBorder(side: lerpToBorder), t);
+      } else {
+        return border.lerpFrom(const CircleBorder(side: lerpToBorder), t);
+      }
+    case LerpTarget.roundedRect:
+      if (to) {
+        return border.lerpTo(
+          const RoundedRectangleBorder(
+            side: lerpToBorder,
+            borderRadius: BorderRadius.all(
+              Radius.circular(10),
+            ),
+          ),
+          t,
+        );
+      } else {
+        return border.lerpFrom(
+          const RoundedRectangleBorder(
+            side: lerpToBorder,
+            borderRadius: BorderRadius.all(
+              Radius.circular(10),
+            ),
+          ),
+          t,
+        );
+      }
+    case LerpTarget.rect:
+      if (to) {
+        return border.lerpTo(const RoundedRectangleBorder(side: lerpToBorder), t);
+      } else {
+        return border.lerpFrom(const RoundedRectangleBorder(side: lerpToBorder), t);
+      }
+    case LerpTarget.stadium:
+      if (to) {
+        return border.lerpTo(const StadiumBorder(side: lerpToBorder), t);
+      } else {
+        return border.lerpFrom(const StadiumBorder(side: lerpToBorder), t);
+      }
+    case LerpTarget.polygon:
+      if (to) {
+        return border.lerpTo(const StarBorder.polygon(side: lerpToBorder, sides: 4), t);
+      } else {
+        return border.lerpFrom(const StarBorder.polygon(side: lerpToBorder, sides: 4), t);
+      }
+    case LerpTarget.star:
+      if (to) {
+        return border.lerpTo(const StarBorder(side: lerpToBorder, innerRadiusRatio: .5), t);
+      } else {
+        return border.lerpFrom(const StarBorder(side: lerpToBorder, innerRadiusRatio: .5), t);
+      }
+  }
+}
diff --git a/examples/api/lib/painting/star_border/star_border.0.dart b/examples/api/lib/painting/star_border/star_border.0.dart
new file mode 100644
index 0000000..d2cb175
--- /dev/null
+++ b/examples/api/lib/painting/star_border/star_border.0.dart
@@ -0,0 +1,454 @@
+// 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.
+
+/// An example showing usage of [StarBorder].
+
+import 'package:flutter/material.dart';
+
+const int _kParameterPrecision = 2;
+
+void main() => runApp(const StarBorderApp());
+
+class StarBorderApp extends StatelessWidget {
+  const StarBorderApp({super.key});
+
+  @override
+  Widget build(BuildContext context) {
+    return MaterialApp(
+      home: Scaffold(
+        appBar: AppBar(
+          title: const Text('StarBorder Example'),
+          backgroundColor: const Color(0xff323232),
+        ),
+        body: const StarBorderExample(),
+      ),
+    );
+  }
+}
+
+class StarBorderExample extends StatefulWidget {
+  const StarBorderExample({super.key});
+
+  @override
+  State<StarBorderExample> createState() => _StarBorderExampleState();
+}
+
+class _StarBorderExampleState extends State<StarBorderExample> {
+  final OptionModel _model = OptionModel();
+  final TextEditingController _textController = TextEditingController();
+
+  @override
+  void initState() {
+    super.initState();
+    _model.addListener(_modelChanged);
+  }
+
+  @override
+  void dispose() {
+    _model.removeListener(_modelChanged);
+    _textController.dispose();
+    super.dispose();
+  }
+
+  void _modelChanged() {
+    setState(() {});
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return DefaultTextStyle(
+      style: const TextStyle(
+        color: Colors.black,
+        fontSize: 14.0,
+        fontFamily: 'Roboto',
+        fontStyle: FontStyle.normal,
+      ),
+      child: ListView(
+        children: <Widget>[
+          Container(
+            color: Colors.grey.shade200,
+            child: Options(_model),
+          ),
+          Padding(
+            padding: const EdgeInsets.all(18.0),
+            child: Row(
+              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+              children: <Widget>[
+                Expanded(
+                  child: ExampleBorder(
+                    border: StarBorder(
+                      side: const BorderSide(),
+                      points: _model.points,
+                      innerRadiusRatio: _model.innerRadiusRatio,
+                      pointRounding: _model.pointRounding,
+                      valleyRounding: _model.valleyRounding,
+                      rotation: _model.rotation,
+                      squash: _model.squash,
+                    ),
+                    title: 'Star',
+                  ),
+                ),
+                Expanded(
+                  child: ExampleBorder(
+                    border: StarBorder.polygon(
+                      side: const BorderSide(),
+                      sides: _model.points,
+                      pointRounding: _model.pointRounding,
+                      rotation: _model.rotation,
+                      squash: _model.squash,
+                    ),
+                    title: 'Polygon',
+                  ),
+                ),
+              ],
+            ),
+          ),
+          Row(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: <Widget>[
+              Expanded(
+                child: Container(
+                  color: Colors.black12,
+                  margin: const EdgeInsets.all(16.0),
+                  padding: const EdgeInsets.all(16.0),
+                  child: SelectableText(_model.starCode),
+                ),
+              ),
+              Expanded(
+                child: Container(
+                  color: Colors.black12,
+                  margin: const EdgeInsets.all(16.0),
+                  padding: const EdgeInsets.all(16.0),
+                  child: SelectableText(_model.polygonCode),
+                ),
+              ),
+            ],
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class ExampleBorder extends StatelessWidget {
+  const ExampleBorder({
+    super.key,
+    required this.border,
+    required this.title,
+  });
+
+  final StarBorder border;
+  final String title;
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      key: UniqueKey(),
+      alignment: Alignment.center,
+      padding: const EdgeInsets.all(20),
+      width: 150,
+      height: 100,
+      decoration: ShapeDecoration(
+        color: Colors.blue.shade100,
+        shape: border,
+      ),
+      child: Text(title),
+    );
+  }
+}
+
+class Options extends StatefulWidget {
+  const Options(this.model, {super.key});
+
+  final OptionModel model;
+
+  @override
+  State<Options> createState() => _OptionsState();
+}
+
+class _OptionsState extends State<Options> {
+  @override
+  void initState() {
+    super.initState();
+    widget.model.addListener(_modelChanged);
+  }
+
+  @override
+  void didUpdateWidget(Options oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (widget.model != oldWidget.model) {
+      oldWidget.model.removeListener(_modelChanged);
+      widget.model.addListener(_modelChanged);
+    }
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    widget.model.removeListener(_modelChanged);
+  }
+
+  void _modelChanged() {
+    setState(() {});
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.fromLTRB(5.0, 0.0, 5.0, 10.0),
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        children: <Widget>[
+          Row(
+            children: <Widget>[
+              Expanded(
+                child: ControlSlider(
+                  label: 'Point Rounding',
+                  value: widget.model.pointRounding,
+                  onChanged: (double value) {
+                    widget.model.pointRounding = value;
+                  },
+                ),
+              ),
+              Expanded(
+                child: ControlSlider(
+                  label: 'Valley Rounding',
+                  value: widget.model.valleyRounding,
+                  onChanged: (double value) {
+                    widget.model.valleyRounding = value;
+                  },
+                ),
+              ),
+            ],
+          ),
+          Row(
+            children: <Widget>[
+              Expanded(
+                child: ControlSlider(
+                  label: 'Squash',
+                  value: widget.model.squash,
+                  onChanged: (double value) {
+                    widget.model.squash = value;
+                  },
+                ),
+              ),
+              Expanded(
+                child: ControlSlider(
+                  label: 'Rotation',
+                  value: widget.model.rotation,
+                  max: 360,
+                  onChanged: (double value) {
+                    widget.model.rotation = value;
+                  },
+                ),
+              ),
+            ],
+          ),
+          Row(
+            children: <Widget>[
+              Expanded(
+                child: Row(
+                  children: <Widget>[
+                    Expanded(
+                      child: ControlSlider(
+                        label: 'Points',
+                        value: widget.model.points,
+                        min: 3,
+                        max: 20,
+                        precision: 1,
+                        onChanged: (double value) {
+                          widget.model.points = value;
+                        },
+                      ),
+                    ),
+                    Tooltip(
+                      message: 'Round the number of points to the nearest integer.',
+                      child: Padding(
+                        padding: const EdgeInsets.all(8.0),
+                        child: OutlinedButton(
+                          child: const Text('Nearest'),
+                          onPressed: () {
+                            widget.model.points = widget.model.points.roundToDouble();
+                          },
+                        ),
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+              Expanded(
+                child: ControlSlider(
+                  label: 'Inner Radius',
+                  value: widget.model.innerRadiusRatio,
+                  onChanged: (double value) {
+                    widget.model.innerRadiusRatio = value;
+                  },
+                ),
+              ),
+            ],
+          ),
+          ElevatedButton(
+            onPressed: () {
+              widget.model.reset();
+            },
+            child: const Text('Reset'),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class OptionModel extends ChangeNotifier {
+  double get pointRounding => _pointRounding;
+  double _pointRounding = 0.0;
+  set pointRounding(double value) {
+    if (value != _pointRounding) {
+      _pointRounding = value;
+      if (_valleyRounding + _pointRounding > 1) {
+        _valleyRounding = 1.0 - _pointRounding;
+      }
+      notifyListeners();
+    }
+  }
+
+  double get valleyRounding => _valleyRounding;
+  double _valleyRounding = 0.0;
+  set valleyRounding(double value) {
+    if (value != _valleyRounding) {
+      _valleyRounding = value;
+      if (_valleyRounding + _pointRounding > 1) {
+        _pointRounding = 1.0 - _valleyRounding;
+      }
+      notifyListeners();
+    }
+  }
+
+  double get squash => _squash;
+  double _squash = 0.0;
+  set squash(double value) {
+    if (value != _squash) {
+      _squash = value;
+      notifyListeners();
+    }
+  }
+
+  double get rotation => _rotation;
+  double _rotation = 0.0;
+  set rotation(double value) {
+    if (value != _rotation) {
+      _rotation = value;
+      notifyListeners();
+    }
+  }
+
+  double get innerRadiusRatio => _innerRadiusRatio;
+  double _innerRadiusRatio = 0.4;
+  set innerRadiusRatio(double value) {
+    if (value != _innerRadiusRatio) {
+      _innerRadiusRatio = value.clamp(0.0001, double.infinity);
+      notifyListeners();
+    }
+  }
+
+  double get points => _points;
+  double _points = 5;
+  set points(double value) {
+    if (value != _points) {
+      _points = value;
+      notifyListeners();
+    }
+  }
+
+  String get starCode {
+    return 'Container(\n'
+        '  decoration: ShapeDecoration(\n'
+        '    shape: StarBorder(\n'
+        '      points: ${points.toStringAsFixed(_kParameterPrecision)},\n'
+        '      rotation: ${rotation.toStringAsFixed(_kParameterPrecision)},\n'
+        '      innerRadiusRatio: ${innerRadiusRatio.toStringAsFixed(_kParameterPrecision)},\n'
+        '      pointRounding: ${pointRounding.toStringAsFixed(_kParameterPrecision)},\n'
+        '      valleyRounding: ${valleyRounding.toStringAsFixed(_kParameterPrecision)},\n'
+        '      squash: ${squash.toStringAsFixed(_kParameterPrecision)},\n'
+        '    ),\n'
+        '  ),\n'
+        ');';
+  }
+
+  String get polygonCode {
+    return 'Container(\n'
+        '  decoration: ShapeDecoration(\n'
+        '    shape: StarBorder.polygon(\n'
+        '      sides: ${points.toStringAsFixed(_kParameterPrecision)},\n'
+        '      rotation: ${rotation.toStringAsFixed(_kParameterPrecision)},\n'
+        '      cornerRounding: ${pointRounding.toStringAsFixed(_kParameterPrecision)},\n'
+        '      squash: ${squash.toStringAsFixed(_kParameterPrecision)},\n'
+        '    ),\n'
+        '  ),\n'
+        ');';
+  }
+
+  void reset() {
+    final OptionModel defaultModel = OptionModel();
+    _pointRounding = defaultModel.pointRounding;
+    _valleyRounding = defaultModel.valleyRounding;
+    _rotation = defaultModel.rotation;
+    _squash = defaultModel.squash;
+    _innerRadiusRatio = defaultModel._innerRadiusRatio;
+    _points = defaultModel.points;
+    notifyListeners();
+  }
+}
+
+class ControlSlider extends StatelessWidget {
+  const ControlSlider({
+    super.key,
+    required this.label,
+    required this.value,
+    required this.onChanged,
+    this.min = 0.0,
+    this.max = 1.0,
+    this.precision = _kParameterPrecision,
+  });
+
+  final String label;
+  final double value;
+  final void Function(double value) onChanged;
+  final double min;
+  final double max;
+  final int precision;
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: const EdgeInsets.all(4.0),
+      child: Row(
+        mainAxisSize: MainAxisSize.min,
+        children: <Widget>[
+          Expanded(
+            flex: 2,
+            child: Text(
+              label,
+              textAlign: TextAlign.end,
+            ),
+          ),
+          Expanded(
+            flex: 5,
+            child: Slider(
+              onChanged: onChanged,
+              min: min,
+              max: max,
+              value: value,
+            ),
+          ),
+          Expanded(
+            child: Text(
+              value.toStringAsFixed(precision),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
diff --git a/packages/flutter/lib/painting.dart b/packages/flutter/lib/painting.dart
index 33dc9da..ce86d81 100644
--- a/packages/flutter/lib/painting.dart
+++ b/packages/flutter/lib/painting.dart
@@ -56,6 +56,7 @@
 export 'src/painting/shader_warm_up.dart';
 export 'src/painting/shape_decoration.dart';
 export 'src/painting/stadium_border.dart';
+export 'src/painting/star_border.dart';
 export 'src/painting/strut_style.dart';
 export 'src/painting/text_painter.dart';
 export 'src/painting/text_span.dart';
diff --git a/packages/flutter/lib/src/material/slider.dart b/packages/flutter/lib/src/material/slider.dart
index e3b3c4e..1a5b1ab 100644
--- a/packages/flutter/lib/src/material/slider.dart
+++ b/packages/flutter/lib/src/material/slider.dart
@@ -144,7 +144,8 @@
        assert(min != null),
        assert(max != null),
        assert(min <= max),
-       assert(value >= min && value <= max),
+       assert(value >= min && value <= max,
+         'Value $value is not between minimum $min and maximum $max'),
        assert(divisions == null || divisions > 0);
 
   /// Creates an adaptive [Slider] based on the target platform, following
diff --git a/packages/flutter/lib/src/painting/star_border.dart b/packages/flutter/lib/src/painting/star_border.dart
new file mode 100644
index 0000000..c5c20b7
--- /dev/null
+++ b/packages/flutter/lib/src/painting/star_border.dart
@@ -0,0 +1,698 @@
+// 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:math' as math;
+import 'dart:ui' as ui show lerpDouble;
+
+import 'package:flutter/foundation.dart';
+
+import 'package:vector_math/vector_math_64.dart' show Matrix4;
+import 'basic_types.dart';
+import 'borders.dart';
+import 'circle_border.dart';
+import 'edge_insets.dart';
+import 'rounded_rectangle_border.dart';
+import 'stadium_border.dart';
+
+// Conversion from radians to degrees.
+const double _kRadToDeg = 180 / math.pi;
+// Conversion from degrees to radians.
+const double _kDegToRad = math.pi / 180;
+
+/// A border that fits a star or polygon-shaped border within the rectangle of
+/// the widget it is applied to.
+///
+/// Typically used with a [ShapeDecoration] to draw a polygonal or star shaped
+/// border.
+///
+/// {@tool dartpad}
+/// This example serves both as a usage example, as well as an explorer for
+/// determining the parameters to use with a [StarBorder]. The resulting code
+/// can be copied and pasted into your app. A [Container] is just one widget
+/// which takes a [ShapeBorder]. [Dialog]s, [OutlinedButton]s,
+/// [ElevatedButton]s, etc. all can be shaped with a [ShapeBorder].
+///
+/// ** See code in examples/api/lib/painting/star_border/star_border.0.dart **
+/// {@end-tool}
+///
+/// See also:
+///
+///  * [BorderSide], which is used to describe how the edge of the shape is
+///  drawn.
+class StarBorder extends OutlinedBorder {
+  /// Create a const star-shaped border with the given number [points] on the
+  /// star.
+  const StarBorder({
+    super.side,
+    this.points = 5,
+    double innerRadiusRatio = 0.4,
+    this.pointRounding = 0,
+    this.valleyRounding = 0,
+    double rotation = 0,
+    this.squash = 0,
+  })  : assert(squash >= 0),
+        assert(squash <= 1),
+        assert(pointRounding >= 0),
+        assert(pointRounding <= 1),
+        assert(valleyRounding >= 0),
+        assert(valleyRounding <= 1),
+        assert(
+            (valleyRounding + pointRounding) <= 1,
+            'The sum of valleyRounding ($valleyRounding) and '
+            'pointRounding ($pointRounding) must not exceed one.'),
+        assert(innerRadiusRatio >= 0),
+        assert(innerRadiusRatio <= 1),
+        assert(points >= 2),
+        _rotationRadians = rotation * _kDegToRad,
+        _innerRadiusRatio = innerRadiusRatio;
+
+  /// Create a const polygon border with the given number of [sides].
+  const StarBorder.polygon({
+    super.side,
+    double sides = 5,
+    this.pointRounding = 0,
+    double rotation = 0,
+    this.squash = 0,
+  })  : assert(squash >= 0),
+        assert(squash <= 1),
+        assert(pointRounding >= 0),
+        assert(pointRounding <= 1),
+        assert(sides >= 2),
+        points = sides,
+        valleyRounding = 0,
+        _rotationRadians = rotation * _kDegToRad,
+        _innerRadiusRatio = null;
+
+  /// The number of points in this star, or sides on a polygon.
+  ///
+  /// This is a floating point number: if this is not a whole number, then an
+  /// additional star point or corner shorter than the others will be added to
+  /// finish the shape. Only whole-numbered values will yield a symmetric shape.
+  ///
+  /// For stars created with [StarBorder], this the number of points on
+  /// the star. For polygons created with [StarBorder.polygon], this is the
+  /// number of sides on the polygon.
+  ///
+  /// Must be greater than or equal to two.
+  final double points;
+
+  /// The ratio of the inner radius of a star with the outer radius.
+  ///
+  /// When making a star using [StarBorder], this is the ratio of the inner
+  /// radius that to the outer radius. If it is one, then the inner radius
+  /// will equal the outer radius.
+  ///
+  /// For polygons created with [StarBorder.polygon], getting this value will
+  /// return the incircle radius of the polygon (the radius of a circle
+  /// inscribed inside the polygon).
+  ///
+  /// Defaults to 0.4 for stars, and must be between zero and one, inclusive.
+  double get innerRadiusRatio {
+    // Polygons are just a special case of a star where the inner radius is the
+    // incircle radius of the polygon (the radius of an inscribed circle).
+    return _innerRadiusRatio ?? math.cos(math.pi / points);
+  }
+
+  final double? _innerRadiusRatio;
+
+  /// The amount of rounding on the points of stars, or the corners of polygons.
+  ///
+  /// This is a value between zero and one which describes how rounded the point
+  /// or corner should be. A value of zero means no rounding (sharp corners),
+  /// and a value of one means that the entire point or corner is a portion of a
+  /// circle.
+  ///
+  /// Defaults to zero. The sum of `pointRounding` and [valleyRounding] must be
+  /// less than or equal to one.
+  final double pointRounding;
+
+  /// The amount of rounding of the interior corners of stars.
+  ///
+  /// This is a value between zero and one which describes how rounded the inner
+  /// corners in a star (the "valley" between points) should be. A value of zero
+  /// means no rounding (sharp corners), and a value of one means that the
+  /// entire corner is a portion of a circle.
+  ///
+  /// Defaults to zero. The sum of [pointRounding] and `valleyRounding` must be
+  /// less than or equal to one. For polygons created with [StarBorder.polygon],
+  /// this will always be zero.
+  final double valleyRounding;
+
+  /// The rotation in clockwise degrees around the center of the shape.
+  ///
+  /// The rotation occurs before the [squash] effect is applied, so that you can
+  /// fine tune where the points of a star or corners of a polygon start.
+  ///
+  /// Defaults to zero, meaning that the first point or corner is pointing up.
+  double get rotation => _rotationRadians * _kRadToDeg;
+  final double _rotationRadians;
+
+  /// How much of the aspect ratio of the attached widget to take on.
+  ///
+  /// If `squash` is non-zero, the border will match the aspect ratio of the
+  /// bounding box of the widget that it is attached to, which can give a
+  /// squashed appearance.
+  ///
+  /// The `squash` parameter lets you control how much of that aspect ratio this
+  /// border takes on.
+  ///
+  /// A value of zero means that the border will be drawn with a square aspect
+  /// ratio at the size of the shortest side of the bounding rectangle, ignoring
+  /// the aspect ratio of the widget, and a value of one means it will be drawn
+  /// with the aspect ratio of the widget. The value of `squash` has no effect
+  /// if the widget is square to begin with.
+  ///
+  /// Defaults to zero, and must be between zero and one, inclusive.
+  final double squash;
+
+  @override
+  EdgeInsetsGeometry get dimensions {
+    switch (side.strokeAlign) {
+      case StrokeAlign.inside:
+        return EdgeInsets.all(side.width);
+      case StrokeAlign.center:
+        return EdgeInsets.all(side.width / 2);
+      case StrokeAlign.outside:
+        return EdgeInsets.zero;
+    }
+  }
+
+  @override
+  ShapeBorder scale(double t) {
+    return StarBorder(
+      points: points,
+      side: side.scale(t),
+      rotation: rotation,
+      innerRadiusRatio: innerRadiusRatio,
+      pointRounding: pointRounding,
+      valleyRounding: valleyRounding,
+      squash: squash,
+    );
+  }
+
+  ShapeBorder? _twoPhaseLerp(
+    double t,
+    double split,
+    ShapeBorder? Function(double t) first,
+    ShapeBorder? Function(double t) second,
+  ) {
+    // If the rectangle has square corners, then skip the extra lerp to round the corners.
+    if (t < split) {
+      return first(t * (1 / split));
+    } else {
+      t = (1 / (1.0 - split)) * (t - split);
+      return second(t);
+    }
+  }
+
+  @override
+  ShapeBorder? lerpFrom(ShapeBorder? a, double t) {
+    if (t == 0) {
+      return a;
+    }
+    if (t == 1.0) {
+      return this;
+    }
+    if (a is StarBorder) {
+      return StarBorder(
+        side: BorderSide.lerp(a.side, side, t),
+        points: ui.lerpDouble(a.points, points, t)!,
+        rotation: ui.lerpDouble(a._rotationRadians, _rotationRadians, t)! * _kRadToDeg,
+        innerRadiusRatio: ui.lerpDouble(a.innerRadiusRatio, innerRadiusRatio, t)!,
+        pointRounding: ui.lerpDouble(a.pointRounding, pointRounding, t)!,
+        valleyRounding: ui.lerpDouble(a.valleyRounding, valleyRounding, t)!,
+        squash: ui.lerpDouble(a.squash, squash, t)!,
+      );
+    }
+
+    if (a is CircleBorder) {
+      if (points >= 2.5) {
+        final double lerpedPoints = ui.lerpDouble(points.round(), points, t)!;
+        return StarBorder(
+          side: BorderSide.lerp(a.side, side, t),
+          points: lerpedPoints,
+          rotation: rotation,
+          innerRadiusRatio: ui.lerpDouble(math.cos(math.pi / lerpedPoints), innerRadiusRatio, t)!,
+          pointRounding: ui.lerpDouble(1.0, pointRounding, t)!,
+          valleyRounding: ui.lerpDouble(0.0, valleyRounding, t)!,
+        );
+      } else {
+        // Have a slightly different lerp for two-pointed stars, since they get
+        // kind of squirrelly with near-zero innerRadiusRatios.
+        final double lerpedPoints = ui.lerpDouble(points, 2, t)!;
+        return StarBorder(
+          side: BorderSide.lerp(a.side, side, t),
+          points: lerpedPoints,
+          rotation: rotation,
+          innerRadiusRatio: ui.lerpDouble(1, innerRadiusRatio, t)!,
+          pointRounding: ui.lerpDouble(0.5, pointRounding, t)!,
+          valleyRounding: ui.lerpDouble(0.5, valleyRounding, t)!,
+        );
+      }
+    }
+
+    if (a is StadiumBorder) {
+      // Lerp from a stadium to a circle first, and from there to a star.
+      final BorderSide lerpedSide = BorderSide.lerp(a.side, side, t);
+      return _twoPhaseLerp(
+        t,
+        0.5,
+        (double t) => a.lerpTo(CircleBorder(side: lerpedSide), t),
+        (double t) => lerpFrom(CircleBorder(side: lerpedSide), t),
+      );
+    }
+    if (a is RoundedRectangleBorder) {
+      // Lerp from a rectangle to a stadium, then from a Stadium to a circle,
+      // then from a circle to a star.
+      final BorderSide lerpedSide = BorderSide.lerp(a.side, side, t);
+      return _twoPhaseLerp(
+        t,
+        1 / 3,
+        (double t) {
+          return StadiumBorder(side: lerpedSide).lerpFrom(a, t);
+        },
+        (double t) {
+          return _twoPhaseLerp(
+            t,
+            0.5,
+            (double t) => StadiumBorder(side: lerpedSide).lerpTo(CircleBorder(side: lerpedSide), t),
+            (double t) => lerpFrom(CircleBorder(side: lerpedSide), t),
+          );
+        },
+      );
+    }
+    return super.lerpFrom(a, t);
+  }
+
+  @override
+  ShapeBorder? lerpTo(ShapeBorder? b, double t) {
+    if (t == 0) {
+      return this;
+    }
+    if (t == 1.0) {
+      return b;
+    }
+    if (b is StarBorder) {
+      return StarBorder(
+        side: BorderSide.lerp(side, b.side, t),
+        points: ui.lerpDouble(points, b.points, t)!,
+        rotation: ui.lerpDouble(_rotationRadians, b._rotationRadians, t)! * _kRadToDeg,
+        innerRadiusRatio: ui.lerpDouble(innerRadiusRatio, b.innerRadiusRatio, t)!,
+        pointRounding: ui.lerpDouble(pointRounding, b.pointRounding, t)!,
+        valleyRounding: ui.lerpDouble(valleyRounding, b.valleyRounding, t)!,
+        squash: ui.lerpDouble(squash, b.squash, t)!,
+      );
+    }
+    if (b is CircleBorder) {
+      // Have a slightly different lerp for two-pointed stars, since they get
+      // kind of squirrelly with near-zero innerRadiusRatios.
+      if (points >= 2.5) {
+        final double lerpedPoints = ui.lerpDouble(points, points.round(), t)!;
+        return StarBorder(
+          side: BorderSide.lerp(side, b.side, t),
+          points: lerpedPoints,
+          rotation: rotation,
+          innerRadiusRatio: ui.lerpDouble(innerRadiusRatio, math.cos(math.pi / lerpedPoints), t)!,
+          pointRounding: ui.lerpDouble(pointRounding, 1.0, t)!,
+          valleyRounding: ui.lerpDouble(valleyRounding, 0.0, t)!,
+        );
+      } else {
+        final double lerpedPoints = ui.lerpDouble(points, 2, t)!;
+        return StarBorder(
+          side: BorderSide.lerp(side, b.side, t),
+          points: lerpedPoints,
+          rotation: rotation,
+          innerRadiusRatio: ui.lerpDouble(innerRadiusRatio, 1, t)!,
+          pointRounding: ui.lerpDouble(pointRounding, 0.5, t)!,
+          valleyRounding: ui.lerpDouble(valleyRounding, 0.5, t)!,
+        );
+      }
+    }
+    if (b is StadiumBorder) {
+      // Lerp to a circle first, then to a stadium.
+      final BorderSide lerpedSide = BorderSide.lerp(side, b.side, t);
+      return _twoPhaseLerp(
+        t,
+        0.5,
+        (double t) => lerpTo(CircleBorder(side: lerpedSide), t),
+        (double t) => b.lerpFrom(CircleBorder(side: lerpedSide), t),
+      );
+    }
+    if (b is RoundedRectangleBorder) {
+      // Lerp to a circle, and then to a stadium, then to a rounded rect.
+      final BorderSide lerpedSide = BorderSide.lerp(side, b.side, t);
+      return _twoPhaseLerp(
+        t,
+        2 / 3,
+        (double t) {
+          return _twoPhaseLerp(
+            t,
+            0.5,
+            (double t) => lerpTo(CircleBorder(side: lerpedSide), t),
+            (double t) => StadiumBorder(side: lerpedSide).lerpFrom(CircleBorder(side: lerpedSide), t),
+          );
+        },
+        (double t) {
+          return StadiumBorder(side: lerpedSide).lerpTo(b, t);
+        },
+      );
+    }
+    return super.lerpTo(b, t);
+  }
+
+  @override
+  StarBorder copyWith({
+    BorderSide? side,
+    double? points,
+    double? innerRadiusRatio,
+    double? pointRounding,
+    double? valleyRounding,
+    double? rotation,
+    double? squash,
+  }) {
+    return StarBorder(
+      side: side ?? this.side,
+      points: points ?? this.points,
+      rotation: rotation ?? this.rotation,
+      innerRadiusRatio: innerRadiusRatio ?? this.innerRadiusRatio,
+      pointRounding: pointRounding ?? this.pointRounding,
+      valleyRounding: valleyRounding ?? this.valleyRounding,
+      squash: squash ?? this.squash,
+    );
+  }
+
+  @override
+  Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
+    final Rect adjustedRect;
+    switch (side.strokeAlign) {
+      case StrokeAlign.inside:
+        adjustedRect = rect.deflate(side.width);
+        break;
+      case StrokeAlign.center:
+        adjustedRect = rect.deflate(side.width / 2);
+        break;
+      case StrokeAlign.outside:
+        adjustedRect = rect;
+        break;
+    }
+    return _StarGenerator(
+      points: points,
+      rotation: _rotationRadians,
+      innerRadiusRatio: innerRadiusRatio,
+      pointRounding: pointRounding,
+      valleyRounding: valleyRounding,
+      squash: squash,
+    ).generate(adjustedRect);
+  }
+
+  @override
+  Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
+    return _StarGenerator(
+      points: points,
+      rotation: _rotationRadians,
+      innerRadiusRatio: innerRadiusRatio,
+      pointRounding: pointRounding,
+      valleyRounding: valleyRounding,
+      squash: squash,
+    ).generate(rect);
+  }
+
+  @override
+  void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {
+    switch (side.style) {
+      case BorderStyle.none:
+        break;
+      case BorderStyle.solid:
+        final Rect adjustedRect;
+        switch (side.strokeAlign) {
+          case StrokeAlign.inside:
+            adjustedRect = rect.deflate(side.width / 2);
+            break;
+          case StrokeAlign.center:
+            adjustedRect = rect;
+            break;
+          case StrokeAlign.outside:
+            adjustedRect = rect.inflate(side.width / 2);
+            break;
+        }
+        final Path path = _StarGenerator(
+          points: points,
+          rotation: _rotationRadians,
+          innerRadiusRatio: innerRadiusRatio,
+          pointRounding: pointRounding,
+          valleyRounding: valleyRounding,
+          squash: squash,
+        ).generate(adjustedRect);
+        canvas.drawPath(path, side.toPaint());
+    }
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (other.runtimeType != runtimeType) {
+      return false;
+    }
+    return other is StarBorder && other.side == side;
+  }
+
+  @override
+  int get hashCode => side.hashCode;
+
+  @override
+  String toString() {
+    return '${objectRuntimeType(this, 'StarBorder')}($side, points: $points, innerRadiusRatio: $innerRadiusRatio)';
+  }
+}
+
+class _PointInfo {
+  _PointInfo({
+    required this.valley,
+    required this.point,
+    required this.valleyArc1,
+    required this.pointArc1,
+    required this.valleyArc2,
+    required this.pointArc2,
+  });
+
+  Offset valley;
+  Offset point;
+  Offset valleyArc1;
+  Offset pointArc1;
+  Offset pointArc2;
+  Offset valleyArc2;
+}
+
+class _StarGenerator {
+  const _StarGenerator({
+    required this.points,
+    required this.innerRadiusRatio,
+    required this.pointRounding,
+    required this.valleyRounding,
+    required this.rotation,
+    required this.squash,
+  })  : assert(points > 1),
+        assert(innerRadiusRatio == null || innerRadiusRatio <= 1),
+        assert(innerRadiusRatio == null || innerRadiusRatio >= 0),
+        assert(squash >= 0),
+        assert(squash <= 1),
+        assert(pointRounding >= 0),
+        assert(pointRounding <= 1),
+        assert(valleyRounding >= 0),
+        assert(valleyRounding <= 1),
+        assert(pointRounding + valleyRounding <= 1);
+
+  final double points;
+  final double innerRadiusRatio;
+  final double pointRounding;
+  final double valleyRounding;
+  final double rotation;
+  final double squash;
+  bool get isStar => innerRadiusRatio != null;
+
+  Path generate(Rect rect) {
+    final double radius = rect.shortestSide / 2;
+    final Offset center = rect.center;
+
+    // The minimum allowed inner radius ratio. Numerical instabilities occur near
+    // zero, so we just don't allow values in that range.
+    const double minInnerRadiusRatio = .002;
+
+    // Map the innerRadiusRatio so that we don't get values close to zero, since
+    // things get a little squirrelly there because the path thinks that the
+    // length of the conicTo is small enough that it can render it as a straight
+    // line, even though it will be scaled up later. This maps the range from
+    // [0, 1] to [minInnerRadiusRatio, 1].
+    final double mappedInnerRadiusRatio = (innerRadiusRatio * (1.0 - minInnerRadiusRatio)) + minInnerRadiusRatio;
+
+    // First, generate the "points" of the star.
+    final List<_PointInfo> points = <_PointInfo>[];
+    final double maxDiameter = 2.0 *
+        _generatePoints(
+          pointList: points,
+          center: center,
+          radius: radius,
+          innerRadius: radius * mappedInnerRadiusRatio,
+        );
+
+    // Calculate the endpoints of each of the arcs, then draw the arcs.
+    final Path path = Path();
+    _drawPoints(path, points);
+
+    Offset scale = Offset(rect.width / maxDiameter, rect.height / maxDiameter);
+    if (rect.shortestSide == rect.width) {
+      scale = Offset(scale.dx, squash * scale.dy + (1 - squash) * scale.dx);
+    } else {
+      scale = Offset(squash * scale.dx + (1 - squash) * scale.dy, scale.dy);
+    }
+
+    // Scale the border so that it matches the size of the widget rectangle, so
+    // that "rotation" of the shape doesn't affect how much of the rectangle it
+    // covers.
+    final Matrix4 squashMatrix = Matrix4.translationValues(rect.center.dx, rect.center.dy, 0);
+    squashMatrix.multiply(Matrix4.diagonal3Values(scale.dx, scale.dy, 1));
+    squashMatrix.multiply(Matrix4.rotationZ(rotation));
+    squashMatrix.multiply(Matrix4.translationValues(-rect.center.dx, -rect.center.dy, 0));
+    return path.transform(squashMatrix.storage);
+  }
+
+  double _generatePoints({
+    required List<_PointInfo> pointList,
+    required Offset center,
+    required double radius,
+    required double innerRadius,
+  }) {
+    final double step = math.pi / points;
+    // Start initial rotation one step before zero.
+    double angle = -math.pi / 2 - step;
+    Offset valley = Offset(
+      center.dx + math.cos(angle) * innerRadius,
+      center.dy + math.sin(angle) * innerRadius,
+    );
+
+    // In order to do overall scale properly, calculate the actual radius at the
+    // point, taking into account the rounding of the points and the weight of
+    // the corner point. This effectively is evaluating the rational quadratic
+    // bezier at the midpoint of the curve.
+    Offset getCurveMidpoint(Offset a, Offset b, Offset c, Offset a1, Offset c1) {
+      final double angle = _getAngle(a, b, c);
+      final double w = _getWeight(angle) / 2;
+      return (a1 / 4 + b * w + c1 / 4) / (0.5 + w);
+    }
+
+    double addPoint(
+      double pointAngle,
+      double pointStep,
+      double pointRadius,
+      double pointInnerRadius,
+    ) {
+      pointAngle += pointStep;
+      final Offset point = Offset(
+        center.dx + math.cos(pointAngle) * pointRadius,
+        center.dy + math.sin(pointAngle) * pointRadius,
+      );
+      pointAngle += pointStep;
+      final Offset nextValley = Offset(
+        center.dx + math.cos(pointAngle) * pointInnerRadius,
+        center.dy + math.sin(pointAngle) * pointInnerRadius,
+      );
+      final Offset valleyArc1 = valley + (point - valley) * valleyRounding;
+      final Offset pointArc1 = point + (valley - point) * pointRounding;
+      final Offset pointArc2 = point + (nextValley - point) * pointRounding;
+      final Offset valleyArc2 = nextValley + (point - nextValley) * valleyRounding;
+
+      pointList.add(_PointInfo(
+        valley: valley,
+        point: point,
+        valleyArc1: valleyArc1,
+        pointArc1: pointArc1,
+        pointArc2: pointArc2,
+        valleyArc2: valleyArc2,
+      ));
+      valley = nextValley;
+      return pointAngle;
+    }
+
+    final double remainder = points - points.truncateToDouble();
+    final bool hasIntegerSides = remainder < 1e-6;
+    final double wholeSides = points - (hasIntegerSides ? 0 : 1);
+    for (int i = 0; i < wholeSides; i += 1) {
+      angle = addPoint(angle, step, radius, innerRadius);
+    }
+
+    double valleyRadius = 0;
+    double pointRadius = 0;
+    final _PointInfo thisPoint = pointList[0];
+    final _PointInfo nextPoint = pointList[1];
+
+    final Offset pointMidpoint =
+        getCurveMidpoint(thisPoint.valley, thisPoint.point, nextPoint.valley, thisPoint.pointArc1, thisPoint.pointArc2);
+    final Offset valleyMidpoint = getCurveMidpoint(
+        thisPoint.point, nextPoint.valley, nextPoint.point, thisPoint.valleyArc2, nextPoint.valleyArc1);
+    valleyRadius = (valleyMidpoint - center).distance;
+    pointRadius = (pointMidpoint - center).distance;
+
+    // Add the final point to close the shape if there are fractional sides to
+    // account for.
+    if (!hasIntegerSides) {
+      final double effectiveInnerRadius = math.max(valleyRadius, innerRadius);
+      final double endingRadius = effectiveInnerRadius + remainder * (radius - effectiveInnerRadius);
+      addPoint(angle, step * remainder, endingRadius, innerRadius);
+    }
+
+    // The rounding added to the valley radius can sometimes push it outside of
+    // the rounding of the point, since the rounding amount can be different, so
+    // we have to evaluate both the valley and the point radii, and pick the
+    // largest.
+    return math.max(valleyRadius, pointRadius);
+  }
+
+  void _drawPoints(Path path, List<_PointInfo> points) {
+    final Offset startingPoint = points.first.pointArc1;
+    path.moveTo(startingPoint.dx, startingPoint.dy);
+    final double pointAngle = _getAngle(points[0].valley, points[0].point, points[1].valley);
+    final double pointWeight = _getWeight(pointAngle);
+    final double valleyAngle = _getAngle(points[1].point, points[1].valley, points[0].point);
+    final double valleyWeight = _getWeight(valleyAngle);
+
+    for (int i = 0; i < points.length; i += 1) {
+      final _PointInfo point = points[i];
+      final _PointInfo nextPoint = points[(i + 1) % points.length];
+      path.lineTo(point.pointArc1.dx, point.pointArc1.dy);
+      if (pointAngle != 180 && pointAngle != 0) {
+        path.conicTo(point.point.dx, point.point.dy, point.pointArc2.dx, point.pointArc2.dy, pointWeight);
+      } else {
+        path.lineTo(point.pointArc2.dx, point.pointArc2.dy);
+      }
+      path.lineTo(point.valleyArc2.dx, point.valleyArc2.dy);
+      if (valleyAngle != 180 && valleyAngle != 0) {
+        path.conicTo(
+            nextPoint.valley.dx, nextPoint.valley.dy, nextPoint.valleyArc1.dx, nextPoint.valleyArc1.dy, valleyWeight);
+      } else {
+        path.lineTo(nextPoint.valleyArc1.dx, nextPoint.valleyArc1.dy);
+      }
+    }
+    path.close();
+  }
+
+  double _getWeight(double angle) {
+    return math.cos((angle / 2) % (math.pi / 2));
+  }
+
+  // Returns the included angle between points ABC in radians.
+  double _getAngle(Offset a, Offset b, Offset c) {
+    if (a == c || b == c || b == a) {
+      return 0;
+    }
+    final Offset u = a - b;
+    final Offset v = c - b;
+    final double dot = u.dx * v.dx + u.dy * v.dy;
+    final double m1 = b.dx == a.dx ? double.infinity : -u.dy / -u.dx;
+    final double m2 = b.dx == c.dx ? double.infinity : -v.dy / -v.dx;
+    double angle = math.atan2(m1 - m2, 1 + m1 * m2).abs();
+    if (dot < 0) {
+      angle += math.pi;
+    }
+    return angle;
+  }
+}
diff --git a/packages/flutter/test/painting/star_border_test.dart b/packages/flutter/test/painting/star_border_test.dart
new file mode 100644
index 0000000..6694c9c
--- /dev/null
+++ b/packages/flutter/test/painting/star_border_test.dart
@@ -0,0 +1,198 @@
+// 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.
+
+@Tags(<String>['reduced-test-set'])
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+void main() {
+  Future<void> testBorder(WidgetTester tester, String name, StarBorder border,
+      {ShapeBorder? lerpTo, ShapeBorder? lerpFrom, double lerpAmount = 0}) async {
+    assert(lerpTo == null || lerpFrom == null); // They can't both be set.
+    ShapeBorder shape;
+    if (lerpTo != null) {
+      shape = border.lerpTo(lerpTo, lerpAmount)!;
+    } else if (lerpFrom != null) {
+      shape = border.lerpFrom(lerpFrom, lerpAmount)!;
+    } else {
+      shape = border;
+    }
+    await tester.pumpWidget(
+      Container(
+        alignment: Alignment.center,
+        width: 200,
+        height: 100,
+        decoration: ShapeDecoration(
+          color: const Color(0xff000000),
+          shape: shape,
+        ),
+      ),
+    );
+    await expectLater(
+      find.byType(Container),
+      matchesGoldenFile('painting.star_border.$name.png'),
+    );
+  }
+
+  test('StarBorder defaults', () {
+    const StarBorder star = StarBorder();
+    expect(star.side, BorderSide.none);
+    expect(star.points, 5);
+    expect(star.innerRadiusRatio, 0.4);
+    expect(star.rotation, 0);
+    expect(star.pointRounding, 0);
+    expect(star.valleyRounding, 0);
+    expect(star.squash, 0);
+
+    const StarBorder polygon = StarBorder.polygon();
+    expect(polygon.points, 5);
+    expect(polygon.pointRounding, 0);
+    expect(polygon.rotation, 0);
+    expect(polygon.squash, 0);
+  });
+
+  test('StarBorder copyWith, ==, hashCode', () {
+    const BorderSide side = BorderSide(width: 10.0, color: Color(0xff123456));
+    final StarBorder copy = const StarBorder().copyWith(
+      side: side,
+      points: 3,
+      innerRadiusRatio: 0.1,
+      pointRounding: 0.2,
+      valleyRounding: 0.3,
+      rotation: 180,
+      squash: 0.4,
+    );
+    const StarBorder expected = StarBorder(
+      side: side,
+      points: 3,
+      innerRadiusRatio: 0.1,
+      pointRounding: 0.2,
+      valleyRounding: 0.3,
+      rotation: 180,
+      squash: 0.4,
+    );
+    expect(const StarBorder(), equals(const StarBorder().copyWith()));
+    expect(copy, equals(expected));
+    expect(copy.hashCode, equals(expected.hashCode));
+  });
+
+  testWidgets('StarBorder basic geometry', (WidgetTester tester) async {
+    await testBorder(tester, 'basic_star', const StarBorder());
+    await testBorder(tester, 'basic_polygon', const StarBorder.polygon());
+  });
+
+  testWidgets('StarBorder parameters', (WidgetTester tester) async {
+    await testBorder(tester, 'points_6', const StarBorder(points: 6));
+    await testBorder(tester, 'points_2', const StarBorder(points: 2));
+    await testBorder(tester, 'inner_radius_0', const StarBorder(innerRadiusRatio: 0.0));
+    await testBorder(tester, 'inner_radius_2', const StarBorder(innerRadiusRatio: 0.2));
+    await testBorder(tester, 'inner_radius_7', const StarBorder(innerRadiusRatio: 0.7));
+    await testBorder(tester, 'point_rounding_2', const StarBorder(pointRounding: 0.2));
+    await testBorder(tester, 'point_rounding_7', const StarBorder(pointRounding: 0.7));
+    await testBorder(tester, 'point_rounding_10', const StarBorder(pointRounding: 1.0));
+    await testBorder(tester, 'valley_rounding_2', const StarBorder(valleyRounding: 0.2));
+    await testBorder(tester, 'valley_rounding_7', const StarBorder(valleyRounding: 0.7));
+    await testBorder(tester, 'valley_rounding_10', const StarBorder(valleyRounding: 1.0));
+    await testBorder(tester, 'squash_2', const StarBorder(squash: 0.2));
+    await testBorder(tester, 'squash_7', const StarBorder(squash: 0.7));
+    await testBorder(tester, 'squash_10', const StarBorder(squash: 1.0));
+    await testBorder(tester, 'rotate_27', const StarBorder(rotation: 27));
+    await testBorder(tester, 'rotate_270', const StarBorder(rotation: 270));
+    await testBorder(tester, 'rotate_360', const StarBorder(rotation: 360));
+    await testBorder(tester, 'side_none', const StarBorder(side: BorderSide(style: BorderStyle.none)));
+    await testBorder(tester, 'side_1', const StarBorder(side: BorderSide(color: Color(0xffff0000))));
+    await testBorder(tester, 'side_10', const StarBorder(side: BorderSide(color: Color(0xffff0000), width: 10)));
+    await testBorder(tester, 'side_align_center',
+        const StarBorder(side: BorderSide(color: Color(0xffff0000), strokeAlign: StrokeAlign.center)));
+    await testBorder(tester, 'side_align_outside',
+        const StarBorder(side: BorderSide(color: Color(0xffff0000), strokeAlign: StrokeAlign.outside)));
+  });
+
+  testWidgets('StarBorder.polygon parameters', (WidgetTester tester) async {
+    await testBorder(tester, 'poly_sides_6', const StarBorder.polygon(sides: 6));
+    await testBorder(tester, 'poly_sides_2', const StarBorder.polygon(sides: 2));
+    await testBorder(tester, 'poly_point_rounding_2', const StarBorder.polygon(pointRounding: 0.2));
+    await testBorder(tester, 'poly_point_rounding_7', const StarBorder.polygon(pointRounding: 0.7));
+    await testBorder(tester, 'poly_point_rounding_10', const StarBorder.polygon(pointRounding: 1.0));
+    await testBorder(tester, 'poly_squash_2', const StarBorder.polygon(squash: 0.2));
+    await testBorder(tester, 'poly_squash_7', const StarBorder.polygon(squash: 0.7));
+    await testBorder(tester, 'poly_squash_10', const StarBorder.polygon(squash: 1.0));
+    await testBorder(tester, 'poly_rotate_27', const StarBorder.polygon(rotation: 27));
+    await testBorder(tester, 'poly_rotate_270', const StarBorder.polygon(rotation: 270));
+    await testBorder(tester, 'poly_rotate_360', const StarBorder.polygon(rotation: 360));
+    await testBorder(tester, 'poly_side_none', const StarBorder.polygon(side: BorderSide(style: BorderStyle.none)));
+    await testBorder(tester, 'poly_side_1', const StarBorder.polygon(side: BorderSide(color: Color(0xffff0000))));
+    await testBorder(
+        tester, 'poly_side_10', const StarBorder.polygon(side: BorderSide(color: Color(0xffff0000), width: 10)));
+    await testBorder(tester, 'poly_side_align_center',
+        const StarBorder.polygon(side: BorderSide(color: Color(0xffff0000), strokeAlign: StrokeAlign.center)));
+    await testBorder(tester, 'poly_side_align_outside',
+        const StarBorder.polygon(side: BorderSide(color: Color(0xffff0000), strokeAlign: StrokeAlign.outside)));
+  });
+
+  testWidgets('StarBorder lerped with StarBorder', (WidgetTester tester) async {
+    const StarBorder from = StarBorder();
+    const ShapeBorder otherBorder = StarBorder(
+      points: 6,
+      pointRounding: 0.5,
+      valleyRounding: 0.5,
+      innerRadiusRatio: 0.5,
+      rotation: 90,
+    );
+    await testBorder(tester, 'to_star_border_2', from, lerpTo: otherBorder, lerpAmount: 0.2);
+    await testBorder(tester, 'to_star_border_7', from, lerpTo: otherBorder, lerpAmount: 0.7);
+    await testBorder(tester, 'to_star_border_10', from, lerpTo: otherBorder, lerpAmount: 1.0);
+    await testBorder(tester, 'from_star_border_2', from, lerpFrom: otherBorder, lerpAmount: 0.2);
+    await testBorder(tester, 'from_star_border_7', from, lerpFrom: otherBorder, lerpAmount: 0.7);
+    await testBorder(tester, 'from_star_border_10', from, lerpFrom: otherBorder, lerpAmount: 1.0);
+  });
+
+  testWidgets('StarBorder lerped with CircleBorder', (WidgetTester tester) async {
+    const StarBorder from = StarBorder();
+    const ShapeBorder otherBorder = CircleBorder();
+    await testBorder(tester, 'to_circle_border_2', from, lerpTo: otherBorder, lerpAmount: 0.2);
+    await testBorder(tester, 'to_circle_border_7', from, lerpTo: otherBorder, lerpAmount: 0.7);
+    await testBorder(tester, 'to_circle_border_10', from, lerpTo: otherBorder, lerpAmount: 1.0);
+    await testBorder(tester, 'from_circle_border_2', from, lerpFrom: otherBorder, lerpAmount: 0.2);
+    await testBorder(tester, 'from_circle_border_7', from, lerpFrom: otherBorder, lerpAmount: 0.7);
+    await testBorder(tester, 'from_circle_border_10', from, lerpFrom: otherBorder, lerpAmount: 1.0);
+  });
+
+  testWidgets('StarBorder lerped with RoundedRectangleBorder', (WidgetTester tester) async {
+    const StarBorder from = StarBorder();
+    const RoundedRectangleBorder rectangleBorder = RoundedRectangleBorder();
+    await testBorder(tester, 'to_rect_border_2', from, lerpTo: rectangleBorder, lerpAmount: 0.2);
+    await testBorder(tester, 'to_rect_border_7', from, lerpTo: rectangleBorder, lerpAmount: 0.7);
+    await testBorder(tester, 'to_rect_border_10', from, lerpTo: rectangleBorder, lerpAmount: 1.0);
+    await testBorder(tester, 'from_rect_border_2', from, lerpFrom: rectangleBorder, lerpAmount: 0.2);
+    await testBorder(tester, 'from_rect_border_7', from, lerpFrom: rectangleBorder, lerpAmount: 0.7);
+    await testBorder(tester, 'from_rect_border_10', from, lerpFrom: rectangleBorder, lerpAmount: 1.0);
+
+    const RoundedRectangleBorder roundedRectBorder = RoundedRectangleBorder(
+      borderRadius: BorderRadius.only(
+        bottomLeft: Radius.circular(10.0),
+        bottomRight: Radius.circular(10.0),
+      ),
+    );
+    await testBorder(tester, 'to_rrect_border_2', from, lerpTo: roundedRectBorder, lerpAmount: 0.2);
+    await testBorder(tester, 'to_rrect_border_7', from, lerpTo: roundedRectBorder, lerpAmount: 0.7);
+    await testBorder(tester, 'to_rrect_border_10', from, lerpTo: roundedRectBorder, lerpAmount: 1.0);
+    await testBorder(tester, 'from_rrect_border_2', from, lerpFrom: roundedRectBorder, lerpAmount: 0.2);
+    await testBorder(tester, 'from_rrect_border_7', from, lerpFrom: roundedRectBorder, lerpAmount: 0.7);
+    await testBorder(tester, 'from_rrect_border_10', from, lerpFrom: roundedRectBorder, lerpAmount: 1.0);
+  });
+
+  testWidgets('StarBorder lerped with StadiumBorder', (WidgetTester tester) async {
+    const StarBorder from = StarBorder();
+    const StadiumBorder stadiumBorder = StadiumBorder();
+
+    await testBorder(tester, 'to_stadium_border_2', from, lerpTo: stadiumBorder, lerpAmount: 0.2);
+    await testBorder(tester, 'to_stadium_border_7', from, lerpTo: stadiumBorder, lerpAmount: 0.7);
+    await testBorder(tester, 'to_stadium_border_10', from, lerpTo: stadiumBorder, lerpAmount: 1.0);
+    await testBorder(tester, 'from_stadium_border_2', from, lerpFrom: stadiumBorder, lerpAmount: 0.2);
+    await testBorder(tester, 'from_stadium_border_7', from, lerpFrom: stadiumBorder, lerpAmount: 0.7);
+    await testBorder(tester, 'from_stadium_border_10', from, lerpFrom: stadiumBorder, lerpAmount: 1.0);
+  });
+}