From d27f53904b91c9370cd292cc2bf821ea87774e3c Mon Sep 17 00:00:00 2001 From: Sander Kersten Date: Wed, 27 Feb 2019 00:16:59 +0100 Subject: [PATCH] Add lerping between Gradients with arbitrary number of colors and stops (#27435) --- .../flutter/lib/src/painting/gradient.dart | 92 +++++-- .../flutter/test/painting/gradient_test.dart | 260 +++++++++++++++++- 2 files changed, 319 insertions(+), 33 deletions(-) diff --git a/packages/flutter/lib/src/painting/gradient.dart b/packages/flutter/lib/src/painting/gradient.dart index a345b1ff2d1..9dd4784479a 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:collection'; import 'dart:math' as math; import 'dart:ui' as ui show Gradient, lerpDouble; @@ -16,22 +17,43 @@ class _ColorsAndStops { 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); - interpolatedStops = []; - for (int i = 0; i < aStops.length; i += 1) - interpolatedStops.add(ui.lerpDouble(aStops[i], bStops[i], t).clamp(0.0, 1.0)); - } +/// Calculate the color at position [t] of the gradient defined by [colors] and [stops]. +Color _sample(List colors, List 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], + (t - stops[index]) / (stops[index + 1] - stops[index]) + ); +} + +_ColorsAndStops _interpolateColorsAndStops( + List aColors, + List aStops, + List bColors, + List bStops, + double t, +) { + assert(aColors.length >= 2); + assert(bColors.length >= 2); + assert(aStops.length == aColors.length); + assert(bStops.length == bColors.length); + final SplayTreeSet stops = SplayTreeSet() + ..addAll(aStops) + ..addAll(bStops); + final List interpolatedStops = stops.toList(growable: false); + final List interpolatedColors = interpolatedStops.map( + (double stop) => Color.lerp(_sample(aColors, aStops, stop), _sample(bColors, bStops, stop), t) + ).toList(growable: false); return _ColorsAndStops(interpolatedColors, interpolatedStops); } @@ -88,8 +110,6 @@ abstract class Gradient { List _impliedStops() { if (stops != null) return stops; - if (colors.length == 2) - return null; assert(colors.length >= 2, 'colors list must have at least two colors'); final double separation = 1.0 / (colors.length - 1); return List.generate( @@ -335,14 +355,14 @@ 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 + if (a == null || (a is LinearGradient)) 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 + if (b == null || (b is LinearGradient)) return LinearGradient.lerp(this, b, t); return super.lerpTo(b, t); } @@ -374,7 +394,13 @@ class LinearGradient extends Gradient { 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); + final _ColorsAndStops interpolated = _interpolateColorsAndStops( + a.colors, + a._impliedStops(), + b.colors, + b._impliedStops(), + t, + ); return LinearGradient( begin: AlignmentGeometry.lerp(a.begin, b.begin, t), end: AlignmentGeometry.lerp(a.end, b.end, t), @@ -604,14 +630,14 @@ class RadialGradient extends Gradient { @override Gradient lerpFrom(Gradient a, double t) { - if (a == null || (a is RadialGradient && a.colors.length == colors.length)) // TODO(ianh): remove limitation + if (a == null || (a is RadialGradient)) 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 + if (b == null || (b is RadialGradient)) return RadialGradient.lerp(this, b, t); return super.lerpTo(b, t); } @@ -643,7 +669,13 @@ class RadialGradient extends Gradient { 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); + final _ColorsAndStops interpolated = _interpolateColorsAndStops( + a.colors, + a._impliedStops(), + b.colors, + b._impliedStops(), + t, + ); return RadialGradient( center: AlignmentGeometry.lerp(a.center, b.center, t), radius: math.max(0.0, ui.lerpDouble(a.radius, b.radius, t)), @@ -835,14 +867,14 @@ class SweepGradient extends Gradient { @override Gradient lerpFrom(Gradient a, double t) { - if (a == null || (a is SweepGradient && a.colors.length == colors.length)) // TODO(ianh): remove limitation + if (a == null || (a is SweepGradient)) 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 + if (b == null || (b is SweepGradient)) return SweepGradient.lerp(this, b, t); return super.lerpTo(b, t); } @@ -873,7 +905,13 @@ class SweepGradient extends Gradient { 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); + final _ColorsAndStops interpolated = _interpolateColorsAndStops( + a.colors, + a._impliedStops(), + b.colors, + b._impliedStops(), + t, + ); return SweepGradient( center: AlignmentGeometry.lerp(a.center, b.center, t), startAngle: math.max(0.0, ui.lerpDouble(a.startAngle, b.startAngle, t)), diff --git a/packages/flutter/test/painting/gradient_test.dart b/packages/flutter/test/painting/gradient_test.dart index f9eb55e97bf..963ffc26bdb 100644 --- a/packages/flutter/test/painting/gradient_test.dart +++ b/packages/flutter/test/painting/gradient_test.dart @@ -56,6 +56,7 @@ void main() { Color(0x3B3B3B3B), Color(0x77777777), ], + stops: [0, 1], )); }); @@ -91,11 +92,84 @@ void main() { end: Alignment(-1.0, 0.0), colors: [ Color(0x3B3B3B3B), + Color(0x55555555), Color(0x77777777), ], stops: [ - 0.25, - 0.75, + 0.0, + 0.5, + 1.0, + ], + )); + }); + + test('LinearGradient lerp test with unequal number of colors', () { + const LinearGradient testGradient1 = LinearGradient( + colors: [ + Color(0x22222222), + Color(0x66666666), + ], + ); + const LinearGradient testGradient2 = LinearGradient( + colors: [ + Color(0x44444444), + Color(0x66666666), + Color(0x88888888), + ], + ); + + final LinearGradient actual = LinearGradient.lerp(testGradient1, testGradient2, 0.5); + expect(actual, const LinearGradient( + colors: [ + Color(0x33333333), + Color(0x55555555), + Color(0x77777777), + ], + stops: [ + 0.0, + 0.5, + 1.0, + ], + )); + }); + + test('LinearGradient lerp test with stops and unequal number of colors', () { + const LinearGradient testGradient1 = LinearGradient( + colors: [ + Color(0x33333333), + Color(0x66666666), + ], + stops: [ + 0.0, + 0.5, + ], + ); + const LinearGradient testGradient2 = LinearGradient( + colors: [ + Color(0x44444444), + Color(0x48484848), + Color(0x88888888), + ], + stops: [ + 0.5, + 0.7, + 1.0, + ], + ); + + final LinearGradient actual = LinearGradient.lerp(testGradient1, testGradient2, 0.5); + expect(actual, const LinearGradient( + colors: [ + Color(0x3B3B3B3B), + Color(0x55555555), + Color(0x57575757), + Color(0x77777777), + ], + stops: [ + 0.0, + 0.5, + 0.7, + 1.0, ], )); }); @@ -221,6 +295,10 @@ void main() { Color(0x3B3B3B3B), Color(0x77777777), ], + stops: [ + 0.0, + 1.0, + ], )); }); @@ -259,11 +337,84 @@ void main() { radius: 15.0, colors: [ Color(0x3B3B3B3B), + Color(0x55555555), Color(0x77777777), ], stops: [ - 0.25, - 0.75, + 0.0, + 0.5, + 1.0 + ], + )); + }); + + test('RadialGradient lerp test with unequal number of colors', () { + const RadialGradient testGradient1 = RadialGradient( + colors: [ + Color(0x22222222), + Color(0x66666666), + ], + ); + const RadialGradient testGradient2 = RadialGradient( + colors: [ + Color(0x44444444), + Color(0x66666666), + Color(0x88888888), + ], + ); + + final RadialGradient actual = RadialGradient.lerp(testGradient1, testGradient2, 0.5); + expect(actual, const RadialGradient( + colors: [ + Color(0x33333333), + Color(0x55555555), + Color(0x77777777), + ], + stops: [ + 0.0, + 0.5, + 1.0, + ], + )); + }); + + test('RadialGradient lerp test with stops and unequal number of colors', () { + const RadialGradient testGradient1 = RadialGradient( + colors: [ + Color(0x33333333), + Color(0x66666666), + ], + stops: [ + 0.0, + 0.5, + ], + ); + const RadialGradient testGradient2 = RadialGradient( + colors: [ + Color(0x44444444), + Color(0x48484848), + Color(0x88888888), + ], + stops: [ + 0.5, + 0.7, + 1.0, + ], + ); + + final RadialGradient actual = RadialGradient.lerp(testGradient1, testGradient2, 0.5); + expect(actual, const RadialGradient( + colors: [ + Color(0x3B3B3B3B), + Color(0x55555555), + Color(0x57575757), + Color(0x77777777), + ], + stops: [ + 0.0, + 0.5, + 0.7, + 1.0, ], )); }); @@ -308,6 +459,10 @@ void main() { Color(0x3B3B3B3B), Color(0x77777777), ], + stops: [ + 0.0, + 1.0, + ], )); final RadialGradient actual2 = RadialGradient.lerp(testGradient1, testGradient3, 0.5); @@ -320,6 +475,10 @@ void main() { Color(0x3B3B3B3B), Color(0x77777777), ], + stops: [ + 0.0, + 1.0, + ], )); }); @@ -352,6 +511,10 @@ void main() { Color(0x3B3B3B3B), Color(0x77777777), ], + stops: [ + 0.0, + 1.0, + ], )); }); @@ -390,11 +553,84 @@ void main() { endAngle: math.pi * 3/4, colors: [ Color(0x3B3B3B3B), + Color(0x55555555), Color(0x77777777), ], stops: [ - 0.25, - 0.75, + 0.0, + 0.5, + 1.0, + ], + )); + }); + + test('SweepGradient lerp test with unequal number of colors', () { + const SweepGradient testGradient1 = SweepGradient( + colors: [ + Color(0x22222222), + Color(0x66666666), + ], + ); + const SweepGradient testGradient2 = SweepGradient( + colors: [ + Color(0x44444444), + Color(0x66666666), + Color(0x88888888), + ], + ); + + final SweepGradient actual = SweepGradient.lerp(testGradient1, testGradient2, 0.5); + expect(actual, const SweepGradient( + colors: [ + Color(0x33333333), + Color(0x55555555), + Color(0x77777777), + ], + stops: [ + 0.0, + 0.5, + 1.0, + ], + )); + }); + + test('SweepGradient lerp test with stops and unequal number of colors', () { + const SweepGradient testGradient1 = SweepGradient( + colors: [ + Color(0x33333333), + Color(0x66666666), + ], + stops: [ + 0.0, + 0.5, + ], + ); + const SweepGradient testGradient2 = SweepGradient( + colors: [ + Color(0x44444444), + Color(0x48484848), + Color(0x88888888), + ], + stops: [ + 0.5, + 0.7, + 1.0, + ], + ); + + final SweepGradient actual = SweepGradient.lerp(testGradient1, testGradient2, 0.5); + expect(actual, const SweepGradient( + colors: [ + Color(0x3B3B3B3B), + Color(0x55555555), + Color(0x57575757), + Color(0x77777777), + ], + stops: [ + 0.0, + 0.5, + 0.7, + 1.0, ], )); }); @@ -431,6 +667,10 @@ void main() { Color(0x33333333), Color(0x66666666), ], + stops: [ + 0.0, + 1.0, + ], ); const RadialGradient testGradient2 = RadialGradient( center: Alignment(0.0, -1.0), @@ -439,6 +679,10 @@ void main() { Color(0x3B3B3B3B), Color(0x77777777), ], + stops: [ + 0.0, + 1.0, + ], ); const RadialGradient testGradient3 = RadialGradient( center: Alignment.topRight, @@ -447,6 +691,10 @@ void main() { Color(0x44444444), Color(0x88888888), ], + stops: [ + 0.0, + 1.0, + ], ); expect(Gradient.lerp(testGradient1, testGradient3, 0.0), testGradient1);