Nanaki's Sandbox
Advertisement

Doesn't exist

Module:Colors/class/doc

local Color = {}
Color.__index = Color
Color.mods = require('Module:Colors/mods')
Color.mods.Color = Color

-- Utility functions --
function dec2hex(number, len)
    local i, chars, result, d = 0, '0123456789ABCDEF', ''
    while number > 0 do
        i = i + 1
        number, d = math.floor(number / 16), (number % 16) + 1
        result = string.sub(chars, d, d) .. result
    end
    return ('0'):rep((len or 1) - result:len()) .. result
end
function round(number, precision)
    precision = precision or 0
    local m = 10 ^ precision
    return math.floor(number * m + 0.5) / m
end
function escape(value)
    return value:gsub('([%%%^%$%(%)%.%[%]%*%+%-%?])', '%%%1')
end
function parameters(value, name)
    value = value:lower()
    if name ~= nil then
        name = name:lower()
        local pattern = '^%s*' .. escape(name) .. '(%b())%s*$'
        local _, _, res = value:find(pattern)
        if res == nil then return false end
        res = res:sub(2, -2)
        return true, unpack(parameters(res))
    else
        local groups, i, VAL = {}, 0, value
        
        for all, func, values in string.gmatch(value, "(([0-9a-z-]+)(%b()))") do
            i = i + 1
            local n = '$'.. func .. i
            groups[n] = {func, values}
            VAL = VAL:gsub(escape(all), n, 1)
        end
        local res, list = {}, mw.text.split(VAL, '%s*,%s*')
        for i,v in ipairs(list) do
            v = mw.text.trim(v)
            if groups[v] ~= nil then
                list[i] = groups[v]
            elseif v:find('%$') then
                error('Syntax error', 2)
            else
                list[i] = v
            end
        end
        return list
    end
end
Color.parameters = parameters
function val(value)
    if value == nil or type(value) == 'table' then return nil end
    value = mw.text.trim(tostring(value))
    if value == '' then return nil end
    return value
end

local messages = mw.loadData('Module:Colors/i18n')
local lang = mw.language.getContentLanguage():getCode()
function Color.msg(code, ...)
    local message
    if messages[lang] then
        message = tostring(messages[lang][code] or messages['en'][code] or '')
    else
        message = tostring(messages['en'][code] or '')
    end
    return message:format(...)
end

Color.types = {}
function Color.types.color(value)
    value = val(value)
    if value == nil then return nil end
    local c = Color.new(value)
    if c == nil then error('Format not supported: ' .. value, 2) end
    return c
end
function Color.types.rgb(value)
    value = val(value)
    if value == nil then return 0 end
    if value:sub(-1,-1) == '%' then
        value = tonumber(value:sub(1, -2)) / 100
    else
        value = tonumber(value) / 255
    end
    if value == nil then return 0 end
    return value
end
function Color.types.number(value, fallback)
    if fallback == 'nil' then fallback = nil
    elseif fallback then fallback = 1
    else fallback = 0 end
    value = val(value)
    if value == nil then return fallback end
    if value:sub(-1,-1) == '%' then
        value = tonumber(value:sub(1, -2)) / 100
    else
        value = tonumber(value)
    end
    if value == nil then return fallback end
    return value
end
function Color.types.string(value)
    return val(value)
end
function Color.types.degrees(value)
    value = val(value)
    if value == nil then return 0 end
    value = value:gsub('%s*deg%s*$', '')
    
    if value:sub(-1,-1) == '%' then
        value = tonumber(value:sub(1, -2)) * 3.6
    else
        value = tonumber(value)
    end
    local sign = 1
    if value == nil then return 0 end
    if value < 0 then sign = -1 end
    return (math.abs(value) % 360) * sign
end
function Color.types.s_rgb(value)
    return {Color.types.rgb(value), not not tostring(value):find('^%s*[%-%+]')}
end
function Color.types.s_number(value, fallback)
    return {Color.types.number(value, fallback), not not tostring(value):find('^%s*[%-%+]')}
end
function Color.types.s_degrees(value)
    return {Color.types.degrees(value), not not tostring(value):find('^%s*[%-%+]')}
end

-- Creators
function Color.new(text)
    text = text:gsub('&#35;', '#'):lower()
    
    -- #ffffffff
    local F, _, rgb = string.find(text or '', '^%s*#([0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f])%s*$') 
    if F then
        return Color.newHEX(rgb)
    end
    -- #ffffff
    local F, _, rgb = string.find(text or '', '^%s*#([0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f])%s*$') 
    if F then
        return Color.newHEX(rgb)
    end
    -- #ffff
    local F, _, rgb = string.find(text or '', '^%s*#([0-9a-f][0-9a-f][0-9a-f][0-9a-f])%s*$')
    if F then
        return Color.newHEX(rgb)
    end
    -- #fff
    local F, _, rgb = string.find(text or '', '^%s*#([0-9a-f][0-9a-f][0-9a-f])%s*$')
    if F then
        return Color.newHEX(rgb)
    end
    
    
    -- rgb(255, 255, 255)
    local F, r, g, b = parameters(text, 'rgb')
    if F then
        return Color.newRGB(
            Color.types.rgb(r),
            Color.types.rgb(g),
            Color.types.rgb(b)
        )
    end
    -- rgba(255, 255, 255, 1)
    local F, r, g, b, a = parameters(text, 'rgba') 
    if F then
        return Color.newRGB(
            Color.types.rgb(r),
            Color.types.rgb(g),
            Color.types.rgb(b),
            Color.types.number(a, true)
        )
    end
    
    -- hsl(0, 0%, 100%)
    local F, h, s, l = parameters(text, 'hsl')
    if F then
        return Color.newHSL(
            Color.types.degrees(h),
            Color.types.number(s),
            Color.types.number(l)
        )
    end
    -- hsla(0, 0%, 100%, 1)
    local F, h, s, l, a = parameters(text, 'hsla')
    if F then
        return Color.newHSL(
            Color.types.degrees(h),
            Color.types.number(s),
            Color.types.number(l),
            Color.types.number(a, true)
        )
    end
    
    return nil
end
function Color.newRGB(r, g, b, a)
    local obj = {}
    setmetatable(obj, Color)
    
    obj.lastupdate = 0
    obj:setRGB(r, g, b)
    obj:setA(a or 1)
    
    return obj
end
function Color.newHSL(h, s, l, a)
    local obj = {}
    setmetatable(obj, Color)
    
    obj.lastupdate = 1
    obj:setHSL(h, s, l)
    obj:setA(a or 1)
    
    return obj
end
function Color.newHEX(rgb)
    if type(rgb) ~= 'string' then error(string.format('String expected, %s given', type(rgb)), 2) end
    local o = val(rgb) or ''
    rgb = (o:lower():gsub('^#', ''))
    local l,r,g,b,a = rgb:len()
    if l == 8 then
        _,_,r,g,b,a = string.find(rgb or '', '^([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])$')
        if r ~= nil then
            r = tonumber(r or '', 16)
            g = tonumber(g or '', 16)
            b = tonumber(b or '', 16)
            a = tonumber(a or '', 16)
        end
    elseif l == 6 then
        _,_,r,g,b = string.find(rgb or '', '^([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])$')
        if r ~= nil then
            r = tonumber(r or '', 16)
            g = tonumber(g or '', 16)
            b = tonumber(b or '', 16)
            a = 255
        end
    elseif l == 4 then
        _,_,r,g,b,a = string.find(rgb or '', '^([0-9a-f])([0-9a-f])([0-9a-f])([0-9a-f])$')
        if r ~= nil then
            r = tonumber((r or ''):rep(2), 16)
            g = tonumber((g or ''):rep(2), 16)
            b = tonumber((b or ''):rep(2), 16)
            a = tonumber((a or ''):rep(2), 16)
        end
    elseif l == 3 then
        _,_,r,g,b = string.find(rgb or '', '^([0-9a-f])([0-9a-f])([0-9a-f])$')
        if r ~= nil then
            r = tonumber((r or ''):rep(2), 16)
            g = tonumber((g or ''):rep(2), 16)
            b = tonumber((b or ''):rep(2), 16)
            a = 255
        end
    end
    mw.log(r,g,b,a)
    if r == nil or g == nil or b == nil or a == nil then
        error('Invalid RGB value: ' .. o, 2)
    end
    
    r = r / 255
    g = g / 255
    b = b / 255
    a = a / 255
    
    return Color.newRGB(r, g, b, a)
end
function Color:copy()
    local obj = {}
    setmetatable(obj, Color)
    
    obj.RED = self.RED
    obj.GREEN = self.GREEN
    obj.BLUE = self.BLUE
    
    obj.HUE = self.HUE
    obj.SATURATION = self.SATURATION
    obj.LIGHTNESS = self.LIGHTNESS
    
    obj.ALPHA = self.ALPHA
    obj.lastupdate = self.lastupdate
    return obj
end
function Color:apply(color)
    if color == nil then return self end
    if type(color) ~= 'table' or color.lastupdate == nil then error(string.format('Color or nil expected, %s given', type(color)), 2) end
    self.RED = color.RED
    self.GREEN = color.GREEN
    self.BLUE = color.BLUE
    
    self.HUE = color.HUE
    self.SATURATION = color.SATURATION
    self.LIGHTNESS = color.LIGHTNESS
    
    self.ALPHA = color.ALPHA
    self.lastupdate = color.lastupdate
    return self
end

-- To string
function Color.tostring(c)
    if c.lastupdate == 0 then
        if c:a() == 1 then
            return c:toHEX()
        else
            return c:toRGBA()
        end
    else
        if c:a() == 1 then
            return c:toHSL()
        else
            return c:toHSLA()
        end
    end
end
Color.__tostring = Color.tostring

-- Methods
function Color:calcRGB()
    local H, S, L = self.HUE, self.SATURATION, self.LIGHTNESS
    
    local C = (1 - math.abs(2 * L - 1)) * S
    local X = C * (1 - math.abs(((H / 60) % 2) - 1))
    local M = L - C / 2
    
    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
    
    self.lastupdate = 0
    return self:setRGB(R + M, G + M, B + M)
end
function Color:calcHSL()
    local R, G, B = self.RED, self.GREEN, self.BLUE
    
    local Cmax = math.max(R, G, B)
    local Cmin = math.min(R, G, B)
    local D = Cmax - Cmin
    
    local L = (Cmin + Cmax) / 2
    local S = D / (1 - math.abs(2 * L - 1))
    local H
    
    if D == 0 then
        H = 0
    elseif Cmax == R then
        H = 60 * (((G - B) / D) % 6)
    elseif Cmax == G then
        H = 60 * (((B - R) / D) + 2)
    elseif Cmax == B then
        H = 60 * (((R - G) / D) + 4)
    end
    
    self.lastupdate = 1
    return self:setHSL(H, S, L)
end

function Color:r()
    if self.lastupdate == 1 then self:calcRGB() end
    return self.RED
end
function Color:g()
    if self.lastupdate == 1 then self:calcRGB() end
    return self.GREEN
end
function Color:b()
    if self.lastupdate == 1 then self:calcRGB() end
    return self.BLUE
end
function Color:rgb()
    if self.lastupdate == 1 then self:calcRGB() end
    return self.RED, self.GREEN, self.BLUE
end

function Color:h()
    if self.lastupdate == 0 then self:calcHSL() end
    return self.HUE
end
function Color:s()
    if self.lastupdate == 0 then self:calcHSL() end
    return self.SATURATION
end
function Color:l()
    if self.lastupdate == 0 then self:calcHSL() end
    return self.LIGHTNESS
end
function Color:hsl()
    if self.lastupdate == 0 then self:calcHSL() end
    return self.HUE, self.SATURATION, self.LIGHTNESS
end

local function adjustValue(v)
    if v <= 0.03928 then
        return v / 12.92
    end
    return math.pow(((v + 0.055) / 1.055), 2.4)
end
function Color:luminance()
    if self.lastupdate == 1 then self:calcRGB() end
    return adjustValue(self.RED)   * 0.2126
         + adjustValue(self.GREEN) * 0.7152
         + adjustValue(self.BLUE)  * 0.0722
end
function Color:brightness(cr, cg, cb)
    if self.lastupdate == 1 then self:calcRGB() end
    return math.sqrt(self.RED   * self.RED   * (cr or 0.241)
                   + self.GREEN * self.GREEN * (cg or 0.691)
                   + self.BLUE  * self.BLUE  * (cb or 0.068))
end
function Color:w3brightness()
    if self.lastupdate == 1 then self:calcRGB() end
    return self.RED   * 0.299
         + self.GREEN * 0.587
         + self.BLUE  * 0.114
end

function Color:setR(val)
    if type(val) ~= 'number' then error(string.format('Number expected, %s given', type(val)), 2) end
    if self.lastupdate == 1 then self:calcRGB() end
    self.lastupdate = 0
    self.RED = math.max(0, math.min(1, val))
    return self
end
function Color:setG(val)
    if type(val) ~= 'number' then error(string.format('Number expected, %s given', type(val)), 2) end
    if self.lastupdate == 1 then self:calcRGB() end
    self.lastupdate = 0
    self.GREEN = math.max(0, math.min(1, val))
    return self
end
function Color:setB(val)
    if type(val) ~= 'number' then error(string.format('Number expected, %s given', type(val)), 2) end
    if self.lastupdate == 1 then self:calcRGB() end
    self.lastupdate = 0
    self.BLUE = math.max(0, math.min(1, val))
    return self
end
function Color:setRGB(r, g, b)
    if self.lastupdate == 1 then self:calcRGB() end
    self.lastupdate = 0
    if r ~= nil then self:setR(r) end
    if g ~= nil then self:setG(g) end
    if b ~= nil then self:setB(b) end
    return self
end

function Color:setH(val)
    if type(val) ~= 'number' then error(string.format('Number expected, %s given', type(val)), 2) end
    if self.lastupdate == 0 then self:calcHSL() end
    self.lastupdate = 1
    self.HUE = val % 360
    return self
end
function Color:setS(val)
    if type(val) ~= 'number' then error(string.format('Number expected, %s given', type(val)), 2) end
    if self.lastupdate == 0 then self:calcHSL() end
    self.lastupdate = 1
    self.SATURATION = math.max(0, math.min(1, val))
    return self
end
function Color:setL(val)
    if type(val) ~= 'number' then error(string.format('Number expected, %s given', type(val)), 2) end
    if self.lastupdate == 0 then self:calcHSL() end
    self.lastupdate = 1
    self.LIGHTNESS = math.max(0, math.min(1, val))
    return self
end
function Color:setHSL(h, s, l)
    if self.lastupdate == 0 then self:calcHSL() end
    self.lastupdate = 1
    if h ~= nil then self:setH(h) end
    if s ~= nil then self:setS(s) end
    if l ~= nil then self:setL(l) end
    return self
end

function Color:a()
    return self.ALPHA
end
function Color:setA(val)
    if type(val) ~= 'number' then error(string.format('Number expected, %s given', type(val)), 2) end
    self.ALPHA = math.max(0, math.min(1, val))
    return self
end
function Color:applyMod(name, arguments)
    local res
    if Color.mods[name].args then
        local ready = {}
        for i,v in ipairs(Color.mods[name].args) do
            local func, r = v:gsub('color%s+or%s+', '')
            if not (r > 0 and pcall(function()
                ready[i] = Color.types.color(arguments[i])
            end)) then
                ready[i] = Color.types[func](arguments[i], 'nil')
            end
        end
        res = Color.mods[name].func(self, unpack(ready))
    else
        res = Color.mods[name].func(self)
    end
    if res == nil then return self end
    if type(res) ~= 'table' or res.lastupdate == nil then error(string.format('mods[\'%s\']: Color or nil expected in return, got %s', name, type(res)), 2) end
    return self:apply(res)
end
function Color:modify(value)
    value = val(value)
    if value == nil then return self, 0 end
    local list = parameters(value)
    
    local count = 0
    for i,v in ipairs(list) do
        local name, params = unpack(v)
        if type(Color.mods[name]) == 'table' then
            count = count + 1
            self:applyMod(name, parameters(params:sub(2, -2)))
        end
    end
    return self, count
end


function Color:toHEX(full)
    local r, g, b = self:rgb()
    r = dec2hex(r * 255, 2)
    g = dec2hex(g * 255, 2)
    b = dec2hex(b * 255, 2)
    local result = '#' .. r .. g .. b
    if not full then
        _, _, r, g, b = result:find('^#([0-9A-F])%1([0-9A-F])%2([0-9A-F])%3$')
        if r then return '#' .. r .. g .. b end
    end
    return result
end
function Color:toRGB()
    local r, g, b = self:rgb()
    return string.format(
        'rgb(%d, %d, %d)',
        round(r * 255),
        round(g * 255),
        round(b * 255)
    )
end
function Color:toRGBA(prec)
    local r, g, b = self:rgb()
    local a = self:a()
    
    local prec = (prec or 1) + 2
    return string.format(
        'rgba(%d, %d, %d, %g)',
        round(r * 255),
        round(g * 255),
        round(b * 255),
        round(a, prec)
    )
end
function Color:toRGB2(prec)
    local r, g, b = self:rgb()
    
    local prec = prec or 1
    return string.format(
        'rgb(%g%%, %g%%, %g%%)',
        round(r * 100, prec),
        round(g * 100, prec),
        round(b * 100, prec)
    )
end
function Color:toRGBA2(prec, precA)
    local r, g, b = self:rgb()
    local a = self:a()
    
    local prec = prec or 1
    local precA = precA or (prec+2)
    return string.format(
        'rgba(%g%%, %g%%, %g%%, %g)',
        round(r * 100, prec),
        round(g * 100, prec),
        round(b * 100, prec),
        round(a, precA)
    )
end
function Color:toHSL(prec)
    local h, s, l = self:hsl()
    
    local prec = prec or 1
    return string.format(
        'hsl(%d, %g%%, %g%%)',
        round(h % 360, prec),
        round(s * 100, prec),
        round(l * 100, prec)
    )
end
function Color:toHSLA(prec, precA)
    local h, s, l = self:hsl()
    local a = self:a()
    
    local prec = prec or 1
    local precA = precA or (prec+2)
    return string.format(
        'hsla(%d, %g%%, %g%%, %g)',
        round(h % 360, prec),
        round(s * 100, prec),
        round(l * 100, prec),
        round(a, precA)
    )
end
return Color
Advertisement