2023年政策修订增补工作正在进行中,欢迎参与!
Module:Color
跳转到导航
跳转到搜索
-- 该模块主要用于操作颜色。
local colorKeywords = {
aliceblue = { 240, 248, 255 },
antiquewhite = { 250, 235, 215 },
aqua = { 0, 255, 255 },
aquamarine = { 127, 255, 212 },
azure = { 240, 255, 255 },
beige = { 245, 245, 220 },
bisque = { 255, 228, 196 },
black = { 0, 0, 0 },
blanchedalmond = { 255, 235, 205 },
blue = { 0, 0, 255 },
blueviolet = { 138, 43, 226 },
brown = { 165, 42, 42 },
burlywood = { 222, 184, 135 },
cadetblue = { 95, 158, 160 },
chartreuse = { 127, 255, 0 },
chocolate = { 210, 105, 30 },
coral = { 255, 127, 80 },
cornflowerblue = { 100, 149, 237 },
cornsilk = { 255, 248, 220 },
crimson = { 220, 20, 60 },
cyan = { 0, 255, 255 },
darkblue = { 0, 0, 139 },
darkcyan = { 0, 139, 139 },
darkgoldenrod = { 184, 134, 11 },
darkgray = { 169, 169, 169 },
darkgreen = { 0, 100, 0 },
darkgrey = { 169, 169, 169 },
darkkhaki = { 189, 183, 107 },
darkmagenta = { 139, 0, 139 },
darkolivegreen = { 85, 107, 47 },
darkorange = { 255, 140, 0 },
darkorchid = { 153, 50, 204 },
darkred = { 139, 0, 0 },
darksalmon = { 233, 150, 122 },
darkseagreen = { 143, 188, 143 },
darkslateblue = { 72, 61, 139 },
darkslategray = { 47, 79, 79 },
darkslategrey = { 47, 79, 79 },
darkturquoise = { 0, 206, 209 },
darkviolet = { 148, 0, 211 },
deeppink = { 255, 20, 147 },
deepskyblue = { 0, 191, 255 },
dimgray = { 105, 105, 105 },
dimgrey = { 105, 105, 105 },
dodgerblue = { 30, 144, 255 },
firebrick = { 178, 34, 34 },
floralwhite = { 255, 250, 240 },
forestgreen = { 34, 139, 34 },
fuchsia = { 255, 0, 255 },
gainsboro = { 220, 220, 220 },
ghostwhite = { 248, 248, 255 },
gold = { 255, 215, 0 },
goldenrod = { 218, 165, 32 },
gray = { 128, 128, 128 },
green = { 0, 128, 0 },
greenyellow = { 173, 255, 47 },
grey = { 128, 128, 128 },
honeydew = { 240, 255, 240 },
hotpink = { 255, 105, 180 },
indianred = { 205, 92, 92 },
indigo = { 75, 0, 130 },
ivory = { 255, 255, 240 },
khaki = { 240, 230, 140 },
lavender = { 230, 230, 250 },
lavenderblush = { 255, 240, 245 },
lawngreen = { 124, 252, 0 },
lemonchiffon = { 255, 250, 205 },
lightblue = { 173, 216, 230 },
lightcoral = { 240, 128, 128 },
lightcyan = { 224, 255, 255 },
lightgoldenrodyellow = { 250, 250, 210 },
lightgray = { 211, 211, 211 },
lightgreen = { 144, 238, 144 },
lightgrey = { 211, 211, 211 },
lightpink = { 255, 182, 193 },
lightsalmon = { 255, 160, 122 },
lightseagreen = { 32, 178, 170 },
lightskyblue = { 135, 206, 250 },
lightslategray = { 119, 136, 153 },
lightslategrey = { 119, 136, 153 },
lightsteelblue = { 176, 196, 222 },
lightyellow = { 255, 255, 224 },
lime = { 0, 255, 0 },
limegreen = { 50, 205, 50 },
linen = { 250, 240, 230 },
magenta = { 255, 0, 255 },
maroon = { 128, 0, 0 },
mediumaquamarine = { 102, 205, 170 },
mediumblue = { 0, 0, 205 },
mediumorchid = { 186, 85, 211 },
mediumpurple = { 147, 112, 219 },
mediumseagreen = { 60, 179, 113 },
mediumslateblue = { 123, 104, 238 },
mediumspringgreen = { 0, 250, 154 },
mediumturquoise = { 72, 209, 204 },
mediumvioletred = { 199, 21, 133 },
midnightblue = { 25, 25, 112 },
mintcream = { 245, 255, 250 },
mistyrose = { 255, 228, 225 },
moccasin = { 255, 228, 181 },
navajowhite = { 255, 222, 173 },
navy = { 0, 0, 128 },
oldlace = { 253, 245, 230 },
olive = { 128, 128, 0 },
olivedrab = { 107, 142, 35 },
orange = { 255, 165, 0 },
orangered = { 255, 69, 0 },
orchid = { 218, 112, 214 },
palegoldenrod = { 238, 232, 170 },
palegreen = { 152, 251, 152 },
paleturquoise = { 175, 238, 238 },
palevioletred = { 219, 112, 147 },
papayawhip = { 255, 239, 213 },
peachpuff = { 255, 218, 185 },
peru = { 205, 133, 63 },
pink = { 255, 192, 203 },
plum = { 221, 160, 221 },
powderblue = { 176, 224, 230 },
purple = { 128, 0, 128 },
red = { 255, 0, 0 },
rosybrown = { 188, 143, 143 },
royalblue = { 65, 105, 225 },
saddlebrown = { 139, 69, 19 },
salmon = { 250, 128, 114 },
sandybrown = { 244, 164, 96 },
seagreen = { 46, 139, 87 },
seashell = { 255, 245, 238 },
sienna = { 160, 82, 45 },
silver = { 192, 192, 192 },
skyblue = { 135, 206, 235 },
slateblue = { 106, 90, 205 },
slategray = { 112, 128, 144 },
slategrey = { 112, 128, 144 },
snow = { 255, 250, 250 },
springgreen = { 0, 255, 127 },
steelblue = { 70, 130, 180 },
tan = { 210, 180, 140 },
teal = { 0, 128, 128 },
thistle = { 216, 191, 216 },
tomato = { 255, 99, 71 },
turquoise = { 64, 224, 208 },
violet = { 238, 130, 238 },
wheat = { 245, 222, 179 },
white = { 255, 255, 255 },
whitesmoke = { 245, 245, 245 },
yellow = { 255, 255, 0 },
yellowgreen = { 154, 205, 50 },
}
local rgbRegex = '^rgb%(%s-(%d-),%s-(%d-)%s-,%s-(%d-)%s-%)$'
local rgbaRegex = '^rgba%(%s-(%d-),%s-(%d-)%s-,%s-(%d-)%s-,%s-([%d%.]+)%s-%)$'
local hslRegex = '^hsl%(%s-(%d-),%s-(%d-)%%%s-,%s-(%d-)%%%s-%)$'
local hslaRegex = '^hsla%(%s-(%d-),%s-(%d-)%%%s-,%s-(%d-)%%%s-,%s-([%d%.]+)%s-%)$'
local hexRegex = '^#(%x%x)(%x%x)(%x%x)$'
local hexShorthandRegex = '^#(%x)(%x)(%x)$'
--[[
Color实例结构
interface ColorInstance {
__index = Color
value: [number, number, number]
format: 'rgb' | 'hsl'
opacity: number
}
]]
local Color = {}
local colorMetaTable = { __index = Color }
--[[
@param {number} min
@param {number} max
@return {number}
]]
local function _random(min, max)
return tonumber(mw.getCurrentFrame():expandTemplate{ title = 'random', args = { min, max } })
end
--[[
@desc 操作颜色加深减淡
@param {[number, number, number]} rgb
@param {'+' | '-'} operator - 加深,减淡
@param {number} ratio 范围:0 ~ 100
@return {[number, number, number]}
]]
local function _computeRgb(rgb, operator, ratio)
local ranges = {}
local cloneRgb = { rgb[1], rgb[2], rgb[3] }
for i, v in ipairs(rgb) do
ranges[i] = {
['-'] = (255 - v) / 100,
['+'] = -v / 100
}
end
for i, v in ipairs(cloneRgb) do
cloneRgb[i] = v + ranges[i][operator] * ratio
if cloneRgb[i] < 0 then cloneRgb[i] = 0 end
if cloneRgb[i] > 255 then cloneRgb[i] = 255 end
end
return cloneRgb
end
--[[
@desc 判断一个字符串或table是否为合法的color值
@param {(string | [number, number, number])} rawValue - 接受一个字符串或数组table,有效的格式有:css颜色关键字,hex颜色,hex简写颜色,rgb函数,rgba函数,hsl函数,hsla函数
@return {boolean}
]]
function Color.isColorStr(rawValue)
if type(rawValue) == 'string' then
if
rawValue:match(rgbRegex) or
rawValue:match(rgbaRegex) or
rawValue:match(hslRegex) or
rawValue:match(hslaRegex) or
colorKeywords[rawValue]
then return true end
rawValue = mw.text.unstripNoWiki(rawValue)
:gsub('^#', '#') -- 为了避免解析器自动换行,一些返回颜色值的模板常用'<nowiki>#</nowiki>'或'#'代替'#'
:gsub('^#', '#') -- Bhsd加的全角字符兼容,不知道为啥
if
rawValue:match(hexRegex) or
rawValue:match(hexShorthandRegex)
then return true end
elseif type(rawValue) == 'table' then
if #rawValue ~= 3 and #rawValue ~= 4 then return false end
for _, v in ipairs(rawValue) do
if type(v) ~= 'number' then return false end
end
return true
end
return false
end
--[[
@desc 创建一个Color实例
@param {(string | [number, number, number])} rawValue - 接受一个字符串或数组table,有效的格式有:css颜色关键字,hex颜色,hex简写颜色,rgb函数,rgba函数,hsl函数,hsla函数
@return {(Color | nil)} - 如果rawValue无效,则返回nil
]]
function Color.create(rawValue)
if not Color.isColorStr(rawValue) then return nil end
if type(rawValue) == 'string' then
rawValue = mw.text.unstripNoWiki(rawValue)
:gsub('^#', '#') -- 为了避免解析器自动换行,一些返回颜色值的模板常用'<nowiki>#</nowiki>'或'#'代替'#'
:gsub('^#', '#') -- Bhsd加的全角字符兼容,不知道为啥
end
local color = setmetatable({}, colorMetaTable)
local r_h, g_s, b_l, opacity -- rgb or hsl
if type(rawValue) == 'string' then
if rawValue:match(rgbRegex) then
color.format = 'rgb'
r_h, g_s, b_l = rawValue:match(rgbRegex)
r_h = tonumber(r_h)
g_s = tonumber(g_s)
b_l = tonumber(b_l)
elseif rawValue:match(rgbaRegex) then
color.format = 'rgb'
r_h, g_s, b_l, opacity = rawValue:match(rgbaRegex)
r_h = tonumber(r_h)
g_s = tonumber(g_s)
b_l = tonumber(b_l)
opacity = tonumber(opacity)
elseif rawValue:match(hslRegex) then
color.format = 'hsl'
r_h, g_s, b_l = rawValue:match(hslRegex)
r_h = tonumber(r_h)
g_s = tonumber(g_s)
b_l = tonumber(b_l)
elseif rawValue:match(hslaRegex) then
color.format = 'hsl'
r_h, g_s, b_l, opacity = rawValue:match(hslaRegex)
r_h = tonumber(r_h)
g_s = tonumber(g_s)
b_l = tonumber(b_l)
opacity = tonumber(opacity)
elseif rawValue:match(hexRegex) then
color.format = 'rgb'
r_h, g_s, b_l = rawValue:match(hexRegex)
r_h = tonumber(r_h, 16)
g_s = tonumber(g_s, 16)
b_l = tonumber(b_l, 16)
elseif rawValue:match(hexShorthandRegex) then
color.format = 'rgb'
r_h, g_s, b_l = rawValue:match(hexShorthandRegex)
r_h = tonumber(r_h, 16) * 17
g_s = tonumber(g_s, 16) * 17
b_l = tonumber(b_l, 16) * 17
else
color.format = 'rgb'
local colorkeywordRgb = colorKeywords[rawValue]
r_h = colorkeywordRgb[1]
g_s = colorkeywordRgb[2]
b_l = colorkeywordRgb[3]
end
elseif type(rawValue) == 'table' then
color.format = 'rgb'
r_h = rawValue[1]
g_s = rawValue[2]
b_l = rawValue[3]
opacity = rawValue[4]
end
color.value = { r_h, g_s, b_l }
color.opacity = opacity or 1
return color
end
--[[
@desc 克隆一个Color对象
@param {Color} this
@return {Color} - 一个新的Color对象
]]
function Color.clone(this)
local rgb = this:rgb().value
return Color.create(rgb):setOpacity(this.opacity)
end
--[[
@desc rgb转hsl
@param {number} r
@param {number} g
@param {number} b
@return [number, number, number] - 返回的所有值均为整数
]]
function Color.rgb2hsl(r, g, b)
r = r / 255
g = g / 255
b = b / 255
local max = math.max(r, g, b)
local min = math.min(r, g, b)
local diff = max - min
local h, s
local l = (max + min) / 2
if max == min then
h = 0
s = 0
elseif max == r and g >= b then
h = 60 * ((g - b) / diff)
elseif max == r and g < b then
h = 60 * ((g - b) / diff) + 360
elseif max == g then
h = 60 * ((b - r) / diff) + 120
elseif max == b then
h = 60 * ((r - g) / diff) + 240
end
if l == 0 or max == min then
s = 0
elseif 0 < 1 and l <= 0.5 then
s = diff / (2 * l)
elseif l > 0.5 then
s = diff / (2 - 2 * l)
end
return {
math.floor(h + 0.5),
math.floor(s * 100 + 0.5),
math.floor(l * 100 + 0.5)
}
end
--[[
@desc hsl转rgb
@param {number} h
@param {number} s - css中使用百分比,但该函数需要传整数 50% => 50
@param {number} l - css中使用百分比,但该函数需要传整数 50% => 50
@return [number, number, number]
]]
function Color.hsl2rgb(h, s, l)
h = h % 360
s = s / 100
l = l / 100
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 vRGB = {}
if h >=0 and h < 60 then
vRGB = {c, x, 0}
elseif h >= 60 and h < 120 then
vRGB = {x, c, 0}
elseif h >= 120 and h < 180 then
vRGB = {0, c, x}
elseif h >= 180 and h < 240 then
vRGB = {0, x, c}
elseif h >= 240 and h < 300 then
vRGB = {x, 0, c}
elseif h >= 300 and h < 360 then
vRGB = {c, 0, x}
end
local r = 255 * (vRGB[1] + m)
local g = 255 * (vRGB[2] + m)
local b = 255 * (vRGB[3] + m)
return {
math.floor(r + 0.5),
math.floor(g + 0.5),
math.floor(b + 0.5)
}
end
--[[
@desc 将color对象的数据转为rgb格式
@param {Color} this
@return {Color} - this
]]
function Color.rgb(this)
if this.format == 'rgb' then return this end
if this.format == 'hsl' then
this.value = Color.hsl2rgb(this.value[1], this.value[2], this.value[3])
this.format = 'rgb'
end
return this
end
--[[
@desc 将color对象的数据转为hsl格式
@param {Color} this
@return {Color} - this
]]
function Color.hsl(this)
if this.format == 'hsl' then return this end
if this.format == 'rgb' then
this.value = Color.rgb2hsl(this.value[1], this.value[2], this.value[3])
this.format = 'hsl'
end
return this
end
--[[
@desc 加深一个颜色(明度-)
@param {Color} this
@param {number} ratio - 范围:0 ~ 100
@return {Color} - this
]]
function Color.darken(this, ratio)
local rgb = this:rgb().value
this.value = _computeRgb(rgb, '+', ratio)
return this
end
--[[
@desc 减淡一个颜色(明度+)
@param {Color} this
@param {number} ratio - 范围:0 ~ 100
@return {Color} - this
]]
function Color.lighten(this, ratio)
local rgb = this:rgb().value
this.value = _computeRgb(rgb, '-', ratio)
return this
end
--[[
@desc 提高一个颜色的饱和度
@param {Color} this
@param {number} ratio - 范围:0 ~ 100
@return {Color} - this
]]
function Color.saturate(this, ratio)
local hsl = this:hsl().value
this.value[2] = hsl[2] + (100 - hsl[2]) * (ratio / 100)
if this.value[2] > 100 then this.value[2] = 100 end
return this
end
--[[
@desc 降低一个颜色的饱和度
@param {Color} this
@param {number} ratio - 范围:0 ~ 100
@return {Color} - this
]]
function Color.desaturate(this, ratio)
local hsl = this:hsl().value
this.value[2] = hsl[2] - hsl[2] * (ratio / 100)
if this.value[2] < 0 then this.value[2] = 0 end
return this
end
--[[
@desc 混合两个颜色
@param {Color} this 颜色1
@param {Color} color 颜色2
@param {number} weight 颜色1比重 范围:0 ~ 100,默认值为50
@return {Color} this
]]
function Color.mix(this, color, weight)
local color1 = this:rgb()
local color2 = color:rgb()
local p = weight == nil and 50 or weight
p = p / 100
local w = 2 * p - 1
local a = color1.opacity - color2.opacity
local w1 = (((w * a == -1) and w or (w + a) / (1 + w * a)) + 1) / 2.0
local w2 = 1 - w1
this.value = {
w1 * color1.value[1] + w2 * color2.value[1],
w1 * color1.value[2] + w2 * color2.value[2],
w1 * color1.value[3] + w2 * color2.value[3]
}
this:setOpacity(color1.opacity * p + color2.opacity * (1 - p))
return this
end
--[[
@desc 设置一个值的不透明度
@param {Color} this
@param {number} value - 范围:0 ~ 1
@return {Color} - this
]]
function Color.setOpacity(this, value)
this.opacity = tonumber(value)
return this
end
--[[
@desc Gamma校正
@param {number} r_g_b
@return {number}
]]
local function adjustGamma(r_g_b)
if r_g_b <= 0.04045 then return r_g_b / 12.92
else return ((r_g_b + 0.055) / 1.055) ^ 2.4 end
end
--[[
@desc 获得颜色的相对亮度
@param {Color} this
@return {number}
]]
function Color.getRelativeLuminance(this)
local rgb = this:rgb().value
return 0.2126 * adjustGamma(rgb[1] / 255) +
0.7152 * adjustGamma(rgb[2] / 255) +
0.0722 * adjustGamma(rgb[3] / 255)
end
--[[
@desc 获得两颜色的对比度比例
@param {Color} this
@param {Color} color
@return {number}
]]
function Color.getContrastRatio(this, color)
local ratio = (this:getRelativeLuminance() + 0.05) / (color:getRelativeLuminance() + 0.05)
if ratio < 1 then return 1 / ratio
else return ratio end
end
--[[
@desc 检测一个颜色是否为亮色
@param {Color} this
@return {boolean}
]]
function Color.isLight(this)
return this:getRelativeLuminance() > (0.05 * 1.05) ^ 0.5 - 0.05
end
--[[
@desc 检测一个颜色是否为暗色
@param {Color} this
@return {boolean}
]]
function Color.isDark(this)
return this:isLight() == false
end
--[[
@desc 根据范围随机产生一个颜色
@param {number} [min = 0] - 范围:0 ~ 255
@param {number} [max = 255] - 范围:0 ~ 255
@return {Color}
]]
function Color.random(min, max)
min = min or 0
max = max or 255
local rgb = {
_random(min, max),
_random(min, max),
_random(min, max)
}
return Color.create(rgb)
end
--[[
@desc 反转一个颜色
@param {Color} this
@return {Color} - this
]]
function Color.reverse(this)
local rgb = this:rgb().value
for i, v in ipairs(rgb) do
rgb[i] = math.abs(v - 255)
end
return this
end
--[[
@desc 将一个Color实例转化为有效的css颜色值字符串
@param {Color} this
@param {('auto' | 'hex' | 'hex-opacity')} [format = 'auto'] - 格式,
为auto时,根据Color对象本身的format进行转换,使用对应的css函数,并保留透明度。在调用前应该先执行rgb()或hsl(),以明确输出格式。
为hex时,返回hex颜色。无视透明度。
为hex-opacity时,返回hex颜色。若不透明度不为1,则假定背景为白色将透明度和颜色进行计算。
]]
function Color.toString(this, format)
local function toHex(num)
local int, float = math.modf(num)
if float > 0.4 then int = int + 1 end
local zero = ''
if int < 16 then zero = '0' end
return zero..string.format('%X', int)
end
format = format or 'auto'
if format == 'auto' then
if this.format == 'rgb' then
if this.opacity >= 0 and this.opacity < 1 then
return 'rgba('..table.concat(this.value, ',')..','..this.opacity..')'
else
return 'rgb('..table.concat(this.value, ',')..')'
end
elseif this.format == 'hsl' then
local hsl = this.value
if this.opacity >= 0 and this.opacity < 1 then
return string.format('hsla(%s, %s%%, %s%%, %s)', hsl[1], hsl[2], hsl[3], this.opacity)
else
return string.format('hsl(%s, %s%%, %s%%)', hsl[1], hsl[2], hsl[3])
end
end
elseif format == 'hex' then
this:rgb()
return '#'..toHex(this.value[1])..toHex(this.value[2])..toHex(this.value[3])
elseif format == 'hex-opacity' then
this:rgb()
local r = this.value[1]
local g = this.value[2]
local b = this.value[3]
r = r + r * (1 - this.opacity)
g = g + g * (1 - this.opacity)
b = b + b * (1 - this.opacity)
if r > 255 then r = 255 end
if g > 255 then g = 255 end
if b > 255 then b = 255 end
return '#'..toHex(r)..toHex(g)..toHex(b)
end
end
return Color