Commit d57cc44f authored by Victor Choueiri's avatar Victor Choueiri Committed by Greg Spencer

Add SweepGradient (#17368)

This PR adds a SweepGradient class, extending Gradient to expose the engine's ui.Gradient.sweep shader.

Similar to LinearGradient and RadialGradient - SweepGradients can be used in a BoxDecoration or passed to a Paint's shader.
parent 6fc7199d
......@@ -23,3 +23,4 @@ Ali Bitek <alibitek@protonmail.ch>
Tetsuhiro Ueda <najeira@gmail.com>
Dan Field <dfield@gmail.com>
Noah Groß <gross@ngsger.de>
Victor Choueiri <victor@ctrlanddev.com>
......@@ -37,8 +37,8 @@ _ColorsAndStops _interpolateColorsAndStops(List<Color> aColors, List<double> aSt
/// A 2D gradient.
///
/// This is an interface that allows [LinearGradient] and [RadialGradient]
/// classes to be used interchangeably in [BoxDecoration]s.
/// This is an interface that allows [LinearGradient], [RadialGradient], and
/// [SweepGradient] classes to be used interchangeably in [BoxDecoration]s.
///
/// See also:
///
......@@ -214,9 +214,9 @@ abstract class Gradient {
/// 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.
/// 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.
///
/// 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
......@@ -258,6 +258,8 @@ abstract class Gradient {
///
/// * [RadialGradient], which displays a gradient in concentric circles, and
/// has an example which shows a different way to use [Gradient] objects.
/// * [SweepGradient], which displays a gradient in a sweeping arc around a
/// center point.
/// * [BoxDecoration], which can take a [LinearGradient] in its
/// [BoxDecoration.gradient] property.
class LinearGradient extends Gradient {
......@@ -278,14 +280,14 @@ class LinearGradient extends Gradient {
/// 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
/// 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).
///
/// 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
/// 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].
......@@ -293,14 +295,14 @@ class LinearGradient extends Gradient {
/// 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
/// 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).
///
/// 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
/// 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].
......@@ -325,10 +327,10 @@ class LinearGradient extends Gradient {
);
}
/// Returns a new [LinearGradient] with its properties (in particular the
/// colors) scaled by the given factor.
/// Returns a new [LinearGradient] with its colors scaled by the given factor.
///
/// If the factor is 0.0 or less, then the gradient is fully transparent.
/// 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
LinearGradient scale(double factor) {
return new LinearGradient(
......@@ -362,7 +364,7 @@ class LinearGradient extends Gradient {
///
/// If neither gradient is null, they must have the same number of [colors].
///
/// The `t` argument represents position on the timeline, with 0.0 meaning
/// 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
......@@ -434,9 +436,9 @@ class LinearGradient extends Gradient {
/// 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.
/// 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.
///
/// 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
......@@ -483,6 +485,8 @@ class LinearGradient extends Gradient {
///
/// * [LinearGradient], which displays a gradient in parallel lines, and has an
/// example which shows a different way to use [Gradient] objects.
/// * [SweepGradient], which displays a gradient in a sweeping arc around a
/// center point.
/// * [BoxDecoration], which can take a [RadialGradient] in its
/// [BoxDecoration.gradient] property.
/// * [CustomPainter], which shows how to use the above sample code in a custom
......@@ -509,11 +513,11 @@ class RadialGradient extends Gradient {
/// 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
/// 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 a [AlignmentDirectional], where the start is the left in
/// 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].
......@@ -548,7 +552,8 @@ class RadialGradient extends Gradient {
/// 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.
/// 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
RadialGradient scale(double factor) {
return new RadialGradient(
......@@ -582,7 +587,7 @@ class RadialGradient extends Gradient {
///
/// If neither gradient is null, they must have the same number of [colors].
///
/// The `t` argument represents position on the timeline, with 0.0 meaning
/// 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
......@@ -651,3 +656,230 @@ class RadialGradient extends Gradient {
return '$runtimeType($center, $radius, $colors, $stops, $tileMode)';
}
}
/// 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].
///
/// ## Sample code
///
/// This sample draws a different color in each quadrant.
///
/// ```dart
/// new Container(
/// decoration: new BoxDecoration(
/// gradient: new SweepGradient(
/// center: FractionalOffset.center,
/// startAngle: 0.0,
/// endAngle: math.pi * 2,
/// colors: const <Color>[
/// const Color(0xFF4285F4), // blue
/// const Color(0xFF34A853), // green
/// const Color(0xFFFBBC05), // yellow
/// const Color(0xFFEA4335), // red
/// const Color(0xFF4285F4), // blue again to seamlessly transition to the start
/// ],
/// stops: const <double>[0.0, 0.25, 0.5, 0.75, 1.0],
/// ),
/// ),
/// )
/// ```
///
/// 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({
this.center: Alignment.center,
this.startAngle: 0.0,
this.endAngle: math.pi * 2,
@required List<Color> colors,
List<double> stops,
this.tileMode: TileMode.clamp,
}) : assert(center != null),
assert(startAngle != null),
assert(endAngle != null),
assert(tileMode != null),
super(colors: colors, stops: stops);
/// 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)
/// ![](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
Shader createShader(Rect rect, { TextDirection textDirection }) {
return new ui.Gradient.sweep(
center.resolve(textDirection).withinRect(rect),
colors, _impliedStops(), tileMode,
startAngle,
endAngle,
);
}
/// 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) {
return new SweepGradient(
center: center,
startAngle: startAngle,
endAngle: endAngle,
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 SweepGradient && a.colors.length == colors.length)) // TODO(ianh): remove limitation
return SweepGradient.lerp(a, this, t);
return super.lerpFrom(a, t);
}
@override
Gradient lerpTo(Gradient b, double t) {
if (b == null || (b is SweepGradient && b.colors.length == colors.length)) // TODO(ianh): remove limitation
return SweepGradient.lerp(this, b, t);
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].
static SweepGradient lerp(SweepGradient a, SweepGradient b, double t) {
assert(t != null);
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 SweepGradient(
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)),
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 SweepGradient typedOther = other;
if (center != typedOther.center ||
startAngle != typedOther.startAngle ||
endAngle != typedOther.endAngle ||
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, startAngle, endAngle, tileMode, hashList(colors), hashList(stops));
@override
String toString() {
return '$runtimeType($center, $startAngle, $endAngle, $colors, $stops, $tileMode)';
}
}
......@@ -233,9 +233,9 @@ class ShaderMask extends SingleChildRenderObjectWidget {
/// The shader callback is called with the current size of the child so that
/// it can customize the shader to the size and location of the child.
///
/// Typically this will use a [LinearGradient] or [RadialGradient] to create
/// the [dart:ui.Shader], though the [dart:ui.ImageShader] class could also be
/// used.
/// Typically this will use a [LinearGradient], [RadialGradient], or
/// [SweepGradient] to create the [dart:ui.Shader], though the
/// [dart:ui.ImageShader] class could also be used.
final ShaderCallback shaderCallback;
/// The [BlendMode] to use when applying the shader to the child.
......
// Copyright 2016 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 'package:flutter_test/flutter_test.dart';
import 'package:flutter/painting.dart';
......@@ -193,6 +194,45 @@ void main() {
);
});
test('SweepGradient with AlignmentDirectional', () {
expect(
() {
return const SweepGradient(
center: AlignmentDirectional.topStart,
colors: const <Color>[ const Color(0xFFFFFFFF), const Color(0xFFFFFFFF) ]
).createShader(new Rect.fromLTWH(0.0, 0.0, 100.0, 100.0));
},
throwsAssertionError,
);
expect(
() {
return const SweepGradient(
center: AlignmentDirectional.topStart,
colors: const <Color>[ const Color(0xFFFFFFFF), const Color(0xFFFFFFFF) ]
).createShader(new Rect.fromLTWH(0.0, 0.0, 100.0, 100.0), textDirection: TextDirection.rtl);
},
returnsNormally,
);
expect(
() {
return const SweepGradient(
center: AlignmentDirectional.topStart,
colors: const <Color>[ const Color(0xFFFFFFFF), const Color(0xFFFFFFFF) ]
).createShader(new Rect.fromLTWH(0.0, 0.0, 100.0, 100.0), textDirection: TextDirection.ltr);
},
returnsNormally,
);
expect(
() {
return const SweepGradient(
center: Alignment.topLeft,
colors: const <Color>[ const Color(0xFFFFFFFF), const Color(0xFFFFFFFF) ]
).createShader(new Rect.fromLTWH(0.0, 0.0, 100.0, 100.0));
},
returnsNormally,
);
});
test('RadialGradient lerp test', () {
const RadialGradient testGradient1 = const RadialGradient(
center: Alignment.topLeft,
......@@ -263,6 +303,106 @@ void main() {
));
});
test('SweepGradient lerp test', () {
const SweepGradient testGradient1 = const SweepGradient(
center: Alignment.topLeft,
startAngle: 0.0,
endAngle: math.pi / 2,
colors: const <Color>[
const Color(0x33333333),
const Color(0x66666666),
],
);
const SweepGradient testGradient2 = const SweepGradient(
center: Alignment.topRight,
startAngle: math.pi / 2,
endAngle: math.pi,
colors: const <Color>[
const Color(0x44444444),
const Color(0x88888888),
],
);
final SweepGradient actual = SweepGradient.lerp(testGradient1, testGradient2, 0.5);
expect(actual, const SweepGradient(
center: const Alignment(0.0, -1.0),
startAngle: math.pi / 4,
endAngle: math.pi * 3/4,
colors: const <Color>[
const Color(0x3B3B3B3B),
const Color(0x77777777),
],
));
});
test('SweepGradient lerp test with stops', () {
const SweepGradient testGradient1 = const SweepGradient(
center: Alignment.topLeft,
startAngle: 0.0,
endAngle: math.pi / 2,
colors: const <Color>[
const Color(0x33333333),
const Color(0x66666666),
],
stops: const <double>[
0.0,
0.5,
],
);
const SweepGradient testGradient2 = const SweepGradient(
center: Alignment.topRight,
startAngle: math.pi / 2,
endAngle: math.pi,
colors: const <Color>[
const Color(0x44444444),
const Color(0x88888888),
],
stops: const <double>[
0.5,
1.0,
],
);
final SweepGradient actual = SweepGradient.lerp(testGradient1, testGradient2, 0.5);
expect(actual, const SweepGradient(
center: const Alignment(0.0, -1.0),
startAngle: math.pi / 4,
endAngle: math.pi * 3/4,
colors: const <Color>[
const Color(0x3B3B3B3B),
const Color(0x77777777),
],
stops: const <double>[
0.25,
0.75,
],
));
});
test('SweepGradient scale test)', () {
const SweepGradient testGradient = const SweepGradient(
center: Alignment.topLeft,
startAngle: 0.0,
endAngle: math.pi / 2,
colors: const <Color>[
const Color(0xff333333),
const Color(0xff666666),
],
);
final SweepGradient actual = testGradient.scale(0.5);
expect(actual, const SweepGradient(
center: Alignment.topLeft,
startAngle: 0.0,
endAngle: math.pi / 2,
colors: const <Color>[
const Color(0x80333333),
const Color(0x80666666),
],
));
});
test('Gradient lerp test (with RadialGradient)', () {
const RadialGradient testGradient1 = const RadialGradient(
center: Alignment.topLeft,
......@@ -357,4 +497,4 @@ void main() {
expect(() { test2a.createShader(rect); }, throwsArgumentError);
expect(() { test2b.createShader(rect); }, throwsArgumentError);
});
}
\ No newline at end of file
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment