arc.dart 12.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5
// 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
import 'package:flutter/animation.dart';
9
import 'package:flutter/foundation.dart';
10
import 'package:flutter/painting.dart';
11 12 13 14 15

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

16 17 18 19 20
/// 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.
21 22 23 24 25 26 27 28 29
///
/// 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:
///
30 31
///  * [Tween], for a discussion on how to use interpolation objects.
///  * [MaterialRectArcTween], which extends this concept to interpolating [Rect]s.
32 33
class MaterialPointArcTween extends Tween<Offset> {
  /// Creates a [Tween] for animating [Offset]s along a circular arc.
34
  ///
35 36 37
  /// 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.
38
  MaterialPointArcTween({
39 40 41 42 43 44 45
    Offset begin,
    Offset end,
  }) : super(begin: begin, end: end);

  bool _dirty = true;

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

49 50 51 52 53
    // 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;
54
    final Offset c = Offset(end.dx, begin.dy);
55 56 57 58 59 60

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

89 90 91 92 93 94 95 96 97
  /// 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;
  }
98
  Offset _center;
99

100 101 102 103 104 105 106 107 108 109
  /// 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;
110

111 112 113 114 115 116 117 118 119 120 121 122 123
  /// 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;
124

125
  /// The end of the arc's sweep in radians, measured from the positive x axis.
126
  /// Positive angles turn clockwise.
127 128 129 130 131 132 133 134 135 136 137
  ///
  /// 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;
138 139

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

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

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

  @override
  String toString() {
173
    return '${objectRuntimeType(this, 'MaterialPointArcTween')}($begin \u2192 $end; center=$center, radius=$radius, beginAngle=$beginAngle, endAngle=$endAngle)';
174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189
  }
}

enum _CornerId {
  topLeft,
  topRight,
  bottomLeft,
  bottomRight
}

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

190 191 192 193 194
const List<_Diagonal> _allDiagonals = <_Diagonal>[
  _Diagonal(_CornerId.topLeft, _CornerId.bottomRight),
  _Diagonal(_CornerId.bottomRight, _CornerId.topLeft),
  _Diagonal(_CornerId.topRight, _CornerId.bottomLeft),
  _Diagonal(_CornerId.bottomLeft, _CornerId.topRight),
195 196
];

197
typedef _KeyFunc<T> = double Function(T input);
198 199

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

213 214
/// A [Tween] that interpolates a [Rect] by having its opposite corners follow
/// circular arcs.
215
///
216 217 218
/// 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.
219
///
220 221
/// Specifically, the rectangle corners whose diagonals are closest to the overall
/// direction of the animation follow arcs defined with [MaterialPointArcTween].
222
///
223 224
/// See also:
///
225 226
///  * [MaterialRectCenterArcTween], which interpolates a rect along a circular
///    arc between the begin and end [Rect]'s centers.
227
///  * [Tween], for a discussion on how to use interpolation objects.
228
///  * [MaterialPointArcTween], the analog for [Offset] interpolation.
229
///  * [RectTween], which does a linear rectangle interpolation.
230 231
///  * [Hero.createRectTween], which can be used to specify the tween that defines
///    a hero's path.
232
class MaterialRectArcTween extends RectTween {
233 234
  /// Creates a [Tween] for animating [Rect]s along a circular arc.
  ///
235 236 237
  /// 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.
238
  MaterialRectArcTween({
239 240 241 242 243 244 245
    Rect begin,
    Rect end,
  }) : super(begin: begin, end: end);

  bool _dirty = true;

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

261 262 263 264 265
  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;
  }
266

267
  Offset _cornerFor(Rect rect, _CornerId id) {
268 269 270 271 272 273
    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;
    }
274
    return Offset.zero;
275 276 277 278
  }

  /// The path of the corresponding [begin], [end] rectangle corners that lead
  /// the animation.
279 280 281 282 283 284 285 286
  MaterialPointArcTween get beginArc {
    if (begin == null)
      return null;
    if (_dirty)
      _initialize();
    return _beginArc;
  }
  MaterialPointArcTween _beginArc;
287 288 289

  /// The path of the corresponding [begin], [end] rectangle corners that trail
  /// the animation.
290 291 292 293 294 295 296 297
  MaterialPointArcTween get endArc {
    if (end == null)
      return null;
    if (_dirty)
      _initialize();
    return _endArc;
  }
  MaterialPointArcTween _endArc;
298 299 300

  @override
  set begin(Rect value) {
301 302 303 304
    if (value != begin) {
      super.begin = value;
      _dirty = true;
    }
305 306 307 308
  }

  @override
  set end(Rect value) {
309 310 311 312
    if (value != end) {
      super.end = value;
      _dirty = true;
    }
313 314 315 316
  }

  @override
  Rect lerp(double t) {
317 318
    if (_dirty)
      _initialize();
319 320 321 322
    if (t == 0.0)
      return begin;
    if (t == 1.0)
      return end;
323
    return Rect.fromPoints(_beginArc.lerp(t), _endArc.lerp(t));
324 325 326 327
  }

  @override
  String toString() {
328
    return '${objectRuntimeType(this, 'MaterialRectArcTween')}($begin \u2192 $end; beginArc=$beginArc, endArc=$endArc)';
329 330
  }
}
331

332 333 334
/// A [Tween] that interpolates a [Rect] by moving it along a circular arc from
/// [begin]'s [Rect.center] to [end]'s [Rect.center] while interpolating the
/// rectangle's width and height.
335 336 337 338 339 340 341 342 343
///
/// The arc that defines that center of the interpolated rectangle as it morphs
/// from [begin] to [end] is a [MaterialPointArcTween].
///
/// See also:
///
///  * [MaterialRectArcTween], A [Tween] that interpolates a [Rect] by having
///    its opposite corners follow circular arcs.
///  * [Tween], for a discussion on how to use interpolation objects.
344
///  * [MaterialPointArcTween], the analog for [Offset] interpolation.
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363
///  * [RectTween], which does a linear rectangle interpolation.
///  * [Hero.createRectTween], which can be used to specify the tween that defines
///    a hero's path.
class MaterialRectCenterArcTween extends RectTween {
  /// Creates a [Tween] for animating [Rect]s along a circular arc.
  ///
  /// 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.
  MaterialRectCenterArcTween({
    Rect begin,
    Rect end,
  }) : super(begin: begin, end: end);

  bool _dirty = true;

  void _initialize() {
    assert(begin != null);
    assert(end != null);
364
    _centerArc = MaterialPointArcTween(
365 366 367 368 369 370
      begin: begin.center,
      end: end.center,
    );
    _dirty = false;
  }

371 372
  /// If [begin] and [end] are non-null, returns a tween that interpolates along
  /// a circular arc between [begin]'s [Rect.center] and [end]'s [Rect.center].
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408
  MaterialPointArcTween get centerArc {
    if (begin == null || end == null)
      return null;
    if (_dirty)
      _initialize();
    return _centerArc;
  }
  MaterialPointArcTween _centerArc;

  @override
  set begin(Rect value) {
    if (value != begin) {
      super.begin = value;
      _dirty = true;
    }
  }

  @override
  set end(Rect value) {
    if (value != end) {
      super.end = value;
      _dirty = true;
    }
  }

  @override
  Rect lerp(double t) {
    if (_dirty)
      _initialize();
    if (t == 0.0)
      return begin;
    if (t == 1.0)
      return end;
    final Offset center = _centerArc.lerp(t);
    final double width = lerpDouble(begin.width, end.width, t);
    final double height = lerpDouble(begin.height, end.height, t);
409
    return Rect.fromLTWH(center.dx - width / 2.0, center.dy - height / 2.0, width, height);
410 411 412 413
  }

  @override
  String toString() {
414
    return '${objectRuntimeType(this, 'MaterialRectCenterArcTween')}($begin \u2192 $end; centerArc=$centerArc)';
415 416
  }
}