diff --git a/modules/color.lua b/modules/color.lua index 414a451..e78e9e7 100644 --- a/modules/color.lua +++ b/modules/color.lua @@ -2,6 +2,7 @@ -- @module color local modules = (...):gsub('%.[^%.]+$', '') .. "." +local constants = require(modules .. "constants") local utils = require(modules .. "utils") local precond = require(modules .. "_private_precond") local color = {} @@ -10,7 +11,7 @@ local color_mt = {} local function new(r, g, b, a) local c = { r, g, b, a } c._c = c - return setmetatable(c, color) + return setmetatable(c, color_mt) end -- HSV utilities (adapted from http://www.cs.rit.edu/~ncs/color/t_convert.html) @@ -21,7 +22,7 @@ local function hsv_to_color(hsv) local i local f, q, p, t local h, s, v - local a = hsv[4] or 255 + local a = hsv[4] or 1 s = hsv[2] v = hsv[3] @@ -29,10 +30,10 @@ local function hsv_to_color(hsv) return new(v, v, v, a) end - h = hsv[1] / 60 + h = hsv[1] * 6 -- sector 0 to 5 i = math.floor(h) - f = h - i + f = h - i -- factorial part of h p = v * (1-s) q = v * (1-s*f) t = v * (1-s*(1-f)) @@ -52,7 +53,7 @@ local function color_to_hsv(c) local r = c[1] local g = c[2] local b = c[3] - local a = c[4] or 255 + local a = c[4] or 1 local h, s, v local min = math.min(r, g, b) @@ -72,7 +73,12 @@ local function color_to_hsv(c) -- r = g = b = 0 s = 0, v is undefined s = 0 h = -1 - return { h, s, v, 255 } + return { h, s, v, 1 } + end + + -- Prevent division by zero. + if delta == 0 then + delta = constants.DBL_EPSILON end if r == max then @@ -83,10 +89,10 @@ local function color_to_hsv(c) h = 4 + ( r - g ) / delta -- magenta/cyan end - h = h * 60 -- degrees + h = h / 6 -- normalize from segment 0..5 if h < 0 then - h = h + 360 + h = h + 1 end return { h, s, v, a } @@ -94,12 +100,12 @@ end --- The public constructor. -- @param x Can be of three types:
--- number red component 0-255 +-- number red component 0-1 -- table {r, g, b, a} -- nil for {0,0,0,0} --- @tparam number g Green component 0-255 --- @tparam number b Blue component 0-255 --- @tparam number a Alpha component 0-255 +-- @tparam number g Green component 0-1 +-- @tparam number b Blue component 0-1 +-- @tparam number a Alpha component 0-1 -- @treturn color out function color.new(r, g, b, a) -- number, number, number, number @@ -126,17 +132,17 @@ function color.new(r, g, b, a) end --- Convert hue,saturation,value table to color object. --- @tparam table hsva {hue 0-359, saturation 0-1, value 0-1, alpha 0-255} +-- @tparam table hsva {hue 0-1, saturation 0-1, value 0-1, alpha 0-1} -- @treturn color out color.hsv_to_color_table = hsv_to_color --- Convert color to hue,saturation,value table -- @tparam color in --- @treturn table hsva {hue 0-359, saturation 0-1, value 0-1, alpha 0-255} +-- @treturn table hsva {hue 0-1, saturation 0-1, value 0-1, alpha 0-1} color.color_to_hsv_table = color_to_hsv --- Convert hue,saturation,value to color object. --- @tparam number h hue 0-359 +-- @tparam number h hue 0-1 -- @tparam number s saturation 0-1 -- @tparam number v value 0-1 -- @treturn color out @@ -145,10 +151,10 @@ function color.from_hsv(h, s, v) end --- Convert hue,saturation,value to color object. --- @tparam number h hue 0-359 +-- @tparam number h hue 0-1 -- @tparam number s saturation 0-1 -- @tparam number v value 0-1 --- @tparam number a alpha 0-255 +-- @tparam number a alpha 0-1 -- @treturn color out function color.from_hsva(h, s, v, a) return hsv_to_color { h, s, v, a } @@ -158,35 +164,60 @@ end -- @tparam color to invert -- @treturn color out function color.invert(c) - return new(255 - c[1], 255 - c[2], 255 - c[3], c[4]) + return new(1 - c[1], 1 - c[2], 1 - c[3], c[4]) end --- Lighten a color by a component-wise fixed amount (alpha unchanged) -- @tparam color to lighten --- @tparam number amount to increase each component by, 0-255 scale +-- @tparam number amount to increase each component by, 0-1 scale -- @treturn color out function color.lighten(c, v) return new( - utils.clamp(c[1] + v * 255, 0, 255), - utils.clamp(c[2] + v * 255, 0, 255), - utils.clamp(c[3] + v * 255, 0, 255), + utils.clamp(c[1] + v, 0, 1), + utils.clamp(c[2] + v, 0, 1), + utils.clamp(c[3] + v, 0, 1), c[4] ) end +--- Interpolate between two colors. +-- @tparam color at start +-- @tparam color at end +-- @tparam number s in 0-1 progress between the two colors +-- @treturn color out function color.lerp(a, b, s) return a + s * (b - a) end +--- Unpack a color into individual components in 0-1. +-- @tparam color to unpack +-- @treturn number r in 0-1 +-- @treturn number g in 0-1 +-- @treturn number b in 0-1 +-- @treturn number a in 0-1 +function color.unpack(c) + return c[1], c[2], c[3], c[4] +end + +--- Unpack a color into individual components in 0-255. +-- @tparam color to unpack +-- @treturn number r in 0-255 +-- @treturn number g in 0-255 +-- @treturn number b in 0-255 +-- @treturn number a in 0-255 +function color.as_255(c) + return c[1] * 255, c[2] * 255, c[3] * 255, c[4] * 255 +end + --- Darken a color by a component-wise fixed amount (alpha unchanged) -- @tparam color to darken --- @tparam number amount to decrease each component by, 0-255 scale +-- @tparam number amount to decrease each component by, 0-1 scale -- @treturn color out function color.darken(c, v) return new( - utils.clamp(c[1] - v * 255, 0, 255), - utils.clamp(c[2] - v * 255, 0, 255), - utils.clamp(c[3] - v * 255, 0, 255), + utils.clamp(c[1] - v, 0, 1), + utils.clamp(c[2] - v, 0, 1), + utils.clamp(c[3] - v, 0, 1), c[4] ) end @@ -207,7 +238,7 @@ end -- directly set alpha channel -- @tparam color to alter --- @tparam number new alpha 0-255 +-- @tparam number new alpha 0-1 -- @treturn color out function color.alpha(c, v) local t = color.new() @@ -215,7 +246,7 @@ function color.alpha(c, v) t[i] = c[i] end - t[4] = v * 255 + t[4] = v return t end @@ -235,17 +266,17 @@ end --- Set a color's hue (saturation, value, alpha unchanged) -- @tparam color to alter --- @tparam hue to set 0-359 +-- @tparam hue to set 0-1 -- @treturn color out function color.hue(col, hue) local c = color_to_hsv(col) - c[1] = (hue + 360) % 360 + c[1] = (hue + 1) % 1 return hsv_to_color(c) end --- Set a color's saturation (hue, value, alpha unchanged) -- @tparam color to alter --- @tparam hue to set 0-359 +-- @tparam saturation to set 0-1 -- @treturn color out function color.saturation(col, percent) local c = color_to_hsv(col) @@ -255,7 +286,7 @@ end --- Set a color's value (saturation, hue, alpha unchanged) -- @tparam color to alter --- @tparam hue to set 0-359 +-- @tparam value to set 0-1 -- @treturn color out function color.value(col, percent) local c = color_to_hsv(col) @@ -263,7 +294,7 @@ function color.value(col, percent) return hsv_to_color(c) end --- http://en.wikipedia.org/wiki/SRGB#The_reverse_transformation +-- https://en.wikipedia.org/wiki/SRGB#From_sRGB_to_CIE_XYZ function color.gamma_to_linear(r, g, b, a) local function convert(c) if c > 1.0 then @@ -280,17 +311,17 @@ function color.gamma_to_linear(r, g, b, a) if type(r) == "table" then local c = {} for i = 1, 3 do - c[i] = convert(r[i] / 255) * 255 + c[i] = convert(r[i]) end - c[4] = convert(r[4] / 255) * 255 + c[4] = r[4] return c else - return convert(r / 255) * 255, convert(g / 255) * 255, convert(b / 255) * 255, a or 255 + return convert(r), convert(g), convert(b), a or 1 end end --- http://en.wikipedia.org/wiki/SRGB#The_forward_transformation_.28CIE_xyY_or_CIE_XYZ_to_sRGB.29 +-- https://en.wikipedia.org/wiki/SRGB#From_CIE_XYZ_to_sRGB function color.linear_to_gamma(r, g, b, a) local function convert(c) if c > 1.0 then @@ -307,13 +338,13 @@ function color.linear_to_gamma(r, g, b, a) if type(r) == "table" then local c = {} for i = 1, 3 do - c[i] = convert(r[i] / 255) * 255 + c[i] = convert(r[i]) end - c[4] = convert(r[4] / 255) * 255 + c[4] = r[4] return c else - return convert(r / 255) * 255, convert(g / 255) * 255, convert(b / 255) * 255, a or 255 + return convert(r), convert(g), convert(b), a or 1 end end @@ -341,24 +372,7 @@ function color.to_string(a) return string.format("[ %3.0f, %3.0f, %3.0f, %3.0f ]", a[1], a[2], a[3], a[4]) end -function color_mt.__index(t, k) - if type(t) == "cdata" then - if type(k) == "number" then - return t._c[k-1] - end - end - - return rawget(color, k) -end - -function color_mt.__newindex(t, k, v) - if type(t) == "cdata" then - if type(k) == "number" then - t._c[k-1] = v - end - end -end - +color_mt.__index = color color_mt.__tostring = color.to_string function color_mt.__call(_, r, g, b, a) diff --git a/spec/color_spec.lua b/spec/color_spec.lua index 5297ca9..7bbf21f 100644 --- a/spec/color_spec.lua +++ b/spec/color_spec.lua @@ -1,24 +1,184 @@ local color = require "modules.color" +local DBL_EPSILON = require("modules.constants").DBL_EPSILON + +local function assert_is_float_equal(a, b) + if math.abs(a - b) > DBL_EPSILON then + assert.is.equal(a, b) + end +end + +local function assert_is_approx_equal(a, b) + if math.abs(a - b) > 0.001 then + assert.is.equal(a, b) + end +end + describe("color:", function() + it("operators: add, subract, multiply", function() + local c = color(1, 1, 1, 1) + assert.is_true(c:is_color()) + local r = c + c + assert.is_true(r:is_color()) + assert_is_float_equal(r[1], 2) + assert_is_float_equal(r[2], 2) + assert_is_float_equal(r[3], 2) + r = c - c + assert.is_true(r:is_color()) + assert_is_float_equal(r[1], 0) + assert_is_float_equal(r[2], 0) + assert_is_float_equal(r[3], 0) + r = c * 5 + assert.is_true(r:is_color()) + assert_is_float_equal(r[1], 5) + assert_is_float_equal(r[2], 5) + assert_is_float_equal(r[3], 5) + end) + + it("rgb -> hsv -> rgb", function() + local c = color(1,1,1,1) + local hsv = c:color_to_hsv_table() + local c1 = color.hsv_to_color_table(hsv) + local c2 = color.from_hsva(hsv[1], hsv[2], hsv[3], hsv[4]) + local c3 = color.from_hsv(hsv[1], hsv[2], hsv[3]) + c3[4] = c[4] + for i=1,4 do + assert_is_float_equal(c[i], c1[i]) + assert_is_float_equal(c[i], c2[i]) + assert_is_float_equal(c[i], c3[i]) + end + assert.is_true(c:is_color()) + assert.is_true(c1:is_color()) + assert.is_true(c2:is_color()) + assert.is_true(c3:is_color()) + end) + + it("hsv -> rgb -> hsv", function() + local hsv1 = { 0, 0.3, 0.8, 0.9 } + for h=0,1, 0.1 do + hsv1[1] = h + local cc = color.hsv_to_color_table(hsv1) + local hsv2 = cc:color_to_hsv_table() + for i=1,4 do + assert_is_approx_equal(hsv1[i], hsv2[i]) + end + end + end) + + it("unpack", function() + local c = color(122/255, 20/255, 122/255, 255/255) + local r, g, b, a = c:unpack() + assert_is_float_equal(c[1], r) + assert_is_float_equal(c[2], g) + assert_is_float_equal(c[3], b) + assert_is_float_equal(c[4], a) + r, g, b, a = c:as_255() + assert_is_float_equal(122, r) + assert_is_float_equal(20, g) + assert_is_float_equal(122, b) + assert_is_float_equal(255, a) + end) + + it("set hsv", function() + -- hsv value conversion values from http://colorizer.org/ + local c = color(122/255, 20/255, 122/255, 1) + local hsv = c:color_to_hsv_table() + assert_is_approx_equal(hsv[1], 300/360) + assert_is_approx_equal(hsv[2], 0.8361) + assert_is_approx_equal(hsv[3], 0.4784) + local r = c:hue(200/360) + assert_is_approx_equal(r[1], 20/255) + assert_is_approx_equal(r[2], 88/255) + assert_is_approx_equal(r[3], 122/255) + r = c:saturation(0.2) + assert_is_approx_equal(r[1], 122/255) + assert_is_approx_equal(r[2], 97.6/255) + assert_is_approx_equal(r[3], 122/255) + r = c:value(0.2) + assert_is_approx_equal(r[1], 51/255) + assert_is_approx_equal(r[2], 8.36/255) + assert_is_approx_equal(r[3], 51/255) + end) + + it("lighten a color", function() + local c = color(0, 0, 0, 0) + local r = c:lighten(0.1) + assert.is.equal(r[1], 0.1) + r = c:lighten(1000) + assert.is.equal(r[1], 1) + end) + + it("darken a color", function() + local c = color(1, 1, 1, 1) + local r = c:darken(0.04) + assert.is.equal(r[1], 0.96) + r = c:darken(1000) + assert.is.equal(r[1], 0) + end) + + it("multiply a color by a scalar", function() + local c = color(1, 1, 1, 1) + local r = c:multiply(0.04) + assert.is.equal(r[1], 0.04) + + r = c:multiply(0) + for i=1,3 do + assert.is.equal(0, r[i]) + end + assert.is.equal(1, r[4]) + end) + + it("modify alpha", function() + local c = color(1, 1, 1, 1) + local r = c:alpha(0.1) + assert.is.equal(r[4], 0.1) + r = c:opacity(0.5) + assert.is.equal(r[4], 0.5) + r = c:opacity(0.5) + :opacity(0.5) + assert.is.equal(r[4], 0.25) + end) + + it("invert", function() + local c = color(1, 0.6, 0.25, 1) + local r = c:invert() + assert_is_float_equal(r[1], 0) + assert_is_float_equal(r[2], 0.4) + assert_is_float_equal(r[3], 0.75) + assert_is_float_equal(r[4], 1) + r = c:invert() + :invert() + for i=1,4 do + assert.is.equal(c[i], r[i]) + end + end) + + it("lerp", function() + local a = color(1, 0.6, 0.25, 1) + local b = color(1, 0.8, 0.75, 0.5) + local r = a:lerp(b, 0.5) + assert_is_float_equal(r[1], 1) + assert_is_float_equal(r[2], 0.7) + assert_is_float_equal(r[3], 0.5) + assert_is_float_equal(r[4], 0.75) + local r_a = a:lerp(b, 0) + local r_b = a:lerp(b, 1) + for i=1,4 do + assert.is.equal(a[i], r_a[i]) + assert.is.equal(b[i], r_b[i]) + end + end) + + it("linear_to_gamma -> gamma_to_linear round trip", function() + local c = color(0.25, 0.25, 0.25, 1) + local r = color.gamma_to_linear(c:linear_to_gamma()) + for i=1,4 do + assert_is_approx_equal(c[i], r[i]) + end + end) + end) --[[ -new(r, g, b, a) -from_hsv(h, s, v) -from_hsva(h, s, v, a) -invert(c) -lighten(c, v) -lerp(a, b, s) -darken(c, v) -multiply(c, v) -alpha(c, v) -opacity(c, v) -hue(color, hue) -saturation(color, percent) -value(color, percent) -gamma_to_linear(r, g, b, a) -linear_to_gamma(r, g, b, a) -is_color(a) to_string(a) ---]] \ No newline at end of file +--]]