blob: d10edd86c2b9d0a9ac0ae7799ab5380cb1b1f915 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'dart:ui' as ui show Gradient, lerpDouble;
import 'package:flutter/foundation.dart';
import 'alignment.dart';
import 'basic_types.dart';
class _ColorsAndStops {
_ColorsAndStops(this.colors, this.stops);
final List<Color> colors;
final List<double> stops;
}
_ColorsAndStops _interpolateColorsAndStops(List<Color> aColors, List<double> aStops, List<Color> bColors, List<double> bStops, double t) {
assert(aColors.length == bColors.length, 'Cannot interpolate between two gradients with a different number of colors.'); // TODO(ianh): remove limitation
assert((aStops == null && aColors.length == 2) || (aStops != null && aStops.length == aColors.length));
assert((bStops == null && bColors.length == 2) || (bStops != null && bStops.length == bColors.length));
final List<Color> interpolatedColors = <Color>[];
for (int i = 0; i < aColors.length; i += 1)
interpolatedColors.add(Color.lerp(aColors[i], bColors[i], t));
List<double> interpolatedStops;
if (aStops != null || bStops != null) {
aStops ??= const <double>[0.0, 1.0];
bStops ??= const <double>[0.0, 1.0];
assert(aStops.length == bStops.length);
for (int i = 0; i < aStops.length; i += 1)
interpolatedStops.add(ui.lerpDouble(aStops[i], bStops[i], t).clamp(0.0, 1.0));
}
return new _ColorsAndStops(interpolatedColors, interpolatedStops);
}
/// A 2D gradient.
///
/// This is an interface that allows [LinearGradient] and [RadialGradient]
/// classes to be used interchangeably in [BoxDecoration]s.
///
/// See also:
///
/// * [dart:ui.Gradient], the class in the [dart:ui] library that is
/// encapsulated by this class and its subclasses.
@immutable
abstract class Gradient {
/// Abstract const constructor. This constructor enables subclasses to provide
/// const constructors so that they can be used in const expressions.
const Gradient();
/// Creates a [Shader] for this gradient to fill the given rect.
///
/// If the gradient's configuration is text-direction-dependent, for example
/// it uses [AlignmentDirectional] objects instead of [Alignment]
/// objects, then the `textDirection` argument must not be null.
Shader createShader(Rect rect, { TextDirection textDirection });
/// 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);
/// Linearly interpolates from `a` to [this].
///
/// 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].
///
/// Instead of calling this directly, use [Gradient.lerp].
@protected
Gradient lerpFrom(Gradient a, double t) {
if (a == null)
return scale(t);
return null;
}
/// Linearly interpolates from [this] to `b`.
///
/// 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].
///
/// Instead of calling this directly, use [Gradient.lerp].
@protected
Gradient lerpTo(Gradient b, double t) {
if (b == null)
return scale(1.0 - t);
return null;
}
/// Linearly interpolates from `begin` to `end`.
///
/// This defers to `end`'s [lerpTo] function if `end` is not null. If `end` is
/// null or if its [lerpTo] returns null, it uses `begin`'s [lerpFrom]
/// function instead. If both return null, it returns `begin` before `t=0.5`
/// and `end` after `t=0.5`.
static Gradient lerp(Gradient begin, Gradient end, double t) {
Gradient result;
if (end != null)
result = end.lerpFrom(begin, t); // if begin is null, this must return non-null
if (result == null && begin != null)
result = begin.lerpTo(end, t); // if end is null, this must return non-null
if (result != null)
return result;
if (begin == null && end == null)
return null;
assert(begin != null && end != null);
return t < 0.5 ? begin.scale(1.0 - (t * 2.0)) : end.scale((t - 0.5) * 2.0);
}
}
/// A 2D linear gradient.
///
/// This class is used by [BoxDecoration] to represent gradients. This abstracts
/// out the arguments to the [new ui.Gradient.linear] constructor from the
/// `dart:ui` library.
///
/// 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
/// least two colors. If there are more than two, a [stops] list must be
/// provided. It must have the same length as [colors], and specifies the
/// position of each color stop between 0.0 and 1.0.
///
/// 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].
///
/// ## Sample code
///
/// This sample draws a picture that looks like vertical window shades by having
/// a [Container] display a [BoxDecoration] with a [LinearGradient].
///
/// ```dart
/// new Container(
/// decoration: new BoxDecoration(
/// gradient: new LinearGradient(
/// begin: Alignment.topLeft,
/// end: new Alignment(0.8, 0.0), // 10% of the width, so there are ten blinds.
/// colors: [const Color(0xFFFFFFEE), const Color(0xFF999999)], // whitish to gray
/// tileMode: TileMode.repeated, // repeats the gradient over the canvas
/// ),
/// ),
/// )
/// ```
///
/// See also:
///
/// * [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 [LinearGradient] in its
/// [BoxDecoration.gradient] property.
class LinearGradient extends Gradient {
/// Creates a linear gradient.
///
/// The [colors] argument must not be null. If [stops] is non-null, it must
/// have the same length as [colors].
const LinearGradient({
this.begin: Alignment.centerLeft,
this.end: Alignment.centerRight,
@required this.colors,
this.stops,
this.tileMode: TileMode.clamp,
}) : assert(begin != null),
assert(end != null),
assert(colors != null),
assert(tileMode != null);
/// The offset at which stop 0.0 of the gradient is placed.
///
/// If this is a [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).
///
/// For example, a begin offset of (-1.0, 0.0) is half way down the
/// left side of the box.
///
/// It can also be a [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 begin;
/// The offset at which stop 1.0 of the gradient is placed.
///
/// If this is a [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).
///
/// For example, a begin offset of (1.0, 0.0) is half way down the
/// right side of the box.
///
/// It can also be a [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 end;
/// 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]. If
/// [colors] has more than two colors, [stops] must be non-null.
///
/// 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 of the vector from
/// start to end.
///
/// If non-null, this list must have the same length as [colors]. If
/// [colors] has more than two colors, [stops] must be non-null.
///
/// 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.
final List<double> stops;
/// How this gradient should tile the plane beyond in the region before
/// [begin] and after [end].
///
/// For details, see [TileMode].
///
/// ![](https://flutter.github.io/assets-for-api-docs/dart-ui/tile_mode_clamp_linear.png)
/// ![](https://flutter.github.io/assets-for-api-docs/dart-ui/tile_mode_mirror_linear.png)
/// ![](https://flutter.github.io/assets-for-api-docs/dart-ui/tile_mode_repeated_linear.png)
final TileMode tileMode;
@override
Shader createShader(Rect rect, { TextDirection textDirection }) {
return new ui.Gradient.linear(
begin.resolve(textDirection).withinRect(rect),
end.resolve(textDirection).withinRect(rect),
colors, stops, tileMode,
);
}
/// Returns a new [LinearGradient] with its properties (in particular the
/// colors) scaled by the given factor.
///
/// If the factor is 0.0 or less, then the gradient is fully transparent.
@override
LinearGradient scale(double factor) {
return new LinearGradient(
begin: begin,
end: end,
colors: colors.map<Color>((Color color) => Color.lerp(null, color, factor)).toList(),
stops: stops,
tileMode: tileMode,
);
}
@override
Gradient lerpFrom(Gradient a, double t) {
if (a == null || (a is LinearGradient && a.colors.length == colors.length)) // TODO(ianh): remove limitation
return LinearGradient.lerp(a, this, t);
return super.lerpFrom(a, t);
}
@override
Gradient lerpTo(Gradient b, double t) {
if (b == null || (b is LinearGradient && b.colors.length == colors.length)) // TODO(ianh): remove limitation
return LinearGradient.lerp(this, b, t);
return super.lerpTo(b, t);
}
/// 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
/// [tileMode] and with the same [colors] but transparent (using [scale]).
///
/// If neither gradient is null, they must have the same number of [colors].
static LinearGradient lerp(LinearGradient a, LinearGradient b, double t) {
if (a == null && b == null)
return null;
if (a == null)
return b.scale(t);
if (b == null)
return a.scale(1.0 - t);
final _ColorsAndStops interpolated = _interpolateColorsAndStops(a.colors, a.stops, b.colors, b.stops, t);
return new LinearGradient(
begin: AlignmentGeometry.lerp(a.begin, b.begin, t),
end: AlignmentGeometry.lerp(a.end, b.end, t),
colors: interpolated.colors,
stops: interpolated.stops,
tileMode: t < 0.5 ? a.tileMode : b.tileMode, // TODO(ianh): interpolate tile mode
);
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (runtimeType != other.runtimeType)
return false;
final LinearGradient typedOther = other;
if (begin != typedOther.begin ||
end != typedOther.end ||
tileMode != typedOther.tileMode ||
colors?.length != typedOther.colors?.length ||
stops?.length != typedOther.stops?.length)
return false;
if (colors != null) {
assert(typedOther.colors != null);
assert(colors.length == typedOther.colors.length);
for (int i = 0; i < colors.length; i += 1) {
if (colors[i] != typedOther.colors[i])
return false;
}
}
if (stops != null) {
assert(typedOther.stops != null);
assert(stops.length == typedOther.stops.length);
for (int i = 0; i < stops.length; i += 1) {
if (stops[i] != typedOther.stops[i])
return false;
}
}
return true;
}
@override
int get hashCode => hashValues(begin, end, tileMode, hashList(colors), hashList(stops));
@override
String toString() {
return '$runtimeType($begin, $end, $colors, $stops, $tileMode)';
}
}
/// A 2D radial gradient.
///
/// This class is used by [BoxDecoration] to represent gradients. This abstracts
/// out the arguments to the [new ui.Gradient.radial] constructor from the
/// `dart:ui` library.
///
/// A 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 can be reused
/// with varying sized boxes without changing the parameters. (This contrasts
/// with [new ui.Gradient.radial], whose arguments are expressed in logical
/// pixels.)
///
/// The [colors] are described by a list of [Color] objects. There must be at
/// least two colors. If there are more than two, a [stops] list must be
/// provided. It must have the same length as [colors], and specifies the
/// position of each color stop between 0.0 and 1.0.
///
/// 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].
///
/// ## Sample code
///
/// This function draws a gradient that looks like a sun in a blue sky.
///
/// ```dart
/// void paintSky(Canvas canvas, Rect rect) {
/// var gradient = new RadialGradient(
/// center: const Alignment(0.7, -0.6), // near the top right
/// radius: 0.2,
/// colors: [
/// const Color(0xFFFFFF00), // yellow sun
/// const Color(0xFF0099FF), // blue sky
/// ],
/// stops: [0.4, 1.0],
/// );
/// // rect is the area we are painting over
/// var paint = new Paint()
/// ..shader = gradient.createShader(rect);
/// canvas.drawRect(rect, paint);
/// }
/// ```
///
/// See also:
///
/// * [LinearGradient], which displays a gradient in parallel lines, and has an
/// example which shows a different way to use [Gradient] objects.
/// * [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({
this.center: Alignment.center,
this.radius: 0.5,
@required this.colors,
this.stops,
this.tileMode: TileMode.clamp,
}) : assert(center != null),
assert(radius != null),
assert(colors != null),
assert(tileMode != null);
/// 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 radial
/// gradient in the center of the box.
///
/// If this is a [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 a [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 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;
/// 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]. If
/// [colors] has more than two colors, [stops] must be non-null.
///
/// 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 concentric rings.
///
/// The rings are centered at [center] and have a radius equal to the value of
/// the stop times [radius].
///
/// If non-null, this list must have the same length as [colors]. If
/// [colors] has more than two colors, [stops] must be non-null.
///
/// 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.
final List<double> stops;
/// How this gradient should tile the plane beyond the outer ring at [radius]
/// pixels from the [center].
///
/// For details, see [TileMode].
///
/// ![](https://flutter.github.io/assets-for-api-docs/dart-ui/tile_mode_clamp_radial.png)
/// ![](https://flutter.github.io/assets-for-api-docs/dart-ui/tile_mode_mirror_radial.png)
/// ![](https://flutter.github.io/assets-for-api-docs/dart-ui/tile_mode_repeated_radial.png)
final TileMode tileMode;
@override
Shader createShader(Rect rect, { TextDirection textDirection }) {
return new ui.Gradient.radial(
center.resolve(textDirection).withinRect(rect),
radius * rect.shortestSide,
colors, stops, tileMode,
);
}
/// Returns a new [RadialGradient] with its colors scaled by the given factor.
///
/// If the factor is 0.0 or less, then the gradient is fully transparent.
@override
RadialGradient scale(double factor) {
return new RadialGradient(
center: center,
radius: radius,
colors: colors.map<Color>((Color color) => Color.lerp(null, color, factor)).toList(),
stops: stops,
tileMode: tileMode,
);
}
@override
Gradient lerpFrom(Gradient a, double t) {
if (a == null || (a is RadialGradient && a.colors.length == colors.length)) // TODO(ianh): remove limitation
return RadialGradient.lerp(a, this, t);
return super.lerpFrom(a, t);
}
@override
Gradient lerpTo(Gradient b, double t) {
if (b == null || (b is RadialGradient && b.colors.length == colors.length)) // TODO(ianh): remove limitation
return RadialGradient.lerp(this, b, t);
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].
static RadialGradient lerp(RadialGradient a, RadialGradient b, double t) {
if (a == null && b == null)
return null;
if (a == null)
return b.scale(t);
if (b == null)
return a.scale(1.0 - t);
final _ColorsAndStops interpolated = _interpolateColorsAndStops(a.colors, a.stops, b.colors, b.stops, t);
return new RadialGradient(
center: AlignmentGeometry.lerp(a.center, b.center, t),
radius: math.max(0.0, ui.lerpDouble(a.radius, b.radius, t)),
colors: interpolated.colors,
stops: interpolated.stops,
tileMode: t < 0.5 ? a.tileMode : b.tileMode, // TODO(ianh): interpolate tile mode
);
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (runtimeType != other.runtimeType)
return false;
final RadialGradient typedOther = other;
if (center != typedOther.center ||
radius != typedOther.radius ||
tileMode != typedOther.tileMode ||
colors?.length != typedOther.colors?.length ||
stops?.length != typedOther.stops?.length)
return false;
if (colors != null) {
assert(typedOther.colors != null);
assert(colors.length == typedOther.colors.length);
for (int i = 0; i < colors.length; i += 1) {
if (colors[i] != typedOther.colors[i])
return false;
}
}
if (stops != null) {
assert(typedOther.stops != null);
assert(stops.length == typedOther.stops.length);
for (int i = 0; i < stops.length; i += 1) {
if (stops[i] != typedOther.stops[i])
return false;
}
}
return true;
}
@override
int get hashCode => hashValues(center, radius, tileMode, hashList(colors), hashList(stops));
@override
String toString() {
return '$runtimeType($center, $radius, $colors, $stops, $tileMode)';
}
}