arc.dart 12.7 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 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
/// 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
19
/// 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
    super.begin,
    super.end,
  });
41 42 43 44

  bool _dirty = true;

  void _initialize() {
45 46 47 48 49
    assert(this.begin != null);
    assert(this.end != null);

    final Offset begin = this.begin!;
    final Offset end = this.end!;
50

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

58
    double sweepAngle() => 2.0 * math.asin(distanceFromAtoB / (2.0 * _radius!));
59 60 61 62

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

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

104 105
  /// The radius of the circular arc, null if [begin] and [end] are horizontally or
  /// vertically aligned, or if either is null.
106
  double? get radius {
107
    if (begin == null || end == null) {
108
      return null;
109 110
    }
    if (_dirty) {
111
      _initialize();
112
    }
113 114
    return _radius;
  }
115
  double? _radius;
116

117 118 119 120 121
  /// 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.
122
  double? get beginAngle {
123
    if (begin == null || end == null) {
124
      return null;
125 126
    }
    if (_dirty) {
127
      _initialize();
128
    }
129 130
    return _beginAngle;
  }
131
  double? _beginAngle;
132

133
  /// The end of the arc's sweep in radians, measured from the positive x axis.
134
  /// Positive angles turn clockwise.
135 136 137
  ///
  /// This will be null if [begin] and [end] are horizontally or vertically
  /// aligned, or if either is null.
138
  double? get endAngle {
139
    if (begin == null || end == null) {
140
      return null;
141 142
    }
    if (_dirty) {
143
      _initialize();
144
    }
145 146
    return _beginAngle;
  }
147
  double? _endAngle;
148 149

  @override
150
  set begin(Offset? value) {
151 152 153 154
    if (value != begin) {
      super.begin = value;
      _dirty = true;
    }
155 156 157
  }

  @override
158
  set end(Offset? value) {
159 160 161 162
    if (value != end) {
      super.end = value;
      _dirty = true;
    }
163 164 165
  }

  @override
166
  Offset lerp(double t) {
167
    if (_dirty) {
168
      _initialize();
169 170
    }
    if (t == 0.0) {
171
      return begin!;
172 173
    }
    if (t == 1.0) {
174
      return end!;
175 176
    }
    if (_beginAngle == null || _endAngle == null) {
177
      return Offset.lerp(begin, end, t)!;
178
    }
179 180 181 182
    final double angle = lerpDouble(_beginAngle, _endAngle, t)!;
    final double x = math.cos(angle) * _radius!;
    final double y = math.sin(angle) * _radius!;
    return _center! + Offset(x, y);
183 184 185 186
  }

  @override
  String toString() {
187
    return '${objectRuntimeType(this, 'MaterialPointArcTween')}($begin \u2192 $end; center=$center, radius=$radius, beginAngle=$beginAngle, endAngle=$endAngle)';
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
  }
}

enum _CornerId {
  topLeft,
  topRight,
  bottomLeft,
  bottomRight
}

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

204 205 206 207 208
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),
209 210
];

211
typedef _KeyFunc<T> = double Function(T input);
212 213

// Select the element for which the key function returns the maximum value.
214
T _maxBy<T>(Iterable<T> input, _KeyFunc<T> keyFunc) {
215 216
  late T maxValue;
  double? maxKey;
217
  for (final T value in input) {
218
    final double key = keyFunc(value);
219 220 221 222 223 224 225 226
    if (maxKey == null || key > maxKey) {
      maxValue = value;
      maxKey = key;
    }
  }
  return maxValue;
}

227 228
/// A [Tween] that interpolates a [Rect] by having its opposite corners follow
/// circular arcs.
229
///
230 231
/// This class specializes the interpolation of [Tween<Rect>] so that instead of
/// growing or shrinking linearly, opposite corners of the rectangle follow arcs
232
/// in a manner consistent with Material Design principles.
233
///
234 235
/// Specifically, the rectangle corners whose diagonals are closest to the overall
/// direction of the animation follow arcs defined with [MaterialPointArcTween].
236
///
237 238
/// See also:
///
239 240
///  * [MaterialRectCenterArcTween], which interpolates a rect along a circular
///    arc between the begin and end [Rect]'s centers.
241
///  * [Tween], for a discussion on how to use interpolation objects.
242
///  * [MaterialPointArcTween], the analog for [Offset] interpolation.
243
///  * [RectTween], which does a linear rectangle interpolation.
244 245
///  * [Hero.createRectTween], which can be used to specify the tween that defines
///    a hero's path.
246
class MaterialRectArcTween extends RectTween {
247 248
  /// Creates a [Tween] for animating [Rect]s along a circular arc.
  ///
249 250 251
  /// 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.
252
  MaterialRectArcTween({
253 254 255
    super.begin,
    super.end,
  });
256 257 258 259

  bool _dirty = true;

  void _initialize() {
260 261
    assert(begin != null);
    assert(end != null);
262
    final Offset centersVector = end!.center - begin!.center;
263
    final _Diagonal diagonal = _maxBy<_Diagonal>(_allDiagonals, (_Diagonal d) => _diagonalSupport(centersVector, d));
264
    _beginArc = MaterialPointArcTween(
265 266
      begin: _cornerFor(begin!, diagonal.beginId),
      end: _cornerFor(end!, diagonal.beginId),
267
    );
268
    _endArc = MaterialPointArcTween(
269 270
      begin: _cornerFor(begin!, diagonal.endId),
      end: _cornerFor(end!, diagonal.endId),
271
    );
272
    _dirty = false;
273 274
  }

275
  double _diagonalSupport(Offset centersVector, _Diagonal diagonal) {
276
    final Offset delta = _cornerFor(begin!, diagonal.endId) - _cornerFor(begin!, diagonal.beginId);
277 278 279
    final double length = delta.distance;
    return centersVector.dx * delta.dx / length + centersVector.dy * delta.dy / length;
  }
280

281
  Offset _cornerFor(Rect rect, _CornerId id) {
282 283 284 285 286 287 288 289 290 291
    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;
    }
  }

  /// The path of the corresponding [begin], [end] rectangle corners that lead
  /// the animation.
292
  MaterialPointArcTween? get beginArc {
293
    if (begin == null) {
294
      return null;
295 296
    }
    if (_dirty) {
297
      _initialize();
298
    }
299 300
    return _beginArc;
  }
301
  late MaterialPointArcTween _beginArc;
302 303 304

  /// The path of the corresponding [begin], [end] rectangle corners that trail
  /// the animation.
305
  MaterialPointArcTween? get endArc {
306
    if (end == null) {
307
      return null;
308 309
    }
    if (_dirty) {
310
      _initialize();
311
    }
312 313
    return _endArc;
  }
314
  late MaterialPointArcTween _endArc;
315 316

  @override
317
  set begin(Rect? value) {
318 319 320 321
    if (value != begin) {
      super.begin = value;
      _dirty = true;
    }
322 323 324
  }

  @override
325
  set end(Rect? value) {
326 327 328 329
    if (value != end) {
      super.end = value;
      _dirty = true;
    }
330 331 332 333
  }

  @override
  Rect lerp(double t) {
334
    if (_dirty) {
335
      _initialize();
336 337
    }
    if (t == 0.0) {
338
      return begin!;
339 340
    }
    if (t == 1.0) {
341
      return end!;
342
    }
343
    return Rect.fromPoints(_beginArc.lerp(t), _endArc.lerp(t));
344 345 346 347
  }

  @override
  String toString() {
348
    return '${objectRuntimeType(this, 'MaterialRectArcTween')}($begin \u2192 $end; beginArc=$beginArc, endArc=$endArc)';
349 350
  }
}
351

352 353 354
/// 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.
355 356 357 358 359 360 361 362 363
///
/// 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.
364
///  * [MaterialPointArcTween], the analog for [Offset] interpolation.
365 366 367 368 369 370 371 372 373 374
///  * [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({
375 376 377
    super.begin,
    super.end,
  });
378 379 380 381 382 383

  bool _dirty = true;

  void _initialize() {
    assert(begin != null);
    assert(end != null);
384
    _centerArc = MaterialPointArcTween(
385 386
      begin: begin!.center,
      end: end!.center,
387 388 389 390
    );
    _dirty = false;
  }

391 392
  /// 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].
393
  MaterialPointArcTween? get centerArc {
394
    if (begin == null || end == null) {
395
      return null;
396 397
    }
    if (_dirty) {
398
      _initialize();
399
    }
400 401
    return _centerArc;
  }
402
  late MaterialPointArcTween _centerArc;
403 404

  @override
405
  set begin(Rect? value) {
406 407 408 409 410 411 412
    if (value != begin) {
      super.begin = value;
      _dirty = true;
    }
  }

  @override
413
  set end(Rect? value) {
414 415 416 417 418 419 420 421
    if (value != end) {
      super.end = value;
      _dirty = true;
    }
  }

  @override
  Rect lerp(double t) {
422
    if (_dirty) {
423
      _initialize();
424 425
    }
    if (t == 0.0) {
426
      return begin!;
427 428
    }
    if (t == 1.0) {
429
      return end!;
430
    }
431
    final Offset center = _centerArc.lerp(t);
432 433
    final double width = lerpDouble(begin!.width, end!.width, t)!;
    final double height = lerpDouble(begin!.height, end!.height, t)!;
434
    return Rect.fromLTWH(center.dx - width / 2.0, center.dy - height / 2.0, width, height);
435 436 437 438
  }

  @override
  String toString() {
439
    return '${objectRuntimeType(this, 'MaterialRectCenterArcTween')}($begin \u2192 $end; centerArc=$centerArc)';
440 441
  }
}