gradient.dart 40.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:collection';
6
import 'dart:math' as math;
7 8 9
import 'dart:ui' as ui show Gradient, lerpDouble;

import 'package:flutter/foundation.dart';
10
import 'package:vector_math/vector_math_64.dart';
11

12
import 'alignment.dart';
13 14
import 'basic_types.dart';

15 16 17 18 19 20
class _ColorsAndStops {
  _ColorsAndStops(this.colors, this.stops);
  final List<Color> colors;
  final List<double> stops;
}

21 22 23 24 25 26 27
/// Calculate the color at position [t] of the gradient defined by [colors] and [stops].
Color _sample(List<Color> colors, List<double> stops, double t) {
  assert(colors != null);
  assert(colors.isNotEmpty);
  assert(stops != null);
  assert(stops.isNotEmpty);
  assert(t != null);
28
  if (t <= stops.first) {
29
    return colors.first;
30 31
  }
  if (t >= stops.last) {
32
    return colors.last;
33
  }
34 35 36 37
  final int index = stops.lastIndexWhere((double s) => s <= t);
  assert(index != -1);
  return Color.lerp(
      colors[index], colors[index + 1],
38
      (t - stops[index]) / (stops[index + 1] - stops[index]),
39
  )!;
40 41 42
}

_ColorsAndStops _interpolateColorsAndStops(
43 44 45 46 47
  List<Color> aColors,
  List<double> aStops,
  List<Color> bColors,
  List<double> bStops,
  double t,
48 49 50 51 52 53 54 55 56 57
) {
  assert(aColors.length >= 2);
  assert(bColors.length >= 2);
  assert(aStops.length == aColors.length);
  assert(bStops.length == bColors.length);
  final SplayTreeSet<double> stops = SplayTreeSet<double>()
    ..addAll(aStops)
    ..addAll(bStops);
  final List<double> interpolatedStops = stops.toList(growable: false);
  final List<Color> interpolatedColors = interpolatedStops.map<Color>(
58
          (double stop) => Color.lerp(_sample(aColors, aStops, stop), _sample(bColors, bStops, stop), t)!,
59
  ).toList(growable: false);
60
  return _ColorsAndStops(interpolatedColors, interpolatedStops);
61 62
}

63 64 65 66 67 68 69 70
/// Base class for transforming gradient shaders without applying the same
/// transform to the entire canvas.
///
/// For example, a [SweepGradient] normally starts its gradation at 3 o'clock
/// and draws clockwise. To have the sweep appear to start at 6 o'clock, supply
/// a [GradientRotation] of `pi/4` radians (i.e. 45 degrees).
@immutable
abstract class GradientTransform {
71 72
  /// Abstract const constructor. This constructor enables subclasses to provide
  /// const constructors so that they can be used in const expressions.
73 74 75 76 77 78 79 80
  const GradientTransform();

  /// When a [Gradient] creates its [Shader], it will call this method to
  /// determine what transform to apply to the shader for the given [Rect] and
  /// [TextDirection].
  ///
  /// Implementers may return null from this method, which achieves the same
  /// final effect as returning [Matrix4.identity].
81
  Matrix4? transform(Rect bounds, {TextDirection? textDirection});
82 83 84 85 86
}

/// A [GradientTransform] that rotates the gradient around the center-point of
/// its bounding box.
///
87
/// {@tool snippet}
88 89 90 91 92 93 94 95 96
///
/// This sample would rotate a sweep gradient by a quarter turn clockwise:
///
/// ```dart
/// const SweepGradient gradient = SweepGradient(
///   colors: <Color>[Color(0xFFFFFFFF), Color(0xFF009900)],
///   transform: GradientRotation(math.pi/4),
/// );
/// ```
97
/// {@end-tool}
98 99 100 101 102 103 104 105 106 107 108
@immutable
class GradientRotation extends GradientTransform {
  /// Constructs a [GradientRotation] for the specified angle.
  ///
  /// The angle is in radians in the clockwise direction.
  const GradientRotation(this.radians);

  /// The angle of rotation in radians in the clockwise direction.
  final double radians;

  @override
109
  Matrix4 transform(Rect bounds, {TextDirection? textDirection}) {
110 111 112 113 114 115 116 117 118 119 120
    assert(bounds != null);
    final double sinRadians = math.sin(radians);
    final double oneMinusCosRadians = 1 - math.cos(radians);
    final Offset center = bounds.center;
    final double originX = sinRadians * center.dy + oneMinusCosRadians * center.dx;
    final double originY = -sinRadians * center.dx + oneMinusCosRadians * center.dy;

    return Matrix4.identity()
      ..translate(originX, originY)
      ..rotateZ(radians);
  }
121 122 123

  @override
  bool operator ==(Object other) {
124
    if (identical(this, other)) {
125
      return true;
126 127
    }
    if (other.runtimeType != runtimeType) {
128
      return false;
129
    }
130 131 132 133 134 135 136 137 138 139 140
    return other is GradientRotation
        && other.radians == radians;
  }

  @override
  int get hashCode => radians.hashCode;

  @override
  String toString() {
    return '${objectRuntimeType(this, 'GradientRotation')}(radians: ${debugFormatDouble(radians)})';
  }
141 142
}

143 144
/// A 2D gradient.
///
145 146
/// This is an interface that allows [LinearGradient], [RadialGradient], and
/// [SweepGradient] classes to be used interchangeably in [BoxDecoration]s.
147 148 149
///
/// See also:
///
150
///  * [Gradient](dart-ui/Gradient-class.html), the class in the [dart:ui] library.
151
///
152 153
@immutable
abstract class Gradient {
154 155 156 157 158 159 160 161
  /// Initialize the gradient's colors and stops.
  ///
  /// The [colors] argument must not be null, and must have at least two colors
  /// (the length is not verified until the [createShader] method is called).
  ///
  /// If specified, the [stops] argument must have the same number of entries as
  /// [colors] (this is also not verified until the [createShader] method is
  /// called).
162 163 164 165 166 167 168
  ///
  /// The [transform] argument can be applied to transform _only_ the gradient,
  /// without rotating the canvas itself or other geometry on the canvas. For
  /// example, a `GradientRotation(math.pi/4)` will result in a [SweepGradient]
  /// that starts from a position of 6 o'clock instead of 3 o'clock, assuming
  /// no other rotation or perspective transformations have been applied to the
  /// [Canvas]. If null, no transformation is applied.
169
  const Gradient({
170
    required this.colors,
171
    this.stops,
172
    this.transform,
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
  }) : assert(colors != null);

  /// The colors the gradient should obtain at each of the stops.
  ///
  /// If [stops] is non-null, this list must have the same length as [stops].
  ///
  /// This list must have at least two colors in it (otherwise, it's not a
  /// gradient!).
  final List<Color> colors;

  /// A list of values from 0.0 to 1.0 that denote fractions along the gradient.
  ///
  /// If non-null, this list must have the same length as [colors].
  ///
  /// If the first value is not 0.0, then a stop with position 0.0 and a color
  /// equal to the first color in [colors] is implied.
  ///
  /// If the last value is not 1.0, then a stop with position 1.0 and a color
  /// equal to the last color in [colors] is implied.
  ///
  /// The values in the [stops] list must be in ascending order. If a value in
  /// the [stops] list is less than an earlier value in the list, then its value
  /// is assumed to equal the previous value.
  ///
  /// If stops is null, then a set of uniformly distributed stops is implied,
  /// with the first stop at 0.0 and the last stop at 1.0.
199
  final List<double>? stops;
200

201 202 203 204
  /// The transform, if any, to apply to the gradient.
  ///
  /// This transform is in addition to any other transformations applied to the
  /// canvas, but does not add any transformations to the canvas.
205
  final GradientTransform? transform;
206

207
  List<double> _impliedStops() {
208
    if (stops != null) {
209
      return stops!;
210
    }
211 212
    assert(colors.length >= 2, 'colors list must have at least two colors');
    final double separation = 1.0 / (colors.length - 1);
213
    return List<double>.generate(
214 215 216 217 218
      colors.length,
      (int index) => index * separation,
      growable: false,
    );
  }
219 220

  /// Creates a [Shader] for this gradient to fill the given rect.
221 222
  ///
  /// If the gradient's configuration is text-direction-dependent, for example
223
  /// it uses [AlignmentDirectional] objects instead of [Alignment]
224
  /// objects, then the `textDirection` argument must not be null.
225 226 227
  ///
  /// The shader's transform will be resolved from the [transform] of this
  /// gradient.
228
  @factory
229
  Shader createShader(Rect rect, { TextDirection? textDirection });
230 231 232 233 234 235 236 237 238 239 240 241

  /// Returns a new gradient with its properties scaled by the given factor.
  ///
  /// A factor of 0.0 (or less) should result in a variant of the gradient that
  /// is invisible; any two factors epsilon apart should be unnoticeably
  /// different from each other at first glance. From this it follows that
  /// scaling a gradient with values from 1.0 to 0.0 over time should cause the
  /// gradient to smoothly disappear.
  ///
  /// Typically this is the same as interpolating from null (with [lerp]).
  Gradient scale(double factor);

242
  /// Linearly interpolates from another [Gradient] to `this`.
243 244 245 246 247 248 249 250
  ///
  /// When implementing this method in subclasses, return null if this class
  /// cannot interpolate from `a`. In that case, [lerp] will try `a`'s [lerpTo]
  /// method instead.
  ///
  /// If `a` is null, this must not return null. The base class implements this
  /// by deferring to [scale].
  ///
251 252 253 254 255 256 257 258 259 260 261 262 263
  /// The `t` argument represents position on the timeline, with 0.0 meaning
  /// that the interpolation has not started, returning `a` (or something
  /// equivalent to `a`), 1.0 meaning that the interpolation has finished,
  /// returning `this` (or something equivalent to `this`), and values in
  /// between meaning that the interpolation is at the relevant point on the
  /// timeline between `a` and `this`. The interpolation can be extrapolated
  /// beyond 0.0 and 1.0, so negative values and values greater than 1.0 are
  /// valid (and can easily be generated by curves such as
  /// [Curves.elasticInOut]).
  ///
  /// Values for `t` are usually obtained from an [Animation<double>], such as
  /// an [AnimationController].
  ///
264 265
  /// Instead of calling this directly, use [Gradient.lerp].
  @protected
266
  Gradient? lerpFrom(Gradient? a, double t) {
267
    if (a == null) {
268
      return scale(t);
269
    }
270 271 272
    return null;
  }

273
  /// Linearly interpolates from `this` to another [Gradient].
274 275 276 277 278 279 280 281 282 283
  ///
  /// This is called if `b`'s [lerpTo] did not know how to handle this class.
  ///
  /// When implementing this method in subclasses, return null if this class
  /// cannot interpolate from `b`. In that case, [lerp] will apply a default
  /// behavior instead.
  ///
  /// If `b` is null, this must not return null. The base class implements this
  /// by deferring to [scale].
  ///
284 285 286 287 288 289 290 291 292 293 294 295
  /// The `t` argument represents position on the timeline, with 0.0 meaning
  /// that the interpolation has not started, returning `this` (or something
  /// equivalent to `this`), 1.0 meaning that the interpolation has finished,
  /// returning `b` (or something equivalent to `b`), and values in between
  /// meaning that the interpolation is at the relevant point on the timeline
  /// between `this` and `b`. The interpolation can be extrapolated beyond 0.0
  /// and 1.0, so negative values and values greater than 1.0 are valid (and can
  /// easily be generated by curves such as [Curves.elasticInOut]).
  ///
  /// Values for `t` are usually obtained from an [Animation<double>], such as
  /// an [AnimationController].
  ///
296 297
  /// Instead of calling this directly, use [Gradient.lerp].
  @protected
298
  Gradient? lerpTo(Gradient? b, double t) {
299
    if (b == null) {
300
      return scale(1.0 - t);
301
    }
302 303 304
    return null;
  }

305 306 307 308 309 310 311
  /// Linearly interpolates between two [Gradient]s.
  ///
  /// This defers to `b`'s [lerpTo] function if `b` is not null. If `b` is
  /// null or if its [lerpTo] returns null, it uses `a`'s [lerpFrom]
  /// function instead. If both return null, it returns `a` before `t == 0.5`
  /// and `b` after `t == 0.5`.
  ///
312
  /// {@macro dart.ui.shadow.lerp}
313
  static Gradient? lerp(Gradient? a, Gradient? b, double t) {
314
    assert(t != null);
315
    Gradient? result;
316
    if (b != null) {
317
      result = b.lerpFrom(a, t); // if a is null, this must return non-null
318 319
    }
    if (result == null && a != null) {
320
      result = a.lerpTo(b, t); // if b is null, this must return non-null
321 322
    }
    if (result != null) {
323
      return result;
324 325
    }
    if (a == null && b == null) {
326
      return null;
327
    }
328
    assert(a != null && b != null);
329
    return t < 0.5 ? a!.scale(1.0 - (t * 2.0)) : b!.scale((t - 0.5) * 2.0);
330
  }
331

332
  Float64List? _resolveTransform(Rect bounds, TextDirection? textDirection) {
333 334
    return transform?.transform(bounds, textDirection: textDirection)?.storage;
  }
335 336 337 338
}

/// A 2D linear gradient.
///
339 340
/// {@youtube 560 315 https://www.youtube.com/watch?v=gYNTcgZVcWw}
///
341
/// This class is used by [BoxDecoration] to represent linear gradients. This
342
/// abstracts out the arguments to the [ui.Gradient.linear] constructor from
343
/// the `dart:ui` library.
344 345 346 347
///
/// A gradient has two anchor points, [begin] and [end]. The [begin] point
/// corresponds to 0.0, and the [end] point corresponds to 1.0. These points are
/// expressed in fractions, so that the same gradient can be reused with varying
348
/// sized boxes without changing the parameters. (This contrasts with [
349 350 351
/// ui.Gradient.linear], whose arguments are expressed in logical pixels.)
///
/// The [colors] are described by a list of [Color] objects. There must be at
352 353 354 355
/// least two colors. The [stops] list, if specified, must have the same length
/// as [colors]. It specifies fractions of the vector from start to end, between
/// 0.0 and 1.0, for each color. If it is null, a uniform distribution is
/// assumed.
356 357 358 359 360 361 362
///
/// The region of the canvas before [begin] and after [end] is colored according
/// to [tileMode].
///
/// Typically this class is used with [BoxDecoration], which does the painting.
/// To use a [LinearGradient] to paint on a canvas directly, see [createShader].
///
363
/// {@tool dartpad}
364 365 366
/// This sample draws a picture with a gradient sweeping through different
/// colors, by having a [Container] display a [BoxDecoration] with a
/// [LinearGradient].
367
///
368
/// ** See code in examples/api/lib/painting/gradient/linear_gradient.0.dart **
369
/// {@end-tool}
370 371 372 373 374
///
/// See also:
///
///  * [RadialGradient], which displays a gradient in concentric circles, and
///    has an example which shows a different way to use [Gradient] objects.
375 376
///  * [SweepGradient], which displays a gradient in a sweeping arc around a
///    center point.
377 378 379
///  * [BoxDecoration], which can take a [LinearGradient] in its
///    [BoxDecoration.gradient] property.
class LinearGradient extends Gradient {
380
  /// Creates a linear gradient.
381 382 383 384
  ///
  /// The [colors] argument must not be null. If [stops] is non-null, it must
  /// have the same length as [colors].
  const LinearGradient({
385 386
    this.begin = Alignment.centerLeft,
    this.end = Alignment.centerRight,
387 388
    required super.colors,
    super.stops,
389
    this.tileMode = TileMode.clamp,
390
    super.transform,
391 392
  }) : assert(begin != null),
       assert(end != null),
393
       assert(tileMode != null);
394

395 396
  /// The offset at which stop 0.0 of the gradient is placed.
  ///
397
  /// If this is an [Alignment], then it is expressed as a vector from
398 399
  /// coordinate (0.0, 0.0), in a coordinate space that maps the center of the
  /// paint box at (0.0, 0.0) and the bottom right at (1.0, 1.0).
400
  ///
401
  /// For example, a begin offset of (-1.0, 0.0) is half way down the
402
  /// left side of the box.
403
  ///
404
  /// It can also be an [AlignmentDirectional], where the start is the
405 406 407
  /// left in left-to-right contexts and the right in right-to-left contexts. If
  /// a text-direction-dependent value is provided here, then the [createShader]
  /// method will need to be given a [TextDirection].
408
  final AlignmentGeometry begin;
409

410
  /// The offset at which stop 1.0 of the gradient is placed.
411
  ///
412
  /// If this is an [Alignment], then it is expressed as a vector from
413 414
  /// coordinate (0.0, 0.0), in a coordinate space that maps the center of the
  /// paint box at (0.0, 0.0) and the bottom right at (1.0, 1.0).
415
  ///
416
  /// For example, a begin offset of (1.0, 0.0) is half way down the
417
  /// right side of the box.
418
  ///
419
  /// It can also be an [AlignmentDirectional], where the start is the left in
420 421
  /// left-to-right contexts and the right in right-to-left contexts. If a
  /// text-direction-dependent value is provided here, then the [createShader]
422
  /// method will need to be given a [TextDirection].
423
  final AlignmentGeometry end;
424 425 426 427 428 429

  /// How this gradient should tile the plane beyond in the region before
  /// [begin] and after [end].
  ///
  /// For details, see [TileMode].
  ///
430
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_linear.png)
431
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_decal_linear.png)
432 433
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_linear.png)
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_linear.png)
434 435 436
  final TileMode tileMode;

  @override
437
  Shader createShader(Rect rect, { TextDirection? textDirection }) {
438
    return ui.Gradient.linear(
439 440
      begin.resolve(textDirection).withinRect(rect),
      end.resolve(textDirection).withinRect(rect),
441
      colors, _impliedStops(), tileMode, _resolveTransform(rect, textDirection),
442 443 444
    );
  }

445
  /// Returns a new [LinearGradient] with its colors scaled by the given factor.
446
  ///
447 448
  /// Since the alpha component of the Color is what is scaled, a factor
  /// of 0.0 or less results in a gradient that is fully transparent.
449
  @override
450
  LinearGradient scale(double factor) {
451
    return LinearGradient(
452 453
      begin: begin,
      end: end,
454
      colors: colors.map<Color>((Color color) => Color.lerp(null, color, factor)!).toList(),
455 456 457 458 459
      stops: stops,
      tileMode: tileMode,
    );
  }

460
  @override
461
  Gradient? lerpFrom(Gradient? a, double t) {
462
    if (a == null || (a is LinearGradient)) {
463
      return LinearGradient.lerp(a as LinearGradient?, this, t);
464
    }
465 466 467 468
    return super.lerpFrom(a, t);
  }

  @override
469
  Gradient? lerpTo(Gradient? b, double t) {
470
    if (b == null || (b is LinearGradient)) {
471
      return LinearGradient.lerp(this, b as LinearGradient?, t);
472
    }
473 474 475
    return super.lerpTo(b, t);
  }

476 477 478 479
  /// Linearly interpolate between two [LinearGradient]s.
  ///
  /// If either gradient is null, this function linearly interpolates from a
  /// a gradient that matches the other gradient in [begin], [end], [stops] and
480
  /// [tileMode] and with the same [colors] but transparent (using [scale]).
481 482
  ///
  /// If neither gradient is null, they must have the same number of [colors].
483
  ///
484
  /// The `t` argument represents a position on the timeline, with 0.0 meaning
485 486 487 488 489 490 491 492 493 494
  /// that the interpolation has not started, returning `a` (or something
  /// equivalent to `a`), 1.0 meaning that the interpolation has finished,
  /// returning `b` (or something equivalent to `b`), and values in between
  /// meaning that the interpolation is at the relevant point on the timeline
  /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and
  /// 1.0, so negative values and values greater than 1.0 are valid (and can
  /// easily be generated by curves such as [Curves.elasticInOut]).
  ///
  /// Values for `t` are usually obtained from an [Animation<double>], such as
  /// an [AnimationController].
495
  static LinearGradient? lerp(LinearGradient? a, LinearGradient? b, double t) {
496
    assert(t != null);
497
    if (a == null && b == null) {
498
      return null;
499 500
    }
    if (a == null) {
501
      return b!.scale(t);
502 503
    }
    if (b == null) {
504
      return a.scale(1.0 - t);
505
    }
506 507 508 509 510 511 512
    final _ColorsAndStops interpolated = _interpolateColorsAndStops(
        a.colors,
        a._impliedStops(),
        b.colors,
        b._impliedStops(),
        t,
    );
513
    return LinearGradient(
514 515
      begin: AlignmentGeometry.lerp(a.begin, b.begin, t)!,
      end: AlignmentGeometry.lerp(a.end, b.end, t)!,
516 517 518
      colors: interpolated.colors,
      stops: interpolated.stops,
      tileMode: t < 0.5 ? a.tileMode : b.tileMode, // TODO(ianh): interpolate tile mode
519 520 521 522
    );
  }

  @override
523
  bool operator ==(Object other) {
524
    if (identical(this, other)) {
525
      return true;
526 527
    }
    if (other.runtimeType != runtimeType) {
528
      return false;
529
    }
530 531 532 533
    return other is LinearGradient
        && other.begin == begin
        && other.end == end
        && other.tileMode == tileMode
534
        && other.transform == transform
535 536
        && listEquals<Color>(other.colors, colors)
        && listEquals<double>(other.stops, stops);
537 538 539
  }

  @override
540 541 542 543 544 545 546 547
  int get hashCode => Object.hash(
    begin,
    end,
    tileMode,
    transform,
    Object.hashAll(colors),
    stops == null ? null : Object.hashAll(stops!),
  );
548 549 550

  @override
  String toString() {
551 552 553 554 555 556 557 558 559 560
    final List<String> description = <String>[
      'begin: $begin',
      'end: $end',
      'colors: $colors',
      if (stops != null) 'stops: $stops',
      'tileMode: $tileMode',
      if (transform != null) 'transform: $transform',
    ];

    return '${objectRuntimeType(this, 'LinearGradient')}(${description.join(', ')})';
561 562 563 564 565
  }
}

/// A 2D radial gradient.
///
566
/// This class is used by [BoxDecoration] to represent radial gradients. This
567
/// abstracts out the arguments to the [ui.Gradient.radial] constructor from
568
/// the `dart:ui` library.
569
///
570 571 572
/// A normal radial gradient has a [center] and a [radius]. The [center] point
/// corresponds to 0.0, and the ring at [radius] from the center corresponds
/// to 1.0. These lengths are expressed in fractions, so that the same gradient
573
/// can be reused with varying sized boxes without changing the parameters.
574
/// (This contrasts with [ui.Gradient.radial], whose arguments are expressed
575
/// in logical pixels.)
576
///
577 578 579 580 581 582 583
/// It is also possible to create a two-point (or focal pointed) radial gradient
/// (which is sometimes referred to as a two point conic gradient, but is not the
/// same as a CSS conic gradient which corresponds to a [SweepGradient]). A [focal]
/// point and [focalRadius] can be specified similarly to [center] and [radius],
/// which will make the rendered gradient appear to be pointed or directed in the
/// direction of the [focal] point. This is only important if [focal] and [center]
/// are not equal or [focalRadius] > 0.0 (as this case is visually identical to a
584
/// normal radial gradient). One important case to avoid is having [focal] and
585 586
/// [center] both resolve to [Offset.zero] when [focalRadius] > 0.0. In such a case,
/// a valid shader cannot be created by the framework.
587 588
///
/// The [colors] are described by a list of [Color] objects. There must be at
589 590 591 592
/// least two colors. The [stops] list, if specified, must have the same length
/// as [colors]. It specifies fractions of the radius between 0.0 and 1.0,
/// giving concentric rings for each color stop. If it is null, a uniform
/// distribution is assumed.
593 594 595 596 597 598 599
///
/// The region of the canvas beyond [radius] from the [center] is colored
/// according to [tileMode].
///
/// Typically this class is used with [BoxDecoration], which does the painting.
/// To use a [RadialGradient] to paint on a canvas directly, see [createShader].
///
600
/// {@tool snippet}
601 602 603 604 605
///
/// This function draws a gradient that looks like a sun in a blue sky.
///
/// ```dart
/// void paintSky(Canvas canvas, Rect rect) {
606 607
///   const RadialGradient gradient = RadialGradient(
///     center: Alignment(0.7, -0.6), // near the top right
608
///     radius: 0.2,
609 610 611
///     colors: <Color>[
///       Color(0xFFFFFF00), // yellow sun
///       Color(0xFF0099FF), // blue sky
612
///     ],
613
///     stops: <double>[0.4, 1.0],
614 615
///   );
///   // rect is the area we are painting over
616
///   final Paint paint = Paint()
617 618 619 620
///     ..shader = gradient.createShader(rect);
///   canvas.drawRect(rect, paint);
/// }
/// ```
621
/// {@end-tool}
622 623 624 625 626
///
/// See also:
///
///  * [LinearGradient], which displays a gradient in parallel lines, and has an
///    example which shows a different way to use [Gradient] objects.
627 628
///  * [SweepGradient], which displays a gradient in a sweeping arc around a
///    center point.
629 630 631 632 633 634 635 636 637 638
///  * [BoxDecoration], which can take a [RadialGradient] in its
///    [BoxDecoration.gradient] property.
///  * [CustomPainter], which shows how to use the above sample code in a custom
///    painter.
class RadialGradient extends Gradient {
  /// Creates a radial gradient.
  ///
  /// The [colors] argument must not be null. If [stops] is non-null, it must
  /// have the same length as [colors].
  const RadialGradient({
639 640
    this.center = Alignment.center,
    this.radius = 0.5,
641 642
    required super.colors,
    super.stops,
643
    this.tileMode = TileMode.clamp,
644
    this.focal,
645
    this.focalRadius = 0.0,
646
    super.transform,
647 648
  }) : assert(center != null),
       assert(radius != null),
649
       assert(tileMode != null),
650
       assert(focalRadius != null);
651

652 653
  /// The center of the gradient, as an offset into the (-1.0, -1.0) x (1.0, 1.0)
  /// square describing the gradient which will be mapped onto the paint box.
654
  ///
655
  /// For example, an alignment of (0.0, 0.0) will place the radial
656
  /// gradient in the center of the box.
657
  ///
658
  /// If this is an [Alignment], then it is expressed as a vector from
659 660
  /// coordinate (0.0, 0.0), in a coordinate space that maps the center of the
  /// paint box at (0.0, 0.0) and the bottom right at (1.0, 1.0).
661
  ///
662
  /// It can also be an [AlignmentDirectional], where the start is the left in
663 664
  /// left-to-right contexts and the right in right-to-left contexts. If a
  /// text-direction-dependent value is provided here, then the [createShader]
665
  /// method will need to be given a [TextDirection].
666
  final AlignmentGeometry center;
667 668 669 670 671 672 673 674 675 676 677 678 679 680

  /// The radius of the gradient, as a fraction of the shortest side
  /// of the paint box.
  ///
  /// For example, if a radial gradient is painted on a box that is
  /// 100.0 pixels wide and 200.0 pixels tall, then a radius of 1.0
  /// will place the 1.0 stop at 100.0 pixels from the [center].
  final double radius;

  /// How this gradient should tile the plane beyond the outer ring at [radius]
  /// pixels from the [center].
  ///
  /// For details, see [TileMode].
  ///
681
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_radial.png)
682
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_decal_radial.png)
683 684
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_radial.png)
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_radial.png)
685
  ///
686
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_radialWithFocal.png)
687
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_decal_radialWithFocal.png)
688 689
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_radialWithFocal.png)
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_radialWithFocal.png)
690 691
  final TileMode tileMode;

692
  /// The focal point of the gradient. If specified, the gradient will appear
693 694
  /// to be focused along the vector from [center] to focal.
  ///
695
  /// See [center] for a description of how the coordinates are mapped.
696
  ///
697 698 699
  /// If this value is specified and [focalRadius] > 0.0, care should be taken
  /// to ensure that either this value or [center] will not both resolve to
  /// [Offset.zero], which would fail to create a valid gradient.
700
  final AlignmentGeometry? focal;
701 702 703 704 705 706

  /// The radius of the focal point of gradient, as a fraction of the shortest
  /// side of the paint box.
  ///
  /// For example, if a radial gradient is painted on a box that is
  /// 100.0 pixels wide and 200.0 pixels tall, then a radius of 1.0
707
  /// will place the 1.0 stop at 100.0 pixels from the [focal] point.
708
  ///
709 710 711 712 713
  /// If this value is specified and is greater than 0.0, either [focal] or
  /// [center] must not resolve to [Offset.zero], which would fail to create
  /// a valid gradient.
  final double focalRadius;

714
  @override
715
  Shader createShader(Rect rect, { TextDirection? textDirection }) {
716
    return ui.Gradient.radial(
717
      center.resolve(textDirection).withinRect(rect),
718
      radius * rect.shortestSide,
719
      colors, _impliedStops(), tileMode,
720
      _resolveTransform(rect, textDirection),
721
      focal == null  ? null : focal!.resolve(textDirection).withinRect(rect),
722
      focalRadius * rect.shortestSide,
723 724 725
    );
  }

726 727
  /// Returns a new [RadialGradient] with its colors scaled by the given factor.
  ///
728 729
  /// Since the alpha component of the Color is what is scaled, a factor
  /// of 0.0 or less results in a gradient that is fully transparent.
730 731
  @override
  RadialGradient scale(double factor) {
732
    return RadialGradient(
733 734
      center: center,
      radius: radius,
735
      colors: colors.map<Color>((Color color) => Color.lerp(null, color, factor)!).toList(),
736 737
      stops: stops,
      tileMode: tileMode,
738
      focal: focal,
739
      focalRadius: focalRadius,
740 741 742 743
    );
  }

  @override
744
  Gradient? lerpFrom(Gradient? a, double t) {
745
    if (a == null || (a is RadialGradient)) {
746
      return RadialGradient.lerp(a as RadialGradient?, this, t);
747
    }
748 749 750 751
    return super.lerpFrom(a, t);
  }

  @override
752
  Gradient? lerpTo(Gradient? b, double t) {
753
    if (b == null || (b is RadialGradient)) {
754
      return RadialGradient.lerp(this, b as RadialGradient?, t);
755
    }
756 757 758 759 760 761 762 763 764 765
    return super.lerpTo(b, t);
  }

  /// Linearly interpolate between two [RadialGradient]s.
  ///
  /// If either gradient is null, this function linearly interpolates from a
  /// a gradient that matches the other gradient in [center], [radius], [stops] and
  /// [tileMode] and with the same [colors] but transparent (using [scale]).
  ///
  /// If neither gradient is null, they must have the same number of [colors].
766
  ///
767
  /// The `t` argument represents a position on the timeline, with 0.0 meaning
768 769 770 771 772 773 774 775 776 777
  /// that the interpolation has not started, returning `a` (or something
  /// equivalent to `a`), 1.0 meaning that the interpolation has finished,
  /// returning `b` (or something equivalent to `b`), and values in between
  /// meaning that the interpolation is at the relevant point on the timeline
  /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and
  /// 1.0, so negative values and values greater than 1.0 are valid (and can
  /// easily be generated by curves such as [Curves.elasticInOut]).
  ///
  /// Values for `t` are usually obtained from an [Animation<double>], such as
  /// an [AnimationController].
778
  static RadialGradient? lerp(RadialGradient? a, RadialGradient? b, double t) {
779
    assert(t != null);
780
    if (a == null && b == null) {
781
      return null;
782 783
    }
    if (a == null) {
784
      return b!.scale(t);
785 786
    }
    if (b == null) {
787
      return a.scale(1.0 - t);
788
    }
789 790 791 792 793 794 795
    final _ColorsAndStops interpolated = _interpolateColorsAndStops(
        a.colors,
        a._impliedStops(),
        b.colors,
        b._impliedStops(),
        t,
    );
796
    return RadialGradient(
797 798
      center: AlignmentGeometry.lerp(a.center, b.center, t)!,
      radius: math.max(0.0, ui.lerpDouble(a.radius, b.radius, t)!),
799 800 801
      colors: interpolated.colors,
      stops: interpolated.stops,
      tileMode: t < 0.5 ? a.tileMode : b.tileMode, // TODO(ianh): interpolate tile mode
802
      focal: AlignmentGeometry.lerp(a.focal, b.focal, t),
803
      focalRadius: math.max(0.0, ui.lerpDouble(a.focalRadius, b.focalRadius, t)!),
804 805 806
    );
  }

807
  @override
808
  bool operator ==(Object other) {
809
    if (identical(this, other)) {
810
      return true;
811 812
    }
    if (other.runtimeType != runtimeType) {
813
      return false;
814
    }
815 816 817 818
    return other is RadialGradient
        && other.center == center
        && other.radius == radius
        && other.tileMode == tileMode
819
        && other.transform == transform
820 821 822 823
        && listEquals<Color>(other.colors, colors)
        && listEquals<double>(other.stops, stops)
        && other.focal == focal
        && other.focalRadius == focalRadius;
824 825 826
  }

  @override
827 828 829 830 831 832 833 834 835 836
  int get hashCode => Object.hash(
    center,
    radius,
    tileMode,
    transform,
    Object.hashAll(colors),
    stops == null ? null : Object.hashAll(stops!),
    focal,
    focalRadius,
  );
837 838 839

  @override
  String toString() {
840 841 842 843 844 845 846 847 848 849 850 851
    final List<String> description = <String>[
      'center: $center',
      'radius: ${debugFormatDouble(radius)}',
      'colors: $colors',
      if (stops != null) 'stops: $stops',
      'tileMode: $tileMode',
      if (focal != null) 'focal: $focal',
      'focalRadius: ${debugFormatDouble(focalRadius)}',
      if (transform != null) 'transform: $transform',
    ];

    return '${objectRuntimeType(this, 'RadialGradient')}(${description.join(', ')})';
852 853
  }
}
854 855 856 857

/// A 2D sweep gradient.
///
/// This class is used by [BoxDecoration] to represent sweep gradients. This
858
/// abstracts out the arguments to the [ui.Gradient.sweep] constructor from
859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876
/// the `dart:ui` library.
///
/// A gradient has a [center], a [startAngle], and an [endAngle]. The [startAngle]
/// corresponds to 0.0, and the [endAngle] corresponds to 1.0. These angles are
/// expressed in radians.
///
/// The [colors] are described by a list of [Color] objects. There must be at
/// least two colors. The [stops] list, if specified, must have the same length
/// as [colors]. It specifies fractions of the vector from start to end, between
/// 0.0 and 1.0, for each color. If it is null, a uniform distribution is
/// assumed.
///
/// The region of the canvas before [startAngle] and after [endAngle] is colored
/// according to [tileMode].
///
/// Typically this class is used with [BoxDecoration], which does the painting.
/// To use a [SweepGradient] to paint on a canvas directly, see [createShader].
///
877
/// {@tool snippet}
878 879 880 881
///
/// This sample draws a different color in each quadrant.
///
/// ```dart
882
/// Container(
883
///   decoration: const BoxDecoration(
884
///     gradient: SweepGradient(
885
///       center: FractionalOffset.center,
886
///       colors: <Color>[
887 888 889 890 891
///         Color(0xFF4285F4), // blue
///         Color(0xFF34A853), // green
///         Color(0xFFFBBC05), // yellow
///         Color(0xFFEA4335), // red
///         Color(0xFF4285F4), // blue again to seamlessly transition to the start
892
///       ],
893
///       stops: <double>[0.0, 0.25, 0.5, 0.75, 1.0],
894 895 896 897 898 899
///     ),
///   )
/// )
/// ```
/// {@end-tool}
///
900
/// {@tool snippet}
901 902 903 904 905 906
///
/// This sample takes the above gradient and rotates it by `math.pi/4` radians,
/// i.e. 45 degrees.
///
/// ```dart
/// Container(
907
///   decoration: const BoxDecoration(
908 909
///     gradient: SweepGradient(
///       center: FractionalOffset.center,
910
///       colors: <Color>[
911 912 913 914 915 916
///         Color(0xFF4285F4), // blue
///         Color(0xFF34A853), // green
///         Color(0xFFFBBC05), // yellow
///         Color(0xFFEA4335), // red
///         Color(0xFF4285F4), // blue again to seamlessly transition to the start
///       ],
917
///       stops: <double>[0.0, 0.25, 0.5, 0.75, 1.0],
918 919
///       transform: GradientRotation(math.pi/4),
///     ),
920
///   ),
921
/// )
922
/// ```
923
/// {@end-tool}
924 925 926 927 928 929 930 931 932 933 934 935 936 937 938
///
/// See also:
///
///  * [LinearGradient], which displays a gradient in parallel lines, and has an
///    example which shows a different way to use [Gradient] objects.
///  * [RadialGradient], which displays a gradient in concentric circles, and
///    has an example which shows a different way to use [Gradient] objects.
///  * [BoxDecoration], which can take a [SweepGradient] in its
///    [BoxDecoration.gradient] property.
class SweepGradient extends Gradient {
  /// Creates a sweep gradient.
  ///
  /// The [colors] argument must not be null. If [stops] is non-null, it must
  /// have the same length as [colors].
  const SweepGradient({
939 940 941
    this.center = Alignment.center,
    this.startAngle = 0.0,
    this.endAngle = math.pi * 2,
942 943
    required super.colors,
    super.stops,
944
    this.tileMode = TileMode.clamp,
945
    super.transform,
946 947 948
  }) : assert(center != null),
       assert(startAngle != null),
       assert(endAngle != null),
949
       assert(tileMode != null);
950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982

  /// The center of the gradient, as an offset into the (-1.0, -1.0) x (1.0, 1.0)
  /// square describing the gradient which will be mapped onto the paint box.
  ///
  /// For example, an alignment of (0.0, 0.0) will place the sweep
  /// gradient in the center of the box.
  ///
  /// If this is an [Alignment], then it is expressed as a vector from
  /// coordinate (0.0, 0.0), in a coordinate space that maps the center of the
  /// paint box at (0.0, 0.0) and the bottom right at (1.0, 1.0).
  ///
  /// It can also be an [AlignmentDirectional], where the start is the left in
  /// left-to-right contexts and the right in right-to-left contexts. If a
  /// text-direction-dependent value is provided here, then the [createShader]
  /// method will need to be given a [TextDirection].
  final AlignmentGeometry center;

  /// The angle in radians at which stop 0.0 of the gradient is placed.
  ///
  /// Defaults to 0.0.
  final double startAngle;

  /// The angle in radians at which stop 1.0 of the gradient is placed.
  ///
  /// Defaults to math.pi * 2.
  final double endAngle;

  /// How this gradient should tile the plane beyond in the region before
  /// [startAngle] and after [endAngle].
  ///
  /// For details, see [TileMode].
  ///
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_sweep.png)
983
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_decal_sweep.png)
984 985 986 987 988
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_mirror_sweep.png)
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_repeated_sweep.png)
  final TileMode tileMode;

  @override
989
  Shader createShader(Rect rect, { TextDirection? textDirection }) {
990
    return ui.Gradient.sweep(
991 992 993 994
      center.resolve(textDirection).withinRect(rect),
      colors, _impliedStops(), tileMode,
      startAngle,
      endAngle,
995
      _resolveTransform(rect, textDirection),
996 997 998 999 1000 1001 1002 1003 1004
    );
  }

  /// Returns a new [SweepGradient] with its colors scaled by the given factor.
  ///
  /// Since the alpha component of the Color is what is scaled, a factor
  /// of 0.0 or less results in a gradient that is fully transparent.
  @override
  SweepGradient scale(double factor) {
1005
    return SweepGradient(
1006 1007 1008
      center: center,
      startAngle: startAngle,
      endAngle: endAngle,
1009
      colors: colors.map<Color>((Color color) => Color.lerp(null, color, factor)!).toList(),
1010 1011 1012 1013 1014 1015
      stops: stops,
      tileMode: tileMode,
    );
  }

  @override
1016
  Gradient? lerpFrom(Gradient? a, double t) {
1017
    if (a == null || (a is SweepGradient)) {
1018
      return SweepGradient.lerp(a as SweepGradient?, this, t);
1019
    }
1020 1021 1022 1023
    return super.lerpFrom(a, t);
  }

  @override
1024
  Gradient? lerpTo(Gradient? b, double t) {
1025
    if (b == null || (b is SweepGradient)) {
1026
      return SweepGradient.lerp(this, b as SweepGradient?, t);
1027
    }
1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048
    return super.lerpTo(b, t);
  }

  /// Linearly interpolate between two [SweepGradient]s.
  ///
  /// If either gradient is null, then the non-null gradient is returned with
  /// its color scaled in the same way as the [scale] function.
  ///
  /// If neither gradient is null, they must have the same number of [colors].
  ///
  /// The `t` argument represents a position on the timeline, with 0.0 meaning
  /// that the interpolation has not started, returning `a` (or something
  /// equivalent to `a`), 1.0 meaning that the interpolation has finished,
  /// returning `b` (or something equivalent to `b`), and values in between
  /// meaning that the interpolation is at the relevant point on the timeline
  /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and
  /// 1.0, so negative values and values greater than 1.0 are valid (and can
  /// easily be generated by curves such as [Curves.elasticInOut]).
  ///
  /// Values for `t` are usually obtained from an [Animation<double>], such as
  /// an [AnimationController].
1049
  static SweepGradient? lerp(SweepGradient? a, SweepGradient? b, double t) {
1050
    assert(t != null);
1051
    if (a == null && b == null) {
1052
      return null;
1053 1054
    }
    if (a == null) {
1055
      return b!.scale(t);
1056 1057
    }
    if (b == null) {
1058
      return a.scale(1.0 - t);
1059
    }
1060 1061 1062 1063 1064 1065 1066
    final _ColorsAndStops interpolated = _interpolateColorsAndStops(
        a.colors,
        a._impliedStops(),
        b.colors,
        b._impliedStops(),
        t,
    );
1067
    return SweepGradient(
1068 1069 1070
      center: AlignmentGeometry.lerp(a.center, b.center, t)!,
      startAngle: math.max(0.0, ui.lerpDouble(a.startAngle, b.startAngle, t)!),
      endAngle: math.max(0.0, ui.lerpDouble(a.endAngle, b.endAngle, t)!),
1071 1072 1073 1074 1075 1076 1077
      colors: interpolated.colors,
      stops: interpolated.stops,
      tileMode: t < 0.5 ? a.tileMode : b.tileMode, // TODO(ianh): interpolate tile mode
    );
  }

  @override
1078
  bool operator ==(Object other) {
1079
    if (identical(this, other)) {
1080
      return true;
1081 1082
    }
    if (other.runtimeType != runtimeType) {
1083
      return false;
1084
    }
1085 1086 1087 1088 1089
    return other is SweepGradient
        && other.center == center
        && other.startAngle == startAngle
        && other.endAngle == endAngle
        && other.tileMode == tileMode
1090
        && other.transform == transform
1091 1092
        && listEquals<Color>(other.colors, colors)
        && listEquals<double>(other.stops, stops);
1093 1094 1095
  }

  @override
1096 1097 1098 1099 1100 1101 1102 1103 1104
  int get hashCode => Object.hash(
    center,
    startAngle,
    endAngle,
    tileMode,
    transform,
    Object.hashAll(colors),
    stops == null ? null : Object.hashAll(stops!),
  );
1105 1106 1107

  @override
  String toString() {
1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118
    final List<String> description = <String>[
      'center: $center',
      'startAngle: ${debugFormatDouble(startAngle)}',
      'endAngle: ${debugFormatDouble(endAngle)}',
      'colors: $colors',
      if (stops != null) 'stops: $stops',
      'tileMode: $tileMode',
      if (transform != null) 'transform: $transform',
    ];

    return '${objectRuntimeType(this, 'SweepGradient')}(${description.join(', ')})';
1119 1120
  }
}