From 1f82733a3b897a3909aa84df26915f26fd782dad Mon Sep 17 00:00:00 2001 From: Ian Hickson Date: Tue, 17 Oct 2017 13:10:10 -0700 Subject: [PATCH] Make BoxDecoration lerp gradients (#12451) This still is very limited in what it can lerp, but it sets the stage for arbitrary lerps later. --- .../lib/src/painting/box_decoration.dart | 28 ++- .../flutter/lib/src/painting/decoration.dart | 38 ++-- .../lib/src/painting/flutter_logo.dart | 11 ++ .../flutter/lib/src/painting/gradient.dart | 180 ++++++++++++++++-- .../test/painting/decoration_test.dart | 9 +- .../flutter/test/painting/gradient_test.dart | 89 ++++++++- 6 files changed, 306 insertions(+), 49 deletions(-) diff --git a/packages/flutter/lib/src/painting/box_decoration.dart b/packages/flutter/lib/src/painting/box_decoration.dart index a008430317..690c9e028d 100644 --- a/packages/flutter/lib/src/painting/box_decoration.dart +++ b/packages/flutter/lib/src/painting/box_decoration.dart @@ -148,14 +148,13 @@ class BoxDecoration extends Decoration { /// Returns a new box decoration that is scaled by the given factor. BoxDecoration scale(double factor) { - // TODO(abarth): Scale ALL the things. return new BoxDecoration( color: Color.lerp(null, color, factor), - image: image, + image: image, // TODO(ianh): fade the image from transparent border: BoxBorder.lerp(null, border, factor), borderRadius: BorderRadius.lerp(null, borderRadius, factor), boxShadow: BoxShadow.lerpList(null, boxShadow, factor), - gradient: gradient, + gradient: gradient?.scale(factor), shape: shape, ); } @@ -165,6 +164,8 @@ class BoxDecoration extends Decoration { @override BoxDecoration lerpFrom(Decoration a, double t) { + if (a == null) + return scale(t); if (a is BoxDecoration) return BoxDecoration.lerp(a, this, t); return super.lerpFrom(a, t); @@ -172,6 +173,8 @@ class BoxDecoration extends Decoration { @override BoxDecoration lerpTo(Decoration b, double t) { + if (b == null) + return scale(1.0 - t); if (b is BoxDecoration) return BoxDecoration.lerp(this, b, t); return super.lerpTo(b, t); @@ -181,6 +184,16 @@ class BoxDecoration extends Decoration { /// /// Interpolates each parameter of the box decoration separately. /// + /// The [shape] is not interpolated. To interpolate the shape, consider using + /// a [ShapeDecoration] with different border shapes. + /// + /// If both values are null, this returns null. Otherwise, it returns a + /// non-null value. If one of the values is null, then the result is obtained + /// by applying [scale] to the other value. If neither value is null and `t == + /// 0.0`, then `a` is returned unmodified; if `t == 1.0` then `b` is returned + /// unmodified. Otherwise, the values are computed by interpolating the + /// properties appropriately. + /// /// See also: /// /// * [Decoration.lerp], which can interpolate between any two types of @@ -195,14 +208,17 @@ class BoxDecoration extends Decoration { return b.scale(t); if (b == null) return a.scale(1.0 - t); - // TODO(abarth): lerp ALL the fields. + if (t == 0.0) + return a; + if (t == 1.0) + return b; return new BoxDecoration( color: Color.lerp(a.color, b.color, t), - image: t < 0.5 ? a.image : b.image, + image: t < 0.5 ? a.image : b.image, // TODO(ianh): cross-fade the image border: BoxBorder.lerp(a.border, b.border, t), borderRadius: BorderRadius.lerp(a.borderRadius, b.borderRadius, t), boxShadow: BoxShadow.lerpList(a.boxShadow, b.boxShadow, t), - gradient: t < 0.5 ? a.gradient : b.gradient, + gradient: Gradient.lerp(a.gradient, b.gradient, t), shape: t < 0.5 ? a.shape : b.shape, ); } diff --git a/packages/flutter/lib/src/painting/decoration.dart b/packages/flutter/lib/src/painting/decoration.dart index 3595f0c479..fb32d99c62 100644 --- a/packages/flutter/lib/src/painting/decoration.dart +++ b/packages/flutter/lib/src/painting/decoration.dart @@ -95,26 +95,26 @@ abstract class Decoration extends Diagnosticable { /// 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 attempts to lerp from `begin` to - /// null if `t<0.5`, or null to `end` if `t≥0.5`. + /// This attempts to use [lerpFrom] and [lerpTo] on `end` and `begin` + /// respectively to find a solution. If the two values can't directly be + /// interpolated, then the interpolation is done via null (at `t == 0.5`). + /// + /// If the values aren't null, then for `t == 0.0` and `t == 1.0` the values + /// `begin` and `end` are return verbatim. static Decoration lerp(Decoration begin, Decoration end, double t) { - Decoration result; - if (end != null) - result = end.lerpFrom(begin, t); - if (result == null && begin != null) - result = begin.lerpTo(end, t); - if (result == null && begin != null && end != null) { - if (t < 0.5) { - result = begin.lerpTo(null, t * 2.0); - } else { - result = end.lerpFrom(null, (t - 0.5) * 2.0); - } - } - if (result == null) - result = t < 0.5 ? begin : end; - return result; + if (begin == null && end == null) + return null; + if (begin == null) + return end.lerpFrom(null, t) ?? end; + if (end == null) + return begin.lerpTo(null, t) ?? begin; + if (t == 0.0) + return begin; + if (t == 1.0) + return end; + return end.lerpFrom(begin, t) + ?? begin.lerpTo(end, t) + ?? (t < 0.5 ? begin.lerpTo(null, t * 2.0) : end.lerpFrom(null, (t - 0.5) * 2.0)); } /// Tests whether the given point, on a rectangle of a given size, diff --git a/packages/flutter/lib/src/painting/flutter_logo.dart b/packages/flutter/lib/src/painting/flutter_logo.dart index a058d62f2a..ee3449c2f7 100644 --- a/packages/flutter/lib/src/painting/flutter_logo.dart +++ b/packages/flutter/lib/src/painting/flutter_logo.dart @@ -123,6 +123,13 @@ class FlutterLogoDecoration extends Decoration { /// /// Interpolates both the color and the style in a continuous fashion. /// + /// If both values are null, this returns null. Otherwise, it returns a + /// non-null value. If one of the values is null, then the result is obtained + /// by scaling the other value's opacity and [margin]. If neither value is + /// null and `t == 0.0`, then `a` is returned unmodified; if `t == 1.0` then + /// `b` is returned unmodified. Otherwise, the values are computed by + /// interpolating the properties appropriately. + /// /// See also [Decoration.lerp]. static FlutterLogoDecoration lerp(FlutterLogoDecoration a, FlutterLogoDecoration b, double t) { assert(a == null || a.debugAssertIsValid()); @@ -151,6 +158,10 @@ class FlutterLogoDecoration extends Decoration { a._opacity * (1.0 - t).clamp(0.0, 1.0), ); } + if (t == 0.0) + return a; + if (t == 1.0) + return b; return new FlutterLogoDecoration._( Color.lerp(a.lightColor, b.lightColor, t), Color.lerp(a.darkColor, b.darkColor, t), diff --git a/packages/flutter/lib/src/painting/gradient.dart b/packages/flutter/lib/src/painting/gradient.dart index 510677cfef..14cc35fdcc 100644 --- a/packages/flutter/lib/src/painting/gradient.dart +++ b/packages/flutter/lib/src/painting/gradient.dart @@ -2,6 +2,7 @@ // 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'; @@ -9,6 +10,30 @@ import 'package:flutter/foundation.dart'; import 'alignment.dart'; import 'basic_types.dart'; +class _ColorsAndStops { + _ColorsAndStops(this.colors, this.stops); + final List colors; + final List stops; +} + +_ColorsAndStops interpolateColorsAndStops(List aColors, List aStops, List bColors, List 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 interpolatedColors = []; + for (int i = 0; i < aColors.length; i += 1) + interpolatedColors.add(Color.lerp(aColors[i], bColors[i], t)); + List interpolatedStops; + if (aStops != null || bStops != null) { + aStops ??= const [0.0, 1.0]; + bStops ??= const [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] @@ -30,6 +55,73 @@ abstract class Gradient { /// 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. @@ -173,9 +265,8 @@ class LinearGradient extends Gradient { /// Returns a new [LinearGradient] with its properties (in particular the /// colors) scaled by the given factor. /// - /// If the factor is 1.0 or greater, then the gradient is returned unmodified. /// If the factor is 0.0 or less, then the gradient is fully transparent. - /// Values in between scale the opacity of the colors. + @override LinearGradient scale(double factor) { return new LinearGradient( begin: begin, @@ -186,6 +277,20 @@ class LinearGradient extends Gradient { ); } + @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 @@ -200,24 +305,13 @@ class LinearGradient extends Gradient { return b.scale(t); if (b == null) return a.scale(1.0 - t); - assert(a.colors.length == b.colors.length, 'Cannot interpolate between two gradients with a different number of colors.'); - assert(a.stops == null || b.stops == null || a.stops.length == b.stops.length); - final List interpolatedColors = []; - for (int i = 0; i < a.colors.length; i += 1) - interpolatedColors.add(Color.lerp(a.colors[i], b.colors[i], t)); - List interpolatedStops; - if (a.stops != null && b.stops != null) { - for (int i = 0; i < a.stops.length; i += 1) - interpolatedStops.add(ui.lerpDouble(a.stops[i], b.stops[i], t)); - } else { - interpolatedStops = a.stops ?? b.stops; - } + 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: interpolatedColors, - stops: interpolatedStops, - tileMode: t < 0.5 ? a.tileMode : b.tileMode, + colors: interpolated.colors, + stops: interpolated.stops, + tileMode: t < 0.5 ? a.tileMode : b.tileMode, // TODO(ianh): interpolate tile mode ); } @@ -403,6 +497,58 @@ 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. + @override + RadialGradient scale(double factor) { + return new RadialGradient( + center: center, + radius: radius, + colors: colors.map((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)) diff --git a/packages/flutter/test/painting/decoration_test.dart b/packages/flutter/test/painting/decoration_test.dart index 96c8af597c..039f2b8d73 100644 --- a/packages/flutter/test/painting/decoration_test.dart +++ b/packages/flutter/test/painting/decoration_test.dart @@ -282,15 +282,14 @@ void main() { }); test('BoxDecoration.lerp - gradients', () { - // We don't lerp the gradients, we just switch from one to the other at t=0.5. - final Gradient gradient = new LinearGradient(colors: [ const Color(0x00000000), const Color(0xFFFFFFFF) ]); + final Gradient gradient = const LinearGradient(colors: const [ const Color(0x00000000), const Color(0xFFFFFFFF) ]); expect( BoxDecoration.lerp( const BoxDecoration(), new BoxDecoration(gradient: gradient), -1.0, ), - const BoxDecoration() + const BoxDecoration(gradient: const LinearGradient(colors: const [ const Color(0x00000000), const Color(0x00FFFFFF) ])) ); expect( BoxDecoration.lerp( @@ -306,7 +305,7 @@ void main() { new BoxDecoration(gradient: gradient), 0.25, ), - const BoxDecoration() + const BoxDecoration(gradient: const LinearGradient(colors: const [ const Color(0x00000000), const Color(0x40FFFFFF) ])) ); expect( BoxDecoration.lerp( @@ -314,7 +313,7 @@ void main() { new BoxDecoration(gradient: gradient), 0.75, ), - new BoxDecoration(gradient: gradient) + const BoxDecoration(gradient: const LinearGradient(colors: const [ const Color(0x00000000), const Color(0xBFFFFFFF) ])) ); expect( BoxDecoration.lerp( diff --git a/packages/flutter/test/painting/gradient_test.dart b/packages/flutter/test/painting/gradient_test.dart index de8725210b..44bae0b1b0 100644 --- a/packages/flutter/test/painting/gradient_test.dart +++ b/packages/flutter/test/painting/gradient_test.dart @@ -38,7 +38,6 @@ void main() { const Color(0x66666666), ], ); - final LinearGradient testGradient2 = const LinearGradient( begin: Alignment.topRight, end: Alignment.topLeft, @@ -47,8 +46,8 @@ void main() { const Color(0x88888888), ], ); - final LinearGradient actual = LinearGradient.lerp(testGradient1, testGradient2, 0.5); + final LinearGradient actual = LinearGradient.lerp(testGradient1, testGradient2, 0.5); expect(actual, const LinearGradient( begin: const Alignment(0.0, -1.0), end: const Alignment(-1.0, 0.0), @@ -152,4 +151,90 @@ void main() { returnsNormally, ); }); + + test('RadialGradient lerp test', () { + final RadialGradient testGradient1 = const RadialGradient( + center: Alignment.topLeft, + radius: 20.0, + colors: const [ + const Color(0x33333333), + const Color(0x66666666), + ], + ); + final RadialGradient testGradient2 = const RadialGradient( + center: Alignment.topRight, + radius: 10.0, + colors: const [ + const Color(0x44444444), + const Color(0x88888888), + ], + ); + + final RadialGradient actual = RadialGradient.lerp(testGradient1, testGradient2, 0.5); + expect(actual, const RadialGradient( + center: const Alignment(0.0, -1.0), + radius: 15.0, + colors: const [ + const Color(0x3B3B3B3B), + const Color(0x77777777), + ], + )); + }); + + test('Gradient lerp test (with RadialGradient)', () { + final RadialGradient testGradient1 = const RadialGradient( + center: Alignment.topLeft, + radius: 20.0, + colors: const [ + const Color(0x33333333), + const Color(0x66666666), + ], + ); + final RadialGradient testGradient2 = const RadialGradient( + center: const Alignment(0.0, -1.0), + radius: 15.0, + colors: const [ + const Color(0x3B3B3B3B), + const Color(0x77777777), + ], + ); + final RadialGradient testGradient3 = const RadialGradient( + center: Alignment.topRight, + radius: 10.0, + colors: const [ + const Color(0x44444444), + const Color(0x88888888), + ], + ); + + expect(Gradient.lerp(testGradient1, testGradient3, 0.0), testGradient1); + expect(Gradient.lerp(testGradient1, testGradient3, 0.5), testGradient2); + expect(Gradient.lerp(testGradient1, testGradient3, 1.0), testGradient3); + expect(Gradient.lerp(testGradient3, testGradient1, 0.0), testGradient3); + expect(Gradient.lerp(testGradient3, testGradient1, 0.5), testGradient2); + expect(Gradient.lerp(testGradient3, testGradient1, 1.0), testGradient1); + }); + + test('Gradient lerp test (LinearGradient to RadialGradient)', () { + final LinearGradient testGradient1 = const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: const [ + const Color(0x33333333), + const Color(0x66666666), + ], + ); + final RadialGradient testGradient2 = const RadialGradient( + center: Alignment.center, + radius: 20.0, + colors: const [ + const Color(0x44444444), + const Color(0x88888888), + ], + ); + + expect(Gradient.lerp(testGradient1, testGradient2, 0.0), testGradient1); + expect(Gradient.lerp(testGradient1, testGradient2, 1.0), testGradient2); + expect(Gradient.lerp(testGradient1, testGradient2, 0.5), testGradient2.scale(0.0)); + }); } \ No newline at end of file