From ecb2c9b8c9d9d2f639a00607fecdb7b3fc23b431 Mon Sep 17 00:00:00 2001 From: Tyler Miller Date: Tue, 16 Jul 2024 21:11:21 -0700 Subject: [PATCH] refactor(Color): improve `Color` lib (types, etc.) - Fixup types and doc-comment descriptions. The `Color` module and `Color` instances are now well-typed providing LSP completion and type-hinting. The `Color` type has been renamed and namspaced under `GhTheme.*`. Types provided by the `Color` module are namespaced under `GhTheme.Color.*`. The reason for the namespacing is because the Lua LSP considers all defined types to be global. - Remove duplicated code in `__call` and `new()`. - Throw an error if the argument given to the `Color` constructor is of invalid type. Previously there was no error and it just returned `nil`. --- CHANGELOG.md | 5 +- lua/github-theme/lib/color.lua | 298 ++++++++++++++++++----------- lua/github-theme/lib/highlight.lua | 4 +- test/github-theme/color_spec.lua | 143 +++++++++----- 4 files changed, 285 insertions(+), 165 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 087a1da4..1c4f0dcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,7 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improved highlight-group overrides (#349) - Assigning `false` or an empty table to a highlight group clears it - Assigning `false` to groups/specs/palettes clears previous settings from the config store -- Loading/sourcing colorscheme now causes recompilation if config or overrides changed, even if `setup()` has been called before +- Loading/sourcing colorscheme now causes recompilation if config or overrides changed, even if `setup()` was called before +- Refactored and improved `Color` lib (LSP types and descriptions, code-dedupe, stricter ctor, etc.) (#352) ### Changes @@ -27,7 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Closed #292 (no longer valid, fixed) - fix(config): `options.darken.floats` is not used (#345) - fix(compiler): consider entire config when hashing (#350) (related-to #262, #340, #341) -- fix(compiler): always write hash to filesystem when compilation occurs incl. when `require('github-theme').compile()` is called directly (#350) +- fix(compiler): always write hash to fs when compile occurs incl. when `require('github-theme').compile()` is called directly (#350) - Fixed #340 and #341 (broken/outdated `overrides` example in docs) - Fixed floats not transparent when `transparent = true` (#337 fixed-by #351) - fix(Color): `Color.from_hsv()` is used for HSL diff --git a/lua/github-theme/lib/color.lua b/lua/github-theme/lib/color.lua index fe58a4e3..eadbbb86 100755 --- a/lua/github-theme/lib/color.lua +++ b/lua/github-theme/lib/color.lua @@ -1,14 +1,25 @@ ----Round float to nearest int ----@param x number Float +---@param expected string +---@param actual any +---@param lvl? number +---@return any never +local function throw(expected, actual, lvl) + error(('expected %s but got: %s'):format(expected, vim.inspect(actual)), (lvl or 1) + 1) +end + +---Round a float to the nearest integer. +---@param f number Float ---@return number -local function round(x) - return x >= 0 and math.floor(x + 0.5) or math.ceil(x - 0.5) +---@nodiscard +local function round(f) + return f >= 0 and math.floor(f + 0.5) or math.ceil(f - 0.5) end ----Clamp value between the min and max values. +---Clamp a `value` between `min` and `max`. ---@param value number ---@param min number ---@param max number +---@return number +---@nodiscard local function clamp(value, min, max) if value < min then return min @@ -21,18 +32,18 @@ end --#region Types ---------------------------------------------------------------- ---RGBA color representation stored in float [0,1] ----@class RGBA +---@class GhTheme.Color.RGBA ---@field red number [0,255] ---@field green number [0,255] ---@field blue number [0,255] ---@field alpha number [0,1] ----@class HSL +---@class GhTheme.Color.HSL ---@field hue number Float [0,360) ---@field saturation number Float [0,100] ---@field lightness number Float [0,100] ----@class HSV +---@class GhTheme.Color.HSV ---@field hue number Float [0,360) ---@field saturation number Float [0,100] ---@field value number Float [0,100] @@ -40,8 +51,12 @@ end --#endregion --#region Helpers -------------------------------------------------------------- -local bitop = bit or bit32 +local bitop = _G.bit or _G.bit32 +---@param r number +---@param g number +---@param b number +---@nodiscard local function calc_hue(r, g, b) local max = math.max(r, g, b) local min = math.min(r, g, b) @@ -67,30 +82,60 @@ end --#endregion -local Color = setmetatable({}, {}) -Color.__index = Color - -function Color.__tostring(self) +---@alias GhTheme.Color.CSSHexString string # A CSS hex color string (i.e. `"#RRGGBB[AA]"`) +---@alias GhTheme.ColorDef GhTheme.Color.CSSHexString|number|GhTheme.Color.RGBA|GhTheme.Color.HSV|GhTheme.Color.HSL + +---Color instance +---@class GhTheme.Color +---@field WHITE GhTheme.Color +---@field BLACK GhTheme.Color +---@field BG GhTheme.Color +local Color = {} +rawset(Color, '__index', Color) +rawset(Color, '__tostring', function(self) return self:to_css() -end - -function Color.new(opts) - if type(opts) == 'string' or type(opts) == 'number' then - return Color.from_hex(opts) - end - if opts.red then - return Color.from_rgba(opts.red, opts.green, opts.blue, opts.alpha) - end - if opts.value then - return Color.from_hsv(opts.hue, opts.saturation, opts.value) - end - if opts.lightness then - return Color.from_hsl(opts.hue, opts.saturation, opts.lightness) +end) + +---Color class +---@class GhTheme.Color.Static +---@overload fun(color: GhTheme.ColorDef): GhTheme.Color +local M = setmetatable(Color --[[@as GhTheme.Color.Static]], { + __call = function(self, ...) + return self.new(...) + end, +}) + +---@param color GhTheme.ColorDef +---@return GhTheme.Color +---@nodiscard +function M.new(color) + local ty = type(color) + + if ty == 'string' or ty == 'number' then + return M.from_hex(color) + elseif ty == 'table' then + if color.red then + return M.from_rgba(color.red, color.green, color.blue, color.alpha) + elseif color.value then + return M.from_hsv(color.hue, color.saturation, color.value) + elseif color.lightness then + return M.from_hsl(color.hue, color.saturation, color.lightness) + end end + + return throw('color specification', color) end -function Color.init(r, g, b, a) - local self = setmetatable({}, Color) +---@private +---@param r number +---@param g number +---@param b number +---@param a? number +---@return GhTheme.Color +---@nodiscard +function M.init(r, g, b, a) + ---@class GhTheme.Color + local self = setmetatable({}, M --[[@as table]]) self.red = clamp(r, 0, 1) self.green = clamp(g, 0, 1) self.blue = clamp(b, 0, 1) @@ -100,30 +145,36 @@ end --#region from_* --------------------------------------------------------------- ----Create color from RGBA 0,255 +---Construct a Color instance from RGB[A] (0 - 255). ---@param r number Integer [0,255] ---@param g number Integer [0,255] ---@param b number Integer [0,255] ----@param a number Float [0,1] ----@return Color -function Color.from_rgba(r, g, b, a) - return Color.init(r / 0xff, g / 0xff, b / 0xff, a or 1) +---@param a? number Float [0,1] +---@return GhTheme.Color +---@nodiscard +function M.from_rgba(r, g, b, a) + return M.init(r / 0xff, g / 0xff, b / 0xff, a or 1) end ----Create a color from a hex number ----@param c number|string Either a literal number or a css-style hex string ('#RRGGBB[AA]') ----@return Color -function Color.from_hex(c) +---Construct a Color instance from a hex string or number. +---@param c number|GhTheme.Color.CSSHexString number or CSS hex string (i.e. '"#RRGGBB[AA]"') +---@return GhTheme.Color +---@nodiscard +function M.from_hex(c) local n = c + if type(c) == 'string' then - local s = c:lower():match('#?([a-f0-9]+)') + local s = c:lower():match('#?([a-f0-9]+)') or throw('number or hex string', c) n = tonumber(s, 16) if #s <= 6 then n = bitop.lshift(n, 8) + 0xff end + elseif type(c) ~= 'number' then + throw('number or hex string', c) end - return Color.init( + ---@cast n -string + return M.init( bitop.rshift(n, 24) / 0xff, bitop.band(bitop.rshift(n, 16), 0xff) / 0xff, bitop.band(bitop.rshift(n, 8), 0xff) / 0xff, @@ -131,53 +182,60 @@ function Color.from_hex(c) ) end ----Create a Color from HSV value +---Construct a Color instance from HSV[A]. ---@param h number Hue. Float [0,360] ---@param s number Saturation. Float [0,100] ---@param v number Value. Float [0,100] ---@param a number? (Optional) Alpha. Float [0,1] ----@return Color -function Color.from_hsv(h, s, v, a) +---@return GhTheme.Color +---@nodiscard +function M.from_hsv(h, s, v, a) h = h % 360 s = clamp(s, 0, 100) / 100 v = clamp(v, 0, 100) / 100 a = clamp(a or 1, 0, 1) + ---@param n number + ---@return number local function f(n) local k = (n + h / 60) % 6 return v - v * s * math.max(math.min(k, 4 - k, 1), 0) end - return Color.init(f(5), f(3), f(1), a) + return M.init(f(5), f(3), f(1), a) end ----Create a Color from HSL value +---Construct a Color instance from HSL[A]. ---@param h number Hue. Float [0,360] ---@param s number Saturation. Float [0,100] ---@param l number Lightness. Float [0,100] ---@param a number? (Optional) Alpha. Float [0,1] ----@return Color -function Color.from_hsl(h, s, l, a) +---@return GhTheme.Color +---@nodiscard +function M.from_hsl(h, s, l, a) h = h % 360 s = clamp(s, 0, 100) / 100 l = clamp(l, 0, 100) / 100 a = clamp(a or 1, 0, 1) local _a = s * math.min(l, 1 - l) + ---@param n number + ---@return number local function f(n) local k = (n + h / 30) % 12 return l - _a * math.max(math.min(k - 3, 9 - k, 1), -1) end - return Color.init(f(0), f(8), f(4), a) + return M.init(f(0), f(8), f(4), a) end --#endregion --#region to_* ----------------------------------------------------------------- ----Convert Color to RGBA ----@return RGBA +---Convert to RGBA table. +---@return GhTheme.Color.RGBA +---@nodiscard function Color:to_rgba() return { red = round(self.red * 0xff), @@ -187,8 +245,9 @@ function Color:to_rgba() } end ----Convert Color to HSV ----@return HSV +---Convert to HSV table. +---@return GhTheme.Color.HSV +---@nodiscard function Color:to_hsv() local res = calc_hue(self.red, self.green, self.blue) local h, min, max = res.hue, res.min, res.max @@ -201,8 +260,9 @@ function Color:to_hsv() return { hue = h, saturation = s * 100, value = v * 100 } end ----Convert the color to HSL. ----@return HSL +---Convert to HSL table. +---@return GhTheme.Color.HSL +---@nodiscard function Color:to_hsl() local res = calc_hue(self.red, self.green, self.blue) local h, min, max = res.hue, res.min, res.max @@ -215,9 +275,10 @@ function Color:to_hsl() return { hue = h, saturation = s * 100, lightness = l * 100 } end ----Convert the color to a hex number representation (`0xRRGGBB[AA]`). ----@param with_alpha boolean Include the alpha component. +---Convert to a hex number representation (`0xRRGGBB[AA]`). +---@param with_alpha? boolean Include the alpha component. ---@return integer +---@nodiscard function Color:to_hex(with_alpha) local ls, bor, fl = bitop.lshift, bitop.bor, math.floor local n = bor( @@ -227,18 +288,27 @@ function Color:to_hex(with_alpha) return with_alpha and bitop.lshift(n, 8) + (self.alpha * 0xff) or n end ----Convert the color to a css hex color (`#RRGGBB[AA]`). ----@param with_alpha boolean Include the alpha component. ----@return string +---Convert to a css hex color string (i.e. `"#RRGGBB[AA]"`). +---@param with_alpha? boolean Include the alpha component. +---@return GhTheme.Color.CSSHexString +---@nodiscard function Color:to_css(with_alpha) local n = self:to_hex(with_alpha) local l = with_alpha and 8 or 6 return string.format('#%0' .. l .. 'x', n) end ----Calculate the relative luminance of the color +---Convert to a css hex color string (i.e. `"#RRGGBB"`). +---@return GhTheme.Color.CSSHexString +---@nodiscard +function Color:to_string() + return tostring(self) +end + +---Returns the relative luminance. ---https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef ---@return number +---@nodiscard function Color:luminance() local r, g, b = self.red, self.green, self.blue r = (r > 0.04045) and ((r + 0.055) / 1.055) ^ 2.4 or (r / 12.92) @@ -252,12 +322,13 @@ end --#region Manipulate ----------------------------------------------------------- ----Returns a new color that a linear blend between two colors ----@param other Color ----@param f number Float [0,1]. 0 being this and 1 being other ----@return Color +---Returns a new Color that is a linear blend between `self` and `other`. +---@param other GhTheme.Color +---@param f number Float [0,1] where 0 is `self` and 1 is `other` +---@return GhTheme.Color +---@nodiscard function Color:blend(other, f) - return Color.init( + return M.init( (other.red - self.red) * f + self.red, (other.green - self.green) * f + self.green, (other.blue - self.blue) * f + self.blue, @@ -265,26 +336,28 @@ function Color:blend(other, f) ) end ----Returns a new color that a linear blend between Background color ----@param f number Float [0,1]. 0 being this and 1 being other ----@return Color +---Returns a new Color that is a linear blend between `Color.BG` and `self`. +---@param f number Float [0,1] where 0 is `Color.BG` and 1 is `self` +---@return GhTheme.Color +---@nodiscard function Color:alpha_blend(f) - return Color.init( - (self.red - Color.BG.red) * f + Color.BG.red, - (self.green - Color.BG.green) * f + Color.BG.green, - (self.blue - Color.BG.blue) * f + Color.BG.blue, + return M.init( + (self.red - self.BG.red) * f + self.BG.red, + (self.green - self.BG.green) * f + self.BG.green, + (self.blue - self.BG.blue) * f + self.BG.blue, self.alpha ) end ----Returns a new shaded color. ----@param f number Amount. Float [-1,1]. -1 is black, 1 is white ----@return Color +---Returns a new Color which is `self` shaded according to `f`. +---@param f number Amount. Float [-1,1]. -1 is black and 1 is white +---@return GhTheme.Color +---@nodiscard function Color:shade(f) local t = f < 0 and 0 or 1.0 local p = f < 0 and f * -1.0 or f - return Color.init( + return M.init( (t - self.red) * p + self.red, (t - self.green) * p + self.green, (t - self.blue) * p + self.blue, @@ -292,59 +365,66 @@ function Color:shade(f) ) end ----Adds value of `v` to the `value` of the current color. This returns either ----a brighter version if +v and darker if -v. +---Adds value of `v` to the `value` of the current color. Returns a new Color +---that is either a brighter version (v >= 0), or darker (v < 0). ---@param v number Value. Float [-100,100]. ----@return Color +---@return GhTheme.Color +---@nodiscard function Color:brighten(v) local hsv = self:to_hsv() local value = clamp(hsv.value + v, 0, 100) - return Color.from_hsv(hsv.hue, hsv.saturation, value) + return M.from_hsv(hsv.hue, hsv.saturation, value) end ----Adds value of `v` to the `lightness` of the current color. This returns ----either a lighter version if +v and darker if -v. +---Adds value of `v` to the `lightness` of the current color. Returns a new Color +---that is either a lighter version if +v and darker if -v. ---@param v number Lightness. Float [-100,100]. ----@return Color +---@return GhTheme.Color +---@nodiscard function Color:lighten(v) local hsl = self:to_hsl() local lightness = clamp(hsl.lightness + v, 0, 100) - return Color.from_hsl(hsl.hue, hsl.saturation, lightness) + return M.from_hsl(hsl.hue, hsl.saturation, lightness) end ----Adds value of `v` to the `saturation` of the current color. This returns ----either a more or less saturated version depending of +/- v. +---Adds value of `v` to the `saturation` of the current color. Returns a new Color +---that is either more or less saturated depending on +/- `v`. ---@param v number Saturation. Float [-100,100]. ----@return Color +---@return GhTheme.Color +---@nodiscard function Color:saturate(v) local hsv = self:to_hsv() local saturation = clamp(hsv.saturation + v, 0, 100) - return Color.from_hsv(hsv.hue, saturation, hsv.value) + return M.from_hsv(hsv.hue, saturation, hsv.value) end ----Adds value of `v` to the `hue` of the current color. This returns a rotation of ----hue based on +/- of v. Resulting `hue` is wrapped [0,360] ----@return Color +---Adds value of `v` to the `hue` of the current color. Returns a new Color where +---the hue is rotated based on +/- of `v`. Resulting `hue` is wrapped [0,360]. +---@param v number amount +---@return GhTheme.Color +---@nodiscard function Color:rotate(v) local hsv = self:to_hsv() local hue = (hsv.hue + v) % 360 - return Color.from_hsv(hue, hsv.saturation, hsv.value) + return M.from_hsv(hue, hsv.saturation, hsv.value) end --#endregion --#region Constants ------------------------------------------------------------ -Color.WHITE = Color.init(1, 1, 1, 1) -Color.BLACK = Color.init(0, 0, 0, 1) -Color.BG = Color.init(0, 0, 0, 1) +M.WHITE = M.init(1, 1, 1, 1) +M.BLACK = M.init(0, 0, 0, 1) +M.BG = M.init(0, 0, 0, 1) --#endregion --#region ty -------------------------------------------------------------- ----Returns the contrast ratio of the other against another ----@param other Color +---Returns the contrast ratio of `self` over `other`. +---@param other GhTheme.Color +---@return number +---@nodiscard function Color:contrast(other) local l1 = self:luminance() local l2 = other:luminance() @@ -354,10 +434,10 @@ function Color:contrast(other) return (l1 + 0.05) / (l2 + 0.05) end ----Check if color passes WCAG AA ----https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html ----@param background Color background to check against +---Returns whether `self` meets the [WCAG Contrast (Minimum) (Level AA)](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum.html). +---@param background GhTheme.Color background to check against ---@return boolean, number +---@nodiscard function Color:valid_wcag_aa(background) local ratio = self:contrast(background) return ratio >= 4.5, ratio @@ -365,20 +445,4 @@ end --#endregion -local mt = getmetatable(Color) -function mt.__call(_, opts) - if type(opts) == 'string' or type(opts) == 'number' then - return Color.from_hex(opts) - end - if opts.red then - return Color.from_rgba(opts.red, opts.green, opts.blue, opts.alpha) - end - if opts.value then - return Color.from_hsv(opts.hue, opts.saturation, opts.value) - end - if opts.lightness then - return Color.from_hsl(opts.hue, opts.saturation, opts.lightness) - end -end - -return Color +return M diff --git a/lua/github-theme/lib/highlight.lua b/lua/github-theme/lib/highlight.lua index 0bcfdafd..88180aa3 100644 --- a/lua/github-theme/lib/highlight.lua +++ b/lua/github-theme/lib/highlight.lua @@ -18,7 +18,7 @@ local M = {} --#endregion ---Validate input input from opts table and return a hex string if opt exists ----@param input string|Color|nil +---@param input string|GhTheme.Color|nil ---@return string local function validate(input) return input and input or 'NONE' @@ -38,7 +38,7 @@ local function parse_style(style) end ---Validate input input from opts table and return a hex string if opt exists ----@param input string|Color|nil +---@param input string|GhTheme.Color|nil ---@return string M.parse_style = parse_style diff --git a/test/github-theme/color_spec.lua b/test/github-theme/color_spec.lua index ce4453ac..fa5d28b3 100644 --- a/test/github-theme/color_spec.lua +++ b/test/github-theme/color_spec.lua @@ -11,66 +11,121 @@ local ex = { } describe('Color', function() - describe('Constructing', function() - it('Should construct from a css hex string', function() - local c = Color.from_hex(ex.str) - assert.are.same(ex.hex, c:to_hex()) - assert.are.same(ex.str, c:to_css()) - end) - - it('Should construct from rgba', function() - local c = Color.from_rgba(unpack(ex.rgba)) - assert.are.same(ex.hex, c:to_hex()) - assert.are.same(ex.str, c:to_css()) - end) - - it('Should construct from hsv', function() - local c = Color.from_hsv(unpack(ex.hsv)) - assert.are.same(ex.hex, c:to_hex()) - assert.are.same(ex.str, c:to_css()) - end) - - it('Should construct from hsl', function() - local c = Color.from_hsl(unpack(ex.hsl)) - assert.are.same(ex.hex, c:to_hex()) - assert.are.same(ex.str, c:to_css()) - end) - end) - - describe('Infer', function() - it('Should infer creation of hex string', function() + describe('constructor', function() + it('should error when given invalid args', function() + assert.has.error(function() + ---@diagnostic disable-next-line: param-type-mismatch + local _ = Color(true) + end) + + assert.has.error(function() + ---@diagnostic disable-next-line: param-type-mismatch + local _ = Color(false) + end) + + assert.has.error(function() + ---@diagnostic disable-next-line: param-type-mismatch + local _ = Color(nil) + end) + + assert.has.error(function() + ---@diagnostic disable-next-line: missing-parameter + local _ = Color() + end) + + assert.has.error(function() + ---@diagnostic disable-next-line: missing-fields + local _ = Color({}) + end) + + assert.has.error(function() + ---@diagnostic disable-next-line: missing-fields + local _ = Color({ val = 0.5 }) + end) + end) + + describe('from_hex()', function() + it('should construct from a css hex string', function() + local c = Color.from_hex(ex.str) + assert.are.same(ex.hex, c:to_hex()) + assert.are.same(ex.str, c:to_css()) + end) + + it('should error when given invalid args', function() + assert.does.match.error(function() + ---@diagnostic disable-next-line: param-type-mismatch + local _ = Color.from_hex('#HIJKLM') + end, '^expected number or hex string') + + assert.does.match.error(function() + ---@diagnostic disable-next-line: param-type-mismatch + local _ = Color.from_hex('') + end, '^expected number or hex string') + + assert.does.match.error(function() + ---@diagnostic disable-next-line: param-type-mismatch + local _ = Color.from_hex({ red = 1, green = 1, blue = 1 }) + end, '^expected number or hex string') + end) + end) + + describe('from_rgba()', function() + it('should construct from rgba', function() + local c = Color.from_rgba(unpack(ex.rgba)) + assert.are.same(ex.hex, c:to_hex()) + assert.are.same(ex.str, c:to_css()) + end) + end) + + describe('from_hsv()', function() + it('should construct from hsv', function() + local c = Color.from_hsv(unpack(ex.hsv)) + assert.are.same(ex.hex, c:to_hex()) + assert.are.same(ex.str, c:to_css()) + end) + end) + + describe('from_hsl()', function() + it('should construct from hsl', function() + local c = Color.from_hsl(unpack(ex.hsl)) + assert.are.same(ex.hex, c:to_hex()) + assert.are.same(ex.str, c:to_css()) + end) + end) + + it('should infer creation of hex string', function() local c = Color(ex.str) assert.are.same(ex.hex, c:to_hex()) assert.are.same(ex.str, c:to_css()) end) - it('Should infer from rgba components', function() + it('should infer from rgba components', function() local c = Color(Color.from_hex(ex.str):to_rgba()) assert.are.same(ex.hex, c:to_hex()) assert.are.same(ex.str, c:to_css()) end) - it('Should infer from hsv components', function() + it('should infer from hsv components', function() local c = Color(Color.from_hex(ex.str):to_hsv()) assert.are.same(ex.hex, c:to_hex()) assert.are.same(ex.str, c:to_css()) end) - it('Should infer from hsl components', function() + it('should infer from hsl components', function() local c = Color(Color.from_hex(ex.str):to_hsl()) -- assert.are.same(ex.hex, c:to_hex()) assert.are.same(ex.str, c:to_css()) end) end) - describe('Converting', function() - it('Should output to css_hex', function() + describe('conversion', function() + it('should output to css_hex', function() local c = Color.from_hex(ex.str) assert.are.same(ex.str, c:to_css()) assert.are.same(ex.str .. 'ff', c:to_css(true)) end) - it('Should output to rgba', function() + it('should output to rgba', function() local c = Color.from_hex(ex.str):to_rgba() assert.are.same(ex.rgba[1], c.red) assert.are.same(ex.rgba[2], c.green) @@ -78,28 +133,28 @@ describe('Color', function() assert.are.same(ex.rgba[4], c.alpha) end) - it('Should output to hsv', function() + it('should output to hsv', function() local c = Color.from_hex(ex.str):to_hsv() assert.are.near(ex.hsv[1], c.hue, 0.1) assert.are.near(ex.hsv[2], c.saturation, 0.1) assert.are.near(ex.hsv[3], c.value, 0.1) end) - it('Should output to hsl', function() + it('should output to hsl', function() local c = Color.from_hex(ex.str):to_hsl() assert.are.near(ex.hsl[1], c.hue, 0.1) assert.are.near(ex.hsl[2], c.saturation, 0.1) assert.are.near(ex.hsl[3], c.lightness, 0.1) end) - it('Should be able to be tostring', function() + it('should coerce to a string', function() local c = Color.from_hex(ex.str) assert.are.same(ex.str, tostring(c)) end) end) - describe('Manipulate', function() - it('Should blend two colors together', function() + describe('manipulation', function() + it('can blend two colors together', function() local one = Color.from_hex(ex.str) local two = Color.from_hex('#7d3a65') @@ -109,7 +164,7 @@ describe('Color', function() assert.are.same(113, blend.blue) end) - it('Should shade color', function() + it('can shade color', function() local c = Color.from_hex(ex.str) assert.are.same(Color.WHITE:to_hex(), c:shade(1):to_hex()) assert.are.same(Color.BLACK:to_hex(), c:shade(-1):to_hex()) @@ -117,25 +172,25 @@ describe('Color', function() assert.are.same('#2e4564', c:shade(-0.2):to_css()) end) - it('Should brighten color', function() + it('can brighten color', function() local c = Color.from_hex(ex.str) assert.are.same('#486b9c', c:brighten(12):to_css()) assert.are.same('#2c415e', c:brighten(-12):to_css()) end) - it('Should lighten color', function() + it('can lighten color', function() local c = Color.from_hex(ex.str) assert.are.same('#4d73a7', c:lighten(12):to_css()) assert.are.same('#273953', c:lighten(-12):to_css()) end) - it('Should saturate color', function() + it('can saturate color', function() local c = Color.from_hex(ex.str) assert.are.same('#274b7d', c:saturate(15):to_css()) assert.are.same('#4d617d', c:saturate(-15):to_css()) end) - it('Should rotate hue color', function() + it('can rotate hue color', function() local c = Color.from_hex(ex.str) assert.are.same('#3a457d', c:rotate(15):to_css()) assert.are.same('#3a677d', c:rotate(-15):to_css())