Packages

A comprehensive color library: 21 color spaces, chromatic adaptation, ICC rendering intents, ΔE2000 / WCAG / APCA contrast, gamut mapping, color mixing and gradients, blend modes, color harmonies, color temperature, spectral pipeline, and a full CSS Color 4 / 5 parser. Zero runtime dependencies.

Retired package: Deprecated

Current section

Files

Jump to
color lib color.ex
Raw

lib/color.ex

defmodule Color do
@named_colors %{
"aliceblue" => "f0f8ff",
"antiquewhite" => "faebd7",
"aqua" => "00ffff",
"aquamarine" => "7fffd4",
"azure" => "f0ffff",
"beige" => "f5f5dc",
"bisque" => "ffe4c4",
"rebeccapurple" => "663399",
"beccapurple" => "663399",
"black" => "000000",
"blanchedalmond" => "ffebcd",
"blue" => "0000ff",
"blueviolet" => "8a2be2",
"brown" => "a52a2a",
"burlywood" => "deb887",
"cadetblue" => "5f9ea0",
"carnation" => "ff5ed0",
"cayenne" => "8d0000",
"chartreuse" => "7fff00",
"chocolate" => "d2691e",
"coral" => "ff7f50",
"cornflowerblue" => "6495ed",
"Cornsilk" => "fff8dc",
"Crimson" => "dc143c",
"Cyan" => "00ffff",
"DarkBlue" => "00008b",
"DarkCyan" => "008b8b",
"DarkGoldenRod" => "b8860b",
"DarkGray" => "a9a9a9",
"DarkGrey" => "a9a9a9",
"DarkGreen" => "006400",
"DarkKhaki" => "bdb76b",
"DarkMagenta" => "8b008b",
"DarkOliveGree" => "556b2f",
"DarkOrange" => "ff8c00",
"DarkOrchid" => "9932cc",
"DarkRed" => "8b0000",
"DarkSalmon" => "e9967a",
"DarkSeaGreen" => "8fbc8f",
"DarkSlateBlue" => "483d8b",
"DarkSlateGray" => "2f4f4f",
"DarkSlateGrey" => "2f4f4f",
"DarkTurquoise" => "00ced1",
"DarkViolet" => "9400d3",
"Darkorange" => "ff8c00",
"DeepPink" => "ff1493",
"DeepSkyBlue" => "00bfbf",
"DimGray" => "696969",
"DimGrey" => "696969",
"DodgerBlue" => "1e90ff",
"Feldspar" => "d19275",
"FireBrick" => "b22222",
"FloralWhite" => "fffaf0",
"ForestGreen" => "228b22",
"Fuchsia" => "ff00ff",
"Gainsboro" => "dcdcdc",
"GhostWhite" => "f8f8ff",
"Gold" => "ffd700",
"GoldenRod" => "daa520",
"Gray" => "808080",
"Grey" => "808080",
"Green" => "008000",
"GreenYellow" => "adff2f",
"HoneyDew" => "f0fff0",
"HotPink" => "ff69b4",
"IndianRed" => "cd5c5c",
"Indigo" => "4b0082",
"Ivory" => "fffff0",
"Khaki" => "f0e68c",
"Lavender" => "e6e6fa",
"LavenderBlush" => "fff0f5",
"LawnGreen" => "7cfc00",
"LemonChiffon" => "fffacd",
"LightBlue" => "add8e6",
"LightCoral" => "f08080",
"LightCyan" => "e0ffff",
"LightGoldenRodYellow" => "fafad2",
"LightGray" => "d3d3d3",
"LightGrey" => "d3d3d3",
"LightGreen" => "90ee90",
"LightPink" => "ffb6c1",
"LightSalmon" => "ffa07a",
"LightSeaGreen" => "20b2aa",
"LightSkyBlue" => "87cefa",
"LightSlateBlue" => "8470ff",
"LightSlateGray" => "778899",
"LightSlateGrey" => "778899",
"LightSteelBlue" => "b0c4de",
"LightYellow" => "ffffe0",
"Lime" => "00ff00",
"LimeGreen" => "32cd32",
"Linen" => "faf0e6",
"Magenta" => "ff00ff",
"Maroon" => "800000",
"MediumAquaMarine" => "66cdaa",
"MediumBlue" => "0000cd",
"MediumOrchid" => "ba55d3",
"MediumPurple" => "9370db",
"MediumSeaGreen" => "3cb371",
"MediumSlateBlue" => "7b68ee",
"MediumSpringGreen" => "00fa9a",
"MediumTurquoise" => "48d1cc",
"MediumVioletRed" => "c71585",
"MidnightBlue" => "191970",
"MintCream" => "f5fffa",
"MistyRose" => "ffe4e1",
"Moccasin" => "ffe4b5",
"NavajoWhite" => "ffdead",
"Navy" => "000080",
"OldLace" => "fdf5e6",
"Olive" => "808000",
"OliveDrab" => "6b8e23",
"Orange" => "ffa500",
"OrangeRed" => "ff4500",
"Orchid" => "da70d6",
"PaleGoldenRod" => "eee8aa",
"PaleGreen" => "98fb98",
"PaleTurquoise" => "afeeee",
"PaleVioletRed" => "db7093",
"PapayaWhip" => "ffefd5",
"PeachPuff" => "ffdab9",
"Peru" => "cd853f",
"Pink" => "ffc0cb",
"Plum" => "dda0dd",
"PowderBlue" => "b0e0e6",
"Purple" => "800080",
"Red" => "ff0000",
"RosyBrown" => "bc8f8f",
"RoyalBlue" => "4169e1",
"SaddleBrown" => "8b4513",
"Salmon" => "fa8072",
"SandyBrown" => "f4a460",
"SeaGreen" => "2e8b57",
"SeaShell" => "fff5ee",
"Sienna" => "a0522d",
"Silver" => "c0c0c0",
"SkyBlue" => "87ceeb",
"SlateBlue" => "6a5acd",
"SlateGray" => "708090",
"SlateGrey" => "708090",
"Snow" => "fffafa",
"SpringGreen" => "00ff7f",
"SteelBlue" => "4682b4",
"Tan" => "d2b48c",
"Teal" => "008080",
"Thistle" => "d8bfd8",
"Tomato" => "ff6347",
"Turquoise" => "40e0d0",
"Violet" => "ee82ee",
"VioletRed" => "d02090",
"Wheat" => "f5deb3",
"White" => "ffffff",
"WhiteSmoke" => "f5f5f5",
"Yellow" => "ffff00",
"YellowGreen" => "9acd32"
}
def name_to_hex(name) do
@named_colors[name]
end
def hex_to_lab(hex) do
hex |> hex_to_rgb |> rgb_to_lab
end
def hex_to_rgb(hex) do
case hex |> String.replace("#", "") |> String.graphemes do
[r, g, b] ->
%Color.RGB{r: Integer.parse(r <> r, 16),
g: Integer.parse(g <> g, 16),
b: Integer.parse(b <> b, 16)}
[r_1, r_2, g_1, g_2, b_1, b_2] ->
%Color.RGB{r: Integer.parse(r_1 <> r_2, 16),
g: Integer.parse(g_1 <> g_2, 16),
b: Integer.parse(b_1 <> b_2, 16)}
end
end
def rgb_to_lab(%Color.RGB{r: r, g: g, b: b} = color) do
color |> rgb_to_xyz |> xyz_to_lab
end
def rgb_to_xyz(%Color.RGB{r: r, g: g, b: b}) do
# Inverse sRGB companding. Linearizes RGB channels with respect to
# energy.
[r, g, b] = Enum.map([r, g, b], fn v ->
if v > 0.04045 do
:math.pow((v + 0.055) / 1.055, 2.4) * 100
else
(v / 12.92) * 100
end
end)
# Convert using the RGB/XYZ matrix at:
# http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html#WSMatrices
%Color.XYZ{x: (r * 0.4124564 + g * 0.3575761 + b * 0.1804375),
y: (r * 0.2126729 + g * 0.7151522 + b * 0.0721750),
z: (r * 0.0193339 + g * 0.1191920 + b * 0.9503041)}
end
def xyz_to_lab(%Color.XYZ{x: x, y: y, z: z}) do
# Calculate the ratio of the XYZ values to the reference white.
# http://www.brucelindbloom.com/index.html?Equations.html
xr = x / 95.047
yr = y / 100.00
zr = z / 108.883
# NOTE: This should be using Rational instead of floating point values,
# otherwise there will be discontinuities.
# http://www.brucelindbloom.com/LContinuity.html
epsilon = 216 / 24389.0
kappa = 24389 / 27.0
# And now transform
# http://en.wikipedia.org/wiki/Lab_color_space#Forward_transformation
# There is a brief explanation there as far as the nature of the calculations,
# as well as a much nicer looking modeling of the algebra.
[fx, fy, fz] = Enum.map([xr, yr, zr], fn t ->
if t > epsilon do
:math.pow(t, 1.0 / 3)
else # t <= epsilon
((kappa * t) + 16) / 116.0
# The 4/29 here is for when t = 0 (black). 4/29 * 116 = 16, and 16 -
# 16 = 0, which is the correct value for L* with black.
# ((1.0/3)*((29.0/6)**2) * t) + (4.0/29)
end
end)
%Color.Lab{L: (116 * fy) - 16,
a: 500 * (fx - fy),
b: 200 * (fy - fz)}
end
@doc """
The Delta E (CIE94) algorithm
http://en.wikipedia.org/wiki/Color_difference#CIE94
There is a newer version, CIEDE2000, that uses slightly more complicated math, but addresses
"the perceptual uniformity issue" left lingering by the CIE94 algorithm. color_1 and color_2 are
both L*a*b* maps, rendered by hex_to_lab/1.
Since our source is treated as sRGB, we use the "graphic arts" presets
for k_L, k_1, and k_2
The calculations go through LCH(ab).
delta_E = :math.sqrt(
((delta_L / (k_L * s_L)) ** 2) +
((delta_C / (k_C * s_C)) ** 2) +
((delta_H / (k_H * s_H)) ** 2)
)
Under some circumstances in real computers, delta_H could be an
imaginary number (it's a square root value), so we're going to treat
this as:
delta_E = :math.sqrt(
((delta_L / (k_L * s_L)) ** 2) +
((delta_C / (k_C * s_C)) ** 2) +
(delta_H2 / ((k_H * s_H) ** 2)))
)
And not perform the square root when calculating delta_H2.
See also http://www.brucelindbloom.com/index.html?Eqn_DeltaE_CIE94.html
"""
def delta_e94(%Color.RGB{} = color_1, color_2), do: delta_e94(color_1 |> rgb_to_lab, color_2)
def delta_e94(%Color.XYZ{} = color_1, color_2), do: delta_e94(color_1 |> xyz_to_lab, color_2)
def delta_e94(%Color.Lab{} = color_1, %Color.RGB{} = color_2),
do: delta_e94(color_1, color_2 |> rgb_to_lab)
def delta_e94(%Color.Lab{} = color_1, %Color.XYZ{} = color_2),
do: delta_e94(color_1, color_2 |> xyz_to_lab)
def delta_e94(%Color.Lab{L: l_1, a: a_1, b: b_1}, %Color.Lab{L: l_2, a: a_2, b: b_2}) do
k_1 = 0.045
k_2 = 0.015
k_L = 1
k_C = k_H = 1
delta_a = a_1 - a_2
delta_b = b_1 - b_2
c_1 = :math.sqrt(:math.pow(a_1, 2) + :math.pow(b_1, 2))
c_2 = :math.sqrt(:math.pow(a_2, 2) + :math.pow(b_2, 2))
delta_L = l_1 - l_2
delta_C = c_1 - c_2
delta_H2 = :math.pow(delta_a, 2) + :math.pow(delta_b, 2) - :math.pow(delta_C, 2)
s_L = 1
s_C = 1 + k_1 * c_1
s_H = 1 + k_2 * c_1
composite_L = :math.pow(delta_L / (k_L * s_L), 2)
composite_C = :math.pow(delta_C / (k_C * s_C), 2)
composite_H = delta_H2 / :math.pow(k_H * s_H, 2)
:math.sqrt(composite_L + composite_C + composite_H)
end
def delta_e94(color_1, color_2) do
delta_e94(name_to_hex(String.downcase(color_1)) || color_1 |> hex_to_lab,
name_to_hex(String.downcase(color_2)) || color_2 |> hex_to_lab)
end
end