gradient.dart 40.7 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

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

import 'package:flutter/foundation.dart';
12
import 'package:vector_math/vector_math_64.dart';
13

14
import 'alignment.dart';
15 16
import 'basic_types.dart';

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

23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
/// 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);
  if (t <= stops.first)
    return colors.first;
  if (t >= stops.last)
    return colors.last;
  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 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138

  @override
  bool operator ==(Object other) {
    if (identical(this, other))
      return true;
    if (other.runtimeType != runtimeType)
      return false;
    return other is GradientRotation
        && other.radians == radians;
  }

  @override
  int get hashCode => radians.hashCode;

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

141 142
/// A 2D gradient.
///
143 144
/// This is an interface that allows [LinearGradient], [RadialGradient], and
/// [SweepGradient] classes to be used interchangeably in [BoxDecoration]s.
145 146 147
///
/// See also:
///
148
///  * [Gradient](dart-ui/Gradient-class.html), the class in the [dart:ui] library.
149
///
150 151
@immutable
abstract class Gradient {
152 153 154 155 156 157 158 159
  /// 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).
160 161 162 163 164 165 166
  ///
  /// 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.
167
  const Gradient({
168
    required this.colors,
169
    this.stops,
170
    this.transform,
171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
  }) : 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.
197
  final List<double>? stops;
198

199 200 201 202
  /// 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.
203
  final GradientTransform? transform;
204

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

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

  /// 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);

239
  /// Linearly interpolates from another [Gradient] to `this`.
240 241 242 243 244 245 246 247
  ///
  /// 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].
  ///
248 249 250 251 252 253 254 255 256 257 258 259 260
  /// 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].
  ///
261 262
  /// Instead of calling this directly, use [Gradient.lerp].
  @protected
263
  Gradient? lerpFrom(Gradient? a, double t) {
264 265 266 267 268
    if (a == null)
      return scale(t);
    return null;
  }

269
  /// Linearly interpolates from `this` to another [Gradient].
270 271 272 273 274 275 276 277 278 279
  ///
  /// 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].
  ///
280 281 282 283 284 285 286 287 288 289 290 291
  /// 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].
  ///
292 293
  /// Instead of calling this directly, use [Gradient.lerp].
  @protected
294
  Gradient? lerpTo(Gradient? b, double t) {
295 296 297 298 299
    if (b == null)
      return scale(1.0 - t);
    return null;
  }

300 301 302 303 304 305 306
  /// 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`.
  ///
307
  /// {@macro dart.ui.shadow.lerp}
308
  static Gradient? lerp(Gradient? a, Gradient? b, double t) {
309
    assert(t != null);
310
    Gradient? result;
311 312 313 314
    if (b != null)
      result = b.lerpFrom(a, t); // if a is null, this must return non-null
    if (result == null && a != null)
      result = a.lerpTo(b, t); // if b is null, this must return non-null
315 316
    if (result != null)
      return result;
317
    if (a == null && b == null)
318
      return null;
319
    assert(a != null && b != null);
320
    return t < 0.5 ? a!.scale(1.0 - (t * 2.0)) : b!.scale((t - 0.5) * 2.0);
321
  }
322

323
  Float64List? _resolveTransform(Rect bounds, TextDirection? textDirection) {
324 325
    return transform?.transform(bounds, textDirection: textDirection)?.storage;
  }
326 327 328 329
}

/// A 2D linear gradient.
///
330 331 332
/// This class is used by [BoxDecoration] to represent linear gradients. This
/// abstracts out the arguments to the [new ui.Gradient.linear] constructor from
/// the `dart:ui` library.
333 334 335 336 337 338 339 340
///
/// 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
/// sized boxes without changing the parameters. (This contrasts with [new
/// ui.Gradient.linear], whose arguments are expressed in logical pixels.)
///
/// The [colors] are described by a list of [Color] objects. There must be at
341 342 343 344
/// 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.
345 346 347 348 349 350 351
///
/// 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].
///
352
/// {@tool dartpad}
353 354 355
/// This sample draws a picture that looks like vertical window shades by having
/// a [Container] display a [BoxDecoration] with a [LinearGradient].
///
356
/// ** See code in examples/api/lib/painting/gradient/linear_gradient.0.dart **
357
/// {@end-tool}
358 359 360 361 362
///
/// See also:
///
///  * [RadialGradient], which displays a gradient in concentric circles, and
///    has an example which shows a different way to use [Gradient] objects.
363 364
///  * [SweepGradient], which displays a gradient in a sweeping arc around a
///    center point.
365 366 367
///  * [BoxDecoration], which can take a [LinearGradient] in its
///    [BoxDecoration.gradient] property.
class LinearGradient extends Gradient {
368
  /// Creates a linear gradient.
369 370 371 372
  ///
  /// The [colors] argument must not be null. If [stops] is non-null, it must
  /// have the same length as [colors].
  const LinearGradient({
373 374
    this.begin = Alignment.centerLeft,
    this.end = Alignment.centerRight,
375 376
    required List<Color> colors,
    List<double>? stops,
377
    this.tileMode = TileMode.clamp,
378
    GradientTransform? transform,
379 380
  }) : assert(begin != null),
       assert(end != null),
381
       assert(tileMode != null),
382
       super(colors: colors, stops: stops, transform: transform);
383

384 385
  /// The offset at which stop 0.0 of the gradient is placed.
  ///
386
  /// If this is an [Alignment], then it is expressed as a vector from
387 388
  /// 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).
389
  ///
390
  /// For example, a begin offset of (-1.0, 0.0) is half way down the
391
  /// left side of the box.
392
  ///
393
  /// It can also be an [AlignmentDirectional], where the start is the
394 395 396
  /// 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].
397
  final AlignmentGeometry begin;
398

399
  /// The offset at which stop 1.0 of the gradient is placed.
400
  ///
401
  /// If this is an [Alignment], then it is expressed as a vector from
402 403
  /// 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).
404
  ///
405
  /// For example, a begin offset of (1.0, 0.0) is half way down the
406
  /// right side of the box.
407
  ///
408
  /// It can also be an [AlignmentDirectional], where the start is the left in
409 410
  /// left-to-right contexts and the right in right-to-left contexts. If a
  /// text-direction-dependent value is provided here, then the [createShader]
411
  /// method will need to be given a [TextDirection].
412
  final AlignmentGeometry end;
413 414 415 416 417 418

  /// How this gradient should tile the plane beyond in the region before
  /// [begin] and after [end].
  ///
  /// For details, see [TileMode].
  ///
419
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_linear.png)
420
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_decal_linear.png)
421 422
  /// ![](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)
423 424 425
  final TileMode tileMode;

  @override
426
  Shader createShader(Rect rect, { TextDirection? textDirection }) {
427
    return ui.Gradient.linear(
428 429
      begin.resolve(textDirection).withinRect(rect),
      end.resolve(textDirection).withinRect(rect),
430
      colors, _impliedStops(), tileMode, _resolveTransform(rect, textDirection),
431 432 433
    );
  }

434
  /// Returns a new [LinearGradient] with its colors scaled by the given factor.
435
  ///
436 437
  /// 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.
438
  @override
439
  LinearGradient scale(double factor) {
440
    return LinearGradient(
441 442
      begin: begin,
      end: end,
443
      colors: colors.map<Color>((Color color) => Color.lerp(null, color, factor)!).toList(),
444 445 446 447 448
      stops: stops,
      tileMode: tileMode,
    );
  }

449
  @override
450
  Gradient? lerpFrom(Gradient? a, double t) {
451
    if (a == null || (a is LinearGradient))
452
      return LinearGradient.lerp(a as LinearGradient?, this, t);
453 454 455 456
    return super.lerpFrom(a, t);
  }

  @override
457
  Gradient? lerpTo(Gradient? b, double t) {
458
    if (b == null || (b is LinearGradient))
459
      return LinearGradient.lerp(this, b as LinearGradient?, t);
460 461 462
    return super.lerpTo(b, t);
  }

463 464 465 466
  /// 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
467
  /// [tileMode] and with the same [colors] but transparent (using [scale]).
468 469
  ///
  /// If neither gradient is null, they must have the same number of [colors].
470
  ///
471
  /// The `t` argument represents a position on the timeline, with 0.0 meaning
472 473 474 475 476 477 478 479 480 481
  /// 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].
482
  static LinearGradient? lerp(LinearGradient? a, LinearGradient? b, double t) {
483
    assert(t != null);
484 485 486
    if (a == null && b == null)
      return null;
    if (a == null)
487
      return b!.scale(t);
488 489
    if (b == null)
      return a.scale(1.0 - t);
490 491 492 493 494 495 496
    final _ColorsAndStops interpolated = _interpolateColorsAndStops(
        a.colors,
        a._impliedStops(),
        b.colors,
        b._impliedStops(),
        t,
    );
497
    return LinearGradient(
498 499
      begin: AlignmentGeometry.lerp(a.begin, b.begin, t)!,
      end: AlignmentGeometry.lerp(a.end, b.end, t)!,
500 501 502
      colors: interpolated.colors,
      stops: interpolated.stops,
      tileMode: t < 0.5 ? a.tileMode : b.tileMode, // TODO(ianh): interpolate tile mode
503 504 505 506
    );
  }

  @override
507
  bool operator ==(Object other) {
508 509
    if (identical(this, other))
      return true;
510
    if (other.runtimeType != runtimeType)
511
      return false;
512 513 514 515
    return other is LinearGradient
        && other.begin == begin
        && other.end == end
        && other.tileMode == tileMode
516
        && other.transform == transform
517 518
        && listEquals<Color>(other.colors, colors)
        && listEquals<double>(other.stops, stops);
519 520 521
  }

  @override
522
  int get hashCode => hashValues(begin, end, tileMode, transform, hashList(colors), hashList(stops));
523 524 525

  @override
  String toString() {
526 527 528 529 530 531 532 533 534 535
    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(', ')})';
536 537 538 539 540
  }
}

/// A 2D radial gradient.
///
541 542 543
/// This class is used by [BoxDecoration] to represent radial gradients. This
/// abstracts out the arguments to the [new ui.Gradient.radial] constructor from
/// the `dart:ui` library.
544
///
545 546 547
/// 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
548
/// can be reused with varying sized boxes without changing the parameters.
549 550
/// (This contrasts with [new ui.Gradient.radial], whose arguments are expressed
/// in logical pixels.)
551
///
552 553 554 555 556 557 558
/// 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
559
/// normal radial gradient).  One important case to avoid is having [focal] and
560 561
/// [center] both resolve to [Offset.zero] when [focalRadius] > 0.0. In such a case,
/// a valid shader cannot be created by the framework.
562 563
///
/// The [colors] are described by a list of [Color] objects. There must be at
564 565 566 567
/// 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.
568 569 570 571 572 573 574
///
/// 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].
///
575
/// {@tool snippet}
576 577 578 579 580
///
/// This function draws a gradient that looks like a sun in a blue sky.
///
/// ```dart
/// void paintSky(Canvas canvas, Rect rect) {
581 582
///   const RadialGradient gradient = RadialGradient(
///     center: Alignment(0.7, -0.6), // near the top right
583
///     radius: 0.2,
584 585 586
///     colors: <Color>[
///       Color(0xFFFFFF00), // yellow sun
///       Color(0xFF0099FF), // blue sky
587
///     ],
588
///     stops: <double>[0.4, 1.0],
589 590
///   );
///   // rect is the area we are painting over
591
///   final Paint paint = Paint()
592 593 594 595
///     ..shader = gradient.createShader(rect);
///   canvas.drawRect(rect, paint);
/// }
/// ```
596
/// {@end-tool}
597 598 599 600 601
///
/// See also:
///
///  * [LinearGradient], which displays a gradient in parallel lines, and has an
///    example which shows a different way to use [Gradient] objects.
602 603
///  * [SweepGradient], which displays a gradient in a sweeping arc around a
///    center point.
604 605 606 607 608 609 610 611 612 613
///  * [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({
614 615
    this.center = Alignment.center,
    this.radius = 0.5,
616 617
    required List<Color> colors,
    List<double>? stops,
618
    this.tileMode = TileMode.clamp,
619
    this.focal,
620
    this.focalRadius = 0.0,
621
    GradientTransform? transform,
622 623
  }) : assert(center != null),
       assert(radius != null),
624
       assert(tileMode != null),
625
       assert(focalRadius != null),
626
       super(colors: colors, stops: stops, transform: transform);
627

628 629
  /// 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.
630
  ///
631
  /// For example, an alignment of (0.0, 0.0) will place the radial
632
  /// gradient in the center of the box.
633
  ///
634
  /// If this is an [Alignment], then it is expressed as a vector from
635 636
  /// 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).
637
  ///
638
  /// It can also be an [AlignmentDirectional], where the start is the left in
639 640
  /// left-to-right contexts and the right in right-to-left contexts. If a
  /// text-direction-dependent value is provided here, then the [createShader]
641
  /// method will need to be given a [TextDirection].
642
  final AlignmentGeometry center;
643 644 645 646 647 648 649 650 651 652 653 654 655 656

  /// 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].
  ///
657
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_radial.png)
658
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_decal_radial.png)
659 660
  /// ![](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)
661
  ///
662
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_clamp_radialWithFocal.png)
663
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_decal_radialWithFocal.png)
664 665
  /// ![](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)
666 667
  final TileMode tileMode;

668
  /// The focal point of the gradient.  If specified, the gradient will appear
669 670
  /// to be focused along the vector from [center] to focal.
  ///
671
  /// See [center] for a description of how the coordinates are mapped.
672
  ///
673 674 675
  /// 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.
676
  final AlignmentGeometry? focal;
677 678 679 680 681 682

  /// 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
683
  /// will place the 1.0 stop at 100.0 pixels from the [focal] point.
684
  ///
685 686 687 688 689
  /// 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;

690
  @override
691
  Shader createShader(Rect rect, { TextDirection? textDirection }) {
692
    return ui.Gradient.radial(
693
      center.resolve(textDirection).withinRect(rect),
694
      radius * rect.shortestSide,
695
      colors, _impliedStops(), tileMode,
696
      _resolveTransform(rect, textDirection),
697
      focal == null  ? null : focal!.resolve(textDirection).withinRect(rect),
698
      focalRadius * rect.shortestSide,
699 700 701
    );
  }

702 703
  /// Returns a new [RadialGradient] with its colors scaled by the given factor.
  ///
704 705
  /// 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.
706 707
  @override
  RadialGradient scale(double factor) {
708
    return RadialGradient(
709 710
      center: center,
      radius: radius,
711
      colors: colors.map<Color>((Color color) => Color.lerp(null, color, factor)!).toList(),
712 713
      stops: stops,
      tileMode: tileMode,
714
      focal: focal,
715
      focalRadius: focalRadius,
716 717 718 719
    );
  }

  @override
720
  Gradient? lerpFrom(Gradient? a, double t) {
721
    if (a == null || (a is RadialGradient))
722
      return RadialGradient.lerp(a as RadialGradient?, this, t);
723 724 725 726
    return super.lerpFrom(a, t);
  }

  @override
727
  Gradient? lerpTo(Gradient? b, double t) {
728
    if (b == null || (b is RadialGradient))
729
      return RadialGradient.lerp(this, b as RadialGradient?, t);
730 731 732 733 734 735 736 737 738 739
    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].
740
  ///
741
  /// The `t` argument represents a position on the timeline, with 0.0 meaning
742 743 744 745 746 747 748 749 750 751
  /// 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].
752
  static RadialGradient? lerp(RadialGradient? a, RadialGradient? b, double t) {
753
    assert(t != null);
754 755 756
    if (a == null && b == null)
      return null;
    if (a == null)
757
      return b!.scale(t);
758 759
    if (b == null)
      return a.scale(1.0 - t);
760 761 762 763 764 765 766
    final _ColorsAndStops interpolated = _interpolateColorsAndStops(
        a.colors,
        a._impliedStops(),
        b.colors,
        b._impliedStops(),
        t,
    );
767
    return RadialGradient(
768 769
      center: AlignmentGeometry.lerp(a.center, b.center, t)!,
      radius: math.max(0.0, ui.lerpDouble(a.radius, b.radius, t)!),
770 771 772
      colors: interpolated.colors,
      stops: interpolated.stops,
      tileMode: t < 0.5 ? a.tileMode : b.tileMode, // TODO(ianh): interpolate tile mode
773
      focal: AlignmentGeometry.lerp(a.focal, b.focal, t),
774
      focalRadius: math.max(0.0, ui.lerpDouble(a.focalRadius, b.focalRadius, t)!),
775 776 777
    );
  }

778
  @override
779
  bool operator ==(Object other) {
780 781
    if (identical(this, other))
      return true;
782
    if (other.runtimeType != runtimeType)
783
      return false;
784 785 786 787
    return other is RadialGradient
        && other.center == center
        && other.radius == radius
        && other.tileMode == tileMode
788
        && other.transform == transform
789 790 791 792
        && listEquals<Color>(other.colors, colors)
        && listEquals<double>(other.stops, stops)
        && other.focal == focal
        && other.focalRadius == focalRadius;
793 794 795
  }

  @override
796
  int get hashCode => hashValues(center, radius, tileMode, transform, hashList(colors), hashList(stops), focal, focalRadius);
797 798 799

  @override
  String toString() {
800 801 802 803 804 805 806 807 808 809 810 811
    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(', ')})';
812 813
  }
}
814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836

/// A 2D sweep gradient.
///
/// This class is used by [BoxDecoration] to represent sweep gradients. This
/// abstracts out the arguments to the [new ui.Gradient.sweep] constructor from
/// 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].
///
837
/// {@tool snippet}
838 839 840 841
///
/// This sample draws a different color in each quadrant.
///
/// ```dart
842
/// Container(
843
///   decoration: const BoxDecoration(
844
///     gradient: SweepGradient(
845 846 847
///       center: FractionalOffset.center,
///       startAngle: 0.0,
///       endAngle: math.pi * 2,
848
///       colors: <Color>[
849 850 851 852 853
///         Color(0xFF4285F4), // blue
///         Color(0xFF34A853), // green
///         Color(0xFFFBBC05), // yellow
///         Color(0xFFEA4335), // red
///         Color(0xFF4285F4), // blue again to seamlessly transition to the start
854
///       ],
855
///       stops: <double>[0.0, 0.25, 0.5, 0.75, 1.0],
856 857 858 859 860 861
///     ),
///   )
/// )
/// ```
/// {@end-tool}
///
862
/// {@tool snippet}
863 864 865 866 867 868
///
/// This sample takes the above gradient and rotates it by `math.pi/4` radians,
/// i.e. 45 degrees.
///
/// ```dart
/// Container(
869
///   decoration: const BoxDecoration(
870 871 872 873
///     gradient: SweepGradient(
///       center: FractionalOffset.center,
///       startAngle: 0.0,
///       endAngle: math.pi * 2,
874
///       colors: <Color>[
875 876 877 878 879 880
///         Color(0xFF4285F4), // blue
///         Color(0xFF34A853), // green
///         Color(0xFFFBBC05), // yellow
///         Color(0xFFEA4335), // red
///         Color(0xFF4285F4), // blue again to seamlessly transition to the start
///       ],
881
///       stops: <double>[0.0, 0.25, 0.5, 0.75, 1.0],
882 883
///       transform: GradientRotation(math.pi/4),
///     ),
884
///   ),
885
/// )
886
/// ```
887
/// {@end-tool}
888 889 890 891 892 893 894 895 896 897 898 899 900 901 902
///
/// 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({
903 904 905
    this.center = Alignment.center,
    this.startAngle = 0.0,
    this.endAngle = math.pi * 2,
906 907
    required List<Color> colors,
    List<double>? stops,
908
    this.tileMode = TileMode.clamp,
909
    GradientTransform? transform,
910 911 912 913
  }) : assert(center != null),
       assert(startAngle != null),
       assert(endAngle != null),
       assert(tileMode != null),
914
       super(colors: colors, stops: stops, transform: transform);
915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947

  /// 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)
948
  /// ![](https://flutter.github.io/assets-for-api-docs/assets/dart-ui/tile_mode_decal_sweep.png)
949 950 951 952 953
  /// ![](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
954
  Shader createShader(Rect rect, { TextDirection? textDirection }) {
955
    return ui.Gradient.sweep(
956 957 958 959
      center.resolve(textDirection).withinRect(rect),
      colors, _impliedStops(), tileMode,
      startAngle,
      endAngle,
960
      _resolveTransform(rect, textDirection),
961 962 963 964 965 966 967 968 969
    );
  }

  /// 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) {
970
    return SweepGradient(
971 972 973
      center: center,
      startAngle: startAngle,
      endAngle: endAngle,
974
      colors: colors.map<Color>((Color color) => Color.lerp(null, color, factor)!).toList(),
975 976 977 978 979 980
      stops: stops,
      tileMode: tileMode,
    );
  }

  @override
981
  Gradient? lerpFrom(Gradient? a, double t) {
982
    if (a == null || (a is SweepGradient))
983
      return SweepGradient.lerp(a as SweepGradient?, this, t);
984 985 986 987
    return super.lerpFrom(a, t);
  }

  @override
988
  Gradient? lerpTo(Gradient? b, double t) {
989
    if (b == null || (b is SweepGradient))
990
      return SweepGradient.lerp(this, b as SweepGradient?, t);
991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011
    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].
1012
  static SweepGradient? lerp(SweepGradient? a, SweepGradient? b, double t) {
1013 1014 1015 1016
    assert(t != null);
    if (a == null && b == null)
      return null;
    if (a == null)
1017
      return b!.scale(t);
1018 1019
    if (b == null)
      return a.scale(1.0 - t);
1020 1021 1022 1023 1024 1025 1026
    final _ColorsAndStops interpolated = _interpolateColorsAndStops(
        a.colors,
        a._impliedStops(),
        b.colors,
        b._impliedStops(),
        t,
    );
1027
    return SweepGradient(
1028 1029 1030
      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)!),
1031 1032 1033 1034 1035 1036 1037
      colors: interpolated.colors,
      stops: interpolated.stops,
      tileMode: t < 0.5 ? a.tileMode : b.tileMode, // TODO(ianh): interpolate tile mode
    );
  }

  @override
1038
  bool operator ==(Object other) {
1039 1040
    if (identical(this, other))
      return true;
1041
    if (other.runtimeType != runtimeType)
1042
      return false;
1043 1044 1045 1046 1047
    return other is SweepGradient
        && other.center == center
        && other.startAngle == startAngle
        && other.endAngle == endAngle
        && other.tileMode == tileMode
1048
        && other.transform == transform
1049 1050
        && listEquals<Color>(other.colors, colors)
        && listEquals<double>(other.stops, stops);
1051 1052 1053
  }

  @override
1054
  int get hashCode => hashValues(center, startAngle, endAngle, tileMode, transform, hashList(colors), hashList(stops));
1055 1056 1057

  @override
  String toString() {
1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068
    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(', ')})';
1069 1070
  }
}