Module documentation
[create]
The documentation for this module is missing. Click here to create it.
-- Colors library for embedded color processing on FANDOM.
-- Supports HSL, RGB and hexadecimal web colors.
-- @module p
-- @version 2.5.1
-- @usage require("Dev:Colors")
-- @author Speedit
-- @release stable; unit tests passed
-- <nowiki>
-- Module package.
local p = {}
local yesno = require('Dev:Yesno')
-- Module utilites, configuration/cache variables.
-- @section locals
-- Site SASS styling parameter cache.
local sassParams = mw.site.sassParams or {
['background-dynamic'] = 'false',
['background-image'] = '',
['background-image-height'] = '0',
['background-image-width'] = '0',
['color-body'] = '#f6f6f6',
['color-body-middle'] = '#f6f6f6',
['color-buttons'] = '#a7d7f9',
['color-community-header'] = '#f6f6f6',
['color-header'] = '#f6f6f6',
['color-links'] = '#0b0080',
['color-page'] = '#ffffff',
}
-- Web color RGB presets.
local presets = mw.loadData('Dev:Colors/presets')
-- Error message data.
local i18n = require('Dev:I18n').loadMessages('Colors')
-- Validation ranges for color types and number formats.
local ranges = {
rgb = { 0, 255 },
hsl = { 0, 1 },
hue = { 0, 360 },
percentage = { -100, 100 },
prop = { 0, 100 },
degree = { -360, 360 }
}
-- Module registry for use in loops.
local registry = {
spaces = { 'rgb', 'hsl' },
ops = { 'rotate', 'saturate', 'lighten' },
props = { 'red', 'green', 'blue', 'hue', 'sat', 'lum' },
}
-- Color item class.
-- @section Color
-- @type Color
local Color = { tup = {}, typ = 'color', alp = 1 }
-- Color instance constructor.
-- @function Color:new
-- @param {string} typ Color space type ('hsl' or 'rgb').
-- @param {table} tup Color tuple in HSL or RGB
-- @param {number} alp Alpha value range 0-1
-- @raise 'no color data provided'
-- @raise 'no valid color type'
-- @return {table} Color instance.
function Color.new(self, tup, typ, alp)
local o = {}
setmetatable(o, self)
self.__index = self
-- is color tuple valid?
if type(tup) ~= 'table' or #tup ~= 3 then
error(i18n:msg('no-data'))
end
-- is color type valid?
local typdir = { rgb = 1, hsl = 1 }
if type(typdir[typ]) == 'nil' then
error(i18n:msg('invalid-type', typ))
end
-- are color tuple entries valid?
for n = 1, 3 do
check( (n == 1 and typ == 'hsl') and 'hue' or typ, tup[n])
end
check('hsl', alp)
o.tup = tup
o.typ = typ
o.alp = alp
return o
end
-- Color hexadecimal string output.
-- @name Color:hex
-- @return {string} Hexadecimal color string.
function Color.hex(self)
local this = clone(self, 'rgb')
local hex = '#'
for i, t in ipairs(this.tup) do
-- Hexadecimal conversion.
hex = #mw.ustring.format('%x', t) == 1 -- leftpad
and hex .. '0' .. mw.ustring.format('%x', t)
or hex .. mw.ustring.format('%x', t)
end
local alp = string.format('%x', this.alp*255)
if alp ~= 'ff' then
hex = #alp == 1 and hex .. '0' .. alp or hex .. alp
end
return hex
end
-- Color string default output.
-- @name Color:string
-- @return {string} Hexadecimal 6-digit or HSLA color string.
function Color.string(self)
return self.alp ~= 1 and self:hsl() or self:hex()
end
-- Color space string output.
-- @name Color:rgb
-- @return RGB color string.
-- @name Color:hsl
-- @return HSL color string.
for i, t in ipairs(registry.spaces) do
Color[t] = function(self)
local this = clone(self, t)
if t == 'hsl' then
for i, t in ipairs(this.tup) do
if ({[2]=1, [3]=1})[i] then
this.tup[i] = tostring(t*100) .. '%'
end
end
end
return this.alp ~= 1
and t .. 'a(' .. table.concat(this.tup, ', ') .. ', ' .. this.alp .. ')'
or t .. '(' .. table.concat(this.tup, ', ') .. ')'
end
end
-- Color property getter-setter.
-- @name Color:red
-- @param {number} val Red value to set. 1 - 255
-- @name Color:green
-- @param {number} val Green value to set. 1 - 255
-- @name Color:blue
-- @param {number} val Blue value to set. 1 - 255
-- @name Color:hue
-- @param {number} val Hue value to set. 0 - 360
-- @name Color:sat
-- @param {number} val Saturation value to set. 0 - 100
-- @name Color:lum
-- @param {number} val Luminosity value to set. 0 - 100
-- @return {table} Color instance.
for i, p in ipairs(registry.props) do
Color[p] = function(self, val)
local n = 1 + (i - 1) % 3
local typ = i < 4 and 'rgb' or 'hsl'
local chk = i == 4 and 'hue' or typ
local this = clone(self, typ)
if val then
if typ == 'hsl' and n > 1 then
val = val / 100
end
check(chk, val)
this.tup[n] = val
return this -- chainable
else
return this.tup[n]
end
end
end
-- Alpha getter-setter for color compositing.
-- @name Color:alpha
-- @param {number} mod Modifier 0 - 100
-- @return {table} Color instance.
function Color.alpha(self, val)
if val then
check('prop', val)
self.alp = val / 100
return self
else
return self.alp
end
end
-- Post-processing operators for web color properties.
-- @name Color:rotate
-- @param {number} mod Modifier -360 - 360
-- @name Color:saturate
-- @param {number} mod Modifier -100 - 100
-- @name Color:lighten
-- @param {number} mod Modifier -100 - 100
-- @return {table} Color instance.
for i, o in ipairs(registry.ops) do
Color[o] = function(self, mod)
local div = o == 'rotate' and 1 or 100
local chk = o == 'rotate' and 'degree' or 'percentage'
local cap = o == 'rotate' and circle or limit
local max = o == 'rotate' and 360 or 1
check(chk, mod)
local this = clone(self, 'hsl')
this.tup[i] = cap(this.tup[i] + (mod / div), max)
return this
end
end
-- Opacification utility for color compositing.
-- @name Color:opacify
-- @param {number} mod Modifier -100 - 100 (100 by default)
-- @return {table} Color instance.
function Color.opacify(self, mod)
check('percentage', mod)
self.alp = limit(self.alp + (mod / 100), 1)
return self
end
-- Color additive mixing utility.
-- @name Color:mix
-- @param {string|table} other Module-compatible color string or instance.
-- @param {number} weight Color weight of original (0 - 100).
-- @return {table} Color instance.
function Color.mix(self, other, weight)
if not p.instance(other) then
other = p.parse(other)
convert(other, 'rgb')
else
other = clone(other, 'rgb')
end
weight = weight or 50
check('prop', weight)
weight = weight/100
local this = clone(self, 'rgb')
for i, t in ipairs(this.tup) do
this.tup[i] = t * weight + other.tup[i] * (1 - weight)
this.tup[i] = limit(this.tup[i], 255)
end
return this
end
-- Color inversion utility.
-- name Color:invert
-- @return {table} Color instance.
function Color.invert(self)
local this = clone(self, 'rgb')
for i, t in ipairs(this.tup) do
this.tup[i] = 255 - t
end
return this
end
-- Complementary color utility.
-- @name Color:complement
-- @return {table} Color instance.
function Color.complement(self)
return self:rotate(180)
end
-- Color brightness testing.
-- @name Color:bright
-- @param {number} lim Luminosity threshold (50 default).
-- @return {bool} Boolean for tone beyond threshold.
function Color.bright(self, lim)
lim = lim and tonumber(lim)/100 or 0.5
local this = clone(self, 'hsl')
return this.tup[3] >= lim
end
-- Color luminance testing.
-- @name Color:luminant
-- @param {number} lim Luminance threshold (50 default).
-- @return {bool} Boolean for luminance beyond threshold.
-- @see [[wikipedia:Relative luminance]]
function Color.luminant(self, lim)
lim = lim and tonumber(lim)/100 or 0.5
check('hsl', lim)
local hsl = clone(self, 'hsl')
local sat = hsl.tup[2]
local lum = hsl.tup[3]
local rgb = clone(self, 'rgb').tup
for i, t in ipairs(rgb) do
rgb[i] = t > 0.4045 and
math.pow(((t + 0.05) / 1.055), 2.4) or
t / 12.92
end
local rel =
rgb[1] * 0.2126 +
rgb[2] * 0.7152 +
rgb[3] * 0.0722
local quo = sat * (0.2038 * (rel - 0.5) / 0.5)
return (lum >= (lim - quo))
end
-- Color status testing.
-- @name Color:chromatic
-- @return {bool} Boolean for color status.
function Color.chromatic(self)
local this = clone(self, 'hsl')
return this.tup[2] ~= 0 and -- sat = not 0
this.tup[3] ~= 0 and -- lum = not 0
this.alp ~= 0 -- alpha = not 0
end
-- Internal color utilities.
-- @section functions
-- Boundary validation for color types.
-- @param {string} t Range type.
-- @param {number} n Number to validate.
-- @raise 'color value $n invalid or out of $t bounds'
-- @return {bool} Validity of number.
function check(t, n)
local min = ranges[t][1] -- Boundary variables
local max = ranges[t][2]
if type(n) ~= 'number' then
error(i18n:msg('invalid-value', type(n), tostring(n)))
elseif n < min or n > max then
error(i18n:msg('out-of-bounds', n, t))
end
end
-- Rounding utility for color tuples.
-- @param {number} tup Color tuple.
-- @param {number} dec Number of decimal places.
-- @return {number} Rounded tuple value.
function round(tup, dec)
local ord = 10^(dec or 0)
return math.floor(tup * ord + 0.5) / ord
end
-- Cloning utility for color items.
-- @param {table} clr Color instance.
-- @param {string} typ Color type of clone.
-- @return {table} New (clone) color instance.
function clone(clr, typ)
local c = Color:new( clr.tup, clr.typ, clr.alp ) -- new color
convert(c, typ) -- conversion
return c -- output
end
-- Range limiter for color processing.
-- @param {number} val Numeric value to limit.
-- @param {number} max Maximum value for limit boundary.
-- @return {number} Limited value.
function limit(val, max)
return math.max(0, math.min(val, max))
end
-- Circular spatial processing for ranges.
-- @param {number} val Numeric value to cycle.
-- @param {number} max Maximum value for cycle boundary.
-- @return {number} Cyclical positive value below max.
function circle(val, max)
if val < 0 then -- negative; below cycle minimum
val = val + max
elseif val > max then -- exceeds cycle maximum
val = val - max
end
return val -- output
end
-- Color space converter.
-- @param {table} clr Color instance.
-- @param {string} typ Color type to output.
-- @return {table} Converted color instance.
function convert(clr, typ)
if clr.typ ~= typ then
clr.typ = typ
if typ == 'rgb' then
clr.tup = hslToRgb(clr.tup)
else
clr.tup = rgbToHsl(clr.tup)
end
end
for i, t in ipairs(clr.tup) do
if clr.typ == 'rgb' then
clr.tup[i] = round(clr.tup[i], 0)
elseif clr.typ == 'hsl' then
clr.tup[i] = i == 1
and round(clr.tup[i], 0)
or round(clr.tup[i], 2)
end
end
end
-- RGB-HSL tuple converter.
-- @param {table} rgb Tuple table of RGB values.
-- @return {table} Tuple table of HSL values.
-- @see http://www.easyrgb.com/en/math.php#m_rgb_hsl
function rgbToHsl(rgb)
for i, t in ipairs(rgb) do
rgb[i] = t/255
end
local r,g,b = rgb[1], rgb[2], rgb[3]
local min = math.min(r, g, b)
local max = math.max(r, g, b)
local d = max - min
local h, s, l = 0, 0, ((min + max) / 2)
if d > 0 then
s = l < 0.5 and d / (max + min) or d / (2 - max - min)
h = max == r and (g - b) / d or
max == g and 2 + (b - r)/d or
max == b and 4 + (r - g)/d
h = circle(h/6, 1)
end
return { h * 360, s, l }
end
-- HSL component conversion subroutine to RGB.
-- @param {number} p Temporary variable 1.
-- @param {number} q Temporary variable 2.
-- @param {number} t Modifier for primary color.
-- @return {number} HSL component.
-- @see http://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/
function hueToRgb(p, q, t)
if t < 0 then
t = t + 1
elseif 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) * (2/3 - t) * 6
else
return p
end
end
-- HSL-RGB tuple converter.
-- @param {table} hsl Tuple table of HSL values.
-- @return {table} Tuple table of RGB values.
function hslToRgb(hsl)
local h, s, l = hsl[1]/360, hsl[2], hsl[3]
local r
local g
local b
local p
local q
if s == 0 then
r,g,b = l,l,l
else
q = l < 0.5 and l * (1 + s) or l + s - l * s
p = 2 * l - q
r = hueToRgb(p, q, h + 1/3)
g = hueToRgb(p, q, h)
b = hueToRgb(p, q, h - 1/3)
end
return { r * 255, g * 255, b * 255 }
end
-- Package methods and members.
-- @section p
-- Creation of RGB color instances.
-- @param {number} r red 1-255
-- @param {number} g green 1-255
-- @param {number} b blue 1-255
-- @param {number} a alpha 0-1
-- @see Color:newco
-- @return {table} Color instance.
function p.fromRgb(r, g, b, a)
return Color:new({ r, g, b }, 'rgb', a or 1);
end
-- Creation of HSL color instances.
-- @param {number} h Hue value. 0-360
-- @param {number} s Saturation value. 0-1
-- @param {number} l Luminance value. 0-1
-- @param {number} a Alpha channel. 0-1
-- @see Color:new
-- @return {table} Color instance.
function p.fromHsl(h, s, l, a)
return Color:new({ h, s, l }, 'hsl', a or 1);
end
-- Parsing logic for color strings.
-- @param {string} str Valid color string.
-- @raise 'cannot parse $str'
-- @see Color:new
-- @return {table} Color instance.
function p.parse(str)
local typ
local tup = {}
local alp = 1
str, _ = mw.ustring.gsub(str, '%s', '')
if p.params and p.params[mw.ustring.match(str, '^%$([%w-]+)$') or ''] then
str = p.params[mw.ustring.match(str, '^%$([%w-]+)$')]
elseif sassParams[mw.ustring.match(str, '^%$([%w-]+)$') or ''] then
str = sassParams[mw.ustring.match(str, '^%$([%w-]+)$')]
end
function extract(str)
for t in mw.ustring.gmatch(str, '([^,]+)') do
local tp = mw.ustring.find(t, '%%') and
tonumber(mw.ustring.match(t, '[^%%]+'))/100 or
t
if #tup == 3 then
alp = tonumber(tp)
else
tup[#tup+1] = tonumber(tp)
end
end
end
local HEX_PTN_3 = '^%#(%x)(%x)(%x)$'
local HEX_PTN_4 = '^%#(%x)(%x)(%x)(%x)$'
local HEX_PTN_6 = '^%#(%x%x)(%x%x)(%x%x)$'
local HEX_PTN_8 = '^%#(%x%x)(%x%x)(%x%x)(%x%x)$'
if mw.ustring.match(str, '^%#[%x]+$') and ({
[4] = 1, [5] = 1, -- #xxxx?
[7] = 1, [9] = 1 -- #xxxxxxx?x?
})[#str] then
if #str == 4 then
tup[1], tup[2], tup[3] = mw.ustring.match(str, HEX_PTN_3)
elseif #str == 5 then
tup[1], tup[2], tup[3], alp = mw.ustring.match(str, HEX_PTN_4)
alp = alp .. alp
elseif #str == 7 then
tup[1], tup[2], tup[3] = mw.ustring.match(str, HEX_PTN_6)
alp = 1
elseif #str == 9 then
tup[1], tup[2], tup[3], alp = mw.ustring.match(str, HEX_PTN_8)
end
for i, t in ipairs(tup) do
tup[i] = tonumber(#t == 2 and t or t .. t, 16)
end
if #str == 5 or #str == 9 then
alp = tonumber(alp, 16)
end
typ = 'rgb'
elseif mw.ustring.match(str, 'rgba?%([%d,.%%]+%)') then
extract(mw.ustring.match(str, '^rgba?%(([0-9.,%%]+)%)$'))
typ = 'rgb'
elseif mw.ustring.match(str, 'hsla?%([%d,.%%]+%)') then
extract(mw.ustring.match(str, '^hsla?%(([0-9.,%%]+)%)$'))
typ = 'hsl'
elseif presets[str] then
local p = presets[str]
tup = { p[1], p[2], p[3] }
typ = 'rgb'
elseif str == 'transparent' then
tup = { 0, 0, 0 }
typ = 'rgb'
alp = 0
else error(i18n:msg('unparse', (str or ''))) end
return Color:new(tup, typ, alp)
end
-- Instance test function for colors.
-- @param {table|string} item Color item or string.
-- @return {bool} Whether the color item was instantiated.
function p.instance(item)
local i = getmetatable(item)
return i and i.typ == 'color' or false
end
-- Color SASS parameter access utility for templating.
-- @param {table} frame Frame invocation object.
-- @usage {{#invoke:colors|wikia|key}}
-- @raise 'invalid SASS parameter name supplied'
-- @return {string} Color string aligning with parameter.
function p.wikia(frame)
if not frame or not frame.args[1] then
error(i18n:msg('invalid-param'))
end
local key = mw.text.trim(frame.args[1])
local val =
p.params and p.params[key] and
p.params[key]
or sassParams[key] and
sassParams[key]
or '<Dev:Colors: ' .. i18n:msg('invalid-param') .. '>'
return mw.text.trim(val)
end
-- Color parameter parser for inline styling.
-- @param {table} frame Frame invocation object.
-- @param {string} frame.args[1]
-- @usage {{#invoke:colors|css|styling}}
-- @raise 'no styling supplied'
-- @return {string} CSS styling with $parameters from p.params.
function p.css(frame)
if not frame.args[1] then
error(i18n:msg('no-style'))
end
local styles = mw.text.trim(frame.args[1])
local o, _ = mw.ustring.gsub(styles, '%$([%w-]+)', p.params)
return o
end
-- Color generator for high-contrast text.
-- @param {table} frame Frame invocation object.
-- @param {string} frame.args[1] Color to process.
-- @param {string} frame.args[2] Dark color to return.
-- @param {string} frame.args[3] Light color to return.
-- @param {string} frame.args.lum Whether luminance is used.
-- @usage {{#invoke:colors|text|bg|dark color|light color}}
-- @raise 'no color supplied'
-- @return {string} Color string '#000000'/$2 or '#ffffff'/$3.
function p.text(frame)
if not frame or not frame.args[1] then
error(i18n:msg('no-color'))
end
local str = mw.text.trim(frame.args[1])
local clr = {
(mw.text.trim(frame.args[2] or '#000000')),
(mw.text.trim(frame.args[3] or '#ffffff')),
}
local b = yesno(frame.args.lum, false)
and p.parse(str):luminant()
or p.parse(str):bright()
return b and clr[1] or clr[2]
end
-- CSS variables stylesheet generator.
-- @param {table} frame Frame invocation object.
-- @param {table} frame.args.s Optional number of spaces.
function p.variables(frame)
local s = frame.args.s
local m = s ~= nil
local n = tonumber(s)
local sep = (m and n) and '\n' or ' '
local ind = (m and n) and string.rep(' ', n) or ''
local prm = p.params
local ret = {}
local sortkeys = {}
for k, p in pairs(prm) do
table.insert(sortkeys, k)
end
table.sort(sortkeys)
table.insert(ret, ':root {')
for i, k in ipairs(sortkeys) do
local val = prm[k]
if
tostring(val) ~= 'false' and
tostring(val) ~= 'true'
then
if tonumber(val) ~= nil then
val = tostring(val) .. 'px'
elseif #mw.text.trim(val) == 0 then
val = '""'
end
local prop = table.concat({
ind,
'--', k, ': ',
val, ';'
})
table.insert(ret, prop)
end
end
table.insert(ret, '}')
return table.concat(ret, sep)
end
-- Template wrapper for [[Template:Colors]].
-- @param {table} frame Frame invocation object.
-- @usage {{#invoke:colors|main}}
function p.main(frame)
local parentFrame = frame:getParent()
local templateArgs = {}
for p, v in pairs(parentFrame.args) do
templateArgs[p] = v
end
local fn = templateArgs[1] and table.remove(templateArgs, 1)
if fn == nil then
error((mw.ustring.gsub(mw.ustring.match(mw.message.new('scribunto-common-nofunction'):plain(), ':%s(.*)%p$'), '^.', mw.ustring.lower)))
end
if c[fn] == nil then
error((mw.ustring.gsub(mw.ustring.match(mw.message.new('scribunto-common-nosuchfunction'):plain(), ':%s(.*)%p$'), '^.', mw.ustring.lower)))
end
parentFrame.args = templateArgs
return c[fn](parentFrame)
end
-- FANDOM color parameters (common SASS colors).
-- @name p.params
-- @usage colors.params['key']
p.params = (function(s)
local ext_params = {
'oasisTypography',
'widthType',
'page-opacity'
}
for k, c in ipairs(ext_params) do s[c] = nil end
-- Brightness conditionals (post-processing).
local page_bright = p.parse('$color-page'):bright()
local page_bright_90 = p.parse('$color-page'):bright(90)
local header_bright = p.parse('$color-community-header'):bright()
local buttons_bright = p.parse('$color-buttons'):bright()
-- Derived opacity values.
local pi_bg_o = page_bright and 90 or 85
-- Derived colors and variables.
--- Main derived parameters.
s['color-text'] = page_bright and '#3a3a3a' or '#d5d4d4'
s['color-contrast'] = page_bright and '#000000' or '#ffffff'
s['color-page-border'] = page_bright
and p.parse('$color-page'):mix('#000', 80):string()
or p.parse('$color-page'):mix('#fff', 80):string()
s['color-button-highlight'] = buttons_bright
and p.parse('$color-buttons'):mix('#000', 80):string()
or p.parse('$color-buttons'):mix('#fff', 80):string()
s['color-button-text'] = buttons_bright and '#000000' or '#ffffff'
--- PortableInfobox color parameters.
s['infobox-background'] =
p.parse('$color-page'):mix('$color-links', pi_bg_o):string()
s['infobox-section-header-background'] =
p.parse('$color-page'):mix('$color-links', 75):string()
--- CommunityHeader color parameters.
s['color-community-header-text'] = header_bright
and '#000000'
or '#ffffff'
s['dropdown-background-color'] = (function(clr)
if page_bright_90 then
return '#ffffff'
elseif page_bright then
return clr:mix('#fff', 90):string()
else
return clr:mix('#000', 90):string()
end
end)(c.parse('$color-page'))
s['dropdown-menu-highlight'] = p.parse('$color-links'):alpha(10):rgb()
--- Custom SASS parameters.
s['is-dark-wiki'] = not page_bright
return s
end)(sassParams)
return p