import 'dart:math'; | |
import 'image_exception.dart'; | |
import 'internal/clamp.dart'; | |
/// Image pixel colors are instantiated as an int object rather than an instance | |
/// of the Color class in order to reduce object allocations. Image pixels are | |
/// stored in 32-bit RGBA format (8 bits per channel). Internally in dart, this | |
/// will be stored in a "small integer" on 64-bit machines, or a | |
/// "medium integer" on 32-bit machines. In Javascript, this will be stored | |
/// in a 64-bit double. | |
/// | |
/// The Color class is used as a namespace for color operations, in an attempt | |
/// to create a cleaner API for color operations. | |
class Color { | |
/// Create a color value from RGB values in the range [0, 255]. | |
static int fromRgb(int red, int green, int blue) { | |
return getColor(red, green, blue); | |
} | |
/// Create a color value from RGBA values in the range [0, 255]. | |
static int fromRgba(int red, int green, int blue, int alpha) { | |
return getColor(red, green, blue, alpha); | |
} | |
/// Create a color value from HSL values in the range [0, 1]. | |
static int fromHsl(num hue, num saturation, num lightness) { | |
var rgb = hslToRGB(hue, saturation, lightness); | |
return getColor(rgb[0], rgb[1], rgb[2]); | |
} | |
/// Create a color value from HSV values in the range [0, 1]. | |
static int fromHsv(num hue, num saturation, num value) { | |
var rgb = hsvToRGB(hue, saturation, value); | |
return getColor(rgb[0], rgb[1], rgb[2]); | |
} | |
/// Create a color value from XYZ values. | |
static int fromXyz(num x, num y, num z) { | |
var rgb = xyzToRGB(x, y, z); | |
return getColor(rgb[0], rgb[1], rgb[2]); | |
} | |
/// Create a color value from CIE-L*ab values. | |
static int fromLab(num L, num a, num b) { | |
var rgb = labToRGB(L, a, b); | |
return getColor(rgb[0], rgb[1], rgb[2]); | |
} | |
/// Compare colors from a 3 or 4 dimensional color space | |
static num distance(List<num> c1, List<num> c2, bool compareAlpha) { | |
num d1 = c1[0] - c2[0]; | |
num d2 = c1[1] - c2[1]; | |
num d3 = c1[2] - c2[2]; | |
if (compareAlpha) { | |
num dA = c1[3] - c2[3]; | |
return sqrt(max(d1 * d1, (d1 - dA) * (d1 - dA)) + | |
max(d2 * d2, (d2 - dA) * (d2 - dA)) + | |
max(d3 * d3, (d3 - dA) * (d3 - dA))); | |
} else { | |
return sqrt(d1 * d1 + d2 * d2 + d3 * d3); | |
} | |
} | |
} | |
/// Blue channel of a color. | |
const int BLUE = 0; | |
/// Green channel of a color. | |
const int GREEN = 1; | |
/// Red channel of a color. | |
const int RED = 2; | |
/// Alpha channel of a color. | |
const int ALPHA = 3; | |
/// Luminance of a color. | |
const int LUMINANCE = 4; | |
/// Get the color with the given [r], [g], [b], and [a] components. | |
/// | |
/// The channel order of a uint32 encoded color is RGBA, to be consistent | |
/// with the image data of a canvas html element. | |
int getColor(int r, int g, int b, [int a = 255]) => | |
(clamp255(a) << 24) | | |
(clamp255(r) << 16) | | |
(clamp255(g) << 8) | | |
(clamp255(b)); | |
/// Get the [channel] from the [color]. | |
int getChannel(int color, int channel) => channel == RED | |
? getRed(color) | |
: channel == GREEN | |
? getGreen(color) | |
: channel == BLUE ? getBlue(color) : getAlpha(color); | |
/// Returns a new color, where the given [color]'s [channel] has been | |
/// replaced with the given [value]. | |
int setChannel(int color, int channel, int value) => channel == RED | |
? setRed(color, value) | |
: channel == GREEN | |
? setGreen(color, value) | |
: channel == BLUE ? setBlue(color, value) : setAlpha(color, value); | |
/// Get the blue channel from the [color]. | |
int getBlue(int color) => (color) & 0xff; | |
/// Returns a new color where the blue channel of [color] has been replaced | |
/// by [value]. | |
int setBlue(int color, int value) => (color & 0xffffff00) | (clamp255(value)); | |
/// Get the green channel from the [color]. | |
int getGreen(int color) => (color >> 8) & 0xff; | |
/// Returns a new color where the green channel of [color] has been replaced | |
/// by [value]. | |
int setGreen(int color, int value) => | |
(color & 0xffff00ff) | (clamp255(value) << 8); | |
/// Get the red channel from the [color]. | |
int getRed(int color) => (color >> 16) & 0xff; | |
/// Returns a new color where the red channel of [color] has been replaced | |
/// by [value]. | |
int setRed(int color, int value) => | |
(color & 0xff00ffff) | (clamp255(value) << 16); | |
/// Get the alpha channel from the [color]. | |
int getAlpha(int color) => (color >> 24) & 0xff; | |
/// Returns a new color where the alpha channel of [color] has been replaced | |
/// by [value]. | |
int setAlpha(int color, int value) => | |
(color & 0x00ffffff) | (clamp255(value) << 24); | |
/// Returns a new color of [src] alpha-blended onto [dst]. The opacity of [src] | |
/// is additionally scaled by [fraction] / 255. | |
int alphaBlendColors(int dst, int src, [int fraction = 0xff]) { | |
double a = (getAlpha(src) / 255.0); | |
if (fraction != 0xff) { | |
a *= (fraction / 255.0); | |
} | |
int sr = (getRed(src) * a).round(); | |
int sg = (getGreen(src) * a).round(); | |
int sb = (getBlue(src) * a).round(); | |
int sa = (getAlpha(src) * a).round(); | |
int dr = (getRed(dst) * (1.0 - a)).round(); | |
int dg = (getGreen(dst) * (1.0 - a)).round(); | |
int db = (getBlue(dst) * (1.0 - a)).round(); | |
int da = (getAlpha(dst) * (1.0 - a)).round(); | |
return getColor(sr + dr, sg + dg, sb + db, sa + da); | |
} | |
/// Returns the luminance (grayscale) value of the [color]. | |
int getLuminance(int color) { | |
int r = getRed(color); | |
int g = getGreen(color); | |
int b = getBlue(color); | |
return (0.299 * r + 0.587 * g + 0.114 * b).round(); | |
} | |
/// Returns the luminance (grayscale) value of the color. | |
int getLuminanceRGB(int r, int g, int b) => | |
(0.299 * r + 0.587 * g + 0.114 * b).round(); | |
/// Convert an HSL color to RGB, where h is specified in normalized degrees | |
/// [0, 1] (where 1 is 360-degrees); s and l are in the range [0, 1]. | |
/// Returns a list [r, g, b] with values in the range [0, 255]. | |
List<int> hslToRGB(num hue, num saturation, num lightness) { | |
if (saturation == 0) { | |
int gray = (lightness * 255.0).toInt(); | |
return [gray, gray, gray]; | |
} | |
hue2rgb(num p, num q, num t) { | |
if (t < 0.0) { | |
t += 1.0; | |
} | |
if (t > 1) { | |
t -= 1.0; | |
} | |
if (t < 1.0 / 6.0) { | |
return p + (q - p) * 6.0 * t; | |
} | |
if (t < 1.0 / 2.0) { | |
return q; | |
} | |
if (t < 2.0 / 3.0) { | |
return p + (q - p) * (2.0 / 3.0 - t) * 6.0; | |
} | |
return p; | |
} | |
var q = lightness < 0.5 | |
? lightness * (1.0 + saturation) | |
: lightness + saturation - lightness * saturation; | |
var p = 2.0 * lightness - q; | |
var r = hue2rgb(p, q, hue + 1.0 / 3.0); | |
var g = hue2rgb(p, q, hue); | |
var b = hue2rgb(p, q, hue - 1.0 / 3.0); | |
return [(r * 255.0).round(), (g * 255.0).round(), (b * 255.0).round()]; | |
} | |
/// Convert an HSV color to RGB, where h is specified in normalized degrees | |
/// [0, 1] (where 1 is 360-degrees); s and l are in the range [0, 1]. | |
/// Returns a list [r, g, b] with values in the range [0, 255]. | |
List<int> hsvToRGB(num hue, num saturation, num brightness) { | |
if (saturation == 0) { | |
var gray = (brightness * 255.0).round(); | |
return [gray, gray, gray]; | |
} | |
num h = (hue - hue.floor()) * 6.0; | |
num f = h - h.floor(); | |
num p = brightness * (1.0 - saturation); | |
num q = brightness * (1.0 - saturation * f); | |
num t = brightness * (1.0 - (saturation * (1.0 - f))); | |
switch (h.toInt()) { | |
case 0: | |
return [ | |
(brightness * 255.0).round(), | |
(t * 255.0).round(), | |
(p * 255.0).round() | |
]; | |
case 1: | |
return [ | |
(q * 255.0).round(), | |
(brightness * 255.0).round(), | |
(p * 255.0).round() | |
]; | |
case 2: | |
return [ | |
(p * 255.0).round(), | |
(brightness * 255.0).round(), | |
(t * 255.0).round() | |
]; | |
case 3: | |
return [ | |
(p * 255.0).round(), | |
(q * 255.0).round(), | |
(brightness * 255.0).round() | |
]; | |
case 4: | |
return [ | |
(t * 255.0).round(), | |
(p * 255.0).round(), | |
(brightness * 255.0).round() | |
]; | |
case 5: | |
return [ | |
(brightness * 255.0).round(), | |
(p * 255.0).round(), | |
(q * 255.0).round() | |
]; | |
default: | |
throw new ImageException('invalid hue'); | |
} | |
} | |
/// Convert an RGB color to HSL, where r, g and b are in the range [0, 255]. | |
/// Returns a list [h, s, l] with values in the range [0, 1]. | |
List<num> rgbToHSL(num r, num g, num b) { | |
r /= 255.0; | |
g /= 255.0; | |
b /= 255.0; | |
var mx = max(r, max(g, b)); | |
var mn = min(r, min(g, b)); | |
num h; | |
var l = (mx + mn) / 2.0; | |
if (mx == mn) { | |
return [0.0, 0.0, l]; | |
} | |
var d = mx - mn; | |
var s = l > 0.5 ? d / (2.0 - mx - mn) : d / (mx + mn); | |
if (mx == r) { | |
h = (g - b) / d + (g < b ? 6.0 : 0.0); | |
} else if (mx == g) { | |
h = (b - r) / d + 2.0; | |
} else { | |
h = (r - g) / d + 4.0; | |
} | |
h /= 6.0; | |
return [h, s, l]; | |
} | |
/// Convert a CIE-L*ab color to XYZ. | |
List<int> labToXYZ(num l, num a, num b) { | |
num y = (l + 16.0) / 116.0; | |
num x = y + (a / 500.0); | |
num z = y - (b / 200.0); | |
if (pow(x, 3) > 0.008856) { | |
x = pow(x, 3); | |
} else { | |
x = (x - 16.0 / 116) / 7.787; | |
} | |
if (pow(y, 3) > 0.008856) { | |
y = pow(y, 3); | |
} else { | |
y = (y - 16.0 / 116.0) / 7.787; | |
} | |
if (pow(z, 3) > 0.008856) { | |
z = pow(z, 3); | |
} else { | |
z = (z - 16.0 / 116.0) / 7.787; | |
} | |
return [(x * 95.047).toInt(), (y * 100.0).toInt(), (z * 108.883).toInt()]; | |
} | |
/// Convert an XYZ color to RGB. | |
List<int> xyzToRGB(num x, num y, num z) { | |
x /= 100; | |
y /= 100; | |
z /= 100; | |
num r = (3.2406 * x) + (-1.5372 * y) + (-0.4986 * z); | |
num g = (-0.9689 * x) + (1.8758 * y) + (0.0415 * z); | |
num b = (0.0557 * x) + (-0.2040 * y) + (1.0570 * z); | |
if (r > 0.0031308) { | |
r = (1.055 * pow(r, 0.4166666667)) - 0.055; | |
} else { | |
r *= 12.92; | |
} | |
if (g > 0.0031308) { | |
g = (1.055 * pow(g, 0.4166666667)) - 0.055; | |
} else { | |
g *= 12.92; | |
} | |
if (b > 0.0031308) { | |
b = (1.055 * pow(b, 0.4166666667)) - 0.055; | |
} else { | |
b *= 12.92; | |
} | |
return [ | |
(r * 255).clamp(0, 255).toInt(), | |
(g * 255).clamp(0, 255).toInt(), | |
(b * 255).clamp(0, 255).toInt() | |
]; | |
} | |
/// Convert a CMYK color to RGB, where c, m, y, k values are in the range | |
/// [0, 255]. Returns a list [r, g, b] with values in the range [0, 255]. | |
List<int> cmykToRGB(num c, num m, num y, num k) { | |
c /= 255.0; | |
m /= 255.0; | |
y /= 255.0; | |
k /= 255.0; | |
return [ | |
(255.0 * (1.0 - c) * (1.0 - k)).round(), | |
(255.0 * (1.0 - m) * (1.0 - k)).round(), | |
(255.0 * (1.0 - y) * (1.0 - k)).round() | |
]; | |
} | |
/// Convert a CIE-L*ab color to RGB. | |
List<int> labToRGB(num l, num a, num b) { | |
const num ref_x = 95.047; | |
const num ref_y = 100.000; | |
const num ref_z = 108.883; | |
num y = (l + 16.0) / 116.0; | |
num x = a / 500.0 + y; | |
num z = y - b / 200.0; | |
num y3 = pow(y, 3); | |
if (y3 > 0.008856) { | |
y = y3; | |
} else { | |
y = (y - 16 / 116) / 7.787; | |
} | |
num x3 = pow(x, 3); | |
if (x3 > 0.008856) { | |
x = x3; | |
} else { | |
x = (x - 16 / 116) / 7.787; | |
} | |
num z3 = pow(z, 3); | |
if (z3 > 0.008856) { | |
z = z3; | |
} else { | |
z = (z - 16 / 116) / 7.787; | |
} | |
x *= ref_x; | |
y *= ref_y; | |
z *= ref_z; | |
x /= 100.0; | |
y /= 100.0; | |
z /= 100.0; | |
// xyz to rgb | |
num R = x * 3.2406 + y * (-1.5372) + z * (-0.4986); | |
num G = x * (-0.9689) + y * 1.8758 + z * 0.0415; | |
num B = x * 0.0557 + y * (-0.2040) + z * 1.0570; | |
if (R > 0.0031308) { | |
R = 1.055 * (pow(R, 1.0 / 2.4)) - 0.055; | |
} else { | |
R = 12.92 * R; | |
} | |
if (G > 0.0031308) { | |
G = 1.055 * (pow(G, 1.0 / 2.4)) - 0.055; | |
} else { | |
G = 12.92 * G; | |
} | |
if (B > 0.0031308) { | |
B = 1.055 * (pow(B, 1.0 / 2.4)) - 0.055; | |
} else { | |
B = 12.92 * B; | |
} | |
return [ | |
(R * 255.0).clamp(0, 255).toInt(), | |
(G * 255.0).clamp(0, 255).toInt(), | |
(B * 255.0).clamp(0, 255).toInt() | |
]; | |
} | |
/// Convert a RGB color to XYZ. | |
List<num> rgbToXYZ(num r, num g, num b) { | |
r = r / 255.0; | |
g = g / 255.0; | |
b = b / 255.0; | |
if (r > 0.04045) { | |
r = pow((r + 0.055) / 1.055, 2.4); | |
} else { | |
r = r / 12.92; | |
} | |
if (g > 0.04045) { | |
g = pow((g + 0.055) / 1.055, 2.4); | |
} else { | |
g = g / 12.92; | |
} | |
if (b > 0.04045) { | |
b = pow((b + 0.055) / 1.055, 2.4); | |
} else { | |
b = b / 12.92; | |
} | |
r = r * 100.0; | |
g = g * 100.0; | |
b = b * 100.0; | |
return [ | |
r * 0.4124 + g * 0.3576 + b * 0.1805, | |
r * 0.2126 + g * 0.7152 + b * 0.0722, | |
r * 0.0193 + g * 0.1192 + b * 0.9505 | |
]; | |
} | |
/// Convert a XYZ color to CIE-L*ab. | |
List<num> xyzToLab(num x, num y, num z) { | |
x = x / 95.047; | |
y = y / 100.0; | |
z = z / 108.883; | |
if (x > 0.008856) { | |
x = pow(x, 1 / 3.0); | |
} else { | |
x = (7.787 * x) + (16 / 116.0); | |
} | |
if (y > 0.008856) { | |
y = pow(y, 1 / 3.0); | |
} else { | |
y = (7.787 * y) + (16 / 116.0); | |
} | |
if (z > 0.008856) { | |
z = pow(z, 1 / 3.0); | |
} else { | |
z = (7.787 * z) + (16 / 116.0); | |
} | |
return [(116.0 * y) - 16, 500.0 * (x - y), 200.0 * (y - z)]; | |
} | |
/// Convert a RGB color to CIE-L*ab. | |
List<num> rgbToLab(num r, num g, num b) { | |
r = r / 255.0; | |
g = g / 255.0; | |
b = b / 255.0; | |
if (r > 0.04045) { | |
r = pow((r + 0.055) / 1.055, 2.4); | |
} else { | |
r = r / 12.92; | |
} | |
if (g > 0.04045) { | |
g = pow((g + 0.055) / 1.055, 2.4); | |
} else { | |
g = g / 12.92; | |
} | |
if (b > 0.04045) { | |
b = pow((b + 0.055) / 1.055, 2.4); | |
} else { | |
b = b / 12.92; | |
} | |
r = r * 100.0; | |
g = g * 100.0; | |
b = b * 100.0; | |
num x = r * 0.4124 + g * 0.3576 + b * 0.1805; | |
num y = r * 0.2126 + g * 0.7152 + b * 0.0722; | |
num z = r * 0.0193 + g * 0.1192 + b * 0.9505; | |
x = x / 95.047; | |
y = y / 100.0; | |
z = z / 108.883; | |
if (x > 0.008856) { | |
x = pow(x, 1 / 3.0); | |
} else { | |
x = (7.787 * x) + (16 / 116.0); | |
} | |
if (y > 0.008856) { | |
y = pow(y, 1 / 3.0); | |
} else { | |
y = (7.787 * y) + (16 / 116.0); | |
} | |
if (z > 0.008856) { | |
z = pow(z, 1 / 3.0); | |
} else { | |
z = (7.787 * z) + (16 / 116.0); | |
} | |
return [(116.0 * y) - 16, 500.0 * (x - y), 200.0 * (y - z)]; | |
} |