Files
gen1recomp/src/render/Palette.lua
T
2026-08-26 12:04:06 -04:00

250 lines
6.5 KiB
Lua

local Palette = {}
local pack = nil
function Palette.packs()
if pack == nil then
local ok, data = pcall(require, "data.gb_palettes")
pack = ok and data or false
end
return pack or {}
end
local MONO_STEPS = { 1.00, 0.72, 0.38, 0.12 }
Palette.MONO_COLOURS = {
{ "GREEN", 0x9b, 0xbc, 0x0f }, -- DMG
{ "LIME", 0xa8, 0xd8, 0x38 },
{ "OLIVE", 0xa6, 0xac, 0x84 }, -- Pocket
{ "AMBER", 0xff, 0xb0, 0x00 }, -- plasma terminal
{ "ORANGE", 0xff, 0x80, 0x30 },
{ "RED", 0xff, 0x50, 0x50 },
{ "ROSE", 0xff, 0x90, 0xb0 },
{ "MAGENTA", 0xf0, 0x60, 0xd0 },
{ "PURPLE", 0xb0, 0x70, 0xf0 },
{ "INDIGO", 0x80, 0x80, 0xf0 },
{ "BLUE", 0x50, 0xa0, 0xff },
{ "CYAN", 0x40, 0xe0, 0xe0 }, -- VFD
{ "TEAL", 0x40, 0xc0, 0xa0 },
{ "MINT", 0x90, 0xe0, 0xb0 },
{ "SAND", 0xe0, 0xc8, 0x90 },
{ "BROWN", 0xc0, 0x90, 0x50 },
{ "WHITE", 0xff, 0xff, 0xff }, -- plain DMG greys
{ "SILVER", 0xd0, 0xd8, 0xe0 },
}
local function monoRamp(r, g, b)
local out = {}
for i = 1, 4 do
local f = MONO_STEPS[i]
out[i] = { math.floor(r * f + 0.5),
math.floor(g * f + 0.5),
math.floor(b * f + 0.5) }
end
return out
end
Palette.monoRamp = monoRamp -- named for the suite
function Palette.packId(group, name)
return "p:" .. group .. "/" .. name
end
function Palette.monoId(r, g, b)
return ("m:%02x%02x%02x"):format(r, g, b)
end
local function unhex(text)
if type(text) ~= "string" or #text ~= 24 then return nil end
local out = {}
for i = 1, 4 do
local at = (i - 1) * 6
local r = tonumber(text:sub(at + 1, at + 2), 16)
local g = tonumber(text:sub(at + 3, at + 4), 16)
local b = tonumber(text:sub(at + 5, at + 6), 16)
if not (r and g and b) then return nil end
out[i] = { r, g, b }
end
return out
end
Palette.unhex = unhex -- named for the suite
local RAMP_INDEX = 2
local GREY_CHROMA = 12
local HUE_ARC = 50
Palette.GREY_CHROMA = GREY_CHROMA -- named for the suite
Palette.HUE_ARC = HUE_ARC
local function hueChroma(c)
local r, g, b = c[1] or 0, c[2] or 0, c[3] or 0
local mx = math.max(r, g, b)
local mn = math.min(r, g, b)
local chroma = mx - mn
if chroma <= 0 then return 0, 0 end
local h
if mx == r then h = ((g - b) / chroma) % 6
elseif mx == g then h = (b - r) / chroma + 2
else h = (r - g) / chroma + 4 end
return h * 60, chroma
end
Palette.hueChroma = hueChroma -- named for the suite
local function hueArc(hues)
local best = 360
for i = 1, #hues do
local widest = 0
for j = 1, #hues do
local d = (hues[j] - hues[i]) % 360
if d > widest then widest = d end
end
if widest < best then best = widest end
end
return best
end
Palette.hueArc = hueArc -- named for the suite
function Palette.categoryOf(ramp)
if type(ramp) ~= "table" or not ramp[4] then return nil end
local hues = {}
for i = 1, 4 do
local h, chroma = hueChroma(ramp[i])
if chroma > GREY_CHROMA then hues[#hues + 1] = h end
end
if #hues == 0 then return "grey" end
return hueArc(hues) <= HUE_ARC and "single" or "full"
end
Palette.CATEGORIES = {
{ key = "full", label = "FULL COLOUR" },
{ key = "single", label = "SINGLE COLOUR" },
{ key = "grey", label = "GREYSCALE" },
}
local byId, buckets = nil, nil
local function buildIndex()
byId, buckets = {}, {}
for _, category in ipairs(Palette.CATEGORIES) do buckets[category.key] = {} end
for _, p in ipairs(Palette.packs()) do
for _, entry in ipairs(p.palettes or {}) do
local name = entry[1]
if type(name) == "string" then
local id = Palette.packId(p.name, name)
byId[id] = entry
local raw = unhex(entry[RAMP_INDEX] or entry[2])
local bucket = raw and buckets[Palette.categoryOf(raw) or ""] or nil
if bucket then
bucket[#bucket + 1] = { name = name, pack = p.name, id = id }
end
end
end
end
end
local function ensureIndex()
if not byId then buildIndex() end
end
function Palette.categories()
ensureIndex()
local out = {}
for i, category in ipairs(Palette.CATEGORIES) do
out[i] = { key = category.key, name = category.label,
palettes = buckets[category.key] or {} }
end
return out
end
function Palette.category(key)
ensureIndex()
if not buckets[key] then return nil end
for _, category in ipairs(Palette.CATEGORIES) do
if category.key == key then
return { key = key, name = category.label, palettes = buckets[key] }
end
end
return nil
end
function Palette.locate(id)
if type(id) ~= "string" or id == "" then return nil end
ensureIndex()
for _, category in ipairs(Palette.CATEGORIES) do
for i, item in ipairs(buckets[category.key] or {}) do
if item.id == id then return category.key, i end
end
end
return nil
end
local function findPack(group, name)
ensureIndex()
return byId[Palette.packId(group, name)]
end
Palette.findPack = findPack -- named for the suite
function Palette.label(id)
if type(id) ~= "string" or id == "" then return nil end
local hex = id:match("^m:(%x+)$")
if hex then
local r = tonumber(hex:sub(1, 2), 16)
local g = tonumber(hex:sub(3, 4), 16)
local b = tonumber(hex:sub(5, 6), 16)
for _, c in ipairs(Palette.MONO_COLOURS) do
if c[2] == r and c[3] == g and c[4] == b then return c[1] end
end
return "MONO"
end
local name = id:match("^p:.*/([^/]+)$")
return name or id
end
local cacheId, cacheRamp = nil, nil
function Palette.ramp(id)
if type(id) ~= "string" or id == "" then return nil end
if id == cacheId then return cacheRamp end
local out = nil
local hex = id:match("^m:(%x+)$")
if hex and #hex == 6 then
out = monoRamp(tonumber(hex:sub(1, 2), 16) or 0,
tonumber(hex:sub(3, 4), 16) or 0,
tonumber(hex:sub(5, 6), 16) or 0)
else
local group, name = id:match("^p:(.*)/([^/]+)$")
if group then
local entry = findPack(group, name)
local ramp = entry and unhex(entry[RAMP_INDEX] or entry[2])
if ramp then
out = { ramp[4], ramp[3], ramp[2], ramp[1] }
end
end
end
cacheId, cacheRamp = id, out
return out
end
function Palette.invalidate()
cacheId, cacheRamp = nil, nil
byId, buckets = nil, nil
end
function Palette.swatch(id)
local ramp = Palette.ramp(id)
if not ramp then return nil end
return { ramp[4], ramp[3], ramp[2], ramp[1] }
end
return Palette