| // 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 '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 |
| 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 = rect.deflate(side.strokeInset); |
| 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 = rect.inflate(side.strokeOffset / 2); |
| 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 |
| // between the points and the valleys, so we have to evaluate both the |
| // valley and the point radii, and pick the largest. Also, since this value |
| // is used later to determine the scale, we need to keep it finite and |
| // non-zero. |
| return clampDouble(math.max(valleyRadius, pointRadius), double.minPositive, double.maxFinite); |
| } |
| |
| 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; |
| } |
| } |