Unverified Commit efa2a474 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

Adding HSLColor and color 'within' matchers for HSVColor and HSLColor (#18294)

This adds an HSLColor class which uses a perceptual color space based upon human perception of colored light (as opposed to HSV, which is based on pigment colors).

You can see the difference in the color spaces here: https://en.wikipedia.org/wiki/HSL_and_HSV

I also added a "within" matcher for both HSLColor and HSVColor that will check if the (floating point) color components are within a certain error.

And tests.
parent bd4cf628
......@@ -167,7 +167,7 @@ class _ChipDemoState extends State<ChipDemo> {
Color _nameToColor(String name) {
assert(name.length > 1);
final int hash = name.hashCode & 0xffff;
final double hue = 360.0 * hash / (1 << 15);
final double hue = (360.0 * hash / (1 << 15)) % 360.0;
return new HSVColor.fromAHSV(1.0, hue, 0.4, 0.90).toColor();
}
......
......@@ -7,11 +7,81 @@ import 'dart:ui' show Color, lerpDouble, hashValues;
import 'package:flutter/foundation.dart';
double _getHue(double red, double green, double blue, double max, double delta) {
double hue;
if (max == 0.0) {
hue = 0.0;
} else if (max == red) {
hue = 60.0 * (((green - blue) / delta) % 6);
} else if (max == green) {
hue = 60.0 * (((blue - red) / delta) + 2);
} else if (max == blue) {
hue = 60.0 * (((red - green) / delta) + 4);
}
/// Set hue to 0.0 when red == green == blue.
hue = hue.isNaN ? 0.0 : hue;
return hue;
}
Color _colorFromHue(
double alpha,
double hue,
double chroma,
double secondary,
double match,
) {
double red;
double green;
double blue;
if (hue < 60.0) {
red = chroma;
green = secondary;
blue = 0.0;
} else if (hue < 120.0) {
red = secondary;
green = chroma;
blue = 0.0;
} else if (hue < 180.0) {
red = 0.0;
green = chroma;
blue = secondary;
} else if (hue < 240.0) {
red = 0.0;
green = secondary;
blue = chroma;
} else if (hue < 300.0) {
red = secondary;
green = 0.0;
blue = chroma;
} else {
red = chroma;
green = 0.0;
blue = secondary;
}
return new Color.fromARGB((alpha * 0xFF).round(), ((red + match) * 0xFF).round(), ((green + match) * 0xFF).round(), ((blue + match) * 0xFF).round());
}
/// A color represented using [alpha], [hue], [saturation], and [value].
///
/// An [HSVColor] is represented in a parameter space that's motivated by human
/// perception. The representation is useful for some color computations (e.g.,
/// rotating the hue through the colors of the rainbow).
/// An [HSVColor] is represented in a parameter space that's based on human
/// perception of color in pigments (e.g. paint and printer's ink). The
/// representation is useful for some color computations (e.g. rotating the hue
/// through the colors), because interpolation and picking of
/// colors as red, green, and blue channels doesn't always produce intuitive
/// results.
///
/// The HSV color space models the way that different pigments are perceived
/// when mixed. The hue describes which pigment is used, the saturation
/// describes which shade of the pigment, and the value resembles mixing the
/// pigment with different amounts of black or white pigment.
///
/// See also:
///
/// * [HSLColor], a color that uses a color space based on human perception of
/// colored light.
/// * [HSV and HSL](https://en.wikipedia.org/wiki/HSL_and_HSV) Wikipedia
/// article, which this implementation is based upon.
@immutable
class HSVColor {
/// Creates a color.
......@@ -22,14 +92,21 @@ class HSVColor {
: assert(alpha != null),
assert(hue != null),
assert(saturation != null),
assert(value != null);
assert(value != null),
assert(alpha >= 0.0),
assert(alpha <= 1.0),
assert(hue >= 0.0),
assert(hue <= 360.0),
assert(saturation >= 0.0),
assert(saturation <= 1.0),
assert(value >= 0.0),
assert(value <= 1.0);
/// Creates an [HSVColor] from an RGB [Color].
///
/// This constructor does not necessarily round-trip with [toColor] because
/// of floating point imprecision.
factory HSVColor.fromColor(Color color) {
final double alpha = color.alpha / 0xFF;
final double red = color.red / 0xFF;
final double green = color.green / 0xFF;
final double blue = color.blue / 0xFF;
......@@ -38,98 +115,66 @@ class HSVColor {
final double min = math.min(red, math.min(green, blue));
final double delta = max - min;
double hue = 0.0;
if (max == 0.0) {
hue = 0.0;
} else if (max == red) {
hue = 60.0 * (((green - blue) / delta) % 6);
} else if (max == green) {
hue = 60.0 * (((blue - red) / delta) + 2);
} else if (max == blue) {
hue = 60.0 * (((red - green) / delta) + 4);
}
/// fix hue to 0.0 when red == green == blue.
hue = hue.isNaN ? 0.0 : hue;
final double alpha = color.alpha / 0xFF;
final double hue = _getHue(red, green, blue, max, delta);
final double saturation = max == 0.0 ? 0.0 : delta / max;
return new HSVColor.fromAHSV(alpha, hue, saturation, max);
}
/// Alpha, from 0.0 to 1.0.
/// Alpha, from 0.0 to 1.0. The describes the transparency of the color.
/// A value of 0.0 is fully transparent, and 1.0 is fully opaque.
final double alpha;
/// Hue, from 0.0 to 360.0.
/// Hue, from 0.0 to 360.0. Describes which color of the spectrum is
/// represented. A value of 0.0 represents red, as does 360.0. Values in
/// between go through all the hues representable in RGB. You can think of
/// this as selecting which pigment will be added to a color.
final double hue;
/// Saturation, from 0.0 to 1.0.
/// Saturation, from 0.0 to 1.0. This describes how colorful the color is.
/// 0.0 implies a shade of grey (i.e. no pigment), and 1.0 implies a color as
/// vibrant as that hue gets. You can think of this as the equivalent of
/// how much of a pigment is added.
final double saturation;
/// Value, from 0.0 to 1.0.
/// Value, from 0.0 to 1.0. The "value" of a color that, in this context,
/// describes how bright a color is. A value of 0.0 indicates black, and 1.0
/// indicates full intensity color. You can think of this as the equivalent of
/// removing black from the color as value increases.
final double value;
/// Returns a copy of this color with the alpha parameter replaced with the given value.
/// Returns a copy of this color with the [alpha] parameter replaced with the
/// given value.
HSVColor withAlpha(double alpha) {
return new HSVColor.fromAHSV(alpha, hue, saturation, value);
}
/// Returns a copy of this color with the hue parameter replaced with the given value.
/// Returns a copy of this color with the [hue] parameter replaced with the
/// given value.
HSVColor withHue(double hue) {
return new HSVColor.fromAHSV(alpha, hue, saturation, value);
}
/// Returns a copy of this color with the saturation parameter replaced with the given value.
/// Returns a copy of this color with the [saturation] parameter replaced with
/// the given value.
HSVColor withSaturation(double saturation) {
return new HSVColor.fromAHSV(alpha, hue, saturation, value);
}
/// Returns a copy of this color with the value parameter replaced with the given value.
/// Returns a copy of this color with the [value] parameter replaced with the
/// given value.
HSVColor withValue(double value) {
return new HSVColor.fromAHSV(alpha, hue, saturation, value);
}
/// Returns this color in RGB.
Color toColor() {
final double h = hue % 360;
final double c = saturation * value;
final double x = c * (1 - (((h / 60.0) % 2) - 1).abs());
final double m = value - c;
double r;
double g;
double b;
if (h < 60.0) {
r = c;
g = x;
b = 0.0;
} else if (h < 120.0) {
r = x;
g = c;
b = 0.0;
} else if (h < 180.0) {
r = 0.0;
g = c;
b = x;
} else if (h < 240.0) {
r = 0.0;
g = x;
b = c;
} else if (h < 300.0) {
r = x;
g = 0.0;
b = c;
} else {
r = c;
g = 0.0;
b = x;
}
return new Color.fromARGB(
(alpha * 0xFF).round(),
((r + m) * 0xFF).round(),
((g + m) * 0xFF).round(),
((b + m) * 0xFF).round()
);
final double chroma = saturation * value;
final double secondary = chroma * (1.0 - (((hue / 60.0) % 2.0) - 1.0).abs());
final double match = value - chroma;
return _colorFromHue(alpha, hue, chroma, secondary, match);
}
HSVColor _scaleAlpha(double factor) {
......@@ -158,6 +203,8 @@ class HSVColor {
/// 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 outside of the valid range for each channel will be clamped.
///
/// Values for `t` are usually obtained from an [Animation<double>], such as
/// an [AnimationController].
static HSVColor lerp(HSVColor a, HSVColor b, double t) {
......@@ -169,10 +216,10 @@ class HSVColor {
if (b == null)
return a._scaleAlpha(1.0 - t);
return new HSVColor.fromAHSV(
lerpDouble(a.alpha, b.alpha, t),
lerpDouble(a.hue, b.hue, t),
lerpDouble(a.saturation, b.saturation, t),
lerpDouble(a.value, b.value, t),
lerpDouble(a.alpha, b.alpha, t).clamp(0.0, 1.0),
lerpDouble(a.hue, b.hue, t) % 360.0,
lerpDouble(a.saturation, b.saturation, t).clamp(0.0, 1.0),
lerpDouble(a.value, b.value, t).clamp(0.0, 1.0),
);
}
......@@ -193,7 +240,193 @@ class HSVColor {
int get hashCode => hashValues(alpha, hue, saturation, value);
@override
String toString() => 'HSVColor($alpha, $hue, $saturation, $value)';
String toString() => '$runtimeType($alpha, $hue, $saturation, $value)';
}
/// A color represented using [alpha], [hue], [saturation], and [lightness].
///
/// An [HSLColor] is represented in a parameter space that's based up human
/// perception of colored light. The representation is useful for some color
/// computations (e.g., combining colors of light), because interpolation and
/// picking of colors as red, green, and blue channels doesn't always produce
/// intuitive results.
///
/// HSL is a perceptual color model, placing fully saturated colors around a
/// circle (conceptually) at a lightness of ​0.5, with a lightness of 0.0 being
/// completely black, and a lightness of 1.0 being completely white. As the
/// lightness increases or decreases from 0.5, the apparent saturation decreases
/// proportionally (even though the [saturation] parameter hasn't changed).
///
/// See also:
///
/// * [HSVColor], a color that uses a color space based on human perception of
/// pigments (e.g. paint and printer's ink).
/// * [HSV and HSL](https://en.wikipedia.org/wiki/HSL_and_HSV) Wikipedia
/// article, which this implementation is based upon.
@immutable
class HSLColor {
/// Creates a color.
///
/// All the arguments must not be null and be in their respective ranges. See
/// the fields for each parameter for a description of their ranges.
const HSLColor.fromAHSL(this.alpha, this.hue, this.saturation, this.lightness)
: assert(alpha != null),
assert(hue != null),
assert(saturation != null),
assert(lightness != null),
assert(alpha >= 0.0),
assert(alpha <= 1.0),
assert(hue >= 0.0),
assert(hue <= 360.0),
assert(saturation >= 0.0),
assert(saturation <= 1.0),
assert(lightness >= 0.0),
assert(lightness <= 1.0);
/// Creates an [HSLColor] from an RGB [Color].
///
/// This constructor does not necessarily round-trip with [toColor] because
/// of floating point imprecision.
factory HSLColor.fromColor(Color color) {
final double red = color.red / 0xFF;
final double green = color.green / 0xFF;
final double blue = color.blue / 0xFF;
final double max = math.max(red, math.max(green, blue));
final double min = math.min(red, math.min(green, blue));
final double delta = max - min;
final double alpha = color.alpha / 0xFF;
final double hue = _getHue(red, green, blue, max, delta);
final double lightness = (max + min) / 2.0;
// Saturation can exceed 1.0 with rounding errors, so clamp it.
final double saturation = lightness == 1.0
? 0.0
: (delta / (1.0 - (2.0 * lightness - 1.0).abs())).clamp(0.0, 1.0);
return new HSLColor.fromAHSL(alpha, hue, saturation, lightness);
}
/// Alpha, from 0.0 to 1.0. The describes the transparency of the color.
/// A value of 0.0 is fully transparent, and 1.0 is fully opaque.
final double alpha;
/// Hue, from 0.0 to 360.0. Describes which color of the spectrum is
/// represented. A value of 0.0 represents red, as does 360.0. Values in
/// between go through all the hues representable in RGB. You can think of
/// this as selecting which color filter is placed over a light.
final double hue;
/// Saturation, from 0.0 to 1.0. This describes how colorful the color is.
/// 0.0 implies a shade of grey (i.e. no pigment), and 1.0 implies a color as
/// vibrant as that hue gets. You can think of this as the purity of the
/// color filter over the light.
final double saturation;
/// Lightness, from 0.0 to 1.0. The lightness of a color describes how bright
/// a color is. A value of 0.0 indicates black, and 1.0 indicates white. You
/// can think of this as the intensity of the light behind the filter. As the
/// lightness approaches 0.5, the colors get brighter and appear more
/// saturated, and over 0.5, the colors start to become less saturated and
/// approach white at 1.0.
final double lightness;
/// Returns a copy of this color with the alpha parameter replaced with the
/// given value.
HSLColor withAlpha(double alpha) {
return new HSLColor.fromAHSL(alpha, hue, saturation, lightness);
}
/// Returns a copy of this color with the [hue] parameter replaced with the
/// given value.
HSLColor withHue(double hue) {
return new HSLColor.fromAHSL(alpha, hue, saturation, lightness);
}
/// Returns a copy of this color with the [saturation] parameter replaced with
/// the given value.
HSLColor withSaturation(double saturation) {
return new HSLColor.fromAHSL(alpha, hue, saturation, lightness);
}
/// Returns a copy of this color with the [lightness] parameter replaced with
/// the given value.
HSLColor withLightness(double lightness) {
return new HSLColor.fromAHSL(alpha, hue, saturation, lightness);
}
/// Returns this HSL color in RGB.
Color toColor() {
final double chroma = (1.0 - (2.0 * lightness - 1.0).abs()) * saturation;
final double secondary = chroma * (1.0 - (((hue / 60.0) % 2.0) - 1.0).abs());
final double match = lightness - chroma / 2.0;
return _colorFromHue(alpha, hue, chroma, secondary, match);
}
HSLColor _scaleAlpha(double factor) {
return withAlpha(alpha * factor);
}
/// Linearly interpolate between two HSLColors.
///
/// The colors are interpolated by interpolating the [alpha], [hue],
/// [saturation], and [lightness] channels separately, which usually leads to
/// a more pleasing effect than [Color.lerp] (which interpolates the red,
/// green, and blue channels separately).
///
/// If either color is null, this function linearly interpolates from a
/// transparent instance of the other color. This is usually preferable to
/// interpolating from [Colors.transparent] (`const Color(0x00000000)`) since
/// that will interpolate from a transparent red and cycle through the hues to
/// match the target color, regardless of what that color's hue is.
///
/// 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 `b` (or something equivalent to `b`), and values between them
/// 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 outside of the valid range for each channel will be clamped.
///
/// Values for `t` are usually obtained from an [Animation<double>], such as
/// an [AnimationController].
static HSLColor lerp(HSLColor a, HSLColor b, double t) {
assert(t != null);
if (a == null && b == null)
return null;
if (a == null)
return b._scaleAlpha(t);
if (b == null)
return a._scaleAlpha(1.0 - t);
return new HSLColor.fromAHSL(
lerpDouble(a.alpha, b.alpha, t).clamp(0.0, 1.0),
lerpDouble(a.hue, b.hue, t) % 360.0,
lerpDouble(a.saturation, b.saturation, t).clamp(0.0, 1.0),
lerpDouble(a.lightness, b.lightness, t).clamp(0.0, 1.0),
);
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other is! HSLColor)
return false;
final HSLColor typedOther = other;
return typedOther.alpha == alpha
&& typedOther.hue == hue
&& typedOther.saturation == saturation
&& typedOther.lightness == lightness;
}
@override
int get hashCode => hashValues(alpha, hue, saturation, lightness);
@override
String toString() => '$runtimeType($alpha, $hue, $saturation, $lightness)';
}
/// A color that has a small table of related colors called a "swatch".
......@@ -204,7 +437,8 @@ class HSVColor {
///
/// * [MaterialColor] and [MaterialAccentColor], which define material design
/// primary and accent color swatches.
/// * [material.Colors], which defines all of the standard material design colors.
/// * [material.Colors], which defines all of the standard material design
/// colors.
class ColorSwatch<T> extends Color {
/// Creates a color that has a small table of related colors called a "swatch".
///
......
......@@ -5,6 +5,8 @@
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart';
const double _doubleColorPrecision = 0.01;
void main() {
test('HSVColor control test', () {
const HSVColor color = const HSVColor.fromAHSV(0.7, 28.0, 0.3, 0.6);
......@@ -27,24 +29,381 @@ void main() {
expect(result.value, 0.5);
});
test('HSVColor.fromColor test', () {
final HSVColor black = new HSVColor.fromColor(const Color.fromARGB(0xFF, 0x00, 0x00, 0x00));
final HSVColor red = new HSVColor.fromColor(const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00));
final HSVColor green = new HSVColor.fromColor(const Color.fromARGB(0xFF, 0x00, 0xFF, 0x00));
final HSVColor blue = new HSVColor.fromColor(const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF));
final HSVColor grey = new HSVColor.fromColor(const Color.fromARGB(0xFF, 0x80, 0x80, 0x80));
expect(black.toColor(), equals(const Color.fromARGB(0xFF, 0x00, 0x00, 0x00)));
expect(red.toColor(), equals(const Color.fromARGB(0xFF, 0xFF, 0x00, 0x00)));
expect(green.toColor(), equals(const Color.fromARGB(0xFF, 0x00, 0xFF, 0x00)));
expect(blue.toColor(), equals(const Color.fromARGB(0xFF, 0x00, 0x00, 0xFF)));
expect(grey.toColor(), equals(const Color.fromARGB(0xFF, 0x80, 0x80, 0x80)));
test('HSVColor hue sweep test', () {
final List<Color> output = <Color>[];
for (double hue = 0.0; hue <= 360.0; hue += 36.0) {
final HSVColor hsvColor = new HSVColor.fromAHSV(1.0, hue, 1.0, 1.0);
final Color color = hsvColor.toColor();
output.add(color);
if (hue != 360.0) {
// Check that it's reversible.
expect(
new HSVColor.fromColor(color),
within<HSVColor>(distance: _doubleColorPrecision, from: hsvColor),
);
}
}
final List<Color> expectedColors = <Color>[
const Color(0xffff0000),
const Color(0xffff9900),
const Color(0xffccff00),
const Color(0xff33ff00),
const Color(0xff00ff66),
const Color(0xff00ffff),
const Color(0xff0066ff),
const Color(0xff3300ff),
const Color(0xffcc00ff),
const Color(0xffff0099),
const Color(0xffff0000),
];
expect(output, equals(expectedColors));
});
test('HSVColor saturation sweep test', () {
final List<Color> output = <Color>[];
for (double saturation = 0.0; saturation < 1.0; saturation += 0.1) {
final HSVColor hslColor = new HSVColor.fromAHSV(1.0, 0.0, saturation, 1.0);
final Color color = hslColor.toColor();
output.add(color);
// Check that it's reversible.
expect(
new HSVColor.fromColor(color),
within<HSVColor>(distance: _doubleColorPrecision, from: hslColor),
);
}
final List<Color> expectedColors = <Color>[
const Color(0xffffffff),
const Color(0xffffe6e6),
const Color(0xffffcccc),
const Color(0xffffb3b3),
const Color(0xffff9999),
const Color(0xffff8080),
const Color(0xffff6666),
const Color(0xffff4d4d),
const Color(0xffff3333),
const Color(0xffff1a1a),
const Color(0xffff0000),
];
expect(output, equals(expectedColors));
});
test('HSVColor value sweep test', () {
final List<Color> output = <Color>[];
for (double value = 0.0; value < 1.0; value += 0.1) {
final HSVColor hsvColor = new HSVColor.fromAHSV(1.0, 0.0, 1.0, value);
final Color color = hsvColor.toColor();
output.add(color);
// Check that it's reversible. Discontinuities at the ends for saturation,
// so we skip those.
if (value >= _doubleColorPrecision && value <= (1.0 - _doubleColorPrecision)) {
expect(
new HSVColor.fromColor(color),
within<HSVColor>(distance: _doubleColorPrecision, from: hsvColor),
);
}
// output.add(new HSVColor.fromAHSV(1.0, 0.0, 1.0, value).toColor());
}
final List<Color> expectedColors = <Color>[
const Color(0xff000000),
const Color(0xff1a0000),
const Color(0xff330000),
const Color(0xff4d0000),
const Color(0xff660000),
const Color(0xff800000),
const Color(0xff990000),
const Color(0xffb30000),
const Color(0xffcc0000),
const Color(0xffe50000),
const Color(0xffff0000),
];
expect(output, equals(expectedColors));
});
test('HSVColor lerps hue correctly.', () {
final List<Color> output = <Color>[];
const HSVColor startColor = const HSVColor.fromAHSV(1.0, 0.0, 1.0, 1.0);
const HSVColor endColor = const HSVColor.fromAHSV(1.0, 360.0, 1.0, 1.0);
for (double t = -0.5; t < 1.5; t += 0.1) {
output.add(HSVColor.lerp(startColor, endColor, t).toColor());
}
final List<Color> expectedColors = <Color>[
const Color(0xff00ffff),
const Color(0xff0066ff),
const Color(0xff3300ff),
const Color(0xffcc00ff),
const Color(0xffff0099),
const Color(0xffff0000),
const Color(0xffff9900),
const Color(0xffccff00),
const Color(0xff33ff00),
const Color(0xff00ff66),
const Color(0xff00ffff),
const Color(0xff0066ff),
const Color(0xff3300ff),
const Color(0xffcc00ff),
const Color(0xffff0099),
const Color(0xffff0000),
const Color(0xffff9900),
const Color(0xffccff00),
const Color(0xff33ff00),
const Color(0xff00ff66),
];
expect(output, equals(expectedColors));
});
test('HSVColor lerps saturation correctly.', () {
final List<Color> output = <Color>[];
const HSVColor startColor = const HSVColor.fromAHSV(1.0, 0.0, 0.0, 1.0);
const HSVColor endColor = const HSVColor.fromAHSV(1.0, 0.0, 1.0, 1.0);
for (double t = -0.1; t < 1.1; t += 0.1) {
output.add(HSVColor.lerp(startColor, endColor, t).toColor());
}
final List<Color> expectedColors = <Color>[
const Color(0xffffffff),
const Color(0xffffffff),
const Color(0xffffe6e6),
const Color(0xffffcccc),
const Color(0xffffb3b3),
const Color(0xffff9999),
const Color(0xffff8080),
const Color(0xffff6666),
const Color(0xffff4d4d),
const Color(0xffff3333),
const Color(0xffff1a1a),
const Color(0xffff0000),
const Color(0xffff0000),
];
expect(output, equals(expectedColors));
});
test('HSVColor lerps value correctly.', () {
final List<Color> output = <Color>[];
const HSVColor startColor = const HSVColor.fromAHSV(1.0, 0.0, 1.0, 0.0);
const HSVColor endColor = const HSVColor.fromAHSV(1.0, 0.0, 1.0, 1.0);
for (double t = -0.1; t < 1.1; t += 0.1) {
output.add(HSVColor.lerp(startColor, endColor, t).toColor());
}
final List<Color> expectedColors = <Color>[
const Color(0xff000000),
const Color(0xff000000),
const Color(0xff1a0000),
const Color(0xff330000),
const Color(0xff4d0000),
const Color(0xff660000),
const Color(0xff800000),
const Color(0xff990000),
const Color(0xffb30000),
const Color(0xffcc0000),
const Color(0xffe50000),
const Color(0xffff0000),
const Color(0xffff0000),
];
expect(output, equals(expectedColors));
});
test('HSLColor control test', () {
const HSLColor color = const HSLColor.fromAHSL(0.7, 28.0, 0.3, 0.6);
expect(color, hasOneLineDescription);
expect(color.hashCode, equals(const HSLColor.fromAHSL(0.7, 28.0, 0.3, 0.6).hashCode));
expect(color.withAlpha(0.8), const HSLColor.fromAHSL(0.8, 28.0, 0.3, 0.6));
expect(color.withHue(123.0), const HSLColor.fromAHSL(0.7, 123.0, 0.3, 0.6));
expect(color.withSaturation(0.9), const HSLColor.fromAHSL(0.7, 28.0, 0.9, 0.6));
expect(color.withLightness(0.1), const HSLColor.fromAHSL(0.7, 28.0, 0.3, 0.1));
expect(color.toColor(), const Color(0xb3b8977a));
final HSLColor result = HSLColor.lerp(color, const HSLColor.fromAHSL(0.3, 128.0, 0.7, 0.2), 0.25);
expect(result.alpha, 0.6);
expect(result.hue, 53.0);
expect(result.saturation, greaterThan(0.3999));
expect(result.saturation, lessThan(0.4001));
expect(result.lightness, 0.5);
});
test('HSLColor hue sweep test', () {
final List<Color> output = <Color>[];
for (double hue = 0.0; hue <= 360.0; hue += 36.0) {
final HSLColor hslColor = new HSLColor.fromAHSL(1.0, hue, 0.5, 0.5);
final Color color = hslColor.toColor();
output.add(color);
if (hue != 360.0) {
// Check that it's reversible.
expect(
new HSLColor.fromColor(color),
within<HSLColor>(distance: _doubleColorPrecision, from: hslColor),
);
}
}
final List<Color> expectedColors = <Color>[
const Color(0xffbf4040),
const Color(0xffbf8c40),
const Color(0xffa6bf40),
const Color(0xff59bf40),
const Color(0xff40bf73),
const Color(0xff40bfbf),
const Color(0xff4073bf),
const Color(0xff5940bf),
const Color(0xffa640bf),
const Color(0xffbf408c),
const Color(0xffbf4040),
];
expect(output, equals(expectedColors));
});
test('HSLColor saturation sweep test', () {
final List<Color> output = <Color>[];
for (double saturation = 0.0; saturation < 1.0; saturation += 0.1) {
final HSLColor hslColor = new HSLColor.fromAHSL(1.0, 0.0, saturation, 0.5);
final Color color = hslColor.toColor();
output.add(color);
// Check that it's reversible.
expect(
new HSLColor.fromColor(color),
within<HSLColor>(distance: _doubleColorPrecision, from: hslColor),
);
}
final List<Color> expectedColors = <Color>[
const Color(0xff808080),
const Color(0xff8c7373),
const Color(0xff996666),
const Color(0xffa65959),
const Color(0xffb34d4d),
const Color(0xffbf4040),
const Color(0xffcc3333),
const Color(0xffd92626),
const Color(0xffe51a1a),
const Color(0xfff20d0d),
const Color(0xffff0000),
];
expect(output, equals(expectedColors));
});
test('HSLColor lightness sweep test', () {
final List<Color> output = <Color>[];
for (double lightness = 0.0; lightness < 1.0; lightness += 0.1) {
final HSLColor hslColor = new HSLColor.fromAHSL(1.0, 0.0, 0.5, lightness);
final Color color = hslColor.toColor();
output.add(color);
// Check that it's reversible. Discontinuities at the ends for saturation,
// so we skip those.
if (lightness >= _doubleColorPrecision && lightness <= (1.0 - _doubleColorPrecision)) {
expect(
new HSLColor.fromColor(color),
within<HSLColor>(distance: _doubleColorPrecision, from: hslColor),
);
}
}
final List<Color> expectedColors = <Color>[
const Color(0xff000000),
const Color(0xff260d0d),
const Color(0xff4d1a1a),
const Color(0xff732626),
const Color(0xff993333),
const Color(0xffbf4040),
const Color(0xffcc6666),
const Color(0xffd98c8c),
const Color(0xffe6b3b3),
const Color(0xfff2d9d9),
const Color(0xffffffff),
];
expect(output, equals(expectedColors));
});
test('HSLColor lerps hue correctly.', () {
final List<Color> output = <Color>[];
const HSLColor startColor = const HSLColor.fromAHSL(1.0, 0.0, 0.5, 0.5);
const HSLColor endColor = const HSLColor.fromAHSL(1.0, 360.0, 0.5, 0.5);
for (double t = -0.5; t < 1.5; t += 0.1) {
output.add(HSLColor.lerp(startColor, endColor, t).toColor());
}
final List<Color> expectedColors = <Color>[
const Color(0xff40bfbf),
const Color(0xff4073bf),
const Color(0xff5940bf),
const Color(0xffa640bf),
const Color(0xffbf408c),
const Color(0xffbf4040),
const Color(0xffbf8c40),
const Color(0xffa6bf40),
const Color(0xff59bf40),
const Color(0xff40bf73),
const Color(0xff40bfbf),
const Color(0xff4073bf),
const Color(0xff5940bf),
const Color(0xffa640bf),
const Color(0xffbf408c),
const Color(0xffbf4040),
const Color(0xffbf8c40),
const Color(0xffa6bf40),
const Color(0xff59bf40),
const Color(0xff40bf73),
];
expect(output, equals(expectedColors));
});
test('HSLColor lerps saturation correctly.', () {
final List<Color> output = <Color>[];
const HSLColor startColor = const HSLColor.fromAHSL(1.0, 0.0, 0.0, 0.5);
const HSLColor endColor = const HSLColor.fromAHSL(1.0, 0.0, 1.0, 0.5);
for (double t = -0.1; t < 1.1; t += 0.1) {
output.add(HSLColor.lerp(startColor, endColor, t).toColor());
}
final List<Color> expectedColors = <Color>[
const Color(0xff808080),
const Color(0xff808080),
const Color(0xff8c7373),
const Color(0xff996666),
const Color(0xffa65959),
const Color(0xffb34d4d),
const Color(0xffbf4040),
const Color(0xffcc3333),
const Color(0xffd92626),
const Color(0xffe51a1a),
const Color(0xfff20d0d),
const Color(0xffff0000),
const Color(0xffff0000),
];
expect(output, equals(expectedColors));
});
test('HSLColor lerps lightness correctly.', () {
final List<Color> output = <Color>[];
const HSLColor startColor = const HSLColor.fromAHSL(1.0, 0.0, 0.5, 0.0);
const HSLColor endColor = const HSLColor.fromAHSL(1.0, 0.0, 0.5, 1.0);
for (double t = -0.1; t < 1.1; t += 0.1) {
output.add(HSLColor.lerp(startColor, endColor, t).toColor());
}
final List<Color> expectedColors = <Color>[
const Color(0xff000000),
const Color(0xff000000),
const Color(0xff260d0d),
const Color(0xff4d1a1a),
const Color(0xff732626),
const Color(0xff993333),
const Color(0xffbf4040),
const Color(0xffcc6666),
const Color(0xffd98c8c),
const Color(0xffe6b3b3),
const Color(0xfff2d9d9),
const Color(0xffffffff),
const Color(0xffffffff),
];
expect(output, equals(expectedColors));
});
test('ColorSwatch test', () {
final int color = nonconst(0xFF027223);
final ColorSwatch<String> greens1 = new ColorSwatch<String>(
color, const <String, Color>{
color,
const <String, Color>{
'2259 C': const Color(0xFF027223),
'2273 C': const Color(0xFF257226),
'2426 XGC': const Color(0xFF00932F),
......@@ -52,7 +411,8 @@ void main() {
},
);
final ColorSwatch<String> greens2 = new ColorSwatch<String>(
color, const <String, Color>{
color,
const <String, Color>{
'2259 C': const Color(0xFF027223),
'2273 C': const Color(0xFF257226),
'2426 XGC': const Color(0xFF00932F),
......
......@@ -653,6 +653,8 @@ typedef num AnyDistanceFunction(Null a, Null b);
const Map<Type, AnyDistanceFunction> _kStandardDistanceFunctions = const <Type, AnyDistanceFunction>{
Color: _maxComponentColorDistance,
HSVColor: _maxComponentHSVColorDistance,
HSLColor: _maxComponentHSLColorDistance,
Offset: _offsetDistance,
int: _intDistance,
double: _doubleDistance,
......@@ -671,6 +673,22 @@ double _maxComponentColorDistance(Color a, Color b) {
return delta.toDouble();
}
// Compares hue by converting it to a 0.0 - 1.0 range, so that the comparison
// can be a similar error percentage per component.
double _maxComponentHSVColorDistance(HSVColor a, HSVColor b) {
double delta = math.max<double>((a.saturation - b.saturation).abs(), (a.value - b.value).abs());
delta = math.max<double>(delta, ((a.hue - b.hue) / 360.0).abs());
return math.max<double>(delta, (a.alpha - b.alpha).abs());
}
// Compares hue by converting it to a 0.0 - 1.0 range, so that the comparison
// can be a similar error percentage per component.
double _maxComponentHSLColorDistance(HSLColor a, HSLColor b) {
double delta = math.max<double>((a.saturation - b.saturation).abs(), (a.lightness - b.lightness).abs());
delta = math.max<double>(delta, ((a.hue - b.hue) / 360.0).abs());
return math.max<double>(delta, (a.alpha - b.alpha).abs());
}
double _rectDistance(Rect a, Rect b) {
double delta = math.max<double>((a.left - b.left).abs(), (a.top - b.top).abs());
delta = math.max<double>(delta, (a.right - b.right).abs());
......
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