Tämän moduulin ohjeistuksen voi tehdä sivulle Moduuli:Värit/ohje

local export = {}


local EPSILON = 1E-12

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

---
-- @param val: liukuluku
-- @param prec: (valinnainen) suurin desimaalien määrä
-- @return Annettu liukuluku merkkijonona ilman ylimääräisiä loppunollia.
--         Esim. "0.79", "1.0".
local function format_float(val, prec)
    if prec then
        prec = "0." .. prec
    end
    return string.format("%" .. (prec or "") .. "f", val):gsub("([01]%.[0-9][1-9]*)0+$", "%1")
end


local function parse_change_str(change_str)
    local match, _, dir, amount = string.find(change_str, "^([+-]?)(%d+)°$")

    if match then
        amount = tonumber(amount)
        return dir, amount
    end

    match, _, dir, amount = string.find(change_str, "^([+-]?)(%d+) ?%%$")

    if match then
        amount = tonumber(amount) / 100
        assert(amount >= 0.0 and amount <= 1.0, "Määrän pitää olla välillä 0 %–100 %")
        return dir, amount
    end

    error("Tuntematon muoto: " .. change_str)
end

local function invert_change_str(change_str)
    local func = function (op)
        if op == '-' then
            return '+'
        else
            return '-'
        end
    end
    return change_str:gsub("^([-+])", func)
end


local function round(val)
    return math.floor(val + 0.5)
end

---
-- @param R [0..255]
-- @param G [0..255]
-- @param B [0..255]
-- @return H [0..360|nil], S [0..1], L [0..1]
--  Arvoilla joilla H = 0 ja S = 0, on erityismerkitys, että sekä H että S ovat määrittelemättömiä.
function export.rgb_to_hsl(R, G, B)
    assert(R >= 0, "Virheellinen parametrin R arvo: " .. R)
    assert(R < 256, "Virheellinen parametrin R arvo: " .. R)
    assert(G >= 0, "Virheellinen parametrin G arvo: " .. G)
    assert(G < 256, "Virheellinen parametrin G arvo: " .. G)
    assert(B >= 0, "Virheellinen parametrin B arvo: " .. B)
    assert(B < 256, "Virheellinen parametrin B arvo: " .. B)

    local r = R / 255
    local g = G / 255
    local b = B / 255

    local min = math.min(r, g, b)
    local max = math.max(r, g, b)
    local C = max - min

    local L = (min + max) / 2

    if C == 0 then
        return 0, 0, L
    end

    local S

    if L == 0 or L == 1 then
        S = 0
    else
        S = C / (1 - math.abs(2*max - C - 1))
    end

    local H
    if max == r then
        H = (g - b) / C + 6
    elseif max == g then
        H = (b - r) / C + 2
    else -- max == b
        H = (r - g) / C + 4
    end

    H = H % 6

    H = H * 60 % 360

    return H, S, L
end





---
-- @param H [0..360|nil]
-- @param S [0..1]
-- @param L [0..1]
-- @return R [0..255], G [0..255], B [0..255]
function export.hsl_to_rgb_(H, S, L)
    assert(H >= 0, "Virheellinen parametrin H arvo: " .. H)
    assert(H < 360, "Virheellinen parametrin H arvo: " .. H)
    assert(S >= 0, "Virheellinen parametrin S arvo: " .. S)
    assert(S <= 1 + EPSILON, "Virheellinen parametrin S arvo: " .. S)
    assert(L >= 0, "Virheellinen parametrin L arvo: " .. L)
    assert(L <= 1 + EPSILON, "Virheellinen parametrin L arvo: " .. L)

    local C = (1 - math.abs(2 * L - 1)) * S

    local Hp = H / 60

    local X = C * (1 - math.abs(Hp % 2 - 1))

    local R1, G1, B1

    assert(Hp >= 0, "Hp < 0")
    assert(Hp < 6, "Hp >= 6")

    if Hp < 1 then
        R1 = C
        G1 = X
        B1 = 0
    elseif Hp < 2 then
        R1 = X
        G1 = C
        B1 = 0
    elseif Hp < 3 then
        R1 = 0
        G1 = C
        B1 = X
    elseif Hp < 4 then
        R1 = 0
        G1 = X
        B1 = C
    elseif Hp < 5 then
        R1 = X
        G1 = 0
        B1 = C
    elseif Hp < 6 then
        R1 = C
        G1 = 0
        B1 = X
    end

    local m = L - C / 2

    local r = R1 + m
    local g = G1 + m
    local b = B1 + m

    return r * 255, g * 255, b * 255
end

function export.hsl_to_rgb(H, S, L)
	local R, G, B = export.hsl_to_rgb_(H, S, L)
	return math.floor(R), math.floor(G), math.floor(B)
end

---
-- Tummentaa tai vaalentaa annetun värin.
-- @param p: negatiivinen tai positiivinen prosenttiluku
function export.change_luminosity(r, g, b, diff)
    local h, s, l = export.rgb_to_hsl(r, g, b)

    return export.hsl_to_rgb(h, s, clamp(0, l + diff, 1))
end

---
-- Tummentaa tai vaalentaa annetun värin.
-- @param p: negatiivinen tai positiivinen prosenttiluku
function export.set_luminosity(r, g, b, new_l)
    local h, s, l = export.rgb_to_hsl(r, g, b)

    return export.hsl_to_rgb(h, s, clamp(0, new_l, 1))
end

---
-- Himmentää tai kirkastaa
-- @param p: negatiivinen tai positiivinen prosenttiluku
function export.change_saturation(r, g, b, diff)
    local h, s, l = export.rgb_to_hsl(r, g, b)

    return export.hsl_to_rgb(h, clamp(0, l + diff, 1), l)
end

---
-- Himmentää tai kirkastaa
-- @param p: negatiivinen tai positiivinen prosenttiluku
function export.set_saturation(r, g, b, new_s)
    local h, s, l = export.rgb_to_hsl(r, g, b)

    return export.hsl_to_rgb(h, new_s, l)
end

---
-- Himmentää tai kirkastaa
-- @param deg: negatiivinen tai positiivinen asteluku
function export.change_hue(r, g, b, deg)
    deg = deg % 360

    assert(deg >= 0 and deg < 360, "Virheellinen parametrin deg arvo")

    local h, s, l = export.rgb_to_hsl(r, g, b)

    return export.hsl_to_rgb((h + deg) % 360, s, l)
end

---
-- Himmentää tai kirkastaa
-- @param deg: negatiivinen tai positiivinen asteluku
function export.set_hue(r, g, b, new_h)
    local h, s, l = export.rgb_to_hsl(r, g, b)

    return export.hsl_to_rgb(new_h, s, l)
end


function export.parse_color(color_str)
    local match, _, r, g, b, a = string.find(color_str, "^rgba%((%d+), *(%d+), *(%d+), *([01])%)$")

    if not match then
        match, _, r, g, b, a = string.find(color_str, "^rgba%((%d+), *(%d+), *(%d+), *([01]%.%d+)%)$")
    end

    if match then
        r = tonumber(r)
        g = tonumber(g)
        b = tonumber(b)
        a = tonumber(a)

        assert(r >= 0 and r <= 255, "Komponentin arvon pitää olla välillä 0–255")
        assert(g >= 0 and g <= 255, "Komponentin arvon pitää olla välillä 0–255")
        assert(b >= 0 and b <= 255, "Komponentin arvon pitää olla välillä 0–255")
        assert(a >= 0 and a <= 1, "Alfa-arvon pitää olla välillä 0.0–1.0")

        return "rgba", r, g, b, a
    end

    match, _, r, g, b = string.find(color_str, "^rgb%((%d+), *(%d+), *(%d+)%)$")

    if match then
        r = tonumber(r)
        g = tonumber(g)
        b = tonumber(b)

        assert(r >= 0 and r <= 255, "Komponentin arvon pitää olla välillä 0–255")
        assert(g >= 0 and g <= 255, "Komponentin arvon pitää olla välillä 0–255")
        assert(b >= 0 and b <= 255, "Komponentin arvon pitää olla välillä 0–255")

        return "rgb", r, g, b, nil
    end

    local r_hex, g_hex, b_hex
    match, _, r_hex, g_hex, b_hex = string.find(color_str, "^#(%x%x)(%x%x)(%x%x)$")

    if match then
        return "hex6", tonumber(r_hex, 16), tonumber(g_hex, 16), tonumber(b_hex, 16), nil
    end

    match, _, r_hex, g_hex, b_hex = string.find(color_str, "^#(%x)(%x)(%x)$")

    if match then
        return "hex3", tonumber(r_hex, 16)*16, tonumber(g_hex, 16)*16, tonumber(b_hex, 16)*16, nil
    end

    local val
    match, _, val = string.find(color_str, "^gray%((%d+%.%d+)%)$")

    if match then
    	val = tonumber(val)
        assert(val and val >= 0.0 and val <= 1.0, "Harmaasävyn arvon pitää olla väliltä 0.0–1.0")
        return "gray0.1", math.floor(val * 255), math.floor(val * 255), math.floor(val * 255), nil
    end

    error("Tuntematon muoto: " .. color_str)

end



function export.format_color_string(r, g, b, a, format)
    if format == "rgba" then
        a = a or 1.0
        return string.format("rgba(%d, %d, %d, %s)", r, g, b, format_float(a))
    end

    if format == "rgb" then
        return string.format("rgb(%d, %d, %d)", r, g, b)
    end

    if format == "hex3" then
        return string.format("#%x%x%x", round(r/16), round(g/16), round(b/16))
    end

    if format == "hex6" then
        return string.format("#%02x%02x%02x", r, g, b)
    end

    if format == "rgb0.1" then
        -- Timelinen käyttämä muoto.
        return string.format("rgb(%s,%s,%s)", format_float(r/255, 3), format_float(g/255, 3), format_float(b/255, 3))
    end

    error("Tuntematon muoto: " .. format)
end


function export.to_format(color_str, output_format)
    local format, r, g, b, a = export.parse_color(color_str)

    return export.format_color_string(r, g, b, a, output_format)
end



function export.alter_color(args)
    local color_str = args.color
    local hue =  args.hue
    local luminosity = args.luminosity
    local saturation = args.saturation
    local output_format = args.output

    local format, r, g, b, a = export.parse_color(color_str)

    if hue then
        local dir, amount = parse_change_str(hue)
        if dir == '+' then
            r, g, b = export.change_hue(r, g, b, amount)
        elseif dir == '-' then
            r, g, b = export.change_hue(r, g, b, -amount)
        else
            r, g, b = export.set_hue(r, g, b, amount)
        end
    end

    if saturation then
        local dir, amount = parse_change_str(saturation)
        if dir == '+' then
            r, g, b = export.change_saturation(r, g, b, amount)
        elseif dir == '-' then
            r, g, b = export.change_saturation(r, g, b, -amount)
        else
            r, g, b = export.set_saturation(r, g, b, amount)
        end
    end

    if luminosity then
        local dir, amount = parse_change_str(luminosity)
        if dir == '+' then
            r, g, b = export.change_luminosity(r, g, b, amount)
        elseif dir == '-' then
            r, g, b = export.change_luminosity(r, g, b, -amount)
        else
            r, g, b = export.set_luminosity(r, g, b, amount)
        end
    end

    return export.format_color_string(r, g, b, a, output_format or format)
end

export['väri'] = function(frame)
    local pframe = frame:getParent()
    local vaaleus
    if pframe.args.tummuus then
        vaaleus = invert_change_str(pframe.args.tummuus)
    end
    local args = {
        color = pframe.args[1],
        hue = pframe.args.kierto,
        luminosity = vaaleus,
        saturation = pframe.args['kylläisyys'],
        output = pframe.args.muoto
    }

    if not args.hue and not args.luminosity and not args.saturation then
        assert(args.output, "Anna vähintään yksi parametreista tummuus, kylläisyys, kierto tai muoto")
        return export.to_format(args.color, args.output)
    end

    return export.alter_color(args)
end


return export