Skip to content

Instantly share code, notes, and snippets.

@long1eu
Created January 19, 2018 11:54
Show Gist options
  • Save long1eu/6170416cc86cb04ca917f1e525e29d01 to your computer and use it in GitHub Desktop.
Save long1eu/6170416cc86cb04ca917f1e525e29d01 to your computer and use it in GitHub Desktop.
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