You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
314 lines
6.6 KiB
Lua
314 lines
6.6 KiB
Lua
--[=[
|
|
@c Color
|
|
@t ui
|
|
@mt mem
|
|
@p value number
|
|
@d Wrapper for 24-bit colors packed as a decimal value. See the static constructors for more information.
|
|
]=]
|
|
|
|
local class = require('class')
|
|
|
|
local format = string.format
|
|
local min, max, abs, floor = math.min, math.max, math.abs, math.floor
|
|
local lshift, rshift = bit.lshift, bit.rshift
|
|
local band, bor = bit.band, bit.bor
|
|
local bnot = bit.bnot
|
|
local isInstance = class.isInstance
|
|
|
|
local Color, get = class('Color')
|
|
|
|
local function check(self, other)
|
|
if not isInstance(self, Color) or not isInstance(other, Color) then
|
|
return error('Cannot perform operation with non-Color object', 2)
|
|
end
|
|
end
|
|
|
|
local function clamp(n, mn, mx)
|
|
return min(max(n, mn), mx)
|
|
end
|
|
|
|
function Color:__init(value)
|
|
value = tonumber(value)
|
|
self._value = value and band(value, 0xFFFFFF) or 0
|
|
end
|
|
|
|
function Color:__tostring()
|
|
return format('Color: %s (%i, %i, %i)', self:toHex(), self:toRGB())
|
|
end
|
|
|
|
function Color:__eq(other) check(self, other)
|
|
return self._value == other._value
|
|
end
|
|
|
|
function Color:__add(other) check(self, other)
|
|
local r = clamp(self.r + other.r, 0, 0xFF)
|
|
local g = clamp(self.g + other.g, 0, 0xFF)
|
|
local b = clamp(self.b + other.b, 0, 0xFF)
|
|
return Color.fromRGB(r, g, b)
|
|
end
|
|
|
|
function Color:__sub(other) check(self, other)
|
|
local r = clamp(self.r - other.r, 0, 0xFF)
|
|
local g = clamp(self.g - other.g, 0, 0xFF)
|
|
local b = clamp(self.b - other.b, 0, 0xFF)
|
|
return Color.fromRGB(r, g, b)
|
|
end
|
|
|
|
function Color:__mul(other)
|
|
if not isInstance(self, Color) then
|
|
self, other = other, self
|
|
end
|
|
other = tonumber(other)
|
|
if other then
|
|
local r = clamp(self.r * other, 0, 0xFF)
|
|
local g = clamp(self.g * other, 0, 0xFF)
|
|
local b = clamp(self.b * other, 0, 0xFF)
|
|
return Color.fromRGB(r, g, b)
|
|
else
|
|
return error('Cannot perform operation with non-numeric object')
|
|
end
|
|
end
|
|
|
|
function Color:__div(other)
|
|
if not isInstance(self, Color) then
|
|
return error('Division with Color is not commutative')
|
|
end
|
|
other = tonumber(other)
|
|
if other then
|
|
local r = clamp(self.r / other, 0, 0xFF)
|
|
local g = clamp(self.g / other, 0, 0xFF)
|
|
local b = clamp(self.b / other, 0, 0xFF)
|
|
return Color.fromRGB(r, g, b)
|
|
else
|
|
return error('Cannot perform operation with non-numeric object')
|
|
end
|
|
end
|
|
|
|
--[=[
|
|
@m fromHex
|
|
@t static
|
|
@p hex string
|
|
@r Color
|
|
@d Constructs a new Color object from a hexadecimal string. The string may or may
|
|
not be prefixed by `#`; all other characters are interpreted as a hex string.
|
|
]=]
|
|
function Color.fromHex(hex)
|
|
return Color(tonumber(hex:match('#?(.*)'), 16))
|
|
end
|
|
|
|
--[=[
|
|
@m fromRGB
|
|
@t static
|
|
@p r number
|
|
@p g number
|
|
@p b number
|
|
@r Color
|
|
@d Constructs a new Color object from RGB values. Values are allowed to overflow
|
|
though one component will not overflow to the next component.
|
|
]=]
|
|
function Color.fromRGB(r, g, b)
|
|
r = band(lshift(r, 16), 0xFF0000)
|
|
g = band(lshift(g, 8), 0x00FF00)
|
|
b = band(b, 0x0000FF)
|
|
return Color(bor(bor(r, g), b))
|
|
end
|
|
|
|
local function fromHue(h, c, m)
|
|
local x = c * (1 - abs(h / 60 % 2 - 1))
|
|
local r, g, b
|
|
if 0 <= h and h < 60 then
|
|
r, g, b = c, x, 0
|
|
elseif 60 <= h and h < 120 then
|
|
r, g, b = x, c, 0
|
|
elseif 120 <= h and h < 180 then
|
|
r, g, b = 0, c, x
|
|
elseif 180 <= h and h < 240 then
|
|
r, g, b = 0, x, c
|
|
elseif 240 <= h and h < 300 then
|
|
r, g, b = x, 0, c
|
|
elseif 300 <= h and h < 360 then
|
|
r, g, b = c, 0, x
|
|
end
|
|
r = (r + m) * 0xFF
|
|
g = (g + m) * 0xFF
|
|
b = (b + m) * 0xFF
|
|
return r, g, b
|
|
end
|
|
|
|
local function toHue(r, g, b)
|
|
r = r / 0xFF
|
|
g = g / 0xFF
|
|
b = b / 0xFF
|
|
local mn = min(r, g, b)
|
|
local mx = max(r, g, b)
|
|
local d = mx - mn
|
|
local h
|
|
if d == 0 then
|
|
h = 0
|
|
elseif mx == r then
|
|
h = (g - b) / d % 6
|
|
elseif mx == g then
|
|
h = (b - r) / d + 2
|
|
elseif mx == b then
|
|
h = (r - g) / d + 4
|
|
end
|
|
h = floor(h * 60 + 0.5)
|
|
return h, d, mx, mn
|
|
end
|
|
|
|
--[=[
|
|
@m fromHSV
|
|
@t static
|
|
@p h number
|
|
@p s number
|
|
@p v number
|
|
@r Color
|
|
@d Constructs a new Color object from HSV values. Hue is allowed to overflow
|
|
while saturation and value are clamped to [0, 1].
|
|
]=]
|
|
function Color.fromHSV(h, s, v)
|
|
h = h % 360
|
|
s = clamp(s, 0, 1)
|
|
v = clamp(v, 0, 1)
|
|
local c = v * s
|
|
local m = v - c
|
|
local r, g, b = fromHue(h, c, m)
|
|
return Color.fromRGB(r, g, b)
|
|
end
|
|
|
|
--[=[
|
|
@m fromHSL
|
|
@t static
|
|
@p h number
|
|
@p s number
|
|
@p l number
|
|
@r Color
|
|
@d Constructs a new Color object from HSL values. Hue is allowed to overflow
|
|
while saturation and lightness are clamped to [0, 1].
|
|
]=]
|
|
function Color.fromHSL(h, s, l)
|
|
h = h % 360
|
|
s = clamp(s, 0, 1)
|
|
l = clamp(l, 0, 1)
|
|
local c = (1 - abs(2 * l - 1)) * s
|
|
local m = l - c * 0.5
|
|
local r, g, b = fromHue(h, c, m)
|
|
return Color.fromRGB(r, g, b)
|
|
end
|
|
|
|
--[=[
|
|
@m toHex
|
|
@r string
|
|
@d Returns a 6-digit hexadecimal string that represents the color value.
|
|
]=]
|
|
function Color:toHex()
|
|
return format('#%06X', self._value)
|
|
end
|
|
|
|
--[=[
|
|
@m toRGB
|
|
@r number
|
|
@r number
|
|
@r number
|
|
@d Returns the red, green, and blue values that are packed into the color value.
|
|
]=]
|
|
function Color:toRGB()
|
|
return self.r, self.g, self.b
|
|
end
|
|
|
|
--[=[
|
|
@m toHSV
|
|
@r number
|
|
@r number
|
|
@r number
|
|
@d Returns the hue, saturation, and value that represents the color value.
|
|
]=]
|
|
function Color:toHSV()
|
|
local h, d, mx = toHue(self.r, self.g, self.b)
|
|
local v = mx
|
|
local s = mx == 0 and 0 or d / mx
|
|
return h, s, v
|
|
end
|
|
|
|
--[=[
|
|
@m toHSL
|
|
@r number
|
|
@r number
|
|
@r number
|
|
@d Returns the hue, saturation, and lightness that represents the color value.
|
|
]=]
|
|
function Color:toHSL()
|
|
local h, d, mx, mn = toHue(self.r, self.g, self.b)
|
|
local l = (mx + mn) * 0.5
|
|
local s = d == 0 and 0 or d / (1 - abs(2 * l - 1))
|
|
return h, s, l
|
|
end
|
|
|
|
--[=[@p value number The raw decimal value that represents the color value.]=]
|
|
function get.value(self)
|
|
return self._value
|
|
end
|
|
|
|
local function getByte(value, offset)
|
|
return band(rshift(value, offset), 0xFF)
|
|
end
|
|
|
|
--[=[@p r number The value that represents the color's red-level.]=]
|
|
function get.r(self)
|
|
return getByte(self._value, 16)
|
|
end
|
|
|
|
--[=[@p g number The value that represents the color's green-level.]=]
|
|
function get.g(self)
|
|
return getByte(self._value, 8)
|
|
end
|
|
|
|
--[=[@p b number The value that represents the color's blue-level.]=]
|
|
function get.b(self)
|
|
return getByte(self._value, 0)
|
|
end
|
|
|
|
local function setByte(value, offset, new)
|
|
local byte = lshift(0xFF, offset)
|
|
value = band(value, bnot(byte))
|
|
return bor(value, band(lshift(new, offset), byte))
|
|
end
|
|
|
|
--[=[
|
|
@m setRed
|
|
@r nil
|
|
@d Sets the color's red-level.
|
|
]=]
|
|
function Color:setRed(r)
|
|
self._value = setByte(self._value, 16, r)
|
|
end
|
|
|
|
--[=[
|
|
@m setGreen
|
|
@r nil
|
|
@d Sets the color's green-level.
|
|
]=]
|
|
function Color:setGreen(g)
|
|
self._value = setByte(self._value, 8, g)
|
|
end
|
|
|
|
--[=[
|
|
@m setBlue
|
|
@r nil
|
|
@d Sets the color's blue-level.
|
|
]=]
|
|
function Color:setBlue(b)
|
|
self._value = setByte(self._value, 0, b)
|
|
end
|
|
|
|
--[=[
|
|
@m copy
|
|
@r Color
|
|
@d Returns a new copy of the original color object.
|
|
]=]
|
|
function Color:copy()
|
|
return Color(self._value)
|
|
end
|
|
|
|
return Color
|