モジュール:Color

提供: 萌えっ娘百科事典
2024年5月9日 (木) 00:12時点におけるLiaMinina (トーク | 投稿記録)による版 ((by SublimeText.Mediawiker))
(差分) ← 古い版 | 最新版 (差分) | 新しい版 → (差分)
移動先: 案内検索

このモジュールについての説明文ページを モジュール:Color/doc に作成できます

-- 该模块主要用于操作颜色。

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('^&#35;', '#')  -- 为了避免解析器自动换行,一些返回颜色值的模板常用'<nowiki>#</nowiki>'或'&#35;'代替'#'
      :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('^&#35;', '#')  -- 为了避免解析器自动换行,一些返回颜色值的模板常用'<nowiki>#</nowiki>'或'&#35;'代替'#'
      :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