--[[ This is a collection of functions for performing color operations, designed to work with the LÖVE game engine. LICENSE is BEER-WARE. # Documentation # Supports following color formats: - `rgb255`: The standard computer representation, with each channel (red, green, blue, alpha) in the [0-255] range. Alpha channel (index 4), can either be explicit or implicit (and taken to be 255.) - `rgb1`: As above, except channels are in the [0-1] range. - `hsl`: Hue-saturation-lightness representation (does not support alpha). Hue channel is in the [0-1] range, instead of the standard [0-2*pi] range. ]] local GAMMA = 2.2 local GAMMA_INV = 1/GAMMA local colors = {} -------------------------------------------------------------------------------- -- Formats local function is_rgb255 (c) -- Assert table with certain length if type(c) ~= 'table' or #c < 3 then return false end -- Check that entries are numbers for i = 1, math.min(#c, 3) do if type(c[i]) ~= 'number' then return false end end -- return true end local is_rgb1 = is_rgb255 local function is_hsl (c) -- Assert table with certain length if type(c) ~= 'table' or #c < 3 then return false end -- Check that entries are numbers for i = 1, math.min(#c, 3) do if type(c[i]) ~= 'number' then return false end end -- return true end -------------------------------------------------------------------------------- -- Conversion local function mult_channels (multiplier, default, color, ...) local r, g, b, a = color, ... if type(color) == 'table' then r, g, b, a = unpack(color) end return { r*multiplier, g*multiplier, b*multiplier, a and a*multiplier or default } end colors.rgb255_to_rgb1 = function (...) return mult_channels(1/255, 1, ...) end colors.rgb1_to_rgb255 = function (...) return mult_channels(255, 255, ...) end colors.rgb_to_hsl = function (color) -- Error check assert(type(color) == 'table' and type(color[1]) == 'number' and type(color[2]) == 'number' and type(color[3]) == 'number') -- Do stuff local r = (color[1] == 255 and 1 or color[1]/256) local g = (color[2] == 255 and 1 or color[2]/256) local b = (color[3] == 255 and 1 or color[3]/256) local max, min = math.max(r, g, b), math.min(r, g, b) if min == max then return { 0, 0, (max+min)/2 } end local h, s, l = (max+min)/2, (max+min)/2, (max+min)/2 local d = max - min s = l > 0.5 and d / (2 - max - min) or d / (max + min) if max == r then h = ((g - b) / d + (g < b and 6 or 0)) / 6 elseif max == g then h = ((b - r) / d + 2) / 6 elseif max == b then h = ((r - g) / d + 4) / 6 end return {h, s, l} end local function hue_to_rgb (p, q, t) assert(type(p) == 'number') assert(type(q) == 'number') assert(type(t) == 'number') if t < 0 then t = t + 1 end if t > 1 then t = t - 1 end if t < 1/6 then return p + (q - p) * 6 * t elseif t < 1/2 then return q elseif t < 2/3 then return p + (q - p) * 6 * (2/3 - t) end return p end colors.hsl_to_rgb = function (color) -- Error check assert(type(color) == 'table' and type(color[1]) == 'number' and type(color[2]) == 'number' and type(color[3]) == 'number') -- Do stuff local h, s, l = color[1], color[2], color[3] local r, g, b if s == 0 then r, g, b = l, l, l else local q = l < 0.5 and l * (1+s) or l+s - l*s local p = 2 * l - q r, g, b = hue_to_rgb(p,q,h+1/3), hue_to_rgb(p,q,h), hue_to_rgb(p,q,h-1/3) end r, g, b = math.floor(math.min(255,r*256)+0.5), math.floor(math.min(255,g*256)+0.5), math.floor(math.min(255,b*256)+0.5) return { r, g, b } end colors.hsl_to_rgb1 = function (...) return colors.rgb255_to_rgb1(colors.hsl_to_rgb(...)) end -------------------------------------------------------------------------------- -- Interpolation colors.interpolate_rgb = function (c1, c2, t) assert(type(c1) == 'table' and type(c1[1]) == 'number' and type(c1[2]) == 'number' and type(c1[3]) == 'number' and (type(c1[4]) == 'number' or c1[4] == nil)) assert(type(c2) == 'table' and type(c2[1]) == 'number' and type(c2[2]) == 'number' and type(c2[3]) == 'number' and (type(c2[4]) == 'number' or c2[4] == nil)) assert(type(t) == 'number') -- local alpha = nil if c1[4] or c2[4] then alpha = (c1[4] or 255) + ((c2[4] or 255)-(c1[4] or 255))*t end return { c1[1]+(c2[1]-c1[1])*t, c1[2]+(c2[2]-c1[2])*t, c1[3]+(c2[3]-c1[3])*t, alpha } end colors.interpolate_rgb_gamma_adjusted = function (c1, c2, t, gamma) local gamma_inv = nil if not gamma then gamma, gamma_inv = GAMMA, GAMMA_INV else assert(type(gamma) == 'number' and gamma == gamma and gamma ~= 0) gamma_inv = 1 / gamma end ---- local c1_r, c1_g, c1_b = c1[1]^GAMMA, c1[2]^GAMMA, c1[3]^GAMMA local c2_r, c2_g, c2_b = c2[1]^GAMMA, c2[2]^GAMMA, c2[3]^GAMMA local c3_r, c3_g, c3_b = c1_r+(c2_r-c1_r)*t, c1_g+(c2_g-c1_g)*t, c1_b+(c2_b-c1_b)*t return c3_r^GAMMA_INV, c3_g^GAMMA_INV, c3_b^GAMMA_INV end local function angle_delta (a1, a2) assert(type(a1) == 'number') assert(type(a2) == 'number') local r1, r2 = (a1-a2)%1, (a2-a1)%1 return r1 < r2 and -r1 or r2 end colors.interpolate_hsl = function (c1, c2, t) -- Error handling assert(type(c1) == 'table' and type(c1[1]) == 'number' and type(c1[2]) == 'number' and type(c1[3]) == 'number') assert(type(c2) == 'table' and type(c2[1]) == 'number' and type(c2[2]) == 'number' and type(c2[3]) == 'number') assert(type(t) == 'number') -- Do stuff local h = c1[1] + angle_delta(c1[1], c2[1]) * t local s = c1[2] + (c2[2] - c1[2]) * t local l = c1[3] + (c2[3] - c1[3]) * t return { h, s, l } end colors.interpolate_rgb_by_way_of_hsl = function (c1, c2, t) -- Error handling assert(type(c1) == 'table' and type(c1[1]) == 'number' and type(c1[2]) == 'number' and type(c1[3]) == 'number') assert(type(c2) == 'table' and type(c2[1]) == 'number' and type(c2[2]) == 'number' and type(c2[3]) == 'number') assert(type(t) == 'number') -- Do stuff local c1_hsl = colors.rgb_to_hsl(c1) local c2_hsl = colors.rgb_to_hsl(c2) local c3_hsl = colors.interpolate_hsl(c1_hsl, c2_hsl, t) return colors.hsl_to_rgb(c3_hsl) end -------------------------------------------------------------------------------- -- Inversion colors.invert_rgb = function (color) assert(type(color) == 'table' and type(color[1]) == 'number' and type(color[2]) == 'number' and type(color[3]) == 'number') return { 255 - color[1], 255 - color[2], 255 - color[3] } end -------------------------------------------------------------------------------- -- Parsing colors.parse_rgb = function (str) -- Error handling assert(type(str) == 'string') -- Do stuff local r, g, b local o = (str:sub(1,1) == '#') and 1 or 0 -- Offset if #str == 3 or #str == 4 then r, g, b = str:sub(o+1,o+1):rep(2), str:sub(o+2,o+2):rep(2), str:sub(o+3,o+3):rep(2) else r, g, b = str:sub(o+1,o+2), str:sub(o+3,o+4), str:sub(o+5,o+6) end return {tonumber('0x'..r), tonumber('0x'..g), tonumber('0x'..b)} end -------------------------------------------------------------------------------- return colors