flutter / mirrors / flutter / refs/heads/flutter-3.4-candidate.4 / . / packages / flutter / lib / src / painting / star_border.dart

// 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, | |

squash: ui.lerpDouble(a.eccentricity, squash, t)!, | |

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, | |

squash: ui.lerpDouble(a.eccentricity, squash, t)!, | |

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, | |

squash: ui.lerpDouble(squash, b.eccentricity, t)!, | |

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, | |

squash: ui.lerpDouble(squash, b.eccentricity, t)!, | |

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; | |

} | |

} |