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