blob: 59ea428ff3c354e414eab5a1602492cd67014134 [file] [log] [blame]
import 'dart:math' as 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]);
}
}
/// Red channel of a color.
const int RED = 0;
/// Green channel of a color.
const int GREEN = 1;
/// Blue channel of a color.
const int BLUE = 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(b) << 16) |
(clamp255(g) << 8) |
clamp255(r);
/**
* Get the [channel] from the [color].
*/
int getChannel(int color, int channel) =>
channel == 0 ? getRed(color) :
channel == 1 ? getGreen(color) :
channel == 2 ? 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 == 0 ? setRed(color, value) :
channel == 1 ? setGreen(color, value) :
channel == 2 ? setBlue(color, value) :
setAlpha(color, value);
/**
* Get the red channel from the [color].
*/
int getRed(int color) =>
(color) & 0xff;
/**
* Returns a new color where the red channel of [color] has been replaced
* by [value].
*/
int setRed(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 blue channel from the [color].
*/
int getBlue(int color) =>
(color >> 16) & 0xff;
/**
* Returns a new color where the blue channel of [color] has been replaced
* by [value].
*/
int setBlue(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];
}
double h = (hue - hue.floor()) * 6.0;
double f = h - h.floor();
double p = brightness * (1.0 - saturation);
double q = brightness * (1.0 - saturation * f);
double 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<double> rgbToHSL(num r, num g, num b) {
r /= 255.0;
g /= 255.0;
b /= 255.0;
var max = Math.max(r, Math.max(g, b));
var min = Math.min(r, Math.min(g, b));
var h;
var s;
var l = (max + min) / 2.0;
if (max == min){
return [0.0, 0.0, l];
}
var d = max - min;
s = l > 0.5 ? d / (2.0 - max - min) : d / (max + min);
if (max == r) {
h = (g - b) / d + (g < b ? 6.0 : 0.0);
} else if (max == 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) {
var y = (l + 16.0) / 116.0;
var x = y + (a / 500.0);
var z = y - (b / 200.0);
if (Math.pow(x, 3) > 0.008856) {
x = Math.pow(x, 3);
} else {
x = (x - 16.0 / 116) / 7.787;
}
if (Math.pow(y, 3) > 0.008856) {
y = Math.pow(y, 3);
} else {
y = (y - 16.0 / 116.0) / 7.787;
}
if (Math.pow(z, 3) > 0.008856) {
z = Math.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) {
var b, g, r;
x /= 100;
y /= 100;
z /= 100;
r = (3.2406 * x) + (-1.5372 * y) + (-0.4986 * z);
g = (-0.9689 * x) + (1.8758 * y) + (0.0415 * z);
b = (0.0557 * x) + (-0.2040 * y) + (1.0570 * z);
if (r > 0.0031308) {
r = (1.055 * Math.pow(r, 0.4166666667)) - 0.055;
} else {
r *= 12.92;
}
if (g > 0.0031308) {
g = (1.055 * Math.pow(g, 0.4166666667)) - 0.055;
} else {
g *= 12.92;
}
if (b > 0.0031308) {
b = (1.055 * Math.pow(b, 0.4166666667)) - 0.055;
} else {
b *= 12.92;
}
return [(r * 255).toInt().clamp(0, 255),
(g * 255).toInt().clamp(0, 255),
(b * 255).toInt().clamp(0, 255)];
}
/**
* 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 double ref_x = 95.047;
const double ref_y = 100.000;
const double ref_z = 108.883;
double y = (l + 16.0) / 116.0;
double x = a / 500.0 + y;
double z = y - b / 200.0;
double y3 = Math.pow(y, 3);
if (y3 > 0.008856) {
y = y3;
} else {
y = (y - 16 / 116) / 7.787;
}
double x3 = Math.pow(x, 3);
if (x3 > 0.008856) {
x = x3;
} else {
x = (x - 16 / 116) / 7.787;
}
double z3 = Math.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
double R = x * 3.2406 + y * (-1.5372) + z * (-0.4986);
double G = x * (-0.9689) + y * 1.8758 + z * 0.0415;
double B = x * 0.0557 + y * (-0.2040) + z * 1.0570;
if (R > 0.0031308) {
R = 1.055 * (Math.pow(R, 1.0 / 2.4)) - 0.055;
} else {
R = 12.92 * R;
}
if (G > 0.0031308) {
G = 1.055 * (Math.pow(G, 1.0 / 2.4)) - 0.055;
} else {
G = 12.92 * G;
}
if (B > 0.0031308) {
B = 1.055 * (Math.pow(B, 1.0 / 2.4)) - 0.055;
} else {
B = 12.92 * B;
}
return [(R * 255.0).toInt().clamp(0, 255),
(G * 255.0).toInt().clamp(0, 255),
(B * 255.0).toInt().clamp(0, 255)];
}