Created
January 19, 2018 11:54
-
-
Save long1eu/6170416cc86cb04ca917f1e525e29d01 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import 'dart:math' as math; | |
import 'dart:math'; | |
import 'package:flutter/foundation.dart'; | |
import 'package:color/color.dart'; | |
import 'dart:ui' as ui; | |
Map<String, ColorDefinition> _kColorDictionary = { | |
Hue.monochrome.name: | |
const ColorDefinition(hue: Hue.monochrome, lowerBounds: const [ | |
const [0, 0], | |
const [100, 0] | |
], saturationRange: const [ | |
0, | |
100 | |
], brightnessRange: const [ | |
0, | |
0 | |
]), | |
Hue.red.name: const ColorDefinition(hue: Hue.red, lowerBounds: const [ | |
const [20, 100], | |
const [30, 92], | |
const [40, 89], | |
const [50, 85], | |
const [60, 78], | |
const [70, 70], | |
const [80, 60], | |
const [90, 55], | |
const [100, 50] | |
], saturationRange: const [ | |
20, | |
100 | |
], brightnessRange: const [ | |
100, | |
50 | |
]), | |
Hue.orange.name: const ColorDefinition(hue: Hue.orange, lowerBounds: const [ | |
const [20, 100], | |
const [30, 93], | |
const [40, 88], | |
const [50, 86], | |
const [60, 85], | |
const [70, 70], | |
const [100, 70] | |
], saturationRange: const [ | |
20, | |
100 | |
], brightnessRange: const [ | |
100, | |
70 | |
]), | |
Hue.yellow.name: const ColorDefinition(hue: Hue.yellow, lowerBounds: const [ | |
const [20, 100], | |
const [30, 93], | |
const [40, 88], | |
const [50, 86], | |
const [60, 85], | |
const [70, 70], | |
const [100, 70] | |
], saturationRange: const [ | |
20, | |
100 | |
], brightnessRange: const [ | |
100, | |
70 | |
]), | |
Hue.green.name: const ColorDefinition(hue: Hue.green, lowerBounds: const [ | |
const [30, 100], | |
const [40, 90], | |
const [50, 85], | |
const [60, 81], | |
const [70, 74], | |
const [80, 64], | |
const [90, 50], | |
const [100, 40] | |
], saturationRange: const [ | |
30, | |
100 | |
], brightnessRange: const [ | |
100, | |
40 | |
]), | |
Hue.blue.name: const ColorDefinition(hue: Hue.blue, lowerBounds: const [ | |
const [20, 100], | |
const [30, 86], | |
const [40, 80], | |
const [50, 74], | |
const [60, 60], | |
const [70, 52], | |
const [80, 44], | |
const [90, 39], | |
const [100, 35] | |
], saturationRange: const [ | |
20, | |
100 | |
], brightnessRange: const [ | |
100, | |
35 | |
]), | |
Hue.purple.name: const ColorDefinition(hue: Hue.purple, lowerBounds: const [ | |
const [20, 100], | |
const [30, 87], | |
const [40, 79], | |
const [50, 70], | |
const [60, 65], | |
const [70, 59], | |
const [80, 52], | |
const [90, 45], | |
const [100, 42] | |
], saturationRange: const [ | |
20, | |
100 | |
], brightnessRange: const [ | |
100, | |
42 | |
]), | |
Hue.pink.name: const ColorDefinition(hue: Hue.pink, lowerBounds: const [ | |
const [20, 100], | |
const [30, 90], | |
const [40, 86], | |
const [60, 84], | |
const [80, 80], | |
const [90, 75], | |
const [100, 73] | |
], saturationRange: const [ | |
20, | |
100 | |
], brightnessRange: const [ | |
100, | |
73 | |
]) | |
}; | |
@immutable | |
class ColorDefinition { | |
const ColorDefinition( | |
{this.hue, this.lowerBounds, this.saturationRange, this.brightnessRange}); | |
final Hue hue; | |
final List<List<int>> lowerBounds; | |
final List<int> saturationRange; | |
final List<int> brightnessRange; | |
} | |
class RandomColor { | |
int seed; | |
RandomColor(); | |
List<ui.Color> randomColor([Options options = const Options()]) { | |
seed = options.seed; | |
// Check if we need to generate multiple colors | |
if (options.count != null) { | |
var colors = []; | |
while (options.count > colors.length) { | |
colors.add(randomColor(options.copyWith(count: null))); | |
seed++; | |
} | |
return colors; | |
} | |
// First we pick a hue (H) | |
var hue = pickHue(options); | |
// Then use H to determine saturation (S) | |
var saturation = pickSaturation(hue, options); | |
// Then use S and H to determine brightness (B). | |
var brightness = pickBrightness(hue, saturation, options); | |
// Then we return the HSB color in the desired format | |
var a = new HslColor(hue, saturation, brightness).toRgbColor(); | |
int alpha = | |
(options.alpha == null ? randomWithin([0, 100]) : options.alpha * 100) | |
.toInt(); | |
return [new ui.Color.fromARGB(alpha, a.r, a.g, a.b)]; | |
} | |
int pickHue(Options options) { | |
var hueRange = checkHueRange(options.hue); | |
var hue = randomWithin(hueRange); | |
// Instead of storing red as two separate ranges, | |
// we group them, using negative numbers | |
if (hue < 0) hue = 360 + hue; | |
return hue; | |
} | |
int pickSaturation(int hue, Options options) { | |
if (options.hue == Hue.monochrome) return 0; | |
if (options.luminosity == Luminosity.random) { | |
return randomWithin([0, 100]); | |
} | |
var saturationRange = getSaturationRange(hue); | |
var sMin = saturationRange[0]; | |
var sMax = saturationRange[1]; | |
if (options.luminosity == Luminosity.bright) { | |
sMin = 55; | |
} else if (options.luminosity == Luminosity.dark) { | |
sMin = sMax - 10; | |
} else if (options.luminosity == Luminosity.light) { | |
sMax = 55; | |
} | |
return randomWithin([sMin, sMax]); | |
} | |
int pickBrightness(int hue, int saturation, Options options) { | |
var bMin = getMinimumBrightness(hue, saturation); | |
var bMax = 100; | |
if (options.luminosity == Luminosity.dark) { | |
bMax = bMin + 20; | |
} else if (options.luminosity == Luminosity.light) { | |
bMin = (bMax + bMin) / 2; | |
} else if (options.luminosity == Luminosity.random) { | |
bMin = 0; | |
bMax = 100; | |
} | |
return randomWithin([bMin, bMax]); | |
} | |
List<int> checkHueRange(Hue hue) { | |
if (hue != null && hue.range[0] > 0 && hue.range[1] < 360) { | |
return hue.range; | |
} | |
return [0, 360]; | |
} | |
static List<int> hexToHsb(String hex) { | |
hex = hex.replaceAll("#", ''); | |
var red = int.parse(hex.substring(0, 2), radix: 16) / 255; | |
var green = int.parse(hex.substring(2, 2), radix: 16) / 255; | |
var blue = int.parse(hex.substring(4, 2), radix: 16) / 255; | |
var cMax = math.max(math.max(red, green), blue).toInt(); | |
var delta = cMax - math.min(math.min(red, green), blue); | |
var saturation = cMax != 0 ? (delta / cMax) : 0; | |
if (cMax == red) { | |
return [60 * (((green - blue) / delta) % 6) ?? 0, saturation, cMax]; | |
} else if (cMax == green) { | |
return [60 * (((blue - red) / delta) + 2) ?? 0, saturation, cMax]; | |
} else { | |
return [60 * (((red - green) / delta) + 4) ?? 0, saturation, cMax]; | |
} | |
} | |
int randomWithin(List<int> range) { | |
if (seed == null) { | |
return (range[0] + new Random().nextDouble() * (range[1] + 1 - range[0])) | |
.floor(); | |
} else { | |
//Seeded random algorithm from http://indiegamr.com/generate-repeatable-random-numbers-in-js/ | |
var max = range[1] ?? 1; | |
var min = range[0] ?? 0; | |
seed = (seed * 9301 + 49297) % 233280; | |
var rnd = seed / 233280.0; | |
return (min + rnd * (max - min)).floor(); | |
} | |
} | |
getSaturationRange(int hue) { | |
return getColorInfo(hue).saturationRange; | |
} | |
getMinimumBrightness(int hue, int saturation) { | |
var lowerBounds = getColorInfo(hue).lowerBounds; | |
for (var i = 0; i < lowerBounds.length - 1; i++) { | |
var s1 = lowerBounds[i][0], v1 = lowerBounds[i][1]; | |
var s2 = lowerBounds[i + 1][0], v2 = lowerBounds[i + 1][1]; | |
if (saturation >= s1 && saturation <= s2) { | |
var m = (v2 - v1) / (s2 - s1), b = v1 - m * s1; | |
return m * saturation + b; | |
} | |
} | |
return 0; | |
} | |
ColorDefinition getColorInfo(int hue) { | |
// Maps red colors to make picking hue easier | |
if (hue >= 334 && hue <= 360) { | |
hue -= 360; | |
} | |
for (String colorName in _kColorDictionary.keys) { | |
var color = _kColorDictionary[colorName]; | |
if (color.hue.range != null && | |
hue >= color.hue.range[0] && | |
hue <= color.hue.range[1]) { | |
return _kColorDictionary[colorName]; | |
} | |
} | |
return null; | |
} | |
List<int> hsvToHsl(List<int> hsv) { | |
var h = hsv[0], s = hsv[1] / 100, v = hsv[2] / 100, k = (2 - s) * v; | |
return [ | |
h, | |
(s * v / (k < 1 ? k : 2 - k).round() * 10000) ~/ 100, | |
(k / 2 * 100).toInt() | |
]; | |
} | |
List<int> hsvToRgb(List<int> hsv) { | |
// this doesn't work for the values of 0 and 360 | |
// here's the hacky fix | |
var h = hsv[0]; | |
if (h == 0) { | |
h = 1; | |
} | |
if (h == 360) { | |
h = 359; | |
} | |
// Rebase the h,s,v values | |
h = h ~/ 360; | |
var s = hsv[1] / 100, v = hsv[2] / 100; | |
var hI = (h * 6).floor(), | |
f = h * 6 - hI, | |
p = v * (1 - s), | |
q = v * (1 - f * s), | |
t = v * (1 - (1 - f) * s), | |
r = 256, | |
g = 256, | |
b = 256; | |
switch (hI) { | |
case 0: | |
r = v.toInt(); | |
g = t.toInt(); | |
b = p.toInt(); | |
break; | |
case 1: | |
r = q.toInt(); | |
g = v.toInt(); | |
b = p.toInt(); | |
break; | |
case 2: | |
r = p.toInt(); | |
g = v.toInt(); | |
b = t.toInt(); | |
break; | |
case 3: | |
r = p.toInt(); | |
g = q.toInt(); | |
b = v.toInt(); | |
break; | |
case 4: | |
r = t.toInt(); | |
g = p.toInt(); | |
b = v.toInt(); | |
break; | |
case 5: | |
r = v.toInt(); | |
g = p.toInt(); | |
b = q.toInt(); | |
break; | |
} | |
var result = [(r * 255).floor(), (g * 255).floor(), (b * 255).floor()]; | |
return result; | |
} | |
String hsvToHex(List<int> hsv) { | |
var rgb = hsvToRgb(hsv); | |
var hex = '#' + | |
componentToHex(rgb[0]) + | |
componentToHex(rgb[1]) + | |
componentToHex(rgb[2]); | |
return hex; | |
} | |
String componentToHex(int c) { | |
var hex = c.toRadixString(16); | |
return hex.length == 1 ? '0' + hex : hex; | |
} | |
} | |
class Options { | |
const Options({this.hue, this.luminosity, this.count, this.seed, this.alpha}); | |
/// Controls the hue of the generated color. If you pass a hexadecimal color | |
/// string using [Hue.fromHex(hex)]. [RandomColor] will extract its hue value | |
/// and use that to generate colors. | |
final Hue hue; | |
/// Controls the luminosity of the generated color. | |
final Luminosity luminosity; | |
/// An integer which specifies the number of colors to generate. | |
final int count; | |
/// An integer or string which when passed will cause randomColor to return | |
/// the same color each time. | |
final int seed; | |
/// A decimal between 0 and 1. Only relevant when using a format with an alpha | |
/// channel ([ColorFormat.rgba] and [ColorFormat.hsla]). Defaults to a random | |
/// value. | |
final double alpha; | |
Options copyWith( | |
{Hue hue, | |
Luminosity luminosity, | |
int count, | |
int seed, | |
double alpha}) => | |
new Options( | |
hue: hue ?? this.hue, | |
luminosity: luminosity ?? this.luminosity, | |
count: count ?? this.count, | |
seed: seed ?? this.seed, | |
alpha: alpha ?? this.alpha); | |
} | |
class Hue { | |
const Hue._(this.name, this.range); | |
final String name; | |
final List<int> range; | |
static const Hue red = const Hue._('red', const [-26, 18]); | |
static const Hue orange = const Hue._('orange', const [19, 46]); | |
static const Hue yellow = const Hue._('yellow', const [47, 62]); | |
static const Hue green = const Hue._('green', const [63, 178]); | |
static const Hue blue = const Hue._('blue', const [179, 257]); | |
static const Hue purple = const Hue._('purple', const [258, 282]); | |
static const Hue pink = const Hue._('pink', const [283, 334]); | |
static const Hue monochrome = const Hue._('monochrome', null); | |
static const List<Hue> values = const [ | |
red, | |
orange, | |
yellow, | |
green, | |
blue, | |
purple, | |
pink, | |
monochrome | |
]; | |
static const List<String> _stringValues = const [ | |
"red", | |
"orange", | |
"yellow", | |
"green", | |
"blue", | |
"purple", | |
"pink", | |
"monochrome" | |
]; | |
static Hue fromHex(String hex) { | |
if (_kColorDictionary.containsKey(hex)) { | |
var color = _kColorDictionary[hex]; | |
if (color.hue != null) return color.hue; | |
} else if (hexRegEx.hasMatch(hex)) { | |
var hue = RandomColor.hexToHsb(hex)[0]; | |
return new Hue._(null, [hue, hue]); | |
} | |
return new Hue._(null, [0, 260]); | |
} | |
static var hexRegEx = | |
new RegExp("^#?([0-9A-F]{3}|[0-9A-F]{6})", caseSensitive: false); | |
@override | |
String toString() => name; | |
@override | |
bool operator ==(Object other) => | |
identical(this, other) || | |
other is Hue && | |
runtimeType == other.runtimeType && | |
name == other.name && | |
range == other.range; | |
@override | |
int get hashCode => name.hashCode ^ range.hashCode; | |
} | |
class Luminosity { | |
const Luminosity._(this._index); | |
final int _index; | |
static const Luminosity bright = const Luminosity._(0); | |
static const Luminosity light = const Luminosity._(1); | |
static const Luminosity dark = const Luminosity._(2); | |
static const Luminosity random = const Luminosity._(3); | |
static const List<Luminosity> values = const [bright, light, dark]; | |
static const List<String> _stringValues = const [ | |
"bright", | |
"light", | |
"dark", | |
"random" | |
]; | |
@override | |
bool operator ==(other) { | |
if (other is Luminosity) { | |
return _index == other._index; | |
} else if (other is String) { | |
return _stringValues.contains(other); | |
} | |
return false; | |
} | |
@override | |
String toString() => _stringValues[_index]; | |
@override | |
int get hashCode => 31 * _index; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment