arc.dart 9.69 KB
Newer Older
1 2 3 4 5
// Copyright 2016 The Chromium 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;
6
import 'dart:ui' show lerpDouble;
7

8 9
import 'package:flutter/animation.dart';
import 'package:flutter/painting.dart';
10 11 12 13 14

// How close the begin and end points must be to an axis to be considered
// vertical or horizontal.
const double _kOnAxisDelta = 2.0;

15 16 17 18 19
/// A [Tween] that interpolates an [Offset] along a circular arc.
///
/// This class specializes the interpolation of [Tween<Offset>] so that instead
/// of a straight line, the intermediate points follow the arc of a circle in a
/// manner consistent with material design principles.
20 21 22 23 24 25 26 27 28
///
/// The arc's radius is related to the bounding box that contains the [begin]
/// and [end] points. If the bounding box is taller than it is wide, then the
/// center of the circle will be horizontally aligned with the end point.
/// Otherwise the center of the circle will be aligned with the begin point.
/// The arc's sweep is always less than or equal to 90 degrees.
///
/// See also:
///
29 30
///  * [Tween], for a discussion on how to use interpolation objects.
///  * [MaterialRectArcTween], which extends this concept to interpolating [Rect]s.
31 32
class MaterialPointArcTween extends Tween<Offset> {
  /// Creates a [Tween] for animating [Offset]s along a circular arc.
33
  ///
34 35 36
  /// The [begin] and [end] properties must be non-null before the tween is
  /// first used, but the arguments can be null if the values are going to be
  /// filled in later.
37
  MaterialPointArcTween({
38 39 40 41 42 43 44
    Offset begin,
    Offset end,
  }) : super(begin: begin, end: end);

  bool _dirty = true;

  void _initialize() {
45 46
    assert(begin != null);
    assert(end != null);
47

48 49 50 51 52
    // An explanation with a diagram can be found at https://goo.gl/vMSdRg
    final Offset delta = end - begin;
    final double deltaX = delta.dx.abs();
    final double deltaY = delta.dy.abs();
    final double distanceFromAtoB = delta.distance;
53
    final Offset c = new Offset(end.dx, begin.dy);
54 55 56 57 58 59

    double sweepAngle() => 2.0 * math.asin(distanceFromAtoB / (2.0 * _radius));

    if (deltaX > _kOnAxisDelta && deltaY > _kOnAxisDelta) {
      if (deltaX < deltaY) {
        _radius = distanceFromAtoB * distanceFromAtoB / (c - begin).distance / 2.0;
60 61 62
        _center = new Offset(end.dx + _radius * (begin.dx - end.dx).sign, end.dy);
        if (begin.dx < end.dx) {
          _beginAngle = sweepAngle() * (begin.dy - end.dy).sign;
63 64
          _endAngle = 0.0;
        } else {
65
          _beginAngle = math.PI + sweepAngle() * (end.dy - begin.dy).sign;
66 67 68 69
          _endAngle = math.PI;
        }
      } else {
        _radius = distanceFromAtoB * distanceFromAtoB / (c - end).distance / 2.0;
70 71
        _center = new Offset(begin.dx, begin.dy + (end.dy - begin.dy).sign * _radius);
        if (begin.dy < end.dy) {
72
          _beginAngle = -math.PI / 2.0;
73
          _endAngle = _beginAngle + sweepAngle() * (end.dx - begin.dx).sign;
74 75
        } else {
          _beginAngle = math.PI / 2.0;
76
          _endAngle = _beginAngle + sweepAngle() * (begin.dx - end.dx).sign;
77 78
        }
      }
79 80 81 82 83
      assert(_beginAngle != null);
      assert(_endAngle != null);
    } else {
      _beginAngle = null;
      _endAngle = null;
84
    }
85
    _dirty = false;
86 87
  }

88 89 90 91 92 93 94 95 96
  /// The center of the circular arc, null if [begin] and [end] are horizontally or
  /// vertically aligned, or if either is null.
  Offset get center {
    if (begin == null || end == null)
      return null;
    if (_dirty)
      _initialize();
    return _center;
  }
97
  Offset _center;
98

99 100 101 102 103 104 105 106 107 108
  /// The radius of the circular arc, null if [begin] and [end] are horizontally or
  /// vertically aligned, or if either is null.
  double get radius {
    if (begin == null || end == null)
      return null;
    if (_dirty)
      _initialize();
    return _radius;
  }
  double _radius;
109

110 111 112 113 114 115 116 117 118 119 120 121 122
  /// The beginning of the arc's sweep in radians, measured from the positive x
  /// axis. Positive angles turn clockwise.
  ///
  /// This will be null if [begin] and [end] are horizontally or vertically
  /// aligned, or if either is null.
  double get beginAngle {
    if (begin == null || end == null)
      return null;
    if (_dirty)
      _initialize();
    return _beginAngle;
  }
  double _beginAngle;
123

124
  /// The end of the arc's sweep in radians, measured from the positive x axis.
125
  /// Positive angles turn clockwise.
126 127 128 129 130 131 132 133 134 135 136
  ///
  /// This will be null if [begin] and [end] are horizontally or vertically
  /// aligned, or if either is null.
  double get endAngle {
    if (begin == null || end == null)
      return null;
    if (_dirty)
      _initialize();
    return _beginAngle;
  }
  double _endAngle;
137 138

  @override
139
  set begin(Offset value) {
140 141 142 143
    if (value != begin) {
      super.begin = value;
      _dirty = true;
    }
144 145 146
  }

  @override
147
  set end(Offset value) {
148 149 150 151
    if (value != end) {
      super.end = value;
      _dirty = true;
    }
152 153 154
  }

  @override
155
  Offset lerp(double t) {
156 157
    if (_dirty)
      _initialize();
158 159 160 161 162
    if (t == 0.0)
      return begin;
    if (t == 1.0)
      return end;
    if (_beginAngle == null || _endAngle == null)
163
      return Offset.lerp(begin, end, t);
164 165 166 167 168 169 170 171
    final double angle = lerpDouble(_beginAngle, _endAngle, t);
    final double x = math.cos(angle) * _radius;
    final double y = math.sin(angle) * _radius;
    return _center + new Offset(x, y);
  }

  @override
  String toString() {
172
    return '$runtimeType($begin \u2192 $end; center=$center, radius=$radius, beginAngle=$beginAngle, endAngle=$endAngle)';
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195
  }
}

enum _CornerId {
  topLeft,
  topRight,
  bottomLeft,
  bottomRight
}

class _Diagonal {
  const _Diagonal(this.beginId, this.endId);
  final _CornerId beginId;
  final _CornerId endId;
}

const List<_Diagonal> _allDiagonals = const <_Diagonal>[
  const _Diagonal(_CornerId.topLeft, _CornerId.bottomRight),
  const _Diagonal(_CornerId.bottomRight, _CornerId.topLeft),
  const _Diagonal(_CornerId.topRight, _CornerId.bottomLeft),
  const _Diagonal(_CornerId.bottomLeft, _CornerId.topRight),
];

196 197 198
typedef dynamic _KeyFunc<T>(T input);

// Select the element for which the key function returns the maximum value.
199 200
T _maxBy<T>(Iterable<T> input, _KeyFunc<T> keyFunc) {
  T maxValue;
201
  dynamic maxKey;
202
  for (T value in input) {
203
    final dynamic key = keyFunc(value);
204 205 206 207 208 209 210 211
    if (maxKey == null || key > maxKey) {
      maxValue = value;
      maxKey = key;
    }
  }
  return maxValue;
}

212 213
/// A [Tween] that interpolates a [Rect] by having its opposite corners follow
/// circular arcs.
214
///
215 216 217
/// This class specializes the interpolation of [Tween<Rect>] so that instead of
/// growing or shrinking linearly, opposite corners of the rectangle follow arcs
/// in a manner consistent with material design principles.
218
///
219 220
/// Specifically, the rectangle corners whose diagonals are closest to the overall
/// direction of the animation follow arcs defined with [MaterialPointArcTween].
221
///
222 223
/// See also:
///
224 225 226
///  * [Tween], for a discussion on how to use interpolation objects.
///  * [MaterialPointArcTween], the analogue for [Offset] interporation.
///  * [RectTween], which does a linear rectangle interpolation.
227
class MaterialRectArcTween extends RectTween {
228 229
  /// Creates a [Tween] for animating [Rect]s along a circular arc.
  ///
230 231 232
  /// The [begin] and [end] properties must be non-null before the tween is
  /// first used, but the arguments can be null if the values are going to be
  /// filled in later.
233
  MaterialRectArcTween({
234 235 236 237 238 239 240
    Rect begin,
    Rect end,
  }) : super(begin: begin, end: end);

  bool _dirty = true;

  void _initialize() {
241 242
    assert(begin != null);
    assert(end != null);
243
    final Offset centersVector = end.center - begin.center;
244
    final _Diagonal diagonal = _maxBy<_Diagonal>(_allDiagonals, (_Diagonal d) => _diagonalSupport(centersVector, d));
245
    _beginArc = new MaterialPointArcTween(
246 247
      begin: _cornerFor(begin, diagonal.beginId),
      end: _cornerFor(end, diagonal.beginId)
248 249
    );
    _endArc = new MaterialPointArcTween(
250 251
      begin: _cornerFor(begin, diagonal.endId),
      end: _cornerFor(end, diagonal.endId)
252
    );
253
    _dirty = false;
254 255
  }

256 257 258 259 260
  double _diagonalSupport(Offset centersVector, _Diagonal diagonal) {
    final Offset delta = _cornerFor(begin, diagonal.endId) - _cornerFor(begin, diagonal.beginId);
    final double length = delta.distance;
    return centersVector.dx * delta.dx / length + centersVector.dy * delta.dy / length;
  }
261

262
  Offset _cornerFor(Rect rect, _CornerId id) {
263 264 265 266 267 268
    switch (id) {
      case _CornerId.topLeft: return rect.topLeft;
      case _CornerId.topRight: return rect.topRight;
      case _CornerId.bottomLeft: return rect.bottomLeft;
      case _CornerId.bottomRight: return rect.bottomRight;
    }
269
    return Offset.zero;
270 271 272 273
  }

  /// The path of the corresponding [begin], [end] rectangle corners that lead
  /// the animation.
274 275 276 277 278 279 280 281
  MaterialPointArcTween get beginArc {
    if (begin == null)
      return null;
    if (_dirty)
      _initialize();
    return _beginArc;
  }
  MaterialPointArcTween _beginArc;
282 283 284

  /// The path of the corresponding [begin], [end] rectangle corners that trail
  /// the animation.
285 286 287 288 289 290 291 292
  MaterialPointArcTween get endArc {
    if (end == null)
      return null;
    if (_dirty)
      _initialize();
    return _endArc;
  }
  MaterialPointArcTween _endArc;
293 294 295

  @override
  set begin(Rect value) {
296 297 298 299
    if (value != begin) {
      super.begin = value;
      _dirty = true;
    }
300 301 302 303
  }

  @override
  set end(Rect value) {
304 305 306 307
    if (value != end) {
      super.end = value;
      _dirty = true;
    }
308 309 310 311
  }

  @override
  Rect lerp(double t) {
312 313
    if (_dirty)
      _initialize();
314 315 316 317 318 319 320 321 322
    if (t == 0.0)
      return begin;
    if (t == 1.0)
      return end;
    return new Rect.fromPoints(_beginArc.lerp(t), _endArc.lerp(t));
  }

  @override
  String toString() {
323
    return '$runtimeType($begin \u2192 $end; beginArc=$beginArc, endArc=$endArc)';
324 325
  }
}