Module:Color

From Puella Magi Wiki
Jump to navigation Jump to search
Module Documentation    [edit]

A utility module for dealing with colors.

Usage

local color = require('Module:Color')
local p = {}

function p.main(frame)
	local newColor = color.darken(frame.args[1])
	-- Main module code goes here.
end

return p

Hex Conversion

Convert RGB values to hex.

{{#invoke:Color|toHex|255|255|255}}

Darken

Darken the color.

{{#invoke:Color|darken|#ffffff}}

Lighten

Lighten the color.

{{#invoke:Color|lighten|#000000}}

At

Returns color at a given Bootstrap/Tailwind-like brightness level, where 0 is the brightest and 1000 is the darkest.

{{#invoke:Color|at|#ffffff|500}}

Invert

Invert the brightness level.

{{#invoke:Color|invert|#ffffff}}

-- ------------------------------------------------------------------
-- Main routine
--

local function rgbFromHex(hex)
	hex = hex:gsub("#", "")
	local r = tonumber(hex:sub(1,2), 16) / 255
	local g = tonumber(hex:sub(3,4), 16) / 255
	local b = tonumber(hex:sub(5,6), 16) / 255
	return r, g, b
end

local function hexFromRGB(r, g, b)
	r = math.floor(r * 255)
	g = math.floor(g * 255)
	b = math.floor(b * 255)
	return string.format("#%02X%02X%02X", r, g, b)
end

local function clamp(val, min, max)
    return math.max(min, math.min(max, val))
end

-- ------------------------------------------------------------------
-- HSL routine
--

local function hslFromRGB(r, g, b)
	local max = math.max(r, g, b)
	local min = math.min(r, g, b)
	local h, s, l
	l = (max + min) / 2
	if max == min then
		h = 0
		s = 0
	else
		local d = max - min
		s = l > 0.5 and d / (2 - max - min) or d / (max + min)
		if max == r then
			h = (g - b) / d + (g < b and 6 or 0)
		elseif max == g then
			h = (b - r) / d + 2
		else
			h = (r - g) / d + 4
		end
		h = h / 6
	end

	return h * 360, s * 100, l * 100
end

local function rgbFromHSL(h, s, l)
	local h = h / 360
	local s = s / 100
	local l = l / 100
	local function hue2rgb(p, q, t)
		if t < 0 then t = t + 1 end
		if t > 1 then t = t - 1 end
		if t < 1/6 then return p + (q - p) * 6 * t end
		if t < 1/2 then return q end
		if t < 2/3 then return p + (q - p) * (2/3 - t) * 6 end
		return p
	end

	local r, g, b
	if s == 0 then
		r, g, b = l, l, l
	else
		local q = l < 0.5 and l * (1 + s) or l + s - l * s
		local p = 2 * l - q

		r = hue2rgb(p, q, h + 1/3)
		g = hue2rgb(p, q, h)
		b = hue2rgb(p, q, h - 1/3)
	end

	return r, g, b
end

-- ------------------------------------------------------------------
-- Oklab routine
-- Oklab constant taken from https://bottosson.github.io/posts/oklab/
--

local function oklabFromRGB(r, g, b)
    local function srgbToLinear(c)
        return (c > 0.04045) and (((c + 0.055) / 1.055) ^ 2.4) or (c / 12.92)
    end

    local lr = srgbToLinear(r)
    local lg = srgbToLinear(g)
    local lb = srgbToLinear(b)
    
    local l = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb
    local m = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb
    local s = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb
    
    local function cbrt(v)
        return (v < 0) and -((-v) ^ (1 / 3)) or (v ^ (1 / 3))
    end

    local l_ = cbrt(l)
    local m_ = cbrt(m)
    local s_ = cbrt(s)
    
    local L = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_
    local a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_
    local b_val = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_
    
    return L, a, b_val
end

local function rgbFromOklab(L, a, b_val)
    local l_ = L + 0.3963377774 * a + 0.2158037573 * b_val
    local m_ = L - 0.1055613458 * a - 0.0638541728 * b_val
    local s_ = L - 0.0894841775 * a - 1.2914855480 * b_val
    
    local l = l_ ^ 3
    local m = m_ ^ 3
    local s = s_ ^ 3
    
    local lr =  4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s
    local lg = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s
    local lb = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s
    
    local function linearToSrgb(c)
    	c = clamp(c, 0, 1)
        local companded = (c > 0.0031308) and (1.055 * (c ^ (1 / 2.4)) - 0.055) or (12.92 * c)
        return clamp(companded, 0, 1)
    end
    
    return linearToSrgb(lr), linearToSrgb(lg), linearToSrgb(lb)
end

-- ------------------------------------------------------------------
-- Utility

local function lighten(hex, intensity)
	local r, g, b = rgbFromHex(hex)
	local L, a, b_val = oklabFromRGB(r, g, b)
	if intensity == nil then intensity = 8 end
	intensity = clamp(intensity, 0, 100) / 100  -- 1-100
	L = clamp(L + intensity, 0, 1)
	r, g, b = rgbFromOklab(L, a, b_val)
	return hexFromRGB(r, g, b)
end

local function darken(hex, intensity)
	local r, g, b = rgbFromHex(hex)
	local L, a, b_val = oklabFromRGB(r, g, b)
	if intensity == nil then intensity = 8 end
	intensity = clamp(intensity, 0, 100) / 100  -- 1-100
	L = clamp(L - intensity, 0, 1)
	r, g, b = rgbFromOklab(L, a, b_val)
	return hexFromRGB(r, g, b)
end

-- Invert remains HSL since uniform color ramp with Oklab can cause
-- inverted colors to become too dark, e.g. #d6f2ff has a color 0.94 
-- but inverse value of 0.06 is near black.
local function invert(hex)
	local r, g, b = rgbFromHex(hex)
	local h, s, l = hslFromRGB(r, g, b)
	l = math.min(100, math.max(0, 100 - l))
	r, g, b = rgbFromHSL(h, s, l)
	return hexFromRGB(r, g, b)
end

local function colorAt(hex, brightness)
	local r, g, b = rgbFromHex(hex)
	local L, a, b_val = oklabFromRGB(r, g, b)
	brightness = clamp(brightness, 0, 1000)
	L = (1000 - brightness) / 1000
	r, g, b = rgbFromOklab(L, a, b_val)
	return hexFromRGB(r, g, b)
end

-- ------------------------------------------------------------------
-- MediaWiki
--

local getArgs = require('Module:Arguments').getArgs
local p = {}

-- Encode

function p.toHex(r, g, b)
	return hexFromRGB(r / 255, g / 255, b / 255)
end

-- Color manipulation

function p.darken(...)
	local args = getArgs(...)
	return darken(unpack(args))
end

function p.lighten(...)
	local args = getArgs(...)
	return lighten(unpack(args))
end

function p.invert(...)
	local args = getArgs(...)
	return invert(unpack(args))
end

function p.at(...)
	local args = getArgs(...)
	return colorAt(unpack(args))
end

return p