置顶公告:【置顶】关于临时开启评论区所有功能的公告(2022.10.22) | 【置顶】关于本站Widget恢复使用的公告
  • 你好~!欢迎来到萌娘百科镜像站!如需查看或编辑,请联系本站管理员注册账号。
  • 本镜像站和其他萌娘百科的镜像站无关,请注意分别。

Module:HctTool

猛汉♂百科,万男皆可猛的百科全书!转载请标注来源页面的网页链接,并声明引自猛汉百科。内容不可商用。
跳到导航 跳到搜索
Template-info.svg 模块文档  [创建] [刷新]
local Hct = require('Module:Hct')
local colorUtils = require('Module:Hct/ColorUtils')
local mathUtils = require('Module:Hct/MathUtils')
local bit32 = require('bit32')

-- 全局变量转换为upvalue
local type = type
local tonumber = tonumber
local insert = table.insert


--[[- switch语法糖。
用法:
switch (值) {
	[匹配] = 值或函数,
}
]]
local function switch(val)
	return function(cases)
		local hit = cases[val]
		if type(hit) == 'function' then
			return hit()
		end
		return hit
	end
end


--- 分离'函数名(参数)'的函数名与参数。
local function separateFuncNameAndArg(str)
	return str:match('^([a-z]+)%(%s*([^%(%)]+)%s*%)$')
end


local function hueToNumber(str)
	return str == 'max' and 300 or tonumber(str)
end


--- 由argb得CSS <color>。
local function argbToString(argb, hashSign)
	return string.format('%s%06X', hashSign or ' #', bit32.band(0xffffff, argb))
end


--- 从'#RRGGBB'之类的字符串得32位argb。
local function hexRgbToArgb(hexRgb)
	-- 暂时忽略透明度
	local rrggbb = switch (hexRgb:len()) {
		[3] = function()
			local r, g, b = hexRgb:match('^(.)(.)(.)')
			return table.concat({r, r, g, g, b, b})
		end,
		[4] = function()
			local r, g, b = hexRgb:match('^(.)(.)(.)')
			return table.concat({r, r, g, g, b, b})
		end,
		[6] = hexRgb,
		[8] = function()
			return hexRgb:sub(1, 6)
		end,
	}
	assert(rrggbb, '16进制RGB格式有误')
	return bit32.bor(0xff000000, tonumber(rrggbb, 16))
end


--- 从诸如'#RRGGBB'、'rgb(r, g, b)'的字符串获取Hct对象。
local function hctFromString(str)
	local PATTERNS = {
		hex = '^#(%x+)$',
		hct = '^([%+%-]?[%d%.]+)%s+([%+%-]?[%d%.max]+)%s+([%+%-]?[%d%.]+)$',
		hct_comma = '^([%+%-]?[%d%.]+)%s*,%s*([%+%-]?[%d%.max]+)%s*,%s*([%+%-]?[%d%.]+)$',
		rgb = '^(%d+)%s+(%d+)%s+(%d+)$',
		rgb_legacy = '^(%d+)%s*,%s*(%d+)%s*,%s*(%d+)$'
	}

	local funcName, argStr = separateFuncNameAndArg(str)

	if not funcName then
		local h, c, t = str:match(PATTERNS.hct)
		if not h then
			h, c, t = str:match(PATTERNS.hct_comma)
		end
		if h then
			return Hct.from(tonumber(h), hueToNumber(c), tonumber(t))
		end
		local hexRgb = str:match(PATTERNS.hex)
		if hexRgb then
			return Hct.fromInt(hexRgbToArgb(hexRgb))
		end
		error('不支持的颜色格式')
	end

	-- 颜色函数
	local hct = switch (funcName) {
		rgb = function ()
			local r, g, b = argStr:match(PATTERNS.rgb)
			if not r then
				r, g, b = argStr:match(PATTERNS.rgb_legacy)
			end
			if r then
				return Hct.fromInt(colorUtils.argbFromRgb(tonumber(r), tonumber(g), tonumber(b)))
			end
			error('rgb()参数有误')
		end,
		hct = function ()
			local h, c, t = argStr:match(PATTERNS.hct)
			if h then
				return Hct.from(tonumber(h), hueToNumber(c), tonumber(t))
			end
			error('hct()参数有误')
		end
	}
	if hct then return hct end

	error('不支持的颜色格式')
end


--- 在HCT空间线性插值。
local function interpolate(hues, chromas, tones, totalCount, doesToString)
	totalCount = totalCount
		and math.max(#hues, #chromas, #tones, math.floor(totalCount))
		or math.max(#hues, #chromas, #tones) * 2 - 1

	local firstHct = Hct.from(
		tonumber(hues[1]),
		hueToNumber(chromas[1]),
		tonumber(tones[1])
	)
	local lastHct = Hct.from(
		tonumber(hues[#hues]),
		hueToNumber(chromas[#chromas]),
		tonumber(tones[#tones])
	)

	-- chroma max相关处理:
	-- - max:当作300(一个绝对超过最大值的值)
	-- - … ~ max / max ~ … / max ~ … ~ max:先计算max,再插值
	-- - max只能出现在首尾
	if #chromas == 1 then
		if chromas[1] == 'max' then
			chromas[1] = 300
		end
	else
		if chromas[1] == 'max' then
			chromas[1] = firstHct:getChroma()
		end
		if chromas[#chromas] == 'max' then
			chromas[#chromas] = lastHct:getChroma()
		end
	end

	local totalGapCount = totalCount - 1
	local function valuesBetween(src)
		local values = {}
		local srcGapCount = #src - 1
		for i = 1, totalGapCount - 1 do
			local intg, frac = math.modf(i * srcGapCount / totalGapCount + 1)
			if frac == 0 then
				insert(values, tonumber(src[intg]))
			else
				insert(
					values, mathUtils.lerp(tonumber(src[intg]), tonumber(src[intg + 1]), frac)
				)
			end
		end
		return values
	end
	hues, chromas, tones = valuesBetween(hues), valuesBetween(chromas), valuesBetween(tones)
	local out = {}
	if doesToString then
		insert(out, argbToString(firstHct:toInt()))
		for i = 1, totalGapCount - 1 do
			insert(out, argbToString(Hct.from(hues[i], chromas[i], tones[i]):toInt()))
		end
		insert(out, argbToString(lastHct:toInt()))
	else
		insert(out, firstHct)
		for i = 1, totalGapCount - 1 do
			insert(out, Hct.from(hues[i], chromas[i], tones[i]))
		end
		insert(out, lastHct)
	end
	return out
end


--- 预处理frame.args。
local function processArgs(frameArgs)
	local args = {}
	local ops = {}

	for k, v in pairs(frameArgs) do
		local argIsntOperation = true
		if type(k) == 'string' then
			local attr, operator = k:match('^([hct])%s*([%+%-]?)$')
			if attr then
				argIsntOperation = false
				vNumber = tonumber(v) or (k == 'c' and v == 'max' and 300)
				assert(vNumber, k..'参数填写有误')
				ops[attr] = { operator, vNumber }
			end
		else
			v = v:match('^%s*(.-)%s*$')
		end
		if argIsntOperation and v ~= '' then
			args[k] = v
		end
	end

	-- 上面的循环排除了空字符串,但是参数'#'允许空字符串
	args['#'] = frameArgs['#']

	return args, ops
end


local p = {}

function p.main(frame)
	local parent = frame:getParent()
	if parent and parent:getTitle() == 'Template:Hct' then
		frame = parent
	end
	return p._main(frame.args)
end


function p._main(frameArgs)
	local args, ops = processArgs(frameArgs)
	local hct

	-- 分析匿名参数
	if args[1] and args[2] and args[3] then
		local hues = mw.text.split(args[1], '%s*~%s*')
		local chromas = mw.text.split(args[2], '%s*~%s*')
		local tones = mw.text.split(args[3], '%s*~%s*')

		if #hues ~= 1 or #chromas ~= 1 or #tones ~= 1 then  -- 线性插值语法
			local colorListStr = table.concat(
				interpolate(hues, chromas, tones, args.num, true), ','
			)
			return args['#'] and colorListStr:gsub('^ #', args['#'], 1) or colorListStr
		end

		hct = Hct.from(tonumber(args[1]), hueToNumber(args[2]), tonumber(args[3]))
	elseif args[1] then
		hct = hctFromString(args[1])
	else
		error('匿名参数填写有误')
	end

	-- 分量操作
	if next(ops) then
		local function newVal(oldVal, op)
			if not op then return oldVal end
			if op[1] == '+' then return oldVal + op[2] end
			if op[1] == '-' then return oldVal - op[2] end
			return op[2]
		end
		hct = Hct.from(
			newVal(hct:getHue(), ops.h),
			newVal(hct:getChroma(), ops.c),
			newVal(hct:getTone(), ops.t)
		)
	end

	-- 获取分量
	if args.get then
		local function serialize(num)
			return (string.format('%.3f', num):gsub('%.?0+$', '', 1))
		end
		if args.get == 'h' then return serialize(hct:getHue()) end
		if args.get == 'c' then return serialize(hct:getChroma()) end
		if args.get == 't' then return serialize(hct:getTone()) end
		if args.get == 'hct' then
			return (string.format(
				'hct(%.3f %.3f %.3f)',
				hct:getHue(),
				hct:getChroma(),
				hct:getTone()):gsub('%.?0+%f[,%)]', '')
			)
		end
		error('get参数值必须是“h”“c”“t”“hct”之一')
	end

	-- 默认是返回该HCT的颜色值
	return argbToString(hct:toInt(), args['#'])
end

return p