commit ea48705ee40a9d82ce5d5566fbd642cd00321396 Author: Ryan Ward Date: Wed May 6 19:39:08 2026 -0700 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/.gitmodules b/.gitmodules new file mode 100755 index 0000000..2034a46 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "multi"] + path = multi + url = https://github.com/rayaman/multi.git diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f9170c9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,74 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a Jeopardy-style quiz application built with LÖVE (Love2D) game framework in Lua. The project uses two custom libraries: +- **multi** (v16.3.0) - A multitasking library for Lua with coroutine-based threading, system threads, and priority management +- **gui** (GuiManager) - A UI library built on top of multi for LÖVE-based interfaces + +## Running the Application + +```bash +# Run with LÖVE +love . + +# Run tests +lua main-test.lua +``` + +## Architecture + +### Core Modules + +| Module | Path | Purpose | +|--------|------|---------| +| `main.lua` | Root | LÖVE entry point with `love.load()`, `love.update()`, `love.draw()` | +| `board.lua` | Root | Builds the Jeopardy game board UI | +| `task_manager.lua` | Root | Manages game flow and question handling | +| `loader.lua` | Root | Loads question data from YAML files | +| `yaml.lua` | Root | YAML parser | +| `multi/` | Library | Multitasking library with threading support | +| `gui/` | Library | UI framework with frames, buttons, textboxes, images | + +### Key Patterns + +**multi library usage:** +```lua +multi, thread = require("multi"):init({print = true, priority=true}) +GLOBAL, THREAD = require("multi.integration.loveManager"):init() + +-- Create threads +multi:newThread("Name", function() + while true do + thread.sleep(1) + -- do work + end +end) + +-- Run in love.update() +multi:uManager(dt) +``` + +**gui library usage:** +```lua +local gui = require("gui") +local color = require("gui.core.color") + +gui:setAspectSize(1920, 1080) +local frame = gui:newFrame(x, y, w, h, sx, sy, sw, sh) +frame.color = color.new("#hex") +``` + +### Data Format + +Questions are loaded via `loader.lua` from YAML files with structure: +- `index.yaml` - Contains categories and settings +- `.yaml` - Contains questions for each category + +### Integration Points + +- `multi.integration.loveManager` - Bridges multi threading with LÖVE's event loop +- `gui.update(dt)` and `gui.draw()` - Called from `love.update()` and `love.draw()` +- UI uses aspect ratio scaling (1920x1080 base) diff --git a/README.md b/README.md new file mode 100755 index 0000000..28126ce --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# ascension-rebirth \ No newline at end of file diff --git a/anime/assets/shounen_name_characters.png b/anime/assets/shounen_name_characters.png new file mode 100644 index 0000000..e88f330 Binary files /dev/null and b/anime/assets/shounen_name_characters.png differ diff --git a/anime/index.yaml b/anime/index.yaml new file mode 100755 index 0000000..b68a36d --- /dev/null +++ b/anime/index.yaml @@ -0,0 +1,25 @@ +settings: + increment: 100 # how much each tier increases by + start: 100 # starting amount see tier's comment + tiers: 5 # 5 tiers 100, 200, 300, 400, 500 based on above settings + dailyDouble: + enabled: true # should daily double be enabled? + minQuestions: 7 # how many questions need to be answered before a daily double can appear? + count: 2 # how many daily doubles should there be? +categories: # the name of each category should correspond to a yaml file with the same name + - name: shounen # name must be alphanumeric, cannot start with a number or contain special characters "-" and "." are ok if they are not at the beginning + displayName: "Shounen" # if blank will use name + image: "assets/14018-3193093789.gif" # if set will display image instead of name, categories are referenced by name which is required + - name: openings + displayName: "Openings" # if blank will use name + - name: sadness + displayName: "Sadness" # if blank will use name + - name: random + displayName: Random # if blank will use name + - name: you-would-have-though + displayName: "You would have\nthought" # if blank will use name + - name: wtf-is-that + displayName: "Wtf is that!?" # if blank will use name + - name: owo + displayName: "owo" # if blank will use name + # image: "assets/anime-gif-boobs-funny-2710211137.gif" diff --git a/anime/shounen.yaml b/anime/shounen.yaml new file mode 100755 index 0000000..21f6617 --- /dev/null +++ b/anime/shounen.yaml @@ -0,0 +1,38 @@ +questions: # expects a list equal to the tier + - title: "Name at least 10 of the characters here" + time-limit: 60 # If omitted there is no time limit + answer: 10 + image: "anime/assets/shounen_name_characters.png" + # Types: builtin + # - WhatIs (default), + # - MultipleChoice, + # - GuessSound (.wav, .mp3, .ogg, .oga, .ogv), + # - GuessImage (.png, jpg), + # - GuessVideo (.ogv) + template: "one-image" # template to use + - title: "2+2=" + time-limit: 10 # If omitted there is no time limit + answer: 4 + imageA: "" + imageB: "" + template: "imageword" # template to use + - title: "4+4=" + time-limit: 10 # If omitted there is no time limit + answer: 8 + # Types: builtin + # - WhatIs (default), + # - MultipleChoice, + # - GuessSound (.wav, .mp3, .ogg, .oga, .ogv), + # - GuessImage (.png, jpg), + # - GuessVideo (.ogv) + template: "whatis" # template to use + - title: "8+8=" + time-limit: 10 # If omitted there is no time limit + answer: 16 + # Types: builtin + # - WhatIs (default), + # - MultipleChoice, + # - GuessSound (.wav, .mp3, .ogg, .oga, .ogv), + # - GuessImage (.png, jpg), + # - GuessVideo (.ogv) + template: "whatis" # template to use \ No newline at end of file diff --git a/anime/templates/imageword.lua b/anime/templates/imageword.lua new file mode 100644 index 0000000..2d16672 --- /dev/null +++ b/anime/templates/imageword.lua @@ -0,0 +1,70 @@ +--[[ Constants + * (all lua builtins that don't allow io/executing code) + color (interface) + gui (interface) + multi (interface bound to a processor) no thread module + + callback true/false correct/wrong +]] +local label +local imageHolder +local imageHolder2 + +local function index(window, q, callback) + frame = window:newFrame(0,0,0,-200,0,.2,1,.8) + frame.visibility = 0 + label = window:newTextLabel(" " ..q.title.. " ",0,0,0,0,0,0,1,.2) + label.align = ALIGN_CENTER + label.textColor = color.white + label.color = color.new("#060ce9") + + if not q.imageA or q.imageA == "" then + error("Missing 'imageA' field for question!") + end + + if not q.imageB or q.imageB == "" then + error("Missing 'imageB' field for question!") + end + + imageHolder = frame:newImageLabel(q.imageA) + imageHolder:setAspectSize(imageHolder.imageWidth,imageHolder.imageHeight) + imageHolder:setDualDim(0,0,imageHolder.imageWidth,imageHolder.imageHeight) + + imageHolder2 = frame:newImageLabel(q.imageB) + imageHolder2:setAspectSize(imageHolder2.imageWidth,imageHolder2.imageHeight) + imageHolder2:setDualDim(0,0,imageHolder2.imageWidth,imageHolder2.imageHeight) + + local correct = window:newTextButton("Correct",0,-200,0,100,0,1,.5) + correct.color = color.new("#52b11b") + local wrong = window:newTextButton("Wrong",0,-200,0,100,.5,1,.5) + wrong.color = color.new("#bd2626") + local skip = window:newTextButton("Skip",0,-100,0,100,.25,1,.5) + skip.color = color.new("#5d5d5d") + window.apply({ + centerX = {true}, + centerY = {true}, + },imageHolder, imageHolder2) + window.apply({ + fitFont={}, + align=window.ALIGN_CENTER, + OnReleased=function(self) + if self.text == "Skip" then + callback() + return + end + callback(self.text == "Correct") + end, + },correct,wrong,skip) +end + +local function update(dt) -- time in seconds that has passed since + _,_,w,h = imageHolder.parent:getAbsolutes() + local x,y,w,h = imageHolder:GetSizeAdjustedToAspectRatio(w,h) + imageHolder:setDualDim(w,h,x,y) + label:fitFont() +end + +return { + index = index, + update = update +} diff --git a/assets/14018-3193093789.gif b/assets/14018-3193093789.gif new file mode 100755 index 0000000..57bfe16 Binary files /dev/null and b/assets/14018-3193093789.gif differ diff --git a/assets/anime-gif-boobs-funny-2710211137.gif b/assets/anime-gif-boobs-funny-2710211137.gif new file mode 100755 index 0000000..2207195 Binary files /dev/null and b/assets/anime-gif-boobs-funny-2710211137.gif differ diff --git a/assets/anime-girls-boobs-minimalism-1455325-1259150827.jpg b/assets/anime-girls-boobs-minimalism-1455325-1259150827.jpg new file mode 100755 index 0000000..841d395 Binary files /dev/null and b/assets/anime-girls-boobs-minimalism-1455325-1259150827.jpg differ diff --git a/assets/convert.lua b/assets/convert.lua new file mode 100755 index 0000000..09b03bd --- /dev/null +++ b/assets/convert.lua @@ -0,0 +1,169 @@ +local json = require("json") +function tprint (tbl, indent) + if not indent then indent = 0 end + for k, v in pairs(tbl) do + formatting = string.rep(" ", indent) .. k .. ": " + if type(v) == "table" then + print(formatting) + tprint(v, indent+1) + else + print(formatting .. tostring(v)) + end + end +end + +function ListItems(dir) + if Dir_Exist(dir) then + temp=List_Files(dir) -- current directory if blank + if GetDirectory(dir)=="C:\\\n" then + a,b=string.find(temp,"C:\\",1,true) + a=a+2 + else + a,b=string.find(temp,"..",1,true) + end + temp=string.sub(temp,a+2) + list=StringLineToTable(temp) + temp=string.sub(temp,1,list[#list-2]) + slist=lines(temp) + table.remove(slist,1) + table.remove(slist,#slist) + temp={} + temp2={} + for i=1,#slist do + table.insert(temp,string.sub(slist[i],40,-1)) + end + return temp + else + print("Directory does not exist") + return nil + end +end + +function lines(str) + local t = {} + local function helper(line) table.insert(t, line) return "" end + helper((str:gsub("(.-)\r?\n", helper))) + return t +end + +function StringLineToTable(s) + local t = {} -- table to store the indices + local i = 0 + while true do + i = string.find(s, "\n", i+1) -- find 'next' newline + if i == nil then return t end + table.insert(t, i) + end +end + +function GetDirectory(dir,flop) + s=List_Files(dir) + drive=string.sub(string.match(s,"drive.."),-1) + local t = {} -- table to store the indices + local i = 0 + while true do + i = string.find(s, "\n", i+1) -- find 'next' newline + if i == nil then + a,b=string.find(s,drive..":\\",1,true) + main = string.gsub(string.sub(s,a,t[4]), "\n", "") + if flop then + main=main:gsub("%\\", "/") + end + return main + end + table.insert(t, i) + end +end + +function List_Files(dir) + if not(dir) then dir="" end + local f = io.popen("dir \""..dir.."\"") + if f then + return f:read("*a") + else + print("failed to read") + end +end + +function GetFiles(dir) + local temp2={} + local dirs=ListItems(dir) + for i=1,#dirs do + if Dir_Exist(string.gsub(GetDirectory(dir).."\\"..dirs[i], "\n", "")) then + else + table.insert(temp2,dirs[i]) + end + end + return temp2 +end + +function Dir_Exist(strFolderName) + local fileHandle, strError = io.open(strFolderName.."\\*.*","r") + if fileHandle ~= nil then + io.close(fileHandle) + return true + else + if string.match(strError,"No such file or directory") then + return false + else + return true + end + end +end + +function GetDirectories(dir) + temp2={} + dirs=ListItems(dir) + for i=1,#dirs do + if Dir_Exist(string.gsub(GetDirectory(dir).."\\"..dirs[i], "\n", "")) then + table.insert(temp2,dirs[i]) + end + end + return temp2 +end + +local id = 0 + +local cards = {} +for _, dir in pairs(GetDirectories("cards")) do + for _, card in pairs(GetFiles("cards/"..dir)) do + id = id + 1 + print("Processing card: cards/"..dir.."/"..card) + table.insert(cards,{ + ID = id, + UUID = "", -- Generated during game + Name = (card:gsub("_"," "):gsub(".png","")), + Source = "assets/cards/" .. dir .. "/" .. card, + Count = 0, + Default_Pack = dir, + Rune_Cost = 0, + Attack_Cost = 0, + Insight_Cost = 0, + Night = false, + Day = false, + Honor = 0, + Type = { + Base = "Hero/Construct/Monster/Dreamscape/Temple", + Faction = "Mechana,Void,Enlightened,Lifebound", + Token = false, + Ongoing = false, + Transformation_Source = "", + }, + Unbanishable = false, + Effects = { + "Gain(#,'Rune,Attack,Insight,Honor',target)", + "Draw(#)", + "Shuffle(#)", + "Discard(#, target)", + "AdjustCost(#,'Rune,Attack,Insight',target)", + "Aquire(#,'Rune,Attack,Insight')", + "XasX('Rune,Attack,Insight', 'Rune,Attack,Insight')", + "Temple(type)", + "Banish(#, target)", + "TopDeck(#,target)" + } + }) + end +end + +json.encode_file("card_data/cards.json", cards) \ No newline at end of file diff --git a/assets/final-jeopardy-1639404433135-3347818171.jpeg b/assets/final-jeopardy-1639404433135-3347818171.jpeg new file mode 100755 index 0000000..3803fea Binary files /dev/null and b/assets/final-jeopardy-1639404433135-3347818171.jpeg differ diff --git a/assets/json.lua b/assets/json.lua new file mode 100755 index 0000000..c90273f --- /dev/null +++ b/assets/json.lua @@ -0,0 +1,400 @@ +-- +-- json.lua +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + +function json.encode_file(file, val) + local f = io.open(file, "w") + f:write(json.encode(val)) + f:close() +end + +function json.decode_file(file) + local f = io.open(file, "r") + local str = f:read("*a") + f:close() + return json.decode(str) +end + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then -- `\`: Escape + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then -- `"`: End of string + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json \ No newline at end of file diff --git a/assets/placeholder.jpg b/assets/placeholder.jpg new file mode 100755 index 0000000..9e9fdf2 Binary files /dev/null and b/assets/placeholder.jpg differ diff --git a/board.lua b/board.lua new file mode 100644 index 0000000..4b69e17 --- /dev/null +++ b/board.lua @@ -0,0 +1,176 @@ +local gui = require("gui") +local color = require("gui.core.color") +local loader = require("loader") +local theme = require("gui.core.theme") +local fmt = require("fmt") +local timer = require("utils") +local multi, thread = require("multi"):init() + +local timesup = love.audio.newSource("timesup.mp3", "static") +local double = love.audio.newSource("double.mp3", "static") + +local boardUpdater = gui:getProcessor():newProcessor("board-updater") +boardUpdater.Start() + +-- Create a table to manage GUI elements +local manage = {} + +-- Function to resize fonts for all managed elements +local function resizeFonts() + -- Use multi-threading to improve performance + multi:newThread(function() + -- Introduce a small delay to avoid too many iterations at once + thread.skip(2) + -- Iterate over each element in the 'manage' table + for i = 1, #manage do + local elem = manage[i] + + -- Check if the current element has a centerFont property + if elem.centerFont then + -- Adjust font size and re-center the text + elem:fitFont(nil, nil, {scale = 2/3}) + elem:centerFont() + end + end + end) +end + +gui.Events.OnResized(resizeFonts) + +local function buildBoard(frame, path) + local data = loader:new(path) + index = data.index + local board, question = frame:newFrame(), frame:newFrame() + board:fullFrame() + question:fullFrame() + question.visible = false + question.color = color.new("#060ce9") + local tiers = index.settings.tiers or 5 + local start = index.settings.start or 100 + local inc = index.settings.increment or 100 + for cat,v in pairs(index.categories) do + local c + if v.image then + c = board:newImageLabel(v.image, 0, 0, 0, 0,(1/#index.categories)*(cat-1),0,1/#index.categories,1/(tiers+1)) + else + c = board:newTextLabel(v.displayName or v.name,0,0,0,0,(1/#index.categories)*(cat-1),0,1/#index.categories,1/(tiers+1)) + c.align = gui.ALIGN_CENTER + c.textColor = color.new("#ffffff") + c.color = color.new("#060ce9") + end + + img = c:newImageButton("assets/placeholder.jpg") + img.visibility = 0 + + img:OnReleased(function(self) + self.visible = false + end) + + img:fullFrame() + table.insert(manage,c) + for tier = 1,tiers do + local t = board:newTextButton("$" .. start + inc*(tier-1),0,0,0,0,(1/#index.categories)*(cat-1),1/(tiers+1)*tier,(1/#index.categories),1/(tiers+1)) + t.textColor = color.new("#9b9024") + t.align = gui.ALIGN_CENTER + t.color = color.new("#060ce9") + t.category = v.name + t.index = tier + t.price = start + inc*(tier-1) + t:OnReleased(boardUpdater:newFunction(function(self) + if self.text == "" then return end + if index.categories[cat].questions == nil then fmt.Printf("Question not defined: File: %v Category: %v - %v\n",path,index.categories[cat].name,start + inc*(tier-1)) return end + local q = index.categories[cat].questions[self.index] + if q == nil then fmt.Printf("Question contains no data: File: %v Category: %v Tier: %v\n",path,index.categories[cat].name,start + inc*(tier-1)) return end + self.textVisibility = 0 + local template = LoadTemplate(q.template, path) + question.visible = true + local player = GetActivePlayer() + local tm + local stop + fmt.Printf("Question: %v \nAnswer: %v\n",q["title"],q["answer"]) + if q["time-limit"] then + tm = timer.startTimer({duration = q["time-limit"]}) + tm.OnStop(function() + -- Make sound? Subtract if daily double + if stop then return end + timesup:play() + end) + end + template.index(question, q, function(ans) + player = GetActivePlayer() + tm:Cleanup() + stop = true + if ans then + player:Add(self.price) + question.visible = false + elseif ans == false then + player:Add(-self.price) + else + question.visible = false + end -- nil is a valid option where you weren't right or wrong, you just skipped + self.text = "" + end) + boardUpdater:newThread("QuestionUpdater",function() + while true do + template.update(dt) + thread.yield() + if self.text == "" then return end + end + end) + end)) + table.insert(manage,t) + end + end + resizeFonts() + resizeFonts() +end + +local qUpdater = gui:getProcessor():newProcessor("question-updater") +qUpdater.Start() + +function LoadTemplate(name, path) + + if love.filesystem.getInfo("templates/" .. name .. ".lua") then + data = love.filesystem.read("templates/" .. name .. ".lua") + elseif love.filesystem.getInfo(path .."/templates/" .. name .. ".lua") then + data = love.filesystem.read(path .."/templates/" .. name .. ".lua") + end + + local template, err = loadstring(data) + + if err ~= nil then + error(err) + end + + local env = { -- lua built-ins except io/os code execution + pairs=pairs, + print=print, + tostring=tostring, + tonumber=tonumber, + type=type, + assert=assert, + ipairs=ipairs, + table=table, + math=math, + string=string, + next=next, + pcall=pcall, + select=select, + xpcall=xpcall, + utf8=utf8, + -- Global vars + color = color, + error = multi.error, + theme = theme, + gui = gui + } + + env._G = env + local isoTemplate = multi.isolateFunction(template, env) + + return isoTemplate() +end + +return { + buildBoard = buildBoard +} \ No newline at end of file diff --git a/conf.lua b/conf.lua new file mode 100755 index 0000000..3c4c566 --- /dev/null +++ b/conf.lua @@ -0,0 +1,38 @@ +function love.conf(t) + t.identity = nil -- The name of the save directory (string) + t.version = "11.5" -- The LOVE version this game was made for (string) + t.console = true -- Attach a console (boolean, Windows only) + + t.window.title = "GuiManagerTest" -- The window title (string) + t.window.icon = nil -- Filepath to an image to use as the window's icon (string) + t.window.width = 1920 -- The window width (number) + t.window.height = 1080 -- The window height (number) + t.window.borderless = false -- Remove all border visuals from the window (boolean) + t.window.resizable = true -- Let the window be user-resizable (boolean) + t.window.minwidth = 1 -- Minimum window width if the window is resizable (number) + t.window.minheight = 1 -- Minimum window height if the window is resizable (number) + t.window.fullscreen = false -- Enable fullscreen (boolean) + t.window.fullscreentype = "desktop" -- Standard fullscreen or desktop fullscreen mode (string) + t.window.vsync = false -- Enable vertical sync (boolean) + t.window.fsaa = 4 -- The number of samples to use with multi-sampled antialiasing (number) + t.window.display = 1 -- Index of the monitor to show the window in (number) + t.window.highdpi = true -- Enable high-dpi mode for the window on a Retina display (boolean) + t.window.srgb = false -- Enable sRGB gamma correction when drawing to the screen (boolean) + t.window.x = nil -- The x-coordinate of the window's position in the specified display (number) + t.window.y = nil -- The y-coordinate of the window's position in the specified display (number) + + t.modules.audio = true -- Enable the audio module (boolean) + t.modules.event = true -- Enable the event module (boolean) + t.modules.graphics = true -- Enable the graphics module (boolean) + t.modules.image = true -- Enable the image module (boolean) + t.modules.joystick = true -- Enable the joystick module (boolean) + t.modules.keyboard = true -- Enable the keyboard module (boolean) + t.modules.math = true -- Enable the math module (boolean) + t.modules.mouse = true -- Enable the mouse module (boolean) + t.modules.physics = true -- Enable the physics module (boolean) + t.modules.sound = true -- Enable the sound module (boolean) + t.modules.system = true -- Enable the system module (boolean) + t.modules.timer = true -- Enable the timer module (boolean) + t.modules.window = true -- Enable the window module (boolean) + t.modules.thread = true -- Enable the thread module (boolean) +end diff --git a/double.mp3 b/double.mp3 new file mode 100644 index 0000000..73bf4a4 Binary files /dev/null and b/double.mp3 differ diff --git a/fmt.lua b/fmt.lua new file mode 100644 index 0000000..56e272c --- /dev/null +++ b/fmt.lua @@ -0,0 +1,355 @@ +--[[ + fmt.lua - Go's fmt.Printf() style formatting for Lua + + Supported verbs: + %v - default format (tostring) + %T - type of the value + %t - boolean (true/false) + %d - integer (decimal) + %b - integer (binary) + %o - integer (octal) + %x - integer (hex, lowercase) + %X - integer (hex, uppercase) + %e - float (scientific, lowercase) + %E - float (scientific, uppercase) + %f - float (decimal point) + %g - float (%e for large, %f otherwise) + %G - float (%E for large, %f otherwise) + %s - string + %q - quoted string (Lua-escaped) + %c - character (from integer codepoint) + %% - literal percent sign + + Width & precision: + %8d - right-aligned, width 8 + %-8d - left-aligned, width 8 + %08d - zero-padded, width 8 + %8.2f - width 8, 2 decimal places + %.5s - truncate string to 5 chars + %*d - width from next argument + %.*f - precision from next argument +--]] + +local fmt = {} + +-- ── helpers ────────────────────────────────────────────────────────────────── + +local function to_int(v) + local n = math.tointeger and math.tointeger(v) or (type(v) == "number" and math.floor(v) or nil) + if n == nil then + error(string.format("fmt: expected integer, got %s (%s)", type(v), tostring(v)), 3) + end + return n +end + +local function to_num(v) + if type(v) ~= "number" then + error(string.format("fmt: expected number, got %s (%s)", type(v), tostring(v)), 3) + end + return v +end + +-- Convert integer to binary string +local function to_binary(n) + n = to_int(n) + if n == 0 then return "0" end + local neg = n < 0 + if neg then n = -n end + local bits = {} + while n > 0 do + table.insert(bits, 1, n % 2) + n = math.floor(n / 2) + end + local s = table.concat(bits) + return neg and "-" .. s or s +end + +-- Apply width/alignment padding +local function apply_width(s, width, left_align, zero_pad, is_numeric) + if not width or width == 0 or #s >= width then return s end + local pad_char = (zero_pad and is_numeric) and "0" or " " + local pad = string.rep(pad_char, width - #s) + if zero_pad and is_numeric then + -- keep sign before zeros: "-007" + if s:sub(1,1) == "-" then + return "-" .. pad .. s:sub(2) + end + return pad .. s + end + return left_align and (s .. pad) or (pad .. s) +end + +-- Apply precision to a string (truncation) +local function apply_str_precision(s, prec) + if prec and #s > prec then + return s:sub(1, prec) + end + return s +end + +-- Quoted / escaped string (like Go %q) +local function quote_string(s) + s = s:gsub('\\', '\\\\') + s = s:gsub('"', '\\"') + s = s:gsub('\n', '\\n') + s = s:gsub('\r', '\\r') + s = s:gsub('\t', '\\t') + s = s:gsub('%z', '\\0') + return '"' .. s .. '"' +end + +-- ── core formatter ──────────────────────────────────────────────────────────── + +--[[ + fmt.Sprintf(format, ...) → string + Returns the formatted string without printing it. +--]] +function fmt.Sprintf(format, ...) + local args = { ... } + local ai = 1 -- argument index + local out = {} -- output buffer + + local i = 1 + local len = #format + + while i <= len do + local c = format:sub(i, i) + + if c ~= "%" then + out[#out+1] = c + i = i + 1 + else + i = i + 1 + if i > len then + error("fmt: trailing '%' in format string", 2) + end + + -- %% literal + if format:sub(i, i) == "%" then + out[#out+1] = "%" + i = i + 1 + else + -- ── parse flags ─────────────────────────────────────────── + local left_align = false + local zero_pad = false + local plus_sign = false + local space_sign = false + local hash_flag = false + + while i <= len do + local f = format:sub(i, i) + if f == "-" then left_align = true + elseif f == "0" then zero_pad = true + elseif f == "+" then plus_sign = true + elseif f == " " then space_sign = true + elseif f == "#" then hash_flag = true + else break + end + i = i + 1 + end + + -- ── parse width ─────────────────────────────────────────── + local width = nil + if i <= len and format:sub(i, i) == "*" then + width = to_int(args[ai]); ai = ai + 1 + i = i + 1 + else + local w = format:match("^%d+", i) + if w then width = tonumber(w); i = i + #w end + end + + -- ── parse precision ─────────────────────────────────────── + local prec = nil + if i <= len and format:sub(i, i) == "." then + i = i + 1 + if i <= len and format:sub(i, i) == "*" then + prec = to_int(args[ai]); ai = ai + 1 + i = i + 1 + else + local p = format:match("^%d*", i) + prec = tonumber(p) or 0 + i = i + #p + end + end + + -- ── parse verb ──────────────────────────────────────────── + if i > len then + error("fmt: missing verb in format string", 2) + end + local verb = format:sub(i, i) + i = i + 1 + + local arg = args[ai]; ai = ai + 1 + local s = "" + local is_numeric = false + + -- %v default + if verb == "v" then + s = tostring(arg) + + -- %T type + elseif verb == "T" then + s = type(arg) + + -- %t boolean + elseif verb == "t" then + s = arg and "true" or "false" + + -- %d decimal integer + elseif verb == "d" then + is_numeric = true + local n = to_int(arg) + s = tostring(math.abs(n)) + if prec then s = string.rep("0", math.max(0, prec - #s)) .. s end + if n < 0 then s = "-" .. s + elseif plus_sign then s = "+" .. s + elseif space_sign then s = " " .. s + end + + -- %b binary + elseif verb == "b" then + is_numeric = true + s = to_binary(arg) + if hash_flag then s = "0b" .. s end + + -- %o octal + elseif verb == "o" then + is_numeric = true + local n = to_int(arg) + local neg = n < 0 + s = string.format("%o", math.abs(n)) + if prec then s = string.rep("0", math.max(0, prec - #s)) .. s end + if hash_flag and s:sub(1,1) ~= "0" then s = "0" .. s end + if neg then s = "-" .. s end + + -- %x hex lowercase + elseif verb == "x" then + is_numeric = true + local n = to_int(arg) + local neg = n < 0 + s = string.format("%x", math.abs(n)) + if prec then s = string.rep("0", math.max(0, prec - #s)) .. s end + if hash_flag then s = "0x" .. s end + if neg then s = "-" .. s end + + -- %X hex uppercase + elseif verb == "X" then + is_numeric = true + local n = to_int(arg) + local neg = n < 0 + s = string.format("%X", math.abs(n)) + if prec then s = string.rep("0", math.max(0, prec - #s)) .. s end + if hash_flag then s = "0X" .. s end + if neg then s = "-" .. s end + + -- %e scientific lowercase + elseif verb == "e" then + is_numeric = true + local p = prec ~= nil and prec or 6 + s = string.format("%." .. p .. "e", to_num(arg)) + if plus_sign and arg >= 0 then s = "+" .. s end + + -- %E scientific uppercase + elseif verb == "E" then + is_numeric = true + local p = prec ~= nil and prec or 6 + s = string.format("%." .. p .. "E", to_num(arg)) + if plus_sign and arg >= 0 then s = "+" .. s end + + -- %f decimal float + elseif verb == "f" then + is_numeric = true + local p = prec ~= nil and prec or 6 + s = string.format("%." .. p .. "f", to_num(arg)) + if plus_sign and arg >= 0 then s = "+" .. s end + if space_sign and arg >= 0 then s = " " .. s end + + -- %g / %G shortest float representation + elseif verb == "g" or verb == "G" then + is_numeric = true + local p = prec ~= nil and prec or -1 + local n = to_num(arg) + local abs_n = math.abs(n) + if p == -1 then + -- mimic Go: use %e if exponent < -4 or >= precision(default 6) + if abs_n ~= 0 and (abs_n < 1e-4 or abs_n >= 1e6) then + s = string.format(verb == "G" and "%.6E" or "%.6e", n) + else + s = string.format("%.6g", n) + if verb == "G" then s = s:upper() end + end + else + s = string.format("%." .. p .. (verb == "G" and "G" or "g"), n) + end + if plus_sign and n >= 0 then s = "+" .. s end + + -- %s string + elseif verb == "s" then + s = tostring(arg) + s = apply_str_precision(s, prec) + + -- %q quoted string + elseif verb == "q" then + s = quote_string(tostring(arg)) + + -- %c character + elseif verb == "c" then + local n = to_int(arg) + -- utf8.char available in Lua 5.3+ + if utf8 then + s = utf8.char(n) + else + s = string.char(n) -- ASCII only fallback + end + + else + error(string.format("fmt: unknown verb %%%s", verb), 2) + end + + -- Apply width padding + s = apply_width(s, width, left_align, zero_pad, is_numeric) + out[#out+1] = s + end + end + end + + return table.concat(out) +end + +--[[ + fmt.Printf(format, ...) + Prints to stdout (no trailing newline, just like Go). +--]] +function fmt.Printf(format, ...) + io.write(fmt.Sprintf(format, ...)) +end + +--[[ + fmt.Println(...) + Prints args separated by spaces with a trailing newline. +--]] +function fmt.Println(...) + local parts = {} + for i = 1, select("#", ...) do + parts[i] = tostring(select(i, ...)) + end + print(table.concat(parts, " ")) +end + +--[[ + fmt.Fprintf(file, format, ...) + Writes formatted output to a file handle. +--]] +function fmt.Fprintf(file, format, ...) + file:write(fmt.Sprintf(format, ...)) +end + +--[[ + fmt.Errorf(format, ...) → string + Returns a formatted error string (for use with error()). +--]] +function fmt.Errorf(format, ...) + return fmt.Sprintf(format, ...) +end + +return fmt diff --git a/font/gyparody.ttf b/font/gyparody.ttf new file mode 100755 index 0000000..e28477f Binary files /dev/null and b/font/gyparody.ttf differ diff --git a/gui/README.md b/gui/README.md new file mode 100755 index 0000000..149e2f3 --- /dev/null +++ b/gui/README.md @@ -0,0 +1,40 @@ +# GuiManager + +This library due to the changes in love2d. Too many things are broken and instead of doing patch work, I've decided to do a total rewrite. Also I'll be able to make use of the new multi manager features and build a better library from the ground up. + +Core Objects: +- ~~Frame~~ ✔️ +- Text: + - ~~Label~~ ✔️ + - ~~Box~~ ✔️ + - ~~Button~~ ✔️ + - utf8 support with textbox (Forgot about this, will have to rework some things) +- Image: + - ~~Label~~ ✔️ + - ~~Button~~ ✔️ + - Animation +- ~~Video~~ ✔️ + +Events: +- Mouse Events + - ~~Enter~~ ✔️ + - ~~Exit~~ ✔️ + - ~~Pressed~~ ✔️ + - ~~Released~~ ✔️ + - ~~Moved~~ ✔️ + - ~~WheelMoved~~ ✔️ + - ~~DragStart~~ ✔️ + - ~~Dragging~~ ✔️ + - ~~DragEnd~~ ✔️ +- Keyboard Events + - ~~Hotkey~~ ✔️ Refer to [KeyConstants](https://love2d.org/wiki/KeyConstant) wiki page + - Some default hotkeys have been added: + - ~~(conn)gui.HotKeys.OnSelectAll~~ ✔️ `Ctrl + A` + - ~~(conn)gui.HotKeys.OnCopy~~ ✔️ `Ctrl + C` + - ~~(conn)gui.HotKeys.OnPaste~~ ✔️ `Ctrl + V` + - ~~(conn)gui.HotKeys.OnUndo~~ ✔️ `Ctrl + Z` + - ~~(conn)gui.HotKeys.OnRedo~~ ✔️ `Ctrl + Y, Ctrl + Shift + Z` +- Other Events + - ~~OnUpdate~~ ✔️ + - ~~OnDraw~~ ✔️ + diff --git a/gui/addons/gifloader.lua b/gui/addons/gifloader.lua new file mode 100755 index 0000000..6b57bdb --- /dev/null +++ b/gui/addons/gifloader.lua @@ -0,0 +1,458 @@ +-- GIF Loader for Love2D with LZW Decompression +-- Note: love.data.compress/decompress don't support LZW, so we implement it + +local GifLoader = {} + +-- Pure Lua LZW decompression for GIF +local function decompressLZW(data, minCodeSize) + local clearCode = 2 ^ minCodeSize + local endCode = clearCode + 1 + local nextCode = endCode + 1 + local codeSize = minCodeSize + 1 + + local dict = {} + for i = 0, clearCode - 1 do + dict[i] = {string.byte(string.char(i))} + end + + local output = {} + local bits = 0 + local bitBuffer = 0 + local pos = 1 + local prevCode = nil + + local function readCode() + while bits < codeSize do + if pos > #data then return nil end + bitBuffer = bitBuffer + bit.lshift(string.byte(data, pos), bits) + bits = bits + 8 + pos = pos + 1 + end + + local code = bit.band(bitBuffer, bit.lshift(1, codeSize) - 1) + bitBuffer = bit.rshift(bitBuffer, codeSize) + bits = bits - codeSize + return code + end + + local first = true + + while true do + local code = readCode() + if not code or code == endCode then break end + + if code == clearCode then + dict = {} + for i = 0, clearCode - 1 do + dict[i] = {string.byte(string.char(i))} + end + nextCode = endCode + 1 + codeSize = minCodeSize + 1 + prevCode = nil + first = true + else + local entry + if dict[code] then + entry = dict[code] + elseif code == nextCode and prevCode then + -- Special case: code not in dict yet + entry = {} + for i = 1, #dict[prevCode] do + entry[i] = dict[prevCode][i] + end + entry[#entry + 1] = dict[prevCode][1] + else + -- Invalid code, stop + break + end + + -- Output the entry + for i = 1, #entry do + table.insert(output, entry[i]) + end + + -- Add new entry to dictionary + if not first and prevCode and nextCode < 4096 then + local newEntry = {} + for i = 1, #dict[prevCode] do + newEntry[i] = dict[prevCode][i] + end + newEntry[#newEntry + 1] = entry[1] + dict[nextCode] = newEntry + nextCode = nextCode + 1 + + -- Increase code size when needed + if nextCode >= bit.lshift(1, codeSize) and codeSize < 12 then + codeSize = codeSize + 1 + end + end + + prevCode = code + first = false + end + end + + -- Convert output bytes to string + local result = {} + for i = 1, #output do + result[i] = string.char(output[i]) + end + return table.concat(result) +end + +function GifLoader.load(filepath) + local fileData = love.filesystem.read(filepath) + if not fileData then + error("Could not read GIF file: " .. filepath) + end + + local gif = { + frames = {}, + frameData = {}, -- Store ImageData for frame composition + delays = {}, + currentFrame = 1, + timer = 0, + width = 0, + height = 0, + playing = true, + loop = true, + getWidth = function(self) + return self.width + end, + getHeight = function(self) + return self.height + end, + } + + -- Parse GIF header + local header = fileData:sub(1, 6) + if header ~= "GIF87a" and header ~= "GIF89a" then + error("Not a valid GIF file") + end + + -- Read logical screen descriptor + local pos = 7 + gif.width = string.byte(fileData, pos) + string.byte(fileData, pos + 1) * 256 + gif.height = string.byte(fileData, pos + 2) + string.byte(fileData, pos + 3) * 256 + + local packed = string.byte(fileData, pos + 4) + local hasGlobalColorTable = bit.band(packed, 0x80) ~= 0 + local backgroundColorIndex = string.byte(fileData, pos + 5) + + pos = pos + 7 + + -- Read global color table + local globalColorTable = {} + if hasGlobalColorTable then + local size = 2 ^ (bit.band(packed, 0x07) + 1) + for i = 1, size do + local r = string.byte(fileData, pos) / 255 + local g = string.byte(fileData, pos + 1) / 255 + local b = string.byte(fileData, pos + 2) / 255 + table.insert(globalColorTable, {r, g, b, 1}) + pos = pos + 3 + end + end + + -- Parse blocks + local delay = 0.1 + local transparentIndex = nil + local disposalMethod = 0 + local delayForNextFrame = 0.1 -- Track delay for next frame + + while pos <= #fileData do + local separator = string.byte(fileData, pos) + + if separator == 0x21 then -- Extension + local label = string.byte(fileData, pos + 1) + pos = pos + 2 + + if label == 0xF9 then -- Graphic Control Extension + local blockSize = string.byte(fileData, pos) + pos = pos + 1 + + local flags = string.byte(fileData, pos) + disposalMethod = bit.rshift(bit.band(flags, 0x1C), 2) + local hasTransparency = bit.band(flags, 0x01) ~= 0 + + local delayTime = string.byte(fileData, pos + 1) + string.byte(fileData, pos + 2) * 256 + -- GIF delay is in hundredths of a second, convert to seconds + -- Many GIFs use 0 or very small delays, set a minimum + if delayTime == 0 then + delayForNextFrame = 0.1 -- Default 100ms + elseif delayTime <= 2 then + delayForNextFrame = 0.02 -- Minimum 20ms for very fast animations + else + delayForNextFrame = delayTime / 100 + end + + if hasTransparency then + transparentIndex = string.byte(fileData, pos + 3) + else + transparentIndex = nil + end + + pos = pos + blockSize + 1 + else + -- Skip other extensions + repeat + local blockSize = string.byte(fileData, pos) + pos = pos + 1 + if blockSize > 0 then + pos = pos + blockSize + end + until blockSize == 0 + end + + elseif separator == 0x2C then -- Image Descriptor + pos = pos + 1 + + local left = string.byte(fileData, pos) + string.byte(fileData, pos + 1) * 256 + local top = string.byte(fileData, pos + 2) + string.byte(fileData, pos + 3) * 256 + local width = string.byte(fileData, pos + 4) + string.byte(fileData, pos + 5) * 256 + local height = string.byte(fileData, pos + 6) + string.byte(fileData, pos + 7) * 256 + + local imgPacked = string.byte(fileData, pos + 8) + local hasLocalColorTable = bit.band(imgPacked, 0x80) ~= 0 + local interlaced = bit.band(imgPacked, 0x40) ~= 0 + + pos = pos + 9 + + local colorTable = globalColorTable + if hasLocalColorTable then + local size = 2 ^ (bit.band(imgPacked, 0x07) + 1) + colorTable = {} + for i = 1, size do + local r = string.byte(fileData, pos) / 255 + local g = string.byte(fileData, pos + 1) / 255 + local b = string.byte(fileData, pos + 2) / 255 + table.insert(colorTable, {r, g, b, 1}) + pos = pos + 3 + end + end + + -- Read LZW minimum code size + local minCodeSize = string.byte(fileData, pos) + pos = pos + 1 + + -- Read compressed image data blocks + local compressedData = {} + while true do + local blockSize = string.byte(fileData, pos) + pos = pos + 1 + if blockSize == 0 then break end + table.insert(compressedData, fileData:sub(pos, pos + blockSize - 1)) + pos = pos + blockSize + end + + -- Decompress image data + local indexStream = decompressLZW(table.concat(compressedData), minCodeSize) + + -- Create image data + local imageData = love.image.newImageData(gif.width, gif.height) + + -- Fill with background if first frame + if #gif.frames == 0 and backgroundColorIndex and globalColorTable[backgroundColorIndex + 1] then + local bg = globalColorTable[backgroundColorIndex + 1] + for y = 0, gif.height - 1 do + for x = 0, gif.width - 1 do + imageData:setPixel(x, y, bg[1], bg[2], bg[3], bg[4]) + end + end + elseif #gif.frames > 0 then + -- Copy previous frame if needed + local prevData = gif.frameData[#gif.frameData] + imageData:paste(prevData, 0, 0, 0, 0, gif.width, gif.height) + end + + -- Draw current frame + if indexStream and #indexStream > 0 then + local idx = 1 + + -- Use mapPixel for faster pixel operations + local function setPixels(x, y, r, g, b, a) + if x >= left and x < left + width and y >= top and y < top + height then + local pixelIdx = (y - top) * width + (x - left) + 1 + if pixelIdx <= #indexStream then + local colorIndex = string.byte(indexStream, pixelIdx) + + -- Skip transparent pixels + if transparentIndex == nil or colorIndex ~= transparentIndex then + if colorTable[colorIndex + 1] then + local color = colorTable[colorIndex + 1] + return color[1], color[2], color[3], color[4] + end + end + end + end + return r, g, b, a + end + + -- Only update the region where the frame is located + for y = top, top + height - 1 do + for x = left, left + width - 1 do + local pixelIdx = (y - top) * width + (x - left) + 1 + if pixelIdx <= #indexStream then + local colorIndex = string.byte(indexStream, pixelIdx) + + -- Skip transparent pixels + if transparentIndex == nil or colorIndex ~= transparentIndex then + if colorTable[colorIndex + 1] then + local color = colorTable[colorIndex + 1] + imageData:setPixel(x, y, color[1], color[2], color[3], color[4]) + end + end + end + end + end + end + + table.insert(gif.frameData, imageData) + table.insert(gif.frames, love.graphics.newImage(imageData)) + table.insert(gif.delays, delayForNextFrame) + + -- Reset delay for next frame + delayForNextFrame = 0.1 + + elseif separator == 0x3B then -- Trailer + break + else + pos = pos + 1 + end + end + + if #gif.frames == 0 then + error("No frames found in GIF") + end + + -- Ensure all frames have valid delays + for i = 1, #gif.delays do + if gif.delays[i] <= 0 or gif.delays[i] ~= gif.delays[i] then -- check for 0 or NaN + gif.delays[i] = 0.1 + end + end + + return gif +end + +function GifLoader.Updater(gif, proc) + local wait = function() + return gif.playing or gif.kill + end + proc:newThread("Gif Handler",function() + while true do + -- Only run if not paused + if gif.kill then -- When we want to clean up + thread.kill() + end + thread.hold(wait) + thread.sleep(gif.delays[gif.currentFrame] * 4) + gif.currentFrame = gif.currentFrame + 1 + + if gif.currentFrame > #gif.frames then + if gif.loop then + gif.currentFrame = 1 + else + gif.currentFrame = #gif.frames + gif.playing = false + gif.timer = 0 + end + end + end + end) +end + +function GifLoader.update(gif, dt) + if not gif.playing or #gif.frames <= 1 then return end + + gif.timer = gif.timer + dt + + -- Simple, accurate frame advancement + if gif.timer >= gif.delays[gif.currentFrame] then + -- Subtract the current frame's delay + gif.timer = gif.timer - gif.delays[gif.currentFrame] + + -- Move to next frame + gif.currentFrame = gif.currentFrame + 1 + + if gif.currentFrame > #gif.frames then + if gif.loop then + gif.currentFrame = 1 + else + gif.currentFrame = #gif.frames + gif.playing = false + gif.timer = 0 + end + end + + -- If we've accumulated too much time (lag spike), cap it + if gif.timer > 0.5 then + gif.timer = 0 + end + end +end + +function GifLoader.draw(gif, x, y, r, sx, sy, ox, oy) + if gif.frames[gif.currentFrame] then + love.graphics.draw(gif.frames[gif.currentFrame], x, y, r or 0, sx or 1, sy or 1, ox or 0, oy or 0) + end +end + +function GifLoader.play(gif) + gif.playing = true +end + +function GifLoader.pause(gif) + gif.playing = false +end + +function GifLoader.reset(gif) + gif.currentFrame = 1 + gif.timer = 0 +end + +function GifLoader.setFixedFramerate(gif, fps) + local delay = 1 / fps + for i = 1, #gif.delays do + gif.delays[i] = delay + end +end + +function GifLoader.getInfo(gif) + return { + width = gif.width, + height = gif.height, + frameCount = #gif.frames, + delays = gif.delays, -- Show actual delays for debugging + totalDuration = (function() + local total = 0 + for i = 1, #gif.delays do + total = total + gif.delays[i] + end + return total + end)() + } +end + +return GifLoader + +-- USAGE: +--[[ +local GifLoader = require("gifloader") + +function love.load() + myGif = GifLoader.load("animation.gif") + myGif.loop = true +end + +function love.update(dt) + GifLoader.update(myGif, dt) +end + +function love.draw() + GifLoader.draw(myGif, 100, 100) + + -- Draw scaled + GifLoader.draw(myGif, 300, 100, 0, 2, 2) +end +]] \ No newline at end of file diff --git a/gui/addons/init.lua b/gui/addons/init.lua new file mode 100755 index 0000000..e69de29 diff --git a/gui/addons/players.lua b/gui/addons/players.lua new file mode 100644 index 0000000..b8fe61e --- /dev/null +++ b/gui/addons/players.lua @@ -0,0 +1,43 @@ +local gui = require("gui") +local theme = require("gui.core.theme") +local color = require("gui.core.color") +local multi, thread = require("multi"):init() +require("gui.addons.system") +local proc = gui:newProcessor() +function gui:newVideoPlayer(source, x, y, w, h, sx, sy, sw, sh) + local window = gui:newWindow(x, y, w, h, source, true, theme:new({ + primary = "#000000", + primaryDark = "#10465c", + primaryText = "#ffffff" + })) + local video = window:newVideo(source, 0, 0, 0, 0, 0, .05, 1, .75) + local play_pause = window:newImageButton("gui/assets/play.png",0,0,0,0,.45,.82,0,.175) + local seek = window:newFrame(0,0,0,0,0,.8,1,.015) + seek.color = color.new("#3c434c") + local seeker = seek:newFrame(0,0,0,0,0,.1,0,.8) + seeker.drawBorder = false + seeker.color = color.new("#0c278a") + play_pause.square = "h" + play_pause.isPaused = true + play_pause:OnReleased(function(self) + if self.isPaused then + self:setImage("gui/assets/pause.png") + video:play() + else + self:setImage("gui/assets/play.png") + video:pause() + end + self.isPaused = not self.isPaused + end) + + local length = video:getDuration() + proc:newThread(function() + while true do + thread.yield() + seeker:setDualDim(nil,nil,nil,nil,nil,nil,video:tell()/length) + end + end) + + -- print() + +end diff --git a/gui/addons/probe.lua b/gui/addons/probe.lua new file mode 100755 index 0000000..9a7d56c --- /dev/null +++ b/gui/addons/probe.lua @@ -0,0 +1,115 @@ +--[[ + scheduler_probe.lua + ------------------- + A drop-in replacement for multi:getLoad() based on scheduler tick-slip + rather than step-count benchmarking. + + THEORY + ------ + Schedule a repeating timer at a fixed interval T. Each time it fires, + measure how much later than T it actually arrived. On an idle scheduler + the slip is near zero. Under load the main loop is busy with other tasks + between iterations, so ticks are delayed. + + We express load as: + + lag = actual_interval - target_interval (seconds) + lag_ratio = lag / target_interval (0 = perfect, 1 = 1 full interval late) + load% = clamp(lag_ratio * 100, 0, 100) + + To smooth out single-frame spikes we keep an exponential moving average + (EMA) of lag_ratio with a configurable smoothing factor. + + WHY THIS IS BETTER THAN THE STEP-COUNT APPROACH + ------------------------------------------------ + - No calibration baseline that drifts with object count or warm-up + - No magic exponents or divisors + - Does not block or create temporary objects on each call + - Measures actual scheduler responsiveness, not raw throughput + - Works correctly regardless of which processor calls it + - A single lightweight TLoop is the only permanent overhead + + USAGE + ----- + local probe = require("scheduler_probe") + probe:install(multi) -- once, at startup + + -- anywhere, non-blocking: + local load, lagMs = multi:getLoad() + -- load : integer 0-100 + -- lagMs : smoothed lag in milliseconds (useful for display) + + OPTIONAL PARAMETERS + ------------------- + probe:install(multi, { + interval = 0.05, -- probe fires every N seconds (default 0.05 = 50ms) + alpha = 0.15, -- EMA smoothing factor 0-1 (default 0.15) + -- lower = smoother but slower to react + -- higher = more reactive but noisier + maxLag = 0.5, -- lag value (seconds) that maps to 100% load (default 0.5) + -- tune this to match your target frame budget + }) +]] + +local probe = {} + +-- EMA state — written by the TLoop callback, read by getLoad() +-- Both are plain numbers so Lua's assignment is atomic within one thread. +local _emaRatio = 0 -- smoothed lag / maxLag, clamped 0-1 +local _lagMs = 0 -- smoothed lag in milliseconds for display +local _installed = false + +function probe:install(multi_obj, opts) + if _installed then return end + _installed = true + + opts = opts or {} + local INTERVAL = opts.interval or 0.05 -- seconds between probes + local ALPHA = opts.alpha or 0.15 -- EMA weight for new sample + local MAX_LAG = opts.maxLag or 0.5 -- seconds of lag = 100% load + + local clock = os.clock + + -- Track when the tick *should* have fired so we can compute slip + -- relative to the scheduled time, not relative to the previous firing. + -- This avoids error accumulation over long runs. + local expectedTime = clock() + INTERVAL + + local tloop = multi_obj:newTLoop(nil, INTERVAL) + tloop:setName("SchedulerProbe") + tloop:setPriority("core") -- run as early as possible each frame + + tloop.OnLoop(function(self, life, dt) + local now = clock() + local lag = math.max(0, now - expectedTime) -- never negative + local ratio = math.min(lag / MAX_LAG, 1) -- clamp to [0,1] + + -- Exponential moving average: new = alpha*sample + (1-alpha)*old + _emaRatio = ALPHA * ratio + (1 - ALPHA) * _emaRatio + _lagMs = ALPHA * lag*1000 + (1 - ALPHA) * _lagMs + + -- Advance expected time by one interval from where it *should* have been, + -- not from now — prevents the probe from drifting under sustained load. + expectedTime = expectedTime + INTERVAL + -- If we fall more than one interval behind (e.g. after a long GC pause), + -- re-anchor so we don't fire in a catch-up burst. + if now > expectedTime + INTERVAL then + expectedTime = now + INTERVAL + end + end) + + -- Replace multi:getLoad() with a non-blocking version that just reads the EMA + function multi_obj:getLoad() + local pct = math.ceil(_emaRatio * 100) + return pct, _lagMs + end + + -- Also expose raw probe state for diagnostics + function multi_obj:getSchedulerLag() + return _lagMs, _emaRatio + end + + return tloop +end + +return probe diff --git a/gui/addons/system.lua b/gui/addons/system.lua new file mode 100755 index 0000000..23450ed --- /dev/null +++ b/gui/addons/system.lua @@ -0,0 +1,950 @@ +local multi, thread = require("multi"):init() +local gui = require("gui") +local theme = require("gui.core.theme") +local color = require("gui.core.color") + +local TM_THEME = theme:new({ + primary = "#124559", + primaryDark = "#01161E", + primaryText = "#AEC3B0" +}) + +local default_theme = TM_THEME +-- ── layout constants ────────────────────────────────────────────────────────── +-- Columns: Indent+Name, State, Status, Uptime, Priority, Pause, Kill +-- Name shrunk so data columns have room to breathe +local COL_WIDTHS = { 300, 130, 120, 100, 130, 100, 100, 70 } +local COL_KEYS = { "name", "kind", "state", "status", "uptime", "priority", "pause", "kill" } +local COL_LABELS = { "Name", "Type", "State", "Status", "Uptime", "Priority", "–", "–" } +local SORT_COLS = { "name", "kind", "state", "status", "uptime", "priority" } + +local ROW_H = 28 +local COL_X = {} +do + local acc = 0 + for i, w in ipairs(COL_WIDTHS) do + COL_X[i] = acc + acc = acc + w + end +end +local TOTAL_W = COL_X[#COL_X] + COL_WIDTHS[#COL_WIDTHS] -- 700 + +-- ── thread state names ──────────────────────────────────────────────────────── +local STATE_NAMES = { + [1] = "holding", + [2] = "sleeping", + [3] = "hold+time", + [4] = "skipping", + [5] = "hold+cyc", + [6] = "yielding", + [7] = "running", +} +local function fmtState(obj) + if obj._isPaused then return "paused" end + local t = obj.task + if t == nil then return "running" end + return STATE_NAMES[t] or ("state:"..tostring(t)) +end + +local PRIORITY_NAMES = { + [1] = "Core", + [4] = "V.High", + [16] = "High", + [64] = "Above", + [256] = "Normal", + [1024] = "Below", + [4096] = "Low", + [16384] = "V.Low", + [65536] = "Idle", +} +local PRIORITY_CYCLE = { 1, 4, 16, 64, 256, 1024, 4096, 16384, 65536 } +local function fmtPriority(obj) + local p = rawget(obj, "Priority") or rawget(obj, "priority") + if not p then return "n/a" end + return PRIORITY_NAMES[p] or tostring(p) +end +local function nextPriority(current) + for i, v in ipairs(PRIORITY_CYCLE) do + if v == current then + return PRIORITY_CYCLE[(i % #PRIORITY_CYCLE) + 1] + end + end + return 256 +end + +-- ── helpers ─────────────────────────────────────────────────────────────────── +local function fmtUptime(secs) + secs = math.floor(secs) + if secs < 60 then return secs .. "s" end + if secs < 3600 then return math.floor(secs/60).."m "..(secs%60).."s" end + return math.floor(secs/3600).."h "..math.floor((secs%3600)/60).."m" +end + +local function nowClock() return os.clock() end + +-- ── data collection ─────────────────────────────────────────────────────────── +-- Returns a flat list of rows with depth so the UI can indent names. +local function collectTasks() + local rows = {} + local stats = multi:getStats() + + local function addProc(fullname, proc, depth) + -- Processor header row + rows[#rows+1] = { + isProc = true, + depth = depth, + name = proc.name or fullname, + fullname = fullname, + kind = "processor", + conns = proc.connections or 0, + subs = proc.subscriptions or 0, + } + -- Tasks (Mainloop actors) + local tasks = proc.tasks or {} + for _, task in pairs(tasks) do + if not task.isProcessThread then + rows[#rows+1] = { + isProc = false, + depth = depth + 1, + name = task:getName() or "?", + fullname = fullname, + kind = tostring(task.Type), + state = fmtState(task), + active = not task:isPaused(), + uptime = nowClock() - (task.UPTIME or nowClock()), + priority = rawget(task, "Priority") or 256, + fmtPri = fmtPriority(task), + obj = task, + isThread = false, + } + end + end + -- Threads + local threads = proc.threads or {} + for _, th in pairs(threads) do + rows[#rows+1] = { + isProc = false, + depth = depth + 1, + name = th:getName() or "?", + fullname = fullname, + kind = tostring(th.Type), + state = fmtState(th), + active = not th:isPaused(), + uptime = nowClock() - (th.UPTIME or nowClock()), + priority = rawget(th, "Priority") or 256, + fmtPri = fmtPriority(th), + obj = th, + isThread = true, + } + end + end + + -- Root first, then sub-processors sorted + if stats["root"] then + addProc("root", stats["root"], 0) + end + local procNames = {} + for k in pairs(stats) do + if k ~= "root" then procNames[#procNames+1] = k end + end + table.sort(procNames) + for _, k in ipairs(procNames) do + addProc(k, stats[k], 1) + end + + return rows +end + +-- ── window constructor (unchanged from original) ────────────────────────────── +local windowCount = 0 +function gui:newWindow(x, y, w, h, text, draggable, theme) + local process = gui:newProcessor(text or "window_"..windowCount) + windowCount = windowCount + 1 + local parent = self + local pointer = love.mouse.getCursor() + local sizewe = love.mouse.getSystemCursor("sizewe") + local sizens = love.mouse.getSystemCursor("sizens") + local sizenesw = love.mouse.getSystemCursor("sizenesw") + local sizenwse = love.mouse.getSystemCursor("sizenwse") + local theme = theme or default_theme + + local header = self:newFrame(x, y, w, 35) + header:setRoundness(10, 10, nil, "top") + local window = header:newFrame(0, 35, 0, h - 35, 0, 0, 1) + window.clipDescendants = true + local left = window:newFrame(0, -4, 4, 0, 0, 0, 0, 1):tag("left") + local right = window:newFrame(-4, -4, 4, 0, 1, 0, 0, 1):tag("right") + local bottom = window:newFrame(4, -4, -8, 4, 0, 1, 1):tag("bottom") + local bottomleft = window:newFrame(0, -4, 4, 4, 0, 1):tag("bleft") + local bottomright = window:newFrame(-4, -4, 4, 4, 1, 1):tag("bright") + gui.apply({ + visibility = 0, + I_enableDragging = {gui.MOUSE_PRIMARY}, + respectHierarchy = {false}, + OnUpdate = function(self) self:topStack() end, + OnDragging = function(self, dx, dy) + local ox, oy, ow, oh = header:getAbsolutes() + local tag = self:getTag() + if tag == "left" or tag == "bleft" then + window:size(0, dy) + header:move(dx, 0) + header:size(-dx, 0) + else + window:size(0, dy) + header:size(dx, 0) + end + local x, y, w, h = header:getAbsolutes() + if w < 200 and (tag == "left" or tag == "bleft") then + header:setDualDim(ox, nil, 200) + elseif w < 200 then + header:setDualDim(nil, nil, 200) + end + local x, y, w, h = window:getAbsolutes() + if h < 100 then window:setDualDim(nil, nil, nil, 100) end + end, + OnDragEnd = function(self) love.mouse.setCursor(pointer) end, + OnEnter = function(self) + local tag = self:getTag() + if tag == "left" or tag == "right" then + love.mouse.setCursor(sizewe) + elseif tag == "bleft" then + love.mouse.setCursor(sizenesw) + elseif tag == "bright" then + love.mouse.setCursor(sizenwse) + else + love.mouse.setCursor(sizens) + end + end, + OnExit = function(self) love.mouse.setCursor(pointer) end, + }, left, right, bottom, bottomleft, bottomright) + + local title = header:newTextLabel(text or "", 5, 0, w - 35, 35) + title.clipDescendants = true + title.visibility = 0 + title.ignore = true + title:setFont(theme.fontPrimary) + title:fitFont() + + function window:setTitle(t) title.text = t end + + local X = header:newTextButton("", -25, -25, 20, 20, 1, 1) + X:setRoundness(10, 10) + X.align = gui.ALIGN_CENTER + X.color = color.red + local darkenX = color.darken(color.red, .2) + X.OnEnter(function(self) self.color = darkenX end) + X.OnExit(function(self) self.color = color.red end) + + if draggable then + header:enableDragging(gui.MOUSE_PRIMARY) + header:OnDragging(function(self, dx, dy) self:move(dx, dy) end) + header:OnDragEnd(function(self) + local x, y, w, h = self:getAbsolutes() + local width, height = love.graphics.getDimensions() + if x <= 0 then self:setDualDim(0) end + if y <= 0 then self:setDualDim(nil, 0) end + if x + w >= width then self:setDualDim(width - w) end + if y + h >= height then self:setDualDim(nil, height - 35) end + end) + end + + window.OnClose = function() return window end % X.OnPressed + window.OnClose(function() + header:setParent(gui.virtual) + love.mouse.setCursor(pointer) + end) + function window:close() window.OnClose:Fire(self) end + function window:open() header:setParent(parent) end + + function window:setTheme(th) + theme = th + title.textColor = theme.colorPrimaryText + header.color = theme.colorPrimaryDark + window.color = theme.colorPrimary + end + function window:getTheme() return theme end + + process:newThread(function() window:setTheme(theme) end) + + window.OnSizeChanged(function() window:refresh() end) + function window:refresh() window:setTheme(theme) end + + window.process = process + window.OnCreated(function(element) + if element:hasType(gui.TYPE_BUTTON) then + element:setFont(theme.fontButton) + element.color = theme.colorButtonNormal + element.textColor = theme.colorButtonText + if not element.__registeredTheme then + element.OnEnter(function(self) self.color = theme.colorButtonHighlight end) + element.OnExit(function(self) self.color = theme.colorButtonNormal end) + end + element:fitFont() + element.__registeredTheme = true + elseif element:hasType(gui.TYPE_TEXT) then + element.color = theme.colorPrimary + element:setFont(theme.fontPrimary) + element.textColor = theme.colorPrimaryText + element:fitFont() + elseif element:hasType(gui.TYPE_FRAME) then + if element.__isHeader then + element.color = theme.colorPrimaryDark + else + element.color = theme.colorPrimary + end + end + end) + return window +end + +-- ── scroll frame (unchanged from original) ──────────────────────────────────── +function gui:newScrollFrame(x, y, w, h, sx, sy, sw, sh) + local viewport = self:newFrame(x, y, w, h, sx, sy, sw, sh) + viewport.clipDescendants = true + viewport.drawBorder = false + + local content = viewport:newFrame(0, 0, w, 0) + content.drawBorder = false + + local scrollY = 0 + local maxScrollY = 0 + local scrollX = 0 + local maxScrollX = 0 + local SCROLL_SPEED = 40 + local SCROLL_BAR_W = 8 + + local vBar = viewport:newFrame(-SCROLL_BAR_W, 0, SCROLL_BAR_W, 0, 1, 0, 0, 1) + vBar.color = {0.3, 0.3, 0.3} + vBar.drawBorder = false + vBar.visible = false + + local vThumb = vBar:newFrame(0, 0, SCROLL_BAR_W, 40) + vThumb.color = {0.6, 0.6, 0.6} + vThumb.drawBorder = false + + local hBar = viewport:newFrame(0, -SCROLL_BAR_W, 0, SCROLL_BAR_W, 0, 1, 1) + hBar.color = {0.3, 0.3, 0.3} + hBar.drawBorder = false + hBar.visible = false + + local hThumb = hBar:newFrame(0, 0, 40, SCROLL_BAR_W) + hThumb.color = {0.6, 0.6, 0.6} + hThumb.drawBorder = false + + local applying = false + + local function getViewSize() + local _, _, vw, vh = viewport:getAbsolutes() + return vw, vh + end + + local function clamp(val, lo, hi) + return math.max(lo, math.min(hi, val)) + end + + local function updateScrollbars() + if applying then return end + local vw, vh = getViewSize() + local _, _, cw, ch = content:getAbsolutes() + + maxScrollY = math.max(0, ch - vh) + if maxScrollY > 0 then + vBar.visible = true + local thumbH = math.max(20, vh * (vh / ch)) + local thumbY = (scrollY / maxScrollY) * (vh - thumbH) + vThumb:setDualDim(0, thumbY, SCROLL_BAR_W, thumbH) + else + vBar.visible = false + scrollY = 0 + end + + maxScrollX = math.max(0, cw - vw) + if maxScrollX > 0 then + hBar.visible = true + local thumbW = math.max(20, vw * (vw / cw)) + local thumbX = (scrollX / maxScrollX) * (vw - thumbW) + hThumb:setDualDim(thumbX, 0, thumbW, SCROLL_BAR_W) + else + hBar.visible = false + scrollX = 0 + end + end + + local function applyScroll() + if applying then return end + applying = true + scrollY = clamp(scrollY, 0, maxScrollY) + scrollX = clamp(scrollX, 0, maxScrollX) + content:setDualDim(-scrollX, -scrollY) + updateScrollbars() + applying = false + end + + viewport.OnWheelMoved(function(x, y) + scrollY = scrollY - y * SCROLL_SPEED + applyScroll() + end) + + vThumb:enableDragging(gui.MOUSE_PRIMARY) + vThumb.OnDragging(function(self, dx, dy) + local _, vh = getViewSize() + local _, _, _, thumbH = vThumb:getAbsolutes() + local trackH = vh - thumbH + if trackH <= 0 then return end + scrollY = scrollY + dy * (maxScrollY / trackH) + applyScroll() + end) + + hThumb:enableDragging(gui.MOUSE_PRIMARY) + hThumb.OnDragging(function(self, dx, dy) + local vw, _ = getViewSize() + local _, _, thumbW = hThumb:getAbsolutes() + local trackW = vw - thumbW + if trackW <= 0 then return end + scrollX = scrollX + dx * (maxScrollX / trackW) + applyScroll() + end) + + content.OnSizeChanged(function() + if applying then return end + local _, _, cw, ch = content:getAbsolutes() + local vw, vh = getViewSize() + maxScrollY = math.max(0, ch - vh) + maxScrollX = math.max(0, cw - vw) + scrollY = clamp(scrollY, 0, maxScrollY) + scrollX = clamp(scrollX, 0, maxScrollX) + updateScrollbars() + end) + + viewport.OnSizeChanged(function() + if applying then return end + applyScroll() + end) + + function content:scrollTo(sy, sx) + scrollY = sy or scrollY + scrollX = sx or scrollX + applyScroll() + end + function content:scrollBy(dy, dx) + scrollY = scrollY + (dy or 0) + scrollX = scrollX + (dx or 0) + applyScroll() + end + function content:scrollToBottom() scrollY = maxScrollY; applyScroll() end + function content:scrollToTop() scrollY = 0; applyScroll() end + function content:setScrollSpeed(speed) SCROLL_SPEED = speed end + function content:getScrollPos() return scrollX, scrollY end + function content:getMaxScroll() return maxScrollX, maxScrollY end + + local _baseSDD = content.setDualDim + function content:setContentSize(cw, ch) + _baseSDD(self, nil, nil, cw or select(3, self:getAbsolutes()), ch) + applyScroll() + end + + local _baseDestroy = viewport.destroy + function viewport:destroy() + content:destroy() + _baseDestroy(self) + end + + applyScroll() + return content +end + +-- ── row pool ────────────────────────────────────────────────────────────────── +local COLOR_PROC_ROW = TM_THEME.colorPrimaryDark +local COLOR_ROW_EVEN = TM_THEME.colorPrimary +local COLOR_ROW_ODD = TM_THEME.colorPrimaryDark +local COLOR_DEAD = { 0.5, 0.1, 0.1 } + +local function makeRowPool(scrollFrame) + local pool = { rows = {}, active = 0 } + + local function makeRow(idx) + local yOff = (idx - 1) * ROW_H + local bg = scrollFrame:newFrame(0, yOff, TOTAL_W, ROW_H) + bg.drawBorder = false + + -- Name label (col 1) with indent support + local nameLabel = bg:newTextLabel("", COL_X[1] + 4, 0, COL_WIDTHS[1] - 4, ROW_H) + nameLabel.align = gui.ALIGN_LEFT + nameLabel.ignore = true + + -- Type, State, Status, Uptime, Priority labels + local kindLbl = bg:newTextLabel("", COL_X[2], 0, COL_WIDTHS[2], ROW_H) + local stateLbl = bg:newTextLabel("", COL_X[3], 0, COL_WIDTHS[3], ROW_H) + local statusLbl = bg:newTextLabel("", COL_X[4], 0, COL_WIDTHS[4], ROW_H) + local uptimeLbl = bg:newTextLabel("", COL_X[5], 0, COL_WIDTHS[5], ROW_H) + local priorityLbl = bg:newTextLabel("", COL_X[6], 0, COL_WIDTHS[6], ROW_H) + for _, lbl in ipairs({kindLbl, stateLbl, statusLbl, uptimeLbl, priorityLbl}) do + lbl.align = gui.ALIGN_CENTER + lbl.ignore = true + end + + -- Pause / Resume button + local pauseBtn = bg:newTextButton("", COL_X[7] + 2, 2, COL_WIDTHS[7] - 4, ROW_H - 4) + pauseBtn.align = gui.ALIGN_CENTER + + -- Kill button (red) + local killBtn = bg:newTextButton("Kill", COL_X[8] + 2, 2, COL_WIDTHS[8] - 4, ROW_H - 4) + killBtn.align = gui.ALIGN_CENTER + killBtn.color = color.darken(color.red, .1) + + local row = { + bg = bg, + nameLabel = nameLabel, + kindLbl = kindLbl, + stateLbl = stateLbl, + statusLbl = statusLbl, + uptimeLbl = uptimeLbl, + priorityLbl = priorityLbl, + pauseBtn = pauseBtn, + killBtn = killBtn, + obj = nil, + isProc = false, + } + + pauseBtn.OnReleased(function() + if not row.obj then return end + if row.obj:isPaused() then + row.obj:Resume() + else + row.obj:Pause() + end + statusLbl.text = row.obj:isPaused() and "Paused" or "Running" + pauseBtn.text = row.obj:isPaused() and "Resume" or "Pause" + end) + + killBtn.OnReleased(function() + if not row.obj or row.isProc then return end + if row.obj.Kill then + row.obj:Kill() + elseif row.obj.Destroy then + row.obj:Destroy() + end + bg.color = COLOR_DEAD + end) + + -- Priority label is clickable to cycle priority + priorityLbl.ignore = false + priorityLbl.OnReleased(function() + if not row.obj or row.isProc then return end + local cur = rawget(row.obj, "Priority") or 256 + local next = nextPriority(cur) + if row.obj.setPriority then + row.obj:setPriority(next) + priorityLbl.text = PRIORITY_NAMES[next] or tostring(next) + end + end) + + return row + end + + function pool:ensure(n) + while #self.rows < n do + self.rows[#self.rows + 1] = makeRow(#self.rows + 1) + end + end + + function pool:apply(data) + self:ensure(#data) + self.active = #data + + for i, d in ipairs(data) do + local row = self.rows[i] + row.isProc = d.isProc + row.obj = d.isProc and nil or d.obj + + -- Row y position + row.bg:setDualDim(nil, (i - 1) * ROW_H) + row.bg.visible = true + + if d.isProc then + -- Processor header row + row.bg.color = COLOR_PROC_ROW + row.nameLabel.text = string.rep(" ", d.depth) .. "[" .. d.name .. "]" + row.kindLbl.text = "processor" + row.stateLbl.text = "" + row.statusLbl.text = "" + row.uptimeLbl.text = d.conns .. "c/" .. d.subs .. "s" + row.priorityLbl.text = "" + row.pauseBtn.text = "" + row.pauseBtn.visible = false + row.killBtn.visible = false + else + row.bg.color = (i % 2 == 0) and COLOR_ROW_EVEN or COLOR_ROW_ODD + row.nameLabel.text = string.rep(" ", d.depth) .. d.name + row.kindLbl.text = d.kind or "" + row.stateLbl.text = d.state or "" + row.statusLbl.text = d.active and "Running" or "Paused" + row.uptimeLbl.text = fmtUptime(d.uptime) + row.priorityLbl.text = d.fmtPri or "" + row.pauseBtn.text = d.active and "Pause" or "Resume" + row.pauseBtn.visible = true + row.killBtn.visible = true + end + end + + -- Hide unused rows + for i = #data + 1, #self.rows do + self.rows[i].bg.visible = false + self.rows[i].obj = nil + end + + scrollFrame:setDualDim(nil, nil, nil, math.max(#data * ROW_H, 1)) + end + + return pool +end + +-- ── error log pool ──────────────────────────────────────────────────────────── +local ERROR_ROW_H = 22 +local MAX_ERRORS = 200 + +local function makeErrorPool(scrollFrame) + local pool = { rows = {}, entries = {} } + + local function makeRow(idx) + local yOff = (idx - 1) * ERROR_ROW_H + local bg = scrollFrame:newFrame(0, yOff, TOTAL_W, ERROR_ROW_H) + bg.color = (idx % 2 == 0) and COLOR_ROW_EVEN or COLOR_ROW_ODD + bg.drawBorder = false + local lbl = bg:newTextLabel("", 4, 0, TOTAL_W - 4, ERROR_ROW_H) + lbl.align = gui.ALIGN_LEFT + lbl.ignore = true + return { bg = bg, lbl = lbl } + end + + function pool:ensure(n) + while #self.rows < n do + self.rows[#self.rows + 1] = makeRow(#self.rows + 1) + end + end + + function pool:addEntry(msg, source) + if #self.entries >= MAX_ERRORS then + table.remove(self.entries, 1) + end + local ts = string.format("[%.1fs]", os.clock()) + self.entries[#self.entries + 1] = ts .. " [" .. (source or "?") .. "] " .. tostring(msg) + self:refresh() + end + + function pool:refresh() + local n = #self.entries + self:ensure(n) + for i, entry in ipairs(self.entries) do + local row = self.rows[i] + row.bg:setDualDim(nil, (i - 1) * ERROR_ROW_H) + row.bg.visible = true + row.lbl.text = entry + end + for i = n + 1, #self.rows do + self.rows[i].bg.visible = false + end + scrollFrame:setDualDim(nil, nil, nil, math.max(n * ERROR_ROW_H, 1)) + scrollFrame:scrollToBottom() + end + + function pool:clear() + self.entries = {} + self:refresh() + end + + return pool +end + +-- ── column header row ───────────────────────────────────────────────────────── +local function makeHeader(parent, onSort) + local hdr = parent:newFrame(0, 0, TOTAL_W, ROW_H) + hdr.color = TM_THEME.colorPrimaryDark + hdr.drawBorder = false + + local sortCol = nil + local sortAsc = true + local indicators = {} + + for i, t in ipairs(COL_LABELS) do + local isSortable = false + for _, k in ipairs(SORT_COLS) do + if k == COL_KEYS[i] then isSortable = true; break end + end + + if isSortable then + local btn = hdr:newTextButton(t, COL_X[i], 0, COL_WIDTHS[i], ROW_H) + btn.align = (i == 1) and gui.ALIGN_LEFT or gui.ALIGN_CENTER + indicators[COL_KEYS[i]] = btn + local key = COL_KEYS[i] + btn.OnReleased(function() + if sortCol == key then + sortAsc = not sortAsc + else + sortCol = key + sortAsc = true + end + -- Reset all sortable headers, then mark the active one + for j, label in ipairs(COL_LABELS) do + local b = indicators[COL_KEYS[j]] + if b then + if COL_KEYS[j] == sortCol then + b.text = label .. (sortAsc and " ▲" or " ▼") + else + b.text = label + end + end + end + if onSort then onSort(key, sortAsc) end + end) + else + local lbl = hdr:newTextLabel(t, COL_X[i], 0, COL_WIDTHS[i], ROW_H) + lbl.align = gui.ALIGN_CENTER + lbl.ignore = true + lbl.textColor = TM_THEME.colorPrimaryText + end + end + return hdr +end + +-- ── tab bar ─────────────────────────────────────────────────────────────────── +local TAB_H = 28 +local function makeTabBar(parent, tabs, onSwitch) + local bar = parent:newFrame(0, 0, 0, TAB_H, 0, 0, 1) + bar.color = TM_THEME.colorPrimaryDark + bar.drawBorder = false + local tabW = math.floor(TOTAL_W / #tabs) + local btns = {} + for i, label in ipairs(tabs) do + local btn = bar:newTextButton(label, (i-1)*tabW, 0, tabW, TAB_H) + btn.align = gui.ALIGN_CENTER + btns[i] = btn + btn.OnReleased(function() + onSwitch(i) + end) + end + return bar, btns +end + +-- ── sort helper ─────────────────────────────────────────────────────────────── +local function sortRows(rows, key, asc) + local function cmp(a, b) + -- Processor rows always float to top within their group; we keep them stable + if a.isProc and b.isProc then return a.fullname < b.fullname end + if a.isProc then return true end + if b.isProc then return false end + local va, vb + if key == "name" then va, vb = a.name or "", b.name or "" + elseif key == "kind" then va, vb = a.kind or "", b.kind or "" + elseif key == "state" then va, vb = a.state or "", b.state or "" + elseif key == "status" then va, vb = (a.active and 0 or 1), (b.active and 0 or 1) + elseif key == "uptime" then va, vb = a.uptime or 0, b.uptime or 0 + elseif key == "priority" then va, vb = a.priority or 256, b.priority or 256 + else va, vb = tostring(a[key] or ""), tostring(b[key] or "") + end + if asc then return va < vb else return va > vb end + end + -- Stable-ish sort: keep proc header immediately before its children + -- For simplicity we sort the flat list but keep proc rows pinned before + -- the first non-proc row that shares the same fullname. + table.sort(rows, cmp) +end + +-- ── public API ──────────────────────────────────────────────────────────────── +local taskManager + +function gui:showTaskManager() + if taskManager then return end + + local WIN_W = TOTAL_W + 20 + local WIN_H = 620 + + taskManager = gui:newWindow(0, 0, WIN_W, WIN_H, "Task Manager", true, TM_THEME) + taskManager.clipDescendants = true + + -- ── tab bar ────────────────────────────────────────────────────────────── + local currentTab = 1 -- 1 = tasks, 2 = errors + local taskPanel, errorPanel + + local tabBar, tabBtns = makeTabBar(taskManager, {"Tasks", "Errors"}, function(idx) + currentTab = idx + taskPanel.visible = (idx == 1) + errorPanel.visible = (idx == 2) + end) + + -- ── tasks panel ────────────────────────────────────────────────────────── + taskPanel = taskManager:newFrame(0, TAB_H, 0, -TAB_H, 0, 0, 1, 1) + taskPanel.drawBorder = false + taskPanel.clipDescendants = true + + -- Load bar strip (sits below tab bar, above column headers) + local LOAD_H = 20 + local loadStrip = taskPanel:newFrame(0, 0, 0, LOAD_H, 0, 0, 1) + loadStrip.color = TM_THEME.colorPrimaryDark + loadStrip.drawBorder = false + + local loadFill = loadStrip:newFrame(0, 2, 1, LOAD_H - 4) -- absolute w=1, no relative anchors + loadFill.color = { 0.1, 0.6, 0.3 } + loadFill.drawBorder = false + + -- Label is created AFTER fill so it draws on top of it + local loadLbl = loadStrip:newTextLabel("Load: …", 4, 0, TOTAL_W - 8, LOAD_H) + loadLbl.align = gui.ALIGN_LEFT + loadLbl.ignore = true + + local sortKey = nil + local sortAsc = true + + local colHdr = makeHeader(taskPanel, function(key, asc) + sortKey = key + sortAsc = asc + end) + colHdr:setDualDim(nil, LOAD_H) + + local scrollFrame = taskPanel:newScrollFrame(0, LOAD_H + ROW_H, 0, -(LOAD_H + ROW_H), 0, 0, 1, 1) + local pool = makeRowPool(scrollFrame) + + -- ── error panel ────────────────────────────────────────────────────────── + errorPanel = taskManager:newFrame(0, TAB_H, 0, -TAB_H, 0, 0, 1, 1) + errorPanel.drawBorder = false + errorPanel.clipDescendants = true + errorPanel.visible = false + + local errHdr = errorPanel:newFrame(0, 0, 0, ROW_H, 0, 0, 1) + errHdr.color = TM_THEME.colorPrimaryDark + errHdr.drawBorder = false + local errTitle = errHdr:newTextLabel("Error Log", 4, 0, 200, ROW_H) + errTitle.align = gui.ALIGN_LEFT + errTitle.ignore = true + + local clearBtn = errHdr:newTextButton("Clear", -70, 2, 66, ROW_H - 4, 1) + clearBtn.align = gui.ALIGN_CENTER + + local errScroll = errorPanel:newScrollFrame(0, ROW_H, 0, -ROW_H, 0, 0, 1, 1) + local errorPool = makeErrorPool(errScroll) + + clearBtn.OnReleased(function() errorPool:clear() end) + + -- ── wire up error capture ───────────────────────────────────────────────── + -- Thread errors fire on the *thread's own* OnError, not the processor's. + -- We use OnObjectCreated to hook every thread as it is born, on every + -- processor (including ones created after the task manager opens). + -- We also walk existing threads retroactively for processors already running. + + local hookedThreads = {} -- weak set so we don't prevent GC + + local function hookThread(th, procName) + if not th.OnError then return end + if hookedThreads[th] then return end + hookedThreads[th] = true + th.OnError(function(self, err,t) + local msg = type(err) == "string" and err or tostring(err or "unknown error") + local name = (th.getName and th:getName()) or "?" + errorPool:addEntry(msg, procName .. "/" .. name) + end) + end + + local function hookProc(proc, procName) + -- Hook threads already alive on this processor + local threads = proc.threads or {} + for _, th in ipairs(threads) do + hookThread(th, procName) + end + -- Hook threads created in future on this processor + proc.OnObjectCreated(function(obj) + if obj.Type == multi.registerType("thread", "threads") then + hookThread(obj, procName) + end + end) + end + + -- Root process + hookProc(multi, "root") + -- All processors currently registered + for _, proc in ipairs(multi:getProcessors()) do + hookProc(proc, proc:getName()) + end + -- Any processors created after this point + multi.OnObjectCreated(function(obj) + if obj.Type == multi.registerType("process", "processes") then + hookProc(obj, obj:getName()) + end + end) + + -- ── stat line ───────────────────────────────────────────────────────────── + local function setStatLine(n) + taskManager:setTitle("Task Manager — " .. n .. " objects") + end + + -- ── background thread: collect tasks ────────────────────────────────────── + local function isOpen() + return not taskManager:isDescendantOf(gui.virtual) + end + + local pendingData = nil + local dirty = false + + taskManager.process:newThread("TM_collect", function() + while true do + thread.hold(isOpen) + local data = collectTasks() + pendingData = data + dirty = true + thread.sleep(1) + end + end) + + -- ── load probe ──────────────────────────────────────────────────────────── + -- Install once. getLoad() is now non-blocking — just reads the EMA state. + local schedulerProbe = require("gui.addons.probe") + schedulerProbe:install(multi) + + -- ── main-thread update ──────────────────────────────────────────────────── + taskManager.OnUpdate(function() + taskManager:topStack() + -- Apply task data + if dirty and pendingData then + dirty = false + local data = pendingData + pendingData = nil + + if sortKey then + sortRows(data, sortKey, sortAsc) + end + + pool:apply(data) + setStatLine(#data) + end + + -- Load bar — getLoad() is now non-blocking, safe to call every frame + local pct, lagMs = multi:getLoad() + local _, _, barW, _ = loadStrip:getAbsolutes() + local fillW = math.max(1, math.floor(barW * pct / 100)) + loadFill:setDualDim(nil, nil, fillW) + if pct < 50 then + loadFill.color = { 0.1, 0.6, 0.3 } + elseif pct < 80 then + loadFill.color = { 0.8, 0.6, 0.1 } + else + loadFill.color = { 0.8, 0.15, 0.1 } + end + loadLbl.text = string.format("Load: %d%% Lag: %.1fms", pct, lagMs) + end) +end + +-- ── hotkey ──────────────────────────────────────────────────────────────────── +ToggleTaskManager = gui:setHotKey({"lctrl","t"}) + + gui:setHotKey({"rctrl","t"}) + +ToggleTaskManager(function() + if not taskManager then + gui:showTaskManager() + elseif taskManager:isActive() then + taskManager:close() + else + taskManager:open() + end +end) + +ToggleTaskManager:Fire() +taskManager:close() diff --git a/gui/assets/pause.png b/gui/assets/pause.png new file mode 100644 index 0000000..4af84bc Binary files /dev/null and b/gui/assets/pause.png differ diff --git a/gui/assets/play.png b/gui/assets/play.png new file mode 100644 index 0000000..a231853 Binary files /dev/null and b/gui/assets/play.png differ diff --git a/gui/changes.md b/gui/changes.md new file mode 100755 index 0000000..4e768b5 --- /dev/null +++ b/gui/changes.md @@ -0,0 +1 @@ +# \ No newline at end of file diff --git a/gui/core/canvas.lua b/gui/core/canvas.lua new file mode 100755 index 0000000..c600705 --- /dev/null +++ b/gui/core/canvas.lua @@ -0,0 +1,26 @@ +local gui = require("gui") + +function newCanvas(domain) + local c + if domain == "visual" then + c = gui:newVisualFrame() + else + c = gui:newVirtualFrame() + end + c:fullFrame() + function c:swap(c1, c2) + local temp = c1.children + c1.children = c2.children + c2.children = temp + for i,v in pairs(c1.children) do + v.parent = c1 + end + for i,v in pairs(c2.children) do + v.parent = c2 + end + end + + return c +end + +return newCanvas \ No newline at end of file diff --git a/gui/core/color.lua b/gui/core/color.lua new file mode 100755 index 0000000..716ac07 --- /dev/null +++ b/gui/core/color.lua @@ -0,0 +1,1660 @@ +local color={} +local mt = { + __add = function (c1,c2) + return color.new(c1[1]+c2[1],c1[2]+c2[2],c1[2]+c2[2]) + end, + __sub = function (c1,c2) + return color.new(c1[1]-c2[1],c1[2]-c2[2],c1[2]-c2[2]) + end, + __mul = function (c1,c2) + return color.new(c1[1]*c2[1],c1[2]*c2[2],c1[2]*c2[2]) + end, + __div = function (c1,c2) + return color.new(c1[1]/c2[1],c1[2]/c2[2],c1[2]/c2[2]) + end, + __mod = function (c1,c2) + return color.new(c1[1]%c2[1],c1[2]%c2[2],c1[2]%c2[2]) + end, + __pow = function (c1,c2) + return color.new(c1[1]^c2[1],c1[2]^c2[2],c1[2]^c2[2]) + end, + __unm = function (c1) + return color.new(-c1[1],-c1[2],-c1[2]) + end, + __tostring = function(c) + return "("..c[1]..","..c[2]..","..c[3]..",".. (c[4] or "1") ..")" + end, + __eq = function (c1,c2) + return (c1[1]==c2[1] and c1[2]==c2[2] and c1[2]==c2[2]) + end, + __lt = function (c1,c2) + return (c1[1]= .5 +end + +function color.getAverageLightness(r, g, b) + if type(r) == "string" then + r, g, b = tonumber(string.sub(r,1,2),16),tonumber(string.sub(r,3,4),16),tonumber(string.sub(r,5,6),16) + elseif type(r) == "table" then + r, g, b = r[1], r[2], r[3] + end + return (r + g + b) / 3 +end + +function color.rgbToHex(r, g, b) + if type(r) == "string" then + r, g, b = tonumber(string.sub(r,1,2),16),tonumber(string.sub(r,3,4),16),tonumber(string.sub(r,5,6),16) + elseif type(r) == "table" then + r, g, b = r[1], r[2], r[3] + end + + r, g, b = love.math.colorToBytes(r,g,b,0) + + local rgb = (r * 0x10000) + (g * 0x100) + b + return string.format("%06x", rgb) + end + +function color.new(r, g, b, a) + local temp + + if type(r) == "string" then + r = r:gsub("%s",""):gsub("%%","") + if r:sub(1,4) == "rgba" then + local sr,sg,sb,sa = r:match("rgba%((%d-),(%d-),(%d-),(%d*%.?%d+)%)") + r,g,b = love.math.colorFromBytes(tonumber(sr),tonumber(sg),tonumber(sb)) + a = tonumber(sa) + elseif r:sub(1,4) == "hsla" then + local sh,ss,sl,sa = r:match("hsla%((%d-),(%d-),(%d-),(%d*%.?%d+)%)") + r,g,b,a = color.hsl(tonumber(sh), tonumber(ss), tonumber(sl), tonumber(sa)) + elseif r:sub(1,3) == "hsl" then + local sh,ss,sl = r:match("hsl%((%d-),(%d-),(%d-)%)") + r,g,b,a = color.hsl(tonumber(sh), tonumber(ss), tonumber(sl)) + elseif r:sub(1,3) == "rgb" then + local sr,sg,sb = r:match("rgb%((%d-),(%d-),(%d-)%)") + r,g,b = love.math.colorFromBytes(tonumber(sr),tonumber(sg),tonumber(sb)) + else + if r:sub(1,1) == "#" then + r = r:sub(2,-1) + end + if #r == 8 then + r, g, b, a = tonumber(string.sub(r,1,2),16),tonumber(string.sub(r,3,4),16),tonumber(string.sub(r,5,6),16),tonumber(string.sub(r,7,8),16) + else + r, g, b = tonumber(string.sub(r,1,2),16),tonumber(string.sub(r,3,4),16),tonumber(string.sub(r,5,6),16) + end + temp = {love.math.colorFromBytes(r, g, b, a or 255)} + end + elseif type(r) == "table" then + return r + end + + if not temp then + temp = {r, b, g, a} + end + + setmetatable(temp, mt) + return temp +end + +function color.random() + return color.new(math.random(0,10000000)/10000000, math.random(0,10000000)/10000000, math.random(0,10000000)/10000000, 1) +end + +function color.indexColor(name,r, g, b) + local c = color.new(r,g,b) + -- Other ways to index a color + color[string.lower(name)] = c + color[string.upper(name)] = c + color[string.upper(string.sub(name,1,1))..string.lower(string.sub(name,2))] = c +end + +function color.darken(c, v) + local currentR,currentG,currentB=c[1],c[2],c[3] + return color.new((currentR*255) * (1 - v),(currentG*255) * (1 - v),(currentB*255) * (1 - v)) +end + +function color.lighten(c, v) + local currentR,currentG,currentB=c[1],c[2],c[3] + return color.new(currentR*255 + (255 - (currentR*255)) * v,currentG*255 + (255 - (currentG*255)) * v,currentB*255 + (255 - (currentB*255)) * v) +end + +-- Add a ton of colors sourced from online + +color.indexColor("WHITE","rgb(255,255,255)") +color.indexColor("MAROON","rgb(128,20,20)") +color.indexColor("DARK_RED","rgb(139,20,20)") +color.indexColor("BROWN","rgb(165,42,42)") +color.indexColor("FIREBRICK","rgb(178,34,34)") +color.indexColor("CRIMSON","rgb(220,20,60)") +color.indexColor("RED","rgb(255,20,20)") +color.indexColor("TOMATO","rgb(255,99,71)") +color.indexColor("CORAL","rgb(255,127,80)") +color.indexColor("INDIAN_RED","rgb(205,92,92)") +color.indexColor("LIGHT_CORAL","rgb(240,128,128)") +color.indexColor("DARK_SALMON","rgb(233,150,122)") +color.indexColor("SALMON","rgb(250,128,114)") +color.indexColor("LIGHT_SALMON","rgb(255,160,122)") +color.indexColor("ORANGE_RED","rgb(255,69,20)") +color.indexColor("DARK_ORANGE","rgb(255,140,20)") +color.indexColor("ORANGE","rgb(255,165,20)") +color.indexColor("GOLD","rgb(255,215,20)") +color.indexColor("DARK_GOLDEN_ROD","rgb(184,134,11)") +color.indexColor("GOLDEN_ROD","rgb(218,165,32)") +color.indexColor("PALE_GOLDEN_ROD","rgb(238,232,170)") +color.indexColor("DARK_KHAKI","rgb(189,183,107)") +color.indexColor("KHAKI","rgb(240,230,140)") +color.indexColor("OLIVE","rgb(128,128,20)") +color.indexColor("YELLOW_GREEN","rgb(154,205,50)") +color.indexColor("DARK_OLIVE_GREEN","rgb(85,107,47)") +color.indexColor("OLIVE_DRAB","rgb(107,142,35)") +color.indexColor("LAWN_GREEN","rgb(124,252,20)") +color.indexColor("CHART_REUSE","rgb(127,255,20)") +color.indexColor("GREEN_YELLOW","rgb(173,255,47)") +color.indexColor("DARK_GREEN","rgb(20,100,20)") +color.indexColor("FOREST_GREEN","rgb(34,139,34)") +color.indexColor("LIME","rgb(20,255,20)") +color.indexColor("LIME_GREEN","rgb(50,205,50)") +color.indexColor("LIGHT_GREEN","rgb(144,238,144)") +color.indexColor("PALE_GREEN","rgb(152,251,152)") +color.indexColor("DARK_SEA_GREEN","rgb(143,188,143)") +color.indexColor("MEDIUM_SPRING_GREEN","rgb(20,250,154)") +color.indexColor("SPRING_GREEN","rgb(20,255,127)") +color.indexColor("SEA_GREEN","rgb(46,139,87)") +color.indexColor("MEDIUM_AQUA_MARINE","rgb(102,205,170)") +color.indexColor("MEDIUM_SEA_GREEN","rgb(60,179,113)") +color.indexColor("LIGHT_SEA_GREEN","rgb(32,178,170)") +color.indexColor("DARK_SLATE_GRAY","rgb(47,79,79)") +color.indexColor("TEAL","rgb(20,128,128)") +color.indexColor("DARK_CYAN","rgb(20,139,139)") +color.indexColor("LIGHT_CYAN","rgb(224,255,255)") +color.indexColor("DARK_TURQUOISE","rgb(20,206,209)") +color.indexColor("TURQUOISE","rgb(64,224,208)") +color.indexColor("MEDIUM_TURQUOISE","rgb(72,209,204)") +color.indexColor("PALE_TURQUOISE","rgb(175,238,238)") +color.indexColor("AQUA_MARINE","rgb(127,255,212)") +color.indexColor("POWDER_BLUE","rgb(176,224,230)") +color.indexColor("CADET_BLUE","rgb(95,158,160)") +color.indexColor("STEEL_BLUE","rgb(70,130,180)") +color.indexColor("CORN_FLOWER_BLUE","rgb(100,149,237)") +color.indexColor("DEEP_SKY_BLUE","rgb(20,191,255)") +color.indexColor("DODGER_BLUE","rgb(30,144,255)") +color.indexColor("LIGHT_BLUE","rgb(173,216,230)") +color.indexColor("SKY_BLUE","rgb(135,206,235)") +color.indexColor("LIGHT_SKY_BLUE","rgb(135,206,250)") +color.indexColor("MIDNIGHT_BLUE","rgb(25,25,112)") +color.indexColor("NAVY","rgb(20,20,128)") +color.indexColor("DARK_BLUE","rgb(20,20,139)") +color.indexColor("MEDIUM_BLUE","rgb(20,20,205)") +color.indexColor("BLUE","rgb(20,20,255)") +color.indexColor("ROYAL_BLUE","rgb(65,105,225)") +color.indexColor("BLUE_VIOLET","rgb(138,43,226)") +color.indexColor("INDIGO","rgb(75,20,130)") +color.indexColor("DARK_SLATE_BLUE","rgb(72,61,139)") +color.indexColor("SLATE_BLUE","rgb(106,90,205)") +color.indexColor("MEDIUM_SLATE_BLUE","rgb(123,104,238)") +color.indexColor("MEDIUM_PURPLE","rgb(147,112,219)") +color.indexColor("DARK_MAGENTA","rgb(139,20,139)") +color.indexColor("DARK_VIOLET","rgb(148,20,211)") +color.indexColor("DARK_ORCHID","rgb(153,50,204)") +color.indexColor("MEDIUM_ORCHID","rgb(186,85,211)") +color.indexColor("PURPLE","rgb(128,20,128)") +color.indexColor("THISTLE","rgb(216,191,216)") +color.indexColor("PLUM","rgb(221,160,221)") +color.indexColor("VIOLET","rgb(238,130,238)") +color.indexColor("MAGENTA","rgb(255,20,255)") +color.indexColor("ORCHID","rgb(218,112,214)") +color.indexColor("MEDIUM_VIOLET_RED","rgb(199,21,133)") +color.indexColor("PALE_VIOLET_RED","rgb(219,112,147)") +color.indexColor("DEEP_PINK","rgb(255,20,147)") +color.indexColor("HOT_PINK","rgb(255,105,180)") +color.indexColor("LIGHT_PINK","rgb(255,182,193)") +color.indexColor("PINK","rgb(255,192,203)") +color.indexColor("ANTIQUE_WHITE","rgb(250,235,215)") +color.indexColor("BEIGE","rgb(245,245,220)") +color.indexColor("BISQUE","rgb(255,228,196)") +color.indexColor("BLANCHED_ALMOND","rgb(255,235,205)") +color.indexColor("WHEAT","rgb(245,222,179)") +color.indexColor("CORN_SILK","rgb(255,248,220)") +color.indexColor("LEMON_CHIFFON","rgb(255,250,205)") +color.indexColor("LIGHT_GOLDEN_ROD_YELLOW","rgb(250,250,210)") +color.indexColor("LIGHT_YELLOW","rgb(255,255,224)") +color.indexColor("SADDLE_BROWN","rgb(139,69,19)") +color.indexColor("CALM_PURPLE","rgb(85,85,127)") +color.indexColor("SIENNA","rgb(160,82,45)") +color.indexColor("CHOCOLATE","rgb(210,105,30)") +color.indexColor("PERU","rgb(205,133,63)") +color.indexColor("SANDY_BROWN","rgb(244,164,96)") +color.indexColor("BURLY_WOOD","rgb(222,184,135)") +color.indexColor("TAN","rgb(210,180,140)") +color.indexColor("ROSY_BROWN","rgb(188,143,143)") +color.indexColor("MOCCASIN","rgb(255,228,181)") +color.indexColor("NAVAJO_WHITE","rgb(255,222,173)") +color.indexColor("PEACH_PUFF","rgb(255,218,185)") +color.indexColor("MISTY_ROSE","rgb(255,228,225)") +color.indexColor("LAVENDER_BLUSH","rgb(255,240,245)") +color.indexColor("LINEN","rgb(250,240,230)") +color.indexColor("OLD_LACE","rgb(253,245,230)") +color.indexColor("PAPAYA_WHIP","rgb(255,239,213)") +color.indexColor("SEA_SHELL","rgb(255,245,238)") +color.indexColor("MINT_CREAM","rgb(245,255,250)") +color.indexColor("SLATE_GRAY","rgb(112,128,144)") +color.indexColor("LIGHT_SLATE_GRAY","rgb(119,136,153)") +color.indexColor("LIGHT_STEEL_BLUE","rgb(176,196,222)") +color.indexColor("LAVENDEr(","rgb(230,230,250)") +color.indexColor("FLORAL_WHITE","rgb(255,250,240)") +color.indexColor("ALICE_BLUE","rgb(240,248,255)") +color.indexColor("GHOST_WHITE","rgb(248,248,255)") +color.indexColor("HONEYDEW","rgb(240,255,240)") +color.indexColor("IVORY","rgb(255,255,240)") +color.indexColor("AZURE","rgb(240,255,255)") +color.indexColor("SNOW","rgb(255,250,250)") +color.indexColor("DIM_GRAY","rgb(105,105,105)") +color.indexColor("GRAY","rgb(128,128,128)") +color.indexColor("DARK_GRAY","rgb(169,169,169)") +color.indexColor("SILVEr(","rgb(192,192,192)") +color.indexColor("LIGHT_GRAY","rgb(211,211,211)") +color.indexColor("GAINSBORO","rgb(220,220,220)") +color.indexColor("WHITE_SMOKE","rgb(245,245,245)") +color.indexColor("AliceBlue","#f0f8ff") +color.indexColor("AntiqueWhite","#faebd7") +color.indexColor("AntiqueWhite1","#ffefdb") +color.indexColor("AntiqueWhite2","#eedfcc") +color.indexColor("AntiqueWhite3","#cdc0b0") +color.indexColor("AntiqueWhite4","#8b8378") +color.indexColor("aquamarine1","#7fffd4") +color.indexColor("aquamarine2","#76eec6") +color.indexColor("aquamarine4","#458b74") +color.indexColor("azure1","#f0ffff") +color.indexColor("azure2","#e0eeee") +color.indexColor("azure3","#c1cdcd") +color.indexColor("azure4","#838b8b") +color.indexColor("beige","#f5f5dc") +color.indexColor("bisque1","#ffe4c4") +color.indexColor("bisque2","#eed5b7") +color.indexColor("bisque3","#cdb79e") +color.indexColor("bisque4","#8b7d6b") +color.indexColor("BlanchedAlmond","#ffebcd") +color.indexColor("blue1","#0000ff") +color.indexColor("blue2","#0000ee") +color.indexColor("blue4","#00008b") +color.indexColor("BlueViolet","#8a2be2") +color.indexColor("brown","#a52a2a") +color.indexColor("brown1","#ff4040") +color.indexColor("brown2","#ee3b3b") +color.indexColor("brown3","#cd3333") +color.indexColor("brown4","#8b2323") +color.indexColor("burlywood","#deb887") +color.indexColor("burlywood1","#ffd39b") +color.indexColor("burlywood2","#eec591") +color.indexColor("burlywood3","#cdaa7d") +color.indexColor("burlywood4","#8b7355") +color.indexColor("CadetBlue","#5f9ea0") +color.indexColor("CadetBlue1","#98f5ff") +color.indexColor("CadetBlue2","#8ee5ee") +color.indexColor("CadetBlue3","#7ac5cd") +color.indexColor("CadetBlue4","#53868b") +color.indexColor("chartreuse1","#7fff00") +color.indexColor("chartreuse2","#76ee00") +color.indexColor("chartreuse3","#66cd00") +color.indexColor("chartreuse4","#458b00") +color.indexColor("chocolate","#d2691e") +color.indexColor("chocolate1","#ff7f24") +color.indexColor("chocolate2","#ee7621") +color.indexColor("chocolate3","#cd661d") +color.indexColor("coral","#ff7f50") +color.indexColor("coral1","#ff7256") +color.indexColor("coral2","#ee6a50") +color.indexColor("coral3","#cd5b45") +color.indexColor("coral4","#8b3e2f") +color.indexColor("CornflowerBlue","#6495ed") +color.indexColor("cornsilk1","#fff8dc") +color.indexColor("cornsilk2","#eee8cd") +color.indexColor("cornsilk3","#cdc8b1") +color.indexColor("cornsilk4","#8b8878") +color.indexColor("cyan1","#00ffff") +color.indexColor("cyan2","#00eeee") +color.indexColor("cyan3","#00cdcd") +color.indexColor("cyan4","#008b8b") +color.indexColor("DarkGoldenrod","#b8860b") +color.indexColor("DarkGoldenrod1","#ffb90f") +color.indexColor("DarkGoldenrod2","#eead0e") +color.indexColor("DarkGoldenrod3","#cd950c") +color.indexColor("DarkGoldenrod4","#8b6508") +color.indexColor("DarkGreen","#006400") +color.indexColor("DarkKhaki","#bdb76b") +color.indexColor("DarkOliveGreen","#556b2f") +color.indexColor("DarkOliveGreen1","#caff70") +color.indexColor("DarkOliveGreen2","#bcee68") +color.indexColor("DarkOliveGreen3","#a2cd5a") +color.indexColor("DarkOliveGreen4","#6e8b3d") +color.indexColor("DarkOrange","#ff8c00") +color.indexColor("DarkOrange1","#ff7f00") +color.indexColor("DarkOrange2","#ee7600") +color.indexColor("DarkOrange3","#cd6600") +color.indexColor("DarkOrange4","#8b4500") +color.indexColor("DarkOrchid","#9932cc") +color.indexColor("DarkOrchid1","#bf3eff") +color.indexColor("DarkOrchid2","#b23aee") +color.indexColor("DarkOrchid3","#9a32cd") +color.indexColor("DarkOrchid4","#68228b") +color.indexColor("DarkSalmon","#e9967a") +color.indexColor("DarkSeaGreen","#8fbc8f") +color.indexColor("DarkSeaGreen1","#c1ffc1") +color.indexColor("DarkSeaGreen2","#b4eeb4") +color.indexColor("DarkSeaGreen3","#9bcd9b") +color.indexColor("DarkSeaGreen4","#698b69") +color.indexColor("DarkSlateBlue","#483d8b") +color.indexColor("DarkSlateGray","#2f4f4f") +color.indexColor("DarkSlateGray1","#97ffff") +color.indexColor("DarkSlateGray2","#8deeee") +color.indexColor("DarkSlateGray3","#79cdcd") +color.indexColor("DarkSlateGray4","#528b8b") +color.indexColor("DarkTurquoise","#00ced1") +color.indexColor("DarkViolet","#9400d3") +color.indexColor("DeepPink1","#ff1493") +color.indexColor("DeepPink2","#ee1289") +color.indexColor("DeepPink3","#cd1076") +color.indexColor("DeepPink4","#8b0a50") +color.indexColor("DeepSkyBlue1","#00bfff") +color.indexColor("DeepSkyBlue2","#00b2ee") +color.indexColor("DeepSkyBlue3","#009acd") +color.indexColor("DeepSkyBlue4","#00688b") +color.indexColor("DimGray","#696969") +color.indexColor("DodgerBlue1","#1e90ff") +color.indexColor("DodgerBlue2","#1c86ee") +color.indexColor("DodgerBlue3","#1874cd") +color.indexColor("DodgerBlue4","#104e8b") +color.indexColor("firebrick","#b22222") +color.indexColor("firebrick1","#ff3030") +color.indexColor("firebrick2","#ee2c2c") +color.indexColor("firebrick3","#cd2626") +color.indexColor("firebrick4","#8b1a1a") +color.indexColor("FloralWhite","#fffaf0") +color.indexColor("ForestGreen","#228b22") +color.indexColor("gainsboro","#dcdcdc") +color.indexColor("GhostWhite","#f8f8ff") +color.indexColor("gold1","#ffd700") +color.indexColor("gold2","#eec900") +color.indexColor("gold3","#cdad00") +color.indexColor("gold4","#8b7500") +color.indexColor("goldenrod","#daa520") +color.indexColor("goldenrod1","#ffc125") +color.indexColor("goldenrod2","#eeb422") +color.indexColor("goldenrod3","#cd9b1d") +color.indexColor("goldenrod4","#8b6914") +color.indexColor("gray","#bebebe") +color.indexColor("green1","#00ff00") +color.indexColor("green2","#00ee00") +color.indexColor("green3","#00cd00") +color.indexColor("green4","#008b00") +color.indexColor("GreenYellow","#adff2f") +color.indexColor("honeydew1","#f0fff0") +color.indexColor("honeydew2","#e0eee0") +color.indexColor("honeydew3","#c1cdc1") +color.indexColor("honeydew4","#838b83") +color.indexColor("HotPink","#ff69b4") +color.indexColor("HotPink1","#ff6eb4") +color.indexColor("HotPink2","#ee6aa7") +color.indexColor("HotPink3","#cd6090") +color.indexColor("HotPink4","#8b3a62") +color.indexColor("IndianRed","#cd5c5c") +color.indexColor("IndianRed1","#ff6a6a") +color.indexColor("IndianRed2","#ee6363") +color.indexColor("IndianRed3","#cd5555") +color.indexColor("IndianRed4","#8b3a3a") +color.indexColor("ivory1","#fffff0") +color.indexColor("ivory2","#eeeee0") +color.indexColor("ivory3","#cdcdc1") +color.indexColor("ivory4","#8b8b83") +color.indexColor("khaki","#f0e68c") +color.indexColor("khaki1","#fff68f") +color.indexColor("khaki2","#eee685") +color.indexColor("khaki3","#cdc673") +color.indexColor("khaki4","#8b864e") +color.indexColor("lavender","#e6e6fa") +color.indexColor("LavenderBlush1","#fff0f5") +color.indexColor("LavenderBlush2","#eee0e5") +color.indexColor("LavenderBlush3","#cdc1c5") +color.indexColor("LavenderBlush4","#8b8386") +color.indexColor("LawnGreen","#7cfc00") +color.indexColor("LemonChiffon1","#fffacd") +color.indexColor("LemonChiffon2","#eee9bf") +color.indexColor("LemonChiffon3","#cdc9a5") +color.indexColor("LemonChiffon4","#8b8970") +color.indexColor("light","#eedd82") +color.indexColor("LightBlue","#add8e6") +color.indexColor("LightBlue1","#bfefff") +color.indexColor("LightBlue2","#b2dfee") +color.indexColor("LightBlue3","#9ac0cd") +color.indexColor("LightBlue4","#68838b") +color.indexColor("LightCoral","#f08080") +color.indexColor("LightCyan1","#e0ffff") +color.indexColor("LightCyan2","#d1eeee") +color.indexColor("LightCyan3","#b4cdcd") +color.indexColor("LightCyan4","#7a8b8b") +color.indexColor("LightGoldenrod1","#ffec8b") +color.indexColor("LightGoldenrod2","#eedc82") +color.indexColor("LightGoldenrod3","#cdbe70") +color.indexColor("LightGoldenrod4","#8b814c") +color.indexColor("LightGoldenrodYellow","#fafad2") +color.indexColor("LightGray","#d3d3d3") +color.indexColor("LightPink","#ffb6c1") +color.indexColor("LightPink1","#ffaeb9") +color.indexColor("LightPink2","#eea2ad") +color.indexColor("LightPink3","#cd8c95") +color.indexColor("LightPink4","#8b5f65") +color.indexColor("LightSalmon1","#ffa07a") +color.indexColor("LightSalmon2","#ee9572") +color.indexColor("LightSalmon3","#cd8162") +color.indexColor("LightSalmon4","#8b5742") +color.indexColor("LightSeaGreen","#20b2aa") +color.indexColor("LightSkyBlue","#87cefa") +color.indexColor("LightSkyBlue1","#b0e2ff") +color.indexColor("LightSkyBlue2","#a4d3ee") +color.indexColor("LightSkyBlue3","#8db6cd") +color.indexColor("LightSkyBlue4","#607b8b") +color.indexColor("LightSlateBlue","#8470ff") +color.indexColor("LightSlateGray","#778899") +color.indexColor("LightSteelBlue","#b0c4de") +color.indexColor("LightSteelBlue1","#cae1ff") +color.indexColor("LightSteelBlue2","#bcd2ee") +color.indexColor("LightSteelBlue3","#a2b5cd") +color.indexColor("LightSteelBlue4","#6e7b8b") +color.indexColor("LightYellow1","#ffffe0") +color.indexColor("LightYellow2","#eeeed1") +color.indexColor("LightYellow3","#cdcdb4") +color.indexColor("LightYellow4","#8b8b7a") +color.indexColor("LimeGreen","#32cd32") +color.indexColor("linen","#faf0e6") +color.indexColor("magenta","#ff00ff") +color.indexColor("magenta2","#ee00ee") +color.indexColor("magenta3","#cd00cd") +color.indexColor("magenta4","#8b008b") +color.indexColor("maroon","#b03060") +color.indexColor("maroon1","#ff34b3") +color.indexColor("maroon2","#ee30a7") +color.indexColor("maroon3","#cd2990") +color.indexColor("maroon4","#8b1c62") +color.indexColor("medium","#66cdaa") +color.indexColor("MediumAquamarine","#66cdaa") +color.indexColor("MediumBlue","#0000cd") +color.indexColor("MediumOrchid","#ba55d3") +color.indexColor("MediumOrchid1","#e066ff") +color.indexColor("MediumOrchid2","#d15fee") +color.indexColor("MediumOrchid3","#b452cd") +color.indexColor("MediumOrchid4","#7a378b") +color.indexColor("MediumPurple","#9370db") +color.indexColor("MediumPurple1","#ab82ff") +color.indexColor("MediumPurple2","#9f79ee") +color.indexColor("MediumPurple3","#8968cd") +color.indexColor("MediumPurple4","#5d478b") +color.indexColor("MediumSeaGreen","#3cb371") +color.indexColor("MediumSlateBlue","#7b68ee") +color.indexColor("MediumSpringGreen","#00fa9a") +color.indexColor("MediumTurquoise","#48d1cc") +color.indexColor("MediumVioletRed","#c71585") +color.indexColor("MidnightBlue","#191970") +color.indexColor("MintCream","#f5fffa") +color.indexColor("MistyRose1","#ffe4e1") +color.indexColor("MistyRose2","#eed5d2") +color.indexColor("MistyRose3","#cdb7b5") +color.indexColor("MistyRose4","#8b7d7b") +color.indexColor("moccasin","#ffe4b5") +color.indexColor("NavajoWhite1","#ffdead") +color.indexColor("NavajoWhite2","#eecfa1") +color.indexColor("NavajoWhite3","#cdb38b") +color.indexColor("NavajoWhite4","#8b795e") +color.indexColor("NavyBlue","#000080") +color.indexColor("OldLace","#fdf5e6") +color.indexColor("OliveDrab","#6b8e23") +color.indexColor("OliveDrab1","#c0ff3e") +color.indexColor("OliveDrab2","#b3ee3a") +color.indexColor("OliveDrab4","#698b22") +color.indexColor("orange1","#ffa500") +color.indexColor("orange2","#ee9a00") +color.indexColor("orange3","#cd8500") +color.indexColor("orange4","#8b5a00") +color.indexColor("OrangeRed1","#ff4500") +color.indexColor("OrangeRed2","#ee4000") +color.indexColor("OrangeRed3","#cd3700") +color.indexColor("OrangeRed4","#8b2500") +color.indexColor("orchid","#da70d6") +color.indexColor("orchid1","#ff83fa") +color.indexColor("orchid2","#ee7ae9") +color.indexColor("orchid3","#cd69c9") +color.indexColor("orchid4","#8b4789") +color.indexColor("pale","#db7093") +color.indexColor("PaleGoldenrod","#eee8aa") +color.indexColor("PaleGreen","#98fb98") +color.indexColor("PaleGreen1","#9aff9a") +color.indexColor("PaleGreen2","#90ee90") +color.indexColor("PaleGreen3","#7ccd7c") +color.indexColor("PaleGreen4","#548b54") +color.indexColor("PaleTurquoise","#afeeee") +color.indexColor("PaleTurquoise1","#bbffff") +color.indexColor("PaleTurquoise2","#aeeeee") +color.indexColor("PaleTurquoise3","#96cdcd") +color.indexColor("PaleTurquoise4","#668b8b") +color.indexColor("PaleVioletRed","#db7093") +color.indexColor("PaleVioletRed1","#ff82ab") +color.indexColor("PaleVioletRed2","#ee799f") +color.indexColor("PaleVioletRed3","#cd6889") +color.indexColor("PaleVioletRed4","#8b475d") +color.indexColor("PapayaWhip","#ffefd5") +color.indexColor("PeachPuff1","#ffdab9") +color.indexColor("PeachPuff2","#eecbad") +color.indexColor("PeachPuff3","#cdaf95") +color.indexColor("PeachPuff4","#8b7765") +color.indexColor("pink","#ffc0cb") +color.indexColor("pink1","#ffb5c5") +color.indexColor("pink2","#eea9b8") +color.indexColor("pink3","#cd919e") +color.indexColor("pink4","#8b636c") +color.indexColor("plum","#dda0dd") +color.indexColor("plum1","#ffbbff") +color.indexColor("plum2","#eeaeee") +color.indexColor("plum3","#cd96cd") +color.indexColor("plum4","#8b668b") +color.indexColor("PowderBlue","#b0e0e6") +color.indexColor("purple","#a020f0") +color.indexColor("purple1","#9b30ff") +color.indexColor("purple2","#912cee") +color.indexColor("purple3","#7d26cd") +color.indexColor("purple4","#551a8b") +color.indexColor("red1","#ff0000") +color.indexColor("red2","#ee0000") +color.indexColor("red3","#cd0000") +color.indexColor("red4","#8b0000") +color.indexColor("RosyBrown","#bc8f8f") +color.indexColor("RosyBrown1","#ffc1c1") +color.indexColor("RosyBrown2","#eeb4b4") +color.indexColor("RosyBrown3","#cd9b9b") +color.indexColor("RosyBrown4","#8b6969") +color.indexColor("RoyalBlue","#4169e1") +color.indexColor("RoyalBlue1","#4876ff") +color.indexColor("RoyalBlue2","#436eee") +color.indexColor("RoyalBlue3","#3a5fcd") +color.indexColor("RoyalBlue4","#27408b") +color.indexColor("SaddleBrown","#8b4513") +color.indexColor("salmon","#fa8072") +color.indexColor("salmon1","#ff8c69") +color.indexColor("salmon2","#ee8262") +color.indexColor("salmon3","#cd7054") +color.indexColor("salmon4","#8b4c39") +color.indexColor("SandyBrown","#f4a460") +color.indexColor("SeaGreen1","#54ff9f") +color.indexColor("SeaGreen2","#4eee94") +color.indexColor("SeaGreen3","#43cd80") +color.indexColor("SeaGreen4","#2e8b57") +color.indexColor("seashell1","#fff5ee") +color.indexColor("seashell2","#eee5de") +color.indexColor("seashell3","#cdc5bf") +color.indexColor("seashell4","#8b8682") +color.indexColor("sienna","#a0522d") +color.indexColor("sienna1","#ff8247") +color.indexColor("sienna2","#ee7942") +color.indexColor("sienna3","#cd6839") +color.indexColor("sienna4","#8b4726") +color.indexColor("SkyBlue","#87ceeb") +color.indexColor("SkyBlue1","#87ceff") +color.indexColor("SkyBlue2","#7ec0ee") +color.indexColor("SkyBlue3","#6ca6cd") +color.indexColor("SkyBlue4","#4a708b") +color.indexColor("SlateBlue","#6a5acd") +color.indexColor("SlateBlue1","#836fff") +color.indexColor("SlateBlue2","#7a67ee") +color.indexColor("SlateBlue3","#6959cd") +color.indexColor("SlateBlue4","#473c8b") +color.indexColor("SlateGray","#708090") +color.indexColor("SlateGray1","#c6e2ff") +color.indexColor("SlateGray2","#b9d3ee") +color.indexColor("SlateGray3","#9fb6cd") +color.indexColor("SlateGray4","#6c7b8b") +color.indexColor("snow1","#fffafa") +color.indexColor("snow2","#eee9e9") +color.indexColor("snow3","#cdc9c9") +color.indexColor("snow4","#8b8989") +color.indexColor("SpringGreen1","#00ff7f") +color.indexColor("SpringGreen2","#00ee76") +color.indexColor("SpringGreen3","#00cd66") +color.indexColor("SpringGreen4","#008b45") +color.indexColor("SteelBlue","#4682b4") +color.indexColor("SteelBlue1","#63b8ff") +color.indexColor("SteelBlue2","#5cacee") +color.indexColor("SteelBlue3","#4f94cd") +color.indexColor("SteelBlue4","#36648b") +color.indexColor("tan","#d2b48c") +color.indexColor("tan1","#ffa54f") +color.indexColor("tan2","#ee9a49") +color.indexColor("tan3","#cd853f") +color.indexColor("tan4","#8b5a2b") +color.indexColor("thistle","#d8bfd8") +color.indexColor("thistle1","#ffe1ff") +color.indexColor("thistle2","#eed2ee") +color.indexColor("thistle3","#cdb5cd") +color.indexColor("thistle4","#8b7b8b") +color.indexColor("tomato1","#ff6347") +color.indexColor("tomato2","#ee5c42") +color.indexColor("tomato3","#cd4f39") +color.indexColor("tomato4","#8b3626") +color.indexColor("turquoise","#40e0d0") +color.indexColor("turquoise1","#00f5ff") +color.indexColor("turquoise2","#00e5ee") +color.indexColor("turquoise3","#00c5cd") +color.indexColor("turquoise4","#00868b") +color.indexColor("violet","#ee82ee") +color.indexColor("VioletRed","#d02090") +color.indexColor("VioletRed1","#ff3e96") +color.indexColor("VioletRed2","#ee3a8c") +color.indexColor("VioletRed3","#cd3278") +color.indexColor("VioletRed4","#8b2252") +color.indexColor("wheat","#f5deb3") +color.indexColor("wheat1","#ffe7ba") +color.indexColor("wheat2","#eed8ae") +color.indexColor("wheat3","#cdba96") +color.indexColor("wheat4","#8b7e66") +color.indexColor("WhiteSmoke","#f5f5f5") +color.indexColor("yellow1","#ffff00") +color.indexColor("yellow2","#eeee00") +color.indexColor("yellow3","#cdcd00") +color.indexColor("yellow4","#8b8b00") +color.indexColor("YellowGreen","#9acd32") +color.indexColor("purple","#7e1e9c") +color.indexColor("green","#12a217") +color.indexColor("blue","#0343df") +color.indexColor("pink","#ff81c0") +color.indexColor("brown","#653700") +color.indexColor("red","#e50000") +color.indexColor("light_blue","#95d0fc") +color.indexColor("teal","#029386") +color.indexColor("orange","#f97306") +color.indexColor("light_green","#96f97b") +color.indexColor("magenta","#c20078") +color.indexColor("yellow","#ffff14") +color.indexColor("sky_blue","#75bbfd") +color.indexColor("grey","#929591") +color.indexColor("lime_green","#89fe05") +color.indexColor("light_purple","#bf77f6") +color.indexColor("violet","#9a0eea") +color.indexColor("dark_green","#033500") +color.indexColor("turquoise","#06c2ac") +color.indexColor("lavender","#c79fef") +color.indexColor("dark_blue","#00035b") +color.indexColor("tan","#d1b26f") +color.indexColor("cyan","#00ffff") +color.indexColor("aqua","#13eac9") +color.indexColor("forest_green","#06470c") +color.indexColor("mauve","#ae7181") +color.indexColor("dark_purple","#35063e") +color.indexColor("bright_green","#01ff07") +color.indexColor("maroon","#650021") +color.indexColor("olive","#6e750e") +color.indexColor("salmon","#ff796c") +color.indexColor("beige","#e6daa6") +color.indexColor("royal_blue","#0504aa") +color.indexColor("navy_blue","#001146") +color.indexColor("lilac","#cea2fd") +color.indexColor("black","#000000") +color.indexColor("hot_pink","#ff028d") +color.indexColor("light_brown","#ad8150") +color.indexColor("pale_green","#c7fdb5") +color.indexColor("peach","#ffb07c") +color.indexColor("olive_green","#677a04") +color.indexColor("dark_pink","#cb416b") +color.indexColor("periwinkle","#8e82fe") +color.indexColor("sea_green","#53fca1") +color.indexColor("lime","#aaff32") +color.indexColor("indigo","#380282") +color.indexColor("mustard","#ceb301") +color.indexColor("light_pink","#ffd1df") +color.indexColor("rose","#cf6275") +color.indexColor("bright_blue","#0165fc") +color.indexColor("neon_green","#0cff0c") +color.indexColor("burnt_orange","#c04e01") +color.indexColor("aquamarine","#04d8b2") +color.indexColor("navy","#01153e") +color.indexColor("grass_green","#3f9b0b") +color.indexColor("pale_blue","#d0fefe") +color.indexColor("dark_red","#840000") +color.indexColor("bright_purple","#be03fd") +color.indexColor("yellow_green","#c0fb2d") +color.indexColor("baby_blue","#a2cffe") +color.indexColor("gold","#dbb40c") +color.indexColor("mint_green","#8fff9f") +color.indexColor("plum","#580f41") +color.indexColor("royal_purple","#4b006e") +color.indexColor("brick_red","#8f1402") +color.indexColor("dark_teal","#014d4e") +color.indexColor("burgundy","#610023") +color.indexColor("khaki","#aaa662") +color.indexColor("blue_green","#137e6d") +color.indexColor("seafoam_green","#7af9ab") +color.indexColor("kelly_green","#02ab2e") +color.indexColor("puke_green","#9aae07") +color.indexColor("pea_green","#8eab12") +color.indexColor("taupe","#b9a281") +color.indexColor("dark_brown","#341c02") +color.indexColor("deep_purple","#36013f") +color.indexColor("chartreuse","#c1f80a") +color.indexColor("bright_pink","#fe01b1") +color.indexColor("light_orange","#fdaa48") +color.indexColor("mint","#9ffeb0") +color.indexColor("pastel_green","#b0ff9d") +color.indexColor("sand","#e2ca76") +color.indexColor("dark_orange","#c65102") +color.indexColor("spring_green","#a9f971") +color.indexColor("puce","#a57e52") +color.indexColor("seafoam","#80f9ad") +color.indexColor("grey_blue","#6b8ba4") +color.indexColor("army_green","#4b5d16") +color.indexColor("dark_grey","#363737") +color.indexColor("dark_yellow","#d5b60a") +color.indexColor("goldenrod","#fac205") +color.indexColor("slate","#516572") +color.indexColor("light_teal","#90e4c1") +color.indexColor("rust","#a83c09") +color.indexColor("deep_blue","#040273") +color.indexColor("pale_pink","#ffcfdc") +color.indexColor("cerulean","#0485d1") +color.indexColor("light_red","#ff474c") +color.indexColor("mustard_yellow","#d2bd0a") +color.indexColor("ochre","#bf9005") +color.indexColor("pale_yellow","#ffff84") +color.indexColor("crimson","#8c000f") +color.indexColor("fuchsia","#ed0dd9") +color.indexColor("hunter_green","#0b4008") +color.indexColor("blue_grey","#607c8e") +color.indexColor("slate_blue","#5b7c99") +color.indexColor("pale_purple","#b790d4") +color.indexColor("sea_blue","#047495") +color.indexColor("pinkish_purple","#d648d7") +color.indexColor("puke","#a5a502") +color.indexColor("light_grey","#d8dcd6") +color.indexColor("leaf_green","#5ca904") +color.indexColor("light_yellow","#fffe7a") +color.indexColor("eggplant","#380835") +color.indexColor("steel_blue","#5a7d9a") +color.indexColor("moss_green","#658b38") +color.indexColor("robin's_egg_blue","#98eff9") +color.indexColor("white","#ffffff") +color.indexColor("grey_green","#789b73") +color.indexColor("sage","#87ae73") +color.indexColor("brick","#a03623") +color.indexColor("burnt_sienna","#b04e0f") +color.indexColor("reddish_brown","#7f2b0a") +color.indexColor("cream","#ffffc2") +color.indexColor("coral","#fc5a50") +color.indexColor("ocean_blue","#03719c") +color.indexColor("greenish","#40a368") +color.indexColor("dark_magenta","#960056") +color.indexColor("red_orange","#fd3c06") +color.indexColor("bluish_purple","#703be7") +color.indexColor("midnight_blue","#020035") +color.indexColor("light_violet","#d6b4fc") +color.indexColor("dusty_rose","#c0737a") +color.indexColor("medium_blue","#2c6fbb") +color.indexColor("greenish_yellow","#cdfd02") +color.indexColor("yellowish_green","#b0dd16") +color.indexColor("purplish_blue","#601ef9") +color.indexColor("greyish_blue","#5e819d") +color.indexColor("grape","#6c3461") +color.indexColor("light_olive","#acbf69") +color.indexColor("cornflower_blue","#5170d7") +color.indexColor("pinkish_red","#f10c45") +color.indexColor("bright_red","#ff000d") +color.indexColor("azure","#069af3") +color.indexColor("blue_purple","#5729ce") +color.indexColor("dark_turquoise","#045c5a") +color.indexColor("electric_blue","#0652ff") +color.indexColor("off_white","#ffffe4") +color.indexColor("powder_blue","#b1d1fc") +color.indexColor("wine","#80013f") +color.indexColor("dull_green","#74a662") +color.indexColor("apple_green","#76cd26") +color.indexColor("light_turquoise","#7ef4cc") +color.indexColor("neon_purple","#bc13fe") +color.indexColor("cobalt","#1e488f") +color.indexColor("pinkish","#d46a7e") +color.indexColor("olive_drab","#6f7632") +color.indexColor("dark_cyan","#0a888a") +color.indexColor("purple_blue","#632de9") +color.indexColor("dark_violet","#34013f") +color.indexColor("dark_lavender","#856798") +color.indexColor("forrest_green","#154406") +color.indexColor("vomit","#a2a415") +color.indexColor("pale_orange","#ffa756") +color.indexColor("greenish_blue","#0b8b87") +color.indexColor("dark_tan","#af884a") +color.indexColor("green_blue","#06b48b") +color.indexColor("bluish_green","#10a674") +color.indexColor("pastel_blue","#a2bffe") +color.indexColor("moss","#769958") +color.indexColor("grass","#5cac2d") +color.indexColor("deep_pink","#cb0162") +color.indexColor("blood_red","#980002") +color.indexColor("sage_green","#88b378") +color.indexColor("aqua_blue","#02d8e9") +color.indexColor("terracotta","#ca6641") +color.indexColor("pastel_purple","#caa0ff") +color.indexColor("sienna","#a9561e") +color.indexColor("dark_olive","#373e02") +color.indexColor("green_yellow","#c9ff27") +color.indexColor("scarlet","#be0119") +color.indexColor("greyish_green","#82a67d") +color.indexColor("chocolate","#3d1c02") +color.indexColor("blue_violet","#5d06e9") +color.indexColor("cornflower","#6a79f7") +color.indexColor("baby_pink","#ffb7ce") +color.indexColor("charcoal","#343837") +color.indexColor("pine_green","#0a481e") +color.indexColor("pumpkin","#e17701") +color.indexColor("greenish_brown","#696112") +color.indexColor("red_brown","#8b2e16") +color.indexColor("brownish_green","#6a6e09") +color.indexColor("tangerine","#ff9408") +color.indexColor("salmon_pink","#fe7b7c") +color.indexColor("aqua_green","#12e193") +color.indexColor("raspberry","#b00149") +color.indexColor("greyish_purple","#887191") +color.indexColor("rose_pink","#f7879a") +color.indexColor("neon_pink","#fe019a") +color.indexColor("cobalt_blue","#030aa7") +color.indexColor("orange_brown","#be6400") +color.indexColor("deep_red","#9a0200") +color.indexColor("orange_red","#fd411e") +color.indexColor("dirty_yellow","#cdc50a") +color.indexColor("orchid","#c875c4") +color.indexColor("reddish_pink","#fe2c54") +color.indexColor("reddish_purple","#910951") +color.indexColor("yellow_orange","#fcb001") +color.indexColor("light_cyan","#acfffc") +color.indexColor("sky","#82cafc") +color.indexColor("light_magenta","#fa5ff7") +color.indexColor("pale_red","#d9544d") +color.indexColor("emerald","#01a049") +color.indexColor("dark_beige","#ac9362") +color.indexColor("ugly_green","#7a9703") +color.indexColor("jade","#1fa774") +color.indexColor("greenish_grey","#96ae8d") +color.indexColor("dark_salmon","#c85a53") +color.indexColor("purplish_pink","#ce5dae") +color.indexColor("dark_aqua","#05696b") +color.indexColor("brownish_orange","#cb7723") +color.indexColor("light_olive_green","#a4be5c") +color.indexColor("light_aqua","#8cffdb") +color.indexColor("clay","#b66a50") +color.indexColor("medium_green","#39ad48") +color.indexColor("burnt_umber","#a0450e") +color.indexColor("dull_blue","#49759c") +color.indexColor("pale_brown","#b1916e") +color.indexColor("emerald_green","#028f1e") +color.indexColor("brownish","#9c6d57") +color.indexColor("mud","#735c12") +color.indexColor("dark_rose","#b5485d") +color.indexColor("brownish_red","#9e3623") +color.indexColor("pink_purple","#db4bda") +color.indexColor("pinky_purple","#c94cbe") +color.indexColor("camo_green","#526525") +color.indexColor("faded_green","#7bb274") +color.indexColor("dusty_pink","#d58a94") +color.indexColor("purple_pink","#e03fd8") +color.indexColor("vomit_green","#89a203") +color.indexColor("deep_green","#02590f") +color.indexColor("reddish_orange","#f8481c") +color.indexColor("mahogany","#4a0100") +color.indexColor("aubergine","#3d0734") +color.indexColor("dull_pink","#d5869d") +color.indexColor("evergreen","#05472a") +color.indexColor("dark_sky_blue","#448ee4") +color.indexColor("very_light_green","#d1ffbd") +color.indexColor("pastel_pink","#ffbacd") +color.indexColor("grey_purple","#826d8c") +color.indexColor("very_light_blue","#d5ffff") +color.indexColor("dark_mauve","#874c62") +color.indexColor("cadet_blue","#4e7496") +color.indexColor("ice_blue","#d7fffe") +color.indexColor("light_tan","#fbeeac") +color.indexColor("dirty_green","#667e2c") +color.indexColor("neon_blue","#04d9ff") +color.indexColor("wine_red","#7b0323") +color.indexColor("chocolate_brown","#411900") +color.indexColor("dull_purple","#84597e") +color.indexColor("yellow_brown","#b79400") +color.indexColor("denim","#3b638c") +color.indexColor("eggshell","#ffffd4") +color.indexColor("jungle_green","#048243") +color.indexColor("dark_peach","#de7e5d") +color.indexColor("poop","#7f5e00") +color.indexColor("umber","#b26400") +color.indexColor("light_lavender","#dfc5fe") +color.indexColor("bright_yellow","#fffd01") +color.indexColor("golden_yellow","#fec615") +color.indexColor("dusty_blue","#5a86ad") +color.indexColor("electric_green","#21fc0d") +color.indexColor("lighter_green","#75fd63") +color.indexColor("slate_grey","#59656d") +color.indexColor("teal_green","#25a36f") +color.indexColor("marine_blue","#01386a") +color.indexColor("avocado","#90b134") +color.indexColor("terra_cotta","#c9643b") +color.indexColor("dusty_purple","#825f87") +color.indexColor("light_maroon","#a24857") +color.indexColor("reddish","#c44240") +color.indexColor("dark_lilac","#9c6da5") +color.indexColor("dark_periwinkle","#665fd1") +color.indexColor("bluish_grey","#748b97") +color.indexColor("puke_yellow","#c2be0e") +color.indexColor("purplish","#94568c") +color.indexColor("ultramarine","#2000b1") +color.indexColor("barney_purple","#a00498") +color.indexColor("forest","#0b5509") +color.indexColor("pea_soup","#929901") +color.indexColor("brownish_yellow","#c9b003") +color.indexColor("bright_teal","#01f9c6") +color.indexColor("bluegreen","#017a79") +color.indexColor("green_brown","#544e03") +color.indexColor("blurple","#5539cc") +color.indexColor("light_sky_blue","#c6fcff") +color.indexColor("periwinkle_blue","#8f99fb") +color.indexColor("pale_violet","#ceaefa") +color.indexColor("darker_green","#087804") +color.indexColor("true_blue","#010fcc") +color.indexColor("green_grey","#77926f") +color.indexColor("grey_brown","#7f7053") +color.indexColor("dark_olive_green","#3c4d03") +color.indexColor("apricot","#ffb16d") +color.indexColor("faded_purple","#916e99") +color.indexColor("darker_blue","#011288") +color.indexColor("cerise","#de0c62") +color.indexColor("khaki_green","#728639") +color.indexColor("burnt_red","#9f2305") +color.indexColor("light_forest_green","#4f9153") +color.indexColor("violet_blue","#510ac9") +color.indexColor("pale_lavender","#eecffe") +color.indexColor("acid_green","#8ffe09") +color.indexColor("purple_grey","#866f85") +color.indexColor("lemon","#fdff52") +color.indexColor("bright_orange","#ff5b00") +color.indexColor("soft_green","#6fc276") +color.indexColor("blush","#f29e8e") +color.indexColor("yellowish_brown","#9b7a01") +color.indexColor("fluorescent_green","#08ff08") +color.indexColor("electric_purple","#aa23ff") +color.indexColor("steel","#738595") +color.indexColor("dull_orange","#d8863b") +color.indexColor("muddy_green","#657432") +color.indexColor("marigold","#fcc006") +color.indexColor("ocean","#017b92") +color.indexColor("light_mauve","#c292a1") +color.indexColor("bordeaux","#7b002c") +color.indexColor("light_blue_green","#7efbb3") +color.indexColor("yellowish","#faee66") +color.indexColor("snot_green","#9dc100") +color.indexColor("light_lime_green","#b9ff66") +color.indexColor("drab_green","#749551") +color.indexColor("faded_blue","#658cbb") +color.indexColor("dark_forest_green","#002d04") +color.indexColor("hot_purple","#cb00f5") +color.indexColor("dark_maroon","#3c0008") +color.indexColor("brown_green","#706c11") +color.indexColor("swamp_green","#748500") +color.indexColor("light_indigo","#6d5acf") +color.indexColor("purpley_blue","#5f34e7") +color.indexColor("lightish_blue","#3d7afd") +color.indexColor("teal_blue","#01889f") +color.indexColor("denim_blue","#3b5b92") +color.indexColor("dark_lime_green","#7ebd01") +color.indexColor("dull_yellow","#eedc5b") +color.indexColor("pistachio","#c0fa8b") +color.indexColor("lemon_yellow","#fdff38") +color.indexColor("red_violet","#9e0168") +color.indexColor("dusky_pink","#cc7a8b") +color.indexColor("dirt","#8a6e45") +color.indexColor("very_dark_green","#062e03") +color.indexColor("medium_purple","#9e43a2") +color.indexColor("shit","#7f5f00") +color.indexColor("dark_mustard","#a88905") +color.indexColor("pea_soup_green","#94a617") +color.indexColor("bubblegum_pink","#fe83cc") +color.indexColor("barbie_pink","#fe46a5") +color.indexColor("military_green","#667c3e") +color.indexColor("pale_teal","#82cbb2") +color.indexColor("bronze","#a87900") +color.indexColor("pinky_red","#fc2647") +color.indexColor("dull_red","#bb3f3f") +color.indexColor("darkish_blue","#014182") +color.indexColor("bluish","#2976bb") +color.indexColor("dark_gold","#b59410") +color.indexColor("yellowy_green","#bff128") +color.indexColor("pine","#2b5d34") +color.indexColor("dark_blue_green","#005249") +color.indexColor("dirty_pink","#ca7b80") +color.indexColor("slate_green","#658d6d") +color.indexColor("prussian_blue","#004577") +color.indexColor("bright_violet","#ad0afd") +color.indexColor("lighter_purple","#a55af4") +color.indexColor("steel_grey","#6f828a") +color.indexColor("russet","#a13905") +color.indexColor("vermillion","#f4320c") +color.indexColor("greyish_brown","#7a6a4f") +color.indexColor("red_purple","#820747") +color.indexColor("red_pink","#fa2a55") +color.indexColor("bright_turquoise","#0ffef9") +color.indexColor("golden_brown","#b27a01") +color.indexColor("cerulean_blue","#056eee") +color.indexColor("soft_blue","#6488ea") +color.indexColor("easter_green","#8cfd7e") +color.indexColor("amber","#feb308") +color.indexColor("mid_blue","#276ab3") +color.indexColor("shit_brown","#7b5804") +color.indexColor("hospital_green","#9be5aa") +color.indexColor("purpleish_blue","#6140ef") +color.indexColor("purply_blue","#661aee") +color.indexColor("silver","#c5c9c7") +color.indexColor("sickly_green","#94b21c") +color.indexColor("melon","#ff7855") +color.indexColor("dusky_rose","#ba6873") +color.indexColor("brown_orange","#b96902") +color.indexColor("darkish_green","#287c37") +color.indexColor("cranberry","#9e003a") +color.indexColor("purpleish","#98568d") +color.indexColor("ecru","#feffca") +color.indexColor("darker_purple","#5f1b6b") +color.indexColor("mocha","#9d7651") +color.indexColor("bright_magenta","#ff08e8") +color.indexColor("coffee","#a6814c") +color.indexColor("sepia","#985e2b") +color.indexColor("faded_red","#d3494e") +color.indexColor("canary_yellow","#fffe40") +color.indexColor("bluey_purple","#6241c7") +color.indexColor("pastel_yellow","#fffe71") +color.indexColor("pale_turquoise","#a5fbd5") +color.indexColor("greyish_pink","#c88d94") +color.indexColor("marine","#042e60") +color.indexColor("purplish_grey","#7a687f") +color.indexColor("camel","#c69f59") +color.indexColor("brownish_grey","#86775f") +color.indexColor("burnt_yellow","#d5ab09") +color.indexColor("cherry_red","#f7022a") +color.indexColor("orangey_brown","#b16002") +color.indexColor("soft_pink","#fdb0c0") +color.indexColor("dark_sea_green","#11875d") +color.indexColor("aqua_marine","#2ee8bb") +color.indexColor("robin_egg_blue","#8af1fe") +color.indexColor("light_sea_green","#98f6b0") +color.indexColor("mud_brown","#60460f") +color.indexColor("sandstone","#c9ae74") +color.indexColor("british_racing_green","#05480d") +color.indexColor("faded_pink","#de9dac") +color.indexColor("maize","#f4d054") +color.indexColor("ocre","#c69c04") +color.indexColor("orange_yellow","#ffad01") +color.indexColor("dark_khaki","#9b8f55") +color.indexColor("light_lime","#aefd6c") +color.indexColor("bright_light_blue","#26f7fd") +color.indexColor("jade_green","#2baf6a") +color.indexColor("barney","#ac1db8") +color.indexColor("adobe","#bd6c48") +color.indexColor("minty_green","#0bf77d") +color.indexColor("light_navy_blue","#2e5a88") +color.indexColor("dusty_green","#76a973") +color.indexColor("very_dark_blue","#000133") +color.indexColor("ocean_green","#3d9973") +color.indexColor("mustard_green","#a8b504") +color.indexColor("poop_brown","#7a5901") +color.indexColor("olive_brown","#645403") +color.indexColor("pink_red","#f5054f") +color.indexColor("light_navy","#155084") +color.indexColor("very_light_purple","#f6cefc") +color.indexColor("ivory","#ffffcb") +color.indexColor("bright_lavender","#c760ff") +color.indexColor("bright_aqua","#0bf9ea") +color.indexColor("robin's_egg","#6dedfd") +color.indexColor("muted_green","#5fa052") +color.indexColor("medium_brown","#7f5112") +color.indexColor("copper","#b66325") +color.indexColor("dark_lime","#84b701") +color.indexColor("strawberry","#fb2943") +color.indexColor("dirt_brown","#836539") +color.indexColor("celery","#c1fd95") +color.indexColor("bright_sky_blue","#02ccfe") +color.indexColor("poo_brown","#885f01") +color.indexColor("pinkish_brown","#b17261") +color.indexColor("celadon","#befdb7") +color.indexColor("bright_lime_green","#65fe08") +color.indexColor("auburn","#9a3001") +color.indexColor("shocking_pink","#fe02a2") +color.indexColor("mulberry","#920a4e") +color.indexColor("carolina_blue","#8ab8fe") +color.indexColor("lightish_green","#61e160") +color.indexColor("light_lilac","#edc8ff") +color.indexColor("pale_olive","#b9cc81") +color.indexColor("pumpkin_orange","#fb7d07") +color.indexColor("yellow_ochre","#cb9d06") +color.indexColor("fire_engine_red","#fe0002") +color.indexColor("deep_sky_blue","#0d75f8") +color.indexColor("watermelon","#fd4659") +color.indexColor("bottle_green","#044a05") +color.indexColor("very_dark_purple","#2a0134") +color.indexColor("wheat","#fbdd7e") +color.indexColor("murky_green","#6c7a0e") +color.indexColor("brownish_purple","#76424e") +color.indexColor("kermit_green","#5cb200") +color.indexColor("primary_blue","#0804f9") +color.indexColor("orangey_red","#fa4224") +color.indexColor("pale_lilac","#e4cbff") +color.indexColor("rust_red","#aa2704") +color.indexColor("dirty_orange","#c87606") +color.indexColor("pinkish_grey","#c8aca9") +color.indexColor("light_plum","#9d5783") +color.indexColor("greeny_blue","#42b395") +color.indexColor("dark_navy","#000435") +color.indexColor("pink/purple","#ef1de7") +color.indexColor("irish_green","#019529") +color.indexColor("baby_poop","#937c00") +color.indexColor("slime_green","#99cc04") +color.indexColor("purplish_red","#b0054b") +color.indexColor("rouge","#ab1239") +color.indexColor("light_rose","#ffc5cb") +color.indexColor("drab","#828344") +color.indexColor("dark_navy_blue","#00022e") +color.indexColor("light_yellow_green","#ccfd7f") +color.indexColor("easter_purple","#c071fe") +color.indexColor("snot","#acbb0d") +color.indexColor("light_salmon","#fea993") +color.indexColor("purpley_pink","#c83cb9") +color.indexColor("poo","#8f7303") +color.indexColor("berry","#990f4b") +color.indexColor("medium_grey","#7d7f7c") +color.indexColor("brown_red","#922b05") +color.indexColor("blood","#770001") +color.indexColor("soft_purple","#a66fb5") +color.indexColor("grey_pink","#c3909b") +color.indexColor("bluey_green","#2bb179") +color.indexColor("midnight","#03012d") +color.indexColor("dark_indigo","#1f0954") +color.indexColor("warm_grey","#978a84") +color.indexColor("sandy_brown","#c4a661") +color.indexColor("cherry","#cf0234") +color.indexColor("blue/purple","#5a06ef") +color.indexColor("gunmetal","#536267") +color.indexColor("deep_violet","#490648") +color.indexColor("tree_green","#2a7e19") +color.indexColor("orangish_brown","#b25f03") +color.indexColor("shamrock_green","#02c14d") +color.indexColor("orangish_red","#f43605") +color.indexColor("greeny_yellow","#c6f808") +color.indexColor("ugly_yellow","#d0c101") +color.indexColor("french_blue","#436bad") +color.indexColor("dusky_purple","#895b7b") +color.indexColor("butter_yellow","#fffd74") +color.indexColor("light_beige","#fffeb6") +color.indexColor("golden","#f5bf03") +color.indexColor("dusky_blue","#475f94") +color.indexColor("lightblue","#7bc8f6") +color.indexColor("purply_pink","#f075e6") +color.indexColor("off_green","#6ba353") +color.indexColor("ocher","#bf9b0c") +color.indexColor("milk_chocolate","#7f4e1e") +color.indexColor("light_peach","#ffd8b1") +color.indexColor("deep_magenta","#a0025c") +color.indexColor("caramel","#af6f09") +color.indexColor("greenish_teal","#32bf84") +color.indexColor("pale_lime","#befd73") +color.indexColor("purple_red","#990147") +color.indexColor("blueberry","#464196") +color.indexColor("asparagus","#77ab56") +color.indexColor("pale_grey","#fdfdfe") +color.indexColor("light_grey_blue","#9dbcd4") +color.indexColor("pale_lime_green","#b1ff65") +color.indexColor("grassy_green","#419c03") +color.indexColor("mossy_green","#638b27") +color.indexColor("earth","#a2653e") +color.indexColor("deep_orange","#dc4d01") +color.indexColor("pale_aqua","#b8ffeb") +color.indexColor("rose_red","#be013c") +color.indexColor("stone","#ada587") +color.indexColor("rusty_orange","#cd5909") +color.indexColor("pea","#a4bf20") +color.indexColor("sick_green","#9db92c") +color.indexColor("darker_pink","#c4387f") +color.indexColor("chestnut","#742802") +color.indexColor("blue/green","#0f9b8e") +color.indexColor("amethyst","#9b5fc0") +color.indexColor("dark_mint_green","#20c073") +color.indexColor("pale_rose","#fdc1c5") +color.indexColor("muted_blue","#3b719f") +color.indexColor("fawn","#cfaf7b") +color.indexColor("buff","#fef69e") +color.indexColor("turquoise_green","#04f489") +color.indexColor("muddy_brown","#886806") +color.indexColor("sea","#3c9992") +color.indexColor("tomato","#ef4026") +color.indexColor("carnation_pink","#ff7fa7") +color.indexColor("banana","#ffff7e") +color.indexColor("neon_yellow","#cfff04") +color.indexColor("greyish","#a8a495") +color.indexColor("mid_green","#50a747") +color.indexColor("muted_purple","#805b87") +color.indexColor("electric_pink","#ff0490") +color.indexColor("sandy","#f1da7a") +color.indexColor("ugly_pink","#cd7584") +color.indexColor("turquoise_blue","#06b1c4") +color.indexColor("light_burgundy","#a8415b") +color.indexColor("greenish_tan","#bccb7a") +color.indexColor("dark_mint","#48c072") +color.indexColor("light_urple","#b36ff6") +color.indexColor("midnight_purple","#280137") +color.indexColor("pinkish_orange","#ff724c") +color.indexColor("pear","#cbf85f") +color.indexColor("dark_plum","#3f012c") +color.indexColor("tealish","#24bca8") +color.indexColor("perrywinkle","#8f8ce7") +color.indexColor("yellowish_orange","#ffab0f") +color.indexColor("pastel_orange","#ff964f") +color.indexColor("iris","#6258c4") +color.indexColor("ultramarine_blue","#1805db") +color.indexColor("navy_green","#35530a") +color.indexColor("seaweed","#18d17b") +color.indexColor("kiwi","#9cef43") +color.indexColor("fluro_green","#0aff02") +color.indexColor("bright_light_green","#2dfe54") +color.indexColor("vivid_green","#2fef10") +color.indexColor("frog_green","#58bc08") +color.indexColor("dull_brown","#876e4b") +color.indexColor("dusk","#4e5481") +color.indexColor("mustard_brown","#ac7e04") +color.indexColor("leafy_green","#51b73b") +color.indexColor("cool_blue","#4984b8") +color.indexColor("almost_black","#070d0d") +color.indexColor("yellow/green","#c8fd3d") +color.indexColor("heliotrope","#d94ff5") +color.indexColor("green_apple","#5edc1f") +color.indexColor("baby_poop_green","#8f9805") +color.indexColor("apple","#6ecb3c") +color.indexColor("purpleish_pink","#df4ec8") +color.indexColor("night_blue","#040348") +color.indexColor("merlot","#730039") +color.indexColor("lightgreen","#76ff7b") +color.indexColor("tomato_red","#ec2d01") +color.indexColor("key_lime","#aeff6e") +color.indexColor("pale_cyan","#b7fffa") +color.indexColor("vomit_yellow","#c7c10c") +color.indexColor("purplish_brown","#6b4247") +color.indexColor("bubblegum","#ff6cb5") +color.indexColor("shamrock","#01b44c") +color.indexColor("mango","#ffa62b") +color.indexColor("lime_yellow","#d0fe1d") +color.indexColor("hot_green","#25ff29") +color.indexColor("grape_purple","#5d1451") +color.indexColor("faded_orange","#f0944d") +color.indexColor("avocado_green","#87a922") +color.indexColor("peacock_blue","#016795") +color.indexColor("weird_green","#3ae57f") +color.indexColor("bright_lilac","#c95efb") +color.indexColor("fern_green","#548d44") +color.indexColor("dirty_blue","#3f829d") +color.indexColor("rust_orange","#c45508") +color.indexColor("heather","#a484ac") +color.indexColor("deep_teal","#00555a") +color.indexColor("dark_seafoam","#1fb57a") +color.indexColor("baby_poo","#ab9004") +color.indexColor("yellowgreen","#bbf90f") +color.indexColor("light_sage","#bcecac") +color.indexColor("light_aquamarine","#7bfdc7") +color.indexColor("spearmint","#1ef876") +color.indexColor("bright_lime","#87fd05") +color.indexColor("vibrant_green","#0add08") +color.indexColor("very_pale_green","#cffdbc") +color.indexColor("faded_yellow","#feff7f") +color.indexColor("bile","#b5c306") +color.indexColor("viridian","#1e9167") +color.indexColor("very_light_pink","#fff4f2") +color.indexColor("puke_brown","#947706") +color.indexColor("medium_pink","#f36196") +color.indexColor("ugly_purple","#a442a0") +color.indexColor("sunshine_yellow","#fffd37") +color.indexColor("seaweed_green","#35ad6b") +color.indexColor("light_periwinkle","#c1c6fc") +color.indexColor("lemon_green","#adf802") +color.indexColor("greeny_brown","#696006") +color.indexColor("dark_grey_blue","#29465b") +color.indexColor("bright_olive","#9cbb04") +color.indexColor("turtle_green","#75b84f") +color.indexColor("pale_sky_blue","#bdf6fe") +color.indexColor("light_mustard","#f7d560") +color.indexColor("diarrhea","#9f8303") +color.indexColor("dark_aquamarine","#017371") +color.indexColor("brownish_pink","#c27e79") +color.indexColor("baby_shit_green","#889717") +color.indexColor("purpley","#8756e4") +color.indexColor("greyblue","#77a1b5") +color.indexColor("hot_magenta","#f504c9") +color.indexColor("blue/grey","#758da3") +color.indexColor("pale","#fff9d0") +color.indexColor("cool_green","#33b864") +color.indexColor("sandy_yellow","#fdee73") +color.indexColor("eggshell_blue","#c4fff7") +color.indexColor("barf_green","#94ac02") +color.indexColor("baby_green","#8cff9e") +color.indexColor("vibrant_purple","#ad03de") +color.indexColor("brown_grey","#8d8468") +color.indexColor("water_blue","#0e87cc") +color.indexColor("lipstick_red","#c0022f") +color.indexColor("banana_yellow","#fafe4b") +color.indexColor("wisteria","#a87dc2") +color.indexColor("purple_brown","#673a3f") +color.indexColor("brown_yellow","#b29705") +color.indexColor("purple/pink","#d725de") +color.indexColor("lemon_lime","#bffe28") +color.indexColor("grey/blue","#647d8e") +color.indexColor("dusty_red","#b9484e") +color.indexColor("deep_rose","#c74767") +color.indexColor("dark_seafoam_green","#3eaf76") +color.indexColor("muddy_yellow","#bfac05") +color.indexColor("carnation","#fd798f") +color.indexColor("yellowy_brown","#ae8b0c") +color.indexColor("violet_red","#a50055") +color.indexColor("twilight_blue","#0a437a") +color.indexColor("pure_blue","#0203e2") +color.indexColor("lightish_red","#fe2f4a") +color.indexColor("brick_orange","#c14a09") +color.indexColor("velvet","#750851") +color.indexColor("sunflower","#ffc512") +color.indexColor("light_mint_green","#a6fbb2") +color.indexColor("light_grass_green","#9af764") +color.indexColor("lavender_blue","#8b88f8") +color.indexColor("rusty_red","#af2f0d") +color.indexColor("lightish_purple","#a552e6") +color.indexColor("dried_blood","#4b0101") +color.indexColor("light_blue_grey","#b7c9e2") +color.indexColor("leaf","#71aa34") +color.indexColor("orangish","#fc824a") +color.indexColor("pale_olive_green","#b1d27b") +color.indexColor("off_yellow","#f1f33f") +color.indexColor("dusty_orange","#f0833a") +color.indexColor("butter","#ffff81") +color.indexColor("royal","#0c1793") +color.indexColor("petrol","#005f6a") +color.indexColor("greenish_cyan","#2afeb7") +color.indexColor("duck_egg_blue","#c3fbf4") +color.indexColor("bubble_gum_pink","#ff69af") +color.indexColor("bluegrey","#85a3b2") +color.indexColor("warm_brown","#964e02") +color.indexColor("twilight","#4e518b") +color.indexColor("saffron","#feb209") +color.indexColor("purple/blue","#5d21d0") +color.indexColor("dark_sand","#a88f59") +color.indexColor("vibrant_blue","#0339f8") +color.indexColor("putty","#beae8a") +color.indexColor("lawn_green","#4da409") +color.indexColor("camouflage_green","#4b6113") +color.indexColor("blush_pink","#fe828c") +color.indexColor("reddy_brown","#6e1005") +color.indexColor("darkish_red","#a90308") +color.indexColor("algae_green","#21c36f") +color.indexColor("dark_coral","#cf524e") +color.indexColor("bright_cyan","#41fdfe") +color.indexColor("piss_yellow","#ddd618") +color.indexColor("pastel_red","#db5856") +color.indexColor("greenish_turquoise","#00fbb0") +color.indexColor("dark","#1b2431") +color.indexColor("ruby","#ca0147") +color.indexColor("poop_green","#6f7c00") +color.indexColor("orangered","#fe420f") +color.indexColor("dandelion","#fedf08") +color.indexColor("claret","#680018") +color.indexColor("pale_mauve","#fed0fc") +color.indexColor("lipstick","#d5174e") +color.indexColor("rosa","#fe86a4") +color.indexColor("darkblue","#030764") +color.indexColor("tan_brown","#ab7e4c") +color.indexColor("shit_green","#758000") +color.indexColor("red_wine","#8c0034") +color.indexColor("pinky","#fc86aa") +color.indexColor("mud_green","#606602") +color.indexColor("light_greenish_blue","#63f7b4") +color.indexColor("dull_teal","#5f9e8f") +color.indexColor("deep_lavender","#8d5eb7") +color.indexColor("vivid_blue","#152eff") +color.indexColor("raw_umber","#a75e09") +color.indexColor("light_mint","#b6ffbb") +color.indexColor("light_light_blue","#cafffb") +color.indexColor("highlighter_green","#1bfc06") +color.indexColor("greeny_grey","#7ea07a") +color.indexColor("bluey_grey","#89a0b0") +color.indexColor("algae","#54ac68") +color.indexColor("sap_green","#5c8b15") +color.indexColor("pale_salmon","#ffb19a") +color.indexColor("metallic_blue","#4f738e") +color.indexColor("ice","#d6fffa") +color.indexColor("gross_green","#a0bf16") +color.indexColor("dodger_blue","#3e82fc") +color.indexColor("warm_pink","#fb5581") +color.indexColor("light_green_blue","#56fca2") +color.indexColor("flat_green","#699d4c") +color.indexColor("dark_blue_grey","#1f3b4d") +color.indexColor("clay_brown","#b2713d") +color.indexColor("sand_yellow","#fce166") +color.indexColor("grapefruit","#fd5956") +color.indexColor("blood_orange","#fe4b03") +color.indexColor("very_pale_blue","#d6fffe") +color.indexColor("old_pink","#c77986") +color.indexColor("neon_red","#ff073a") +color.indexColor("golden_rod","#f9bc08") +color.indexColor("plum_purple","#4e0550") +color.indexColor("pale_peach","#ffe5ad") +color.indexColor("green_again","#16d43f") +color.indexColor("dark_yellow_green","#728f02") +color.indexColor("carmine","#9d0216") +color.indexColor("deep_sea_blue","#015482") +color.indexColor("dark_hot_pink","#d90166") +color.indexColor("warm_blue","#4b57db") +color.indexColor("light_khaki","#e6f2a2") +color.indexColor("icky_green","#8fae22") +color.indexColor("greenblue","#23c48b") +color.indexColor("dirty_purple","#734a65") +color.indexColor("rich_blue","#021bf9") +color.indexColor("mushroom","#ba9e88") +color.indexColor("flat_blue","#3c73a8") +color.indexColor("dark_slate_blue","#214761") +color.indexColor("dark_sage","#598556") +color.indexColor("coral_pink","#ff6163") +color.indexColor("true_green","#089404") +color.indexColor("darkish_purple","#751973") +color.indexColor("dark_taupe","#7f684e") +color.indexColor("cool_grey","#95a3a6") +color.indexColor("canary","#fdff63") +color.indexColor("booger_green","#96b403") +color.indexColor("muted_pink","#d1768f") +color.indexColor("hazel","#8e7618") +color.indexColor("dark_royal_blue","#02066f") +color.indexColor("vivid_purple","#9900fa") +color.indexColor("racing_green","#014600") +color.indexColor("leather","#ac7434") +color.indexColor("green/blue","#01c08d") +color.indexColor("sunflower_yellow","#ffda03") +color.indexColor("rich_purple","#720058") +color.indexColor("pale_magenta","#d767ad") +color.indexColor("light_yellowish_green","#c2ff89") +color.indexColor("indigo_blue","#3a18b1") +color.indexColor("dark_fuchsia","#9d0759") +color.indexColor("yellow_tan","#ffe36e") +color.indexColor("wintergreen","#20f986") +color.indexColor("violet_pink","#fb5ffc") +color.indexColor("topaz","#13bbaf") +color.indexColor("seafoam_blue","#78d1b6") +color.indexColor("light_gold","#fddc5c") +color.indexColor("grey/green","#86a17d") +color.indexColor("foam_green","#90fda9") +color.indexColor("creme","#ffffb6") +color.indexColor("clear_blue","#247afd") +color.indexColor("ugly_blue","#31668a") +color.indexColor("terracota","#cb6843") +color.indexColor("very_dark_brown","#1d0200") +color.indexColor("straw","#fcf679") +color.indexColor("parchment","#fefcaf") +color.indexColor("orangey_yellow","#fdb915") +color.indexColor("greyish_teal","#719f91") +color.indexColor("sapphire","#2138ab") +color.indexColor("nice_blue","#107ab0") +color.indexColor("browny_orange","#ca6b02") +color.indexColor("washed_out_green","#bcf5a6") +color.indexColor("tiffany_blue","#7bf2da") +color.indexColor("light_seafoam","#a0febf") +color.indexColor("light_neon_green","#4efd54") +color.indexColor("light_bright_green","#53fe5c") +color.indexColor("light_bluish_green","#76fda8") +color.indexColor("rosy_pink","#f6688e") +color.indexColor("peachy_pink","#ff9a8a") +color.indexColor("pale_light_green","#b1fc99") +color.indexColor("old_rose","#c87f89") +color.indexColor("fern","#63a950") +color.indexColor("dusk_blue","#26538d") +color.indexColor("camo","#7f8f4e") +color.indexColor("burnt_siena","#b75203") +color.indexColor("tealish_green","#0cdc73") +color.indexColor("swamp","#698339") +color.indexColor("sand_brown","#cba560") +color.indexColor("rust_brown","#8b3103") +color.indexColor("orangeish","#fd8d49") +color.indexColor("light_royal_blue","#3a2efe") +color.indexColor("cocoa","#875f42") +color.indexColor("baby_purple","#ca9bf7") +color.indexColor("raw_sienna","#9a6200") +color.indexColor("radioactive_green","#2cfa1f") +color.indexColor("light_pea_green","#c4fe82") +color.indexColor("cinnamon","#ac4f06") +color.indexColor("squash","#f2ab15") +color.indexColor("charcoal_grey","#3c4142") +color.indexColor("bright_yellow_green","#9dff00") +color.indexColor("baby_puke_green","#b6c406") +color.indexColor("poison_green","#40fd14") +color.indexColor("light_lavendar","#efc0fe") +color.indexColor("indian_red","#850e04") +color.indexColor("dark_cream","#fff39a") +color.indexColor("toupe","#c7ac7d") +color.indexColor("butterscotch","#fdb147") +color.indexColor("burple","#6832e3") +color.indexColor("tan_green","#a9be70") +color.indexColor("sun_yellow","#ffdf22") +color.indexColor("pale_gold","#fdde6c") +color.indexColor("light_light_green","#c8ffb0") +color.indexColor("lichen","#8fb67b") +color.indexColor("green/yellow","#b5ce08") +color.indexColor("darkgreen","#054907") +color.indexColor("azul","#1d5dec") +color.indexColor("sunny_yellow","#fff917") +color.indexColor("sickly_yellow","#d0e429") +color.indexColor("kelley_green","#009337") +color.indexColor("bruise","#7e4071") +color.indexColor("browny_green","#6f6c0a") +color.indexColor("battleship_grey","#6b7c85") +color.indexColor("off_blue","#5684ae") +color.indexColor("manilla","#fffa86") +color.indexColor("greenish_beige","#c9d179") +color.indexColor("deep_brown","#410200") +color.indexColor("darkish_pink","#da467d") +color.indexColor("custard","#fffd78") +color.indexColor("ugly_brown","#7d7103") +color.indexColor("stormy_blue","#507b9c") +color.indexColor("liliac","#c48efd") +color.indexColor("baby_shit_brown","#ad900d") +color.indexColor("reddish_grey","#997570") +color.indexColor("powder_pink","#ffb2d0") +color.indexColor("eggplant_purple","#430541") +color.indexColor("egg_shell","#fffcc4") +color.indexColor("very_light_brown","#d3b683") +color.indexColor("tea_green","#bdf8a3") +color.indexColor("orange_pink","#ff6f52") +color.indexColor("light_grey_green","#b7e1a1") +color.indexColor("kiwi_green","#8ee53f") +color.indexColor("boring_green","#63b365") +color.indexColor("light_pastel_green","#b2fba5") +color.indexColor("candy_pink","#ff63e9") +color.indexColor("purply","#983fb2") +color.indexColor("purpley_grey","#947e94") +color.indexColor("dusty_lavender","#ac86a8") +color.indexColor("desert","#ccad60") +color.indexColor("deep_lilac","#966ebd") +color.indexColor("pig_pink","#e78ea5") +color.indexColor("olive_yellow","#c2b709") +color.indexColor("light_seafoam_green","#a7ffb5") +color.indexColor("light_moss_green","#a6c875") +color.indexColor("lavender_pink","#dd85d7") +color.indexColor("deep_aqua","#08787f") +color.indexColor("bland","#afa88b") +color.indexColor("strong_pink","#ff0789") +color.indexColor("green_teal","#0cb577") +color.indexColor("deep_turquoise","#017374") +color.indexColor("dark_green_blue","#1f6357") +color.indexColor("bright_sea_green","#05ffa6") +color.indexColor("booger","#9bb53c") +color.indexColor("blue_with_a_hint_of_purple","#533cc6") +color.indexColor("blue_blue","#2242c7") +color.indexColor("windows_blue","#3778bf") +color.indexColor("toxic_green","#61de2a") +color.indexColor("strong_blue","#0c06f7") +color.indexColor("spruce","#0a5f38") +color.indexColor("pinkish_tan","#d99b82") +color.indexColor("macaroni_and_cheese","#efb435") +color.indexColor("grey_teal","#5e9b8a") +color.indexColor("dusty_teal","#4c9085") +color.indexColor("dark_grass_green","#388004") +color.indexColor("cement","#a5a391") +color.indexColor("yellowish_tan","#fcfc81") +color.indexColor("warm_purple","#952e8f") +color.indexColor("tea","#65ab7c") +color.indexColor("really_light_blue","#d4ffff") +color.indexColor("nasty_green","#70b23f") +color.indexColor("light_eggplant","#894585") +color.indexColor("fresh_green","#69d84f") +color.indexColor("electric_lime","#a8ff04") +color.indexColor("dust","#b2996e") +color.indexColor("dark_pastel_green","#56ae57") +color.indexColor("cloudy_blue","#acc2d9") +color.indexColor("highlighter_blue","#30C5FF") +for i=0,255 do + color.indexColor("gray"..i,i,i,i) +end + +return color \ No newline at end of file diff --git a/gui/core/simulate.lua b/gui/core/simulate.lua new file mode 100755 index 0000000..89172d1 --- /dev/null +++ b/gui/core/simulate.lua @@ -0,0 +1,91 @@ +local gui = require("gui") +local multi, thread = require("multi"):init() +local transition = require("gui.elements.transitions") + +-- Triggers press then release +local function getPosition(obj, x, y) + if not x or y then + local cx, cy, w, h = obj:getAbsolutes() + return cx + w/2, cy + h/2 + else + return x, y + end +end + +proc = gui:getProcessor() + +local simulate = {} + +function simulate:Press(button, x, y, istouch) + if self then + x, y = getPosition(self, x, y) + elseif x == nil or y == nil then + x, y = love.mouse.getPosition() + end + love.mouse.setPosition(x, y) + gui.Events.OnMousePressed:Fire(x, y, button or gui.MOUSE_PRIMARY, istouch or false) +end + +function simulate:Release(button, x, y, istouch) + if self then + x, y = getPosition(self, x, y) + elseif x == nil or y == nil then + x, y = love.mouse.getPosition() + end + love.mouse.setPosition(x, y) + gui.Events.OnMouseReleased:Fire(x, y, button or gui.MOUSE_PRIMARY, istouch or false) +end + +simulate.Click = proc:newFunction(function(self, button, x, y, istouch) + if self then + x, y = getPosition(self, x, y) + elseif x == nil or y == nil then + x, y = love.mouse.getPosition() + end + love.mouse.setPosition(x, y) + gui.Events.OnMousePressed:Fire(x, y, button or gui.MOUSE_PRIMARY, istouch or false) + thread.skip(1) + gui.Events.OnMouseReleased:Fire(x, y, button or gui.MOUSE_PRIMARY, istouch or false) +end, true) + +simulate.Move = proc:newFunction(function(self, dx, dy, x, y, istouch) + local dx, dy = dx or 0, dy or 0 + + if self then + x, y = getPosition(self, x, y) + elseif x == nil or y == nil then + x, y = love.mouse.getPosition() + end + + if dx == 0 and dy == 0 then + _x, _y = love.mouse.getPosition() + if x == _x and y == _y then + return + end + local dx, dy = 0, 0 + dx = x - _x + dy = y - _y + return simulate.Move(nil, dx, dy) + end + gui.Events.OnMouseMoved:Fire(x, y, 0, 0, istouch or false) + thread.skip(1) + local gx = transition.glide(0, dx, .25) + local gy = transition.glide(0, dy, .25) + local xx = gx() + xx.OnStep(function(p) + _x, _y = love.mouse.getPosition() + love.mouse.setPosition(x + p, _y) + end) + local yy = gy() + yy.OnStep(function(p) + _x, _y = love.mouse.getPosition() + love.mouse.setPosition(_x, y + p) + end) + local event = xx.OnStop * yy.OnStop + if not(dx==0 and dy == 0) then + thread.hold(event) + end + gui.Events.OnMouseMoved:Fire(x + dx, y + dy, 0, 0, istouch or false) +end, true) + +return simulate \ No newline at end of file diff --git a/gui/core/theme.lua b/gui/core/theme.lua new file mode 100755 index 0000000..e1ca818 --- /dev/null +++ b/gui/core/theme.lua @@ -0,0 +1,135 @@ +local color = require("gui.core.color") +local theme = {} +local defaultFont = love.graphics.getFont() +theme.__index = theme + +local function generate_harmonious_colors(num_colors, lightness) + local base_hue = math.random(0, 360) -- random starting hue + local colors = {} + for i = 1, num_colors do + local new_hue = (base_hue + (360 / num_colors) * i) % 360 -- offset hue by 1/n of the color wheel + if lightness == "dark" then + table.insert(colors, color.new(color.hsl(new_hue, math.random(45, 55), math.random(30, 40)))) + elseif lightness == "light" then + table.insert(colors, color.new(color.hsl(new_hue, math.random(45, 55), math.random(60, 80)))) + else + table.insert(colors, color.new(color.hsl(new_hue, math.random(45, 55), math.random(30, 80)))) + end + + end + return colors +end + +function theme:random(seed, lightness, rand) + local seed = seed or math.random(0,9999999999) + math.randomseed(seed) + local harmonious_colors = generate_harmonious_colors(3, lightness) + local t = theme:new(unpack(harmonious_colors)) + + if lightness == "dark" then + t.colorPrimaryText = color.lighten(t.colorPrimaryText, .8) + t.colorButtonText = color.lighten(t.colorButtonText, .7) + elseif lightness == "light" then + t.colorPrimaryText = color.darken(t.colorPrimaryText, .8) + t.colorButtonText = color.darken(t.colorButtonText, .7) + else + if color.getAverageLightness(t.colorPrimary)<.5 then + t.colorPrimaryText = color.lighten(t.colorPrimaryText, .5) + t.colorButtonNormal = color.lighten(t.colorButtonNormal, .2) + else + t.colorPrimaryText = color.darken(t.colorPrimaryText, .3) + end + + if color.getAverageLightness(t.colorPrimary)<.5 then + t.colorButtonText = color.lighten(t.colorButtonText, .5) + else + t.colorButtonText = color.darken(t.colorButtonText, .3) + end + end + + + t.seed = seed + return t +end + +function theme:dump() + return '"' .. table.concat({color.rgbToHex(self.colorPrimary), color.rgbToHex(self.colorPrimaryText), color.rgbToHex(self.colorButtonText)},"\",\"") .. '"' +end + +local function newColor(c,default) + if not c then + return default + end + return color.new(c) +end + +function theme:new(colorPrimary, primaryText, buttonText, buttonNormal, primaryTextFont, buttonTextFont) + local c = {} + setmetatable(c, theme) + + if type(colorPrimary) == "table" then + local opts = colorPrimary + c.colorPrimary = newColor(opts.primary) + c.colorPrimaryDark = newColor(opts.primaryDark, color.darken(c.colorPrimary,.4)) + c.colorPrimaryText = newColor(opts.primaryText) + c.colorButtonNormal = newColor(opts.buttonNormal, color.darken(c.colorPrimary,.2)) + c.colorButtonHighlight = newColor(opts.buttonHighlight, color.lighten(c.colorPrimary,.2)) + c.colorButtonText = newColor(opts.buttonText, c.colorPrimaryText) + c.fontPrimary = opts.textFont or defaultFont + c.fontButton = opts.buttonTextFont or defaultFont + for i,v in pairs(colorPrimary) do + if not c[i] then + c[i] = v -- only overwrite non managed fields + end + end + return c + end + + c.colorPrimary = color.new(colorPrimary) + c.colorPrimaryDark = color.darken(c.colorPrimary,.4) + c.colorPrimaryText = color.new(primaryText) + c.colorButtonNormal = newColor(buttonNormal) or color.darken(c.colorPrimary,.2) + c.colorButtonHighlight = color.lighten(c.colorButtonNormal,.2) + c.colorButtonText = color.new(buttonText) + c.fontPrimary = primaryTextFont or defaultFont + c.fontButton = buttonTextFont or defaultFont + return c +end + +function theme:setColorPrimary(c) + self.colorPrimary = color.new(c) +end + +function theme:setColorPrimaryDark(c) + self.colorPrimaryDark = color.new(c) +end + +function theme:setColorPrimaryText(c) + self.colorPrimaryText = color.new(c) +end + +function theme:setColorButtonNormal(c) + self.colorButtonNormal = color.new(c) +end + +function theme:setColorButtonHighlight(c) + self.colorButtonHighlight = color.new(c) +end + +function theme:setColorButtonText(c) + self.colorButtonText = color.new(c) +end + +function theme:setFontPrimary(c) + self.fontPrimary = c +end + +function theme:setFontButton(c) + self.fontButton = c +end + +function theme:getSeed() + return self.seed +end + +return theme \ No newline at end of file diff --git a/gui/docs/gui-library-docs.md b/gui/docs/gui-library-docs.md new file mode 100644 index 0000000..e410a19 --- /dev/null +++ b/gui/docs/gui-library-docs.md @@ -0,0 +1,866 @@ +# GUI Library Documentation + +A component-based UI framework for LÖVE2D (Love2D) built on top of the `multi` concurrency library. The library provides a scene graph with dual-dimension layout, event-driven input handling, and a rich set of built-in element types. + +--- + +## Table of Contents + +1. [Setup & Initialization](#setup--initialization) +2. [Core Concepts](#core-concepts) + - [The Scene Graph](#the-scene-graph) + - [Dual-Dimension Layout (DualDim)](#dual-dimension-layout-dualdim) + - [Element Types (Bitmask)](#element-types-bitmask) + - [Form Factors](#form-factors) +3. [Creating Elements](#creating-elements) + - [Frames](#frames) + - [Text Labels](#text-labels) + - [Text Buttons](#text-buttons) + - [Text Boxes (Input)](#text-boxes-input) + - [Image Labels](#image-labels) + - [Image Buttons](#image-buttons) + - [Videos](#videos) +4. [Layout & Positioning](#layout--positioning) +5. [Events & Connections](#events--connections) + - [Global GUI Events](#global-gui-events) + - [Per-Element Events](#per-element-events) + - [Hot Keys](#hot-keys) +6. [Element Methods](#element-methods) + - [Positioning & Sizing](#positioning--sizing) + - [Visual Properties](#visual-properties) + - [Hierarchy & Parenting](#hierarchy--parenting) + - [Utilities](#utilities) +7. [Text Elements](#text-elements) + - [Font Management](#font-management) + - [Text Box Internals](#text-box-internals) +8. [Image Elements](#image-elements) +9. [Clipping & Scissor](#clipping--scissor) +10. [Roundness & Shape](#roundness--shape) +11. [Aspect Ratio & Resize Handling](#aspect-ratio--resize-handling) +12. [The `apply` Helper](#the-apply-helper) +13. [Tagging System](#tagging-system) +14. [Cloning Elements](#cloning-elements) +15. [Processors & Threading](#processors--threading) +16. [Drawing Internals](#drawing-internals) +17. [Virtual GUI](#virtual-gui) + +--- + +## Setup & Initialization + +```lua +local gui = require("path.to.gui") +``` + +The library self-initializes on `require`. It hooks into LÖVE's callback system automatically (quit, resize, mouse, keyboard, touch, gamepad, etc.) and starts its internal update and draw processors. + +In your `love.update` and `love.draw`: + +```lua +function love.update(dt) + gui.update(dt) +end + +function love.draw() + gui.draw() +end +``` + +> **Note:** The library hooks LÖVE callbacks via a `Hook` function that wraps any pre-existing handler you define. Define your own `love.*` callbacks **before** `require`-ing the library, or they will be chained automatically. + +--- + +## Core Concepts + +### The Scene Graph + +The library maintains two root nodes: + +| Root | Description | +|---|---| +| `gui` | The main scene root. All elements created with `gui:newXxx()` are parented here by default. | +| `gui.virtual` | A secondary root for off-screen or hidden elements. Children here are not drawn but still have their absolute positions updated. | + +Elements form a tree. Every element has a `parent`, a `children` table, and inherits methods from `gui` via `__index`. + +### Dual-Dimension Layout (DualDim) + +Every element stores its position and size as a **dual dimension**: a combination of a scale component (relative to the parent) and an offset component (absolute pixels). + +``` +actualX = parent.w * scale.pos.x + offset.pos.x + parent.x +actualY = parent.h * scale.pos.y + offset.pos.y + parent.y +actualW = parent.w * scale.size.x + offset.size.x +actualH = parent.h * scale.size.y + offset.size.y +``` + +Constructor signature for `newDualDim` / all `newXxx` creation functions: + +``` +x, y, w, h -- pixel offset for position and size +sx, sy, sw, sh -- scale (0–1) for position and size +``` + +Examples: + +```lua +-- 200×100 box at pixel position (50, 50): +gui:newFrame(50, 50, 200, 100) + +-- Full-screen frame (uses scale only): +local f = gui:newFrame() +f:fullFrame() -- sets scale size to (1,1) and offset to (0,0,0,0) + +-- Half-width, 40px tall, starting at 25% from left: +gui:newFrame(0, 100, 0, 40, 0.25, 0, 0.5, 0) +``` + +Retrieve the computed screen-space rectangle at any time: + +```lua +local x, y, w, h = element:getAbsolutes() +``` + +### Element Types (Bitmask) + +Types are stored as a bitmask so an element can have multiple roles: + +| Constant | Value | Meaning | +|---|---|---| +| `gui.TYPE_FRAME` | 0 | Basic container | +| `gui.TYPE_IMAGE` | 1 | Renders an image | +| `gui.TYPE_TEXT` | 2 | Renders text | +| `gui.TYPE_BOX` | 4 | Text input cursor/selection overlay | +| `gui.TYPE_VIDEO` | 8 | Renders a video | +| `gui.TYPE_BUTTON` | 16 | Interactive button (sets hand cursor) | +| `gui.TYPE_ANIM` | 32 | Animation / spritesheet | + +Test membership: + +```lua +if element:hasType(gui.TYPE_TEXT) then ... end +if element:hasType(gui.TYPE_TEXT + gui.TYPE_BOX) then ... end -- is a text box +``` + +### Form Factors + +Controls the shape used for both fills and hit-testing: + +| Constant | Shape | +|---|---| +| `gui.FORM_RECTANGLE` | Rounded or plain rectangle (default) | +| `gui.FORM_CIRCLE` | Circle; `w` and `h` are set to `2*r` | +| `gui.FORM_ARC` | Arc segment | + +--- + +## Creating Elements + +All creation functions are called on a **parent** element (or on `gui` itself for top-level elements). The new element is automatically inserted into the parent's `children` table. + +### Frames + +A plain container with a background fill and optional border. + +```lua +local frame = parent:newFrame(x, y, w, h, sx, sy, sw, sh) +``` + +A **virtual frame** is parented to `gui.virtual` regardless of the caller: + +```lua +local vframe = parent:newVirtualFrame(x, y, w, h, sx, sy, sw, sh) +``` + +A **visual frame** is a regular frame tagged `"visual"`. Mouse events on it and its descendants are suppressed (useful for purely decorative overlays): + +```lua +local overlay = parent:newVisualFrame(x, y, w, h, sx, sy, sw, sh) +``` + +### Text Labels + +A non-interactive text element. + +```lua +local label = parent:newTextLabel("Hello world", x, y, w, h, sx, sy, sw, sh) +``` + +### Text Buttons + +A text element that fires pointer events and shows a hand cursor on hover. + +```lua +local btn = parent:newTextButton("Click me", x, y, w, h, sx, sy, sw, sh) +btn.OnPressed(function(self, x, y) print("pressed!") end) +``` + +### Text Boxes (Input) + +A single-line text input field. + +```lua +local box = parent:newTextBox("default text", x, y, w, h, sx, sy, sw, sh) +box.OnReturn(function(self, text) print("Submitted:", text) end) +``` + +Keyboard navigation, backspace/delete, selection (click-drag or Ctrl+A), copy/paste/cut, and undo/redo are all handled automatically when the box has focus. + +### Image Labels + +A non-interactive image element. + +```lua +local img = parent:newImageLabel("path/to/image.png", x, y, w, h, sx, sy, sw, sh) +``` + +GIF files are detected automatically by the `.gif` extension and animated. + +### Image Buttons + +An image element that fires pointer events and shows a hand cursor on hover. + +```lua +local ibtn = parent:newImageButton("icon.png", x, y, w, h, sx, sy, sw, sh) +ibtn.OnPressed(function(self, x, y) print("image clicked") end) +``` + +### Videos + +Wraps a LÖVE `Video` object. + +```lua +local vid = parent:newVideo("clip.ogv", x, y, w, h, sx, sy, sw, sh) +vid:play() +vid.OnVideoFinished(function(self) print("done") end) +``` + +Video methods: + +| Method | Description | +|---|---| +| `vid:setVideo(path_or_video)` | Load or swap the video source | +| `vid:play()` | Start playback | +| `vid:pause()` | Pause without rewinding | +| `vid:stop()` | Pause and rewind | +| `vid:rewind()` | Seek to start | +| `vid:seek(seconds)` | Jump to position | +| `vid:tell()` | Return current playback position (seconds) | +| `vid:getDuration()` | Return total duration (seconds) | +| `vid:setVolume(vol)` | Set audio volume (0–1) | +| `vid:getVideo()` | Return the underlying LÖVE Video object | + +--- + +## Layout & Positioning + +### Setting the Dual Dimension + +```lua +-- Fires OnSizeChanged +element:setDualDim(x, y, w, h, sx, sy, sw, sh) + +-- Silent version (no event) +element:rawSetDualDim(x, y, w, h, sx, sy, sw, sh) + +-- Read back +local x, y, w, h, sx, sy, sw, sh = element:getDualDim() +``` + +Pass `nil` for any argument to keep the current value. + +### Moving and Resizing + +```lua +-- Delta move (fires OnPositionChanged) +element:move(dx, dy) + +-- Delta resize (fires OnSizeChanged) +element:size(dw, dh) + +-- Move but clamp to parent bounds +element:moveInBounds(dx, dy) +``` + +### Centering + +```lua +element:centerX(true) -- horizontally center within parent +element:centerY(true) -- vertically center within parent +``` + +These attach internal loops that continuously recompute the offset whenever the element's size or position changes. + +### Convenience + +```lua +element:fullFrame() -- scale size (1,1), offset (0,0,0,0) — fills parent +``` + +### Dragging + +```lua +element:enableDragging(button) -- button = love mouse button number (1=left, 2=right, …) +element:enableDragging(nil) -- disable dragging +``` + +While dragging, `OnDragging`, `OnDragStart`, and `OnDragEnd` are fired. + +### Z-Order + +```lua +element:topStack() -- move to end of parent.children (drawn last = on top) +element:bottomStack() -- move to front of parent.children (drawn first = behind) +``` + +--- + +## Events & Connections + +Events use the `multi` connection system. Connect a handler by calling the connection as a function: + +```lua +element.OnPressed(function(self, x, y, button, istouch, presses) + -- ... +end) +``` + +Connections support composition: + +```lua +-- OR: fires when either fires +(connA + connB)(handler) + +-- AND: fires only when both conditions are met +(connA * connB)(handler) +``` + +### Global GUI Events + +These fire for the entire application window regardless of which element is focused. + +| Event | LÖVE callback | Arguments | +|---|---|---| +| `gui.Events.OnQuit` | `love.quit` | — | +| `gui.Events.OnDirectoryDropped` | `love.directorydropped` | `dir` | +| `gui.Events.OnDisplayRotated` | `love.displayrotated` | `index, orient` | +| `gui.Events.OnFilesDropped` | `love.filedropped` | `file` | +| `gui.Events.OnFocus` | `love.focus` | `focused` | +| `gui.Events.OnMouseFocus` | `love.mousefocus` | `focused` | +| `gui.Events.OnResized` | `love.resize` | `w, h` | +| `gui.Events.OnVisible` | `love.visible` | `visible` | +| `gui.Events.OnKeyPressed` | `love.keypressed` | `key, scancode, isrepeat` | +| `gui.Events.OnKeyReleased` | `love.keyreleased` | `key, scancode` | +| `gui.Events.OnTextEdited` | `love.textedited` | `text, start, length` | +| `gui.Events.OnTextInputed` | `love.textinput` | `text` | +| `gui.Events.OnMouseMoved` | `love.mousemoved` | `x, y, dx, dy, istouch` | +| `gui.Events.OnMousePressed` | `love.mousepressed` | `x, y, button, istouch, presses` | +| `gui.Events.OnMouseReleased` | `love.mousereleased` | `x, y, button, istouch, presses` | +| `gui.Events.OnWheelMoved` | `love.wheelmoved` | `x, y` | +| `gui.Events.OnTouchMoved` | `love.touchmoved` | `id, x, y, dx, dy, pressure` | +| `gui.Events.OnTouchPressed` | `love.touchpressed` | `id, x, y, dx, dy, pressure` | +| `gui.Events.OnTouchReleased` | `love.touchreleased` | `id, x, y, dx, dy, pressure` | +| `gui.Events.OnGamepadPressed` | `love.gamepadpressed` | `joystick, button` | +| `gui.Events.OnGamepadReleased` | `love.gamepadreleased` | `joystick, button` | +| `gui.Events.OnGamepadAxis` | `love.gamepadaxis` | `joystick, axis, value` | +| `gui.Events.OnJoystickAdded` | `love.joystickadded` | `joystick` | +| `gui.Events.OnJoystickRemoved` | `love.joystickremoved` | `joystick` | +| `gui.Events.OnJoystickHat` | `love.joystickhat` | `joystick, hat, dir` | +| `gui.Events.OnJoystickPressed` | `love.joystickpressed` | `joystick, button` | +| `gui.Events.OnJoystickReleased` | `love.joystickreleased` | `joystick, button` | +| `gui.Events.OnCreated` | internal | `element` — fires when any element is created | +| `gui.Events.OnObjectFocusChanged` | internal | `old, new` — fires when click focus changes | + +### Per-Element Events + +These are attached to each element instance. All mouse/pointer events are automatically pre-filtered: they only fire when the element is `active` and (for most events) when the pointer is within the element's bounds. + +| Event | Fires when… | +|---|---| +| `OnLoad` | (manual) element is "loaded" — user-defined | +| `OnPressed` | pointer pressed **inside** element | +| `OnPressedOuter` | pointer pressed **outside** element | +| `OnReleased` | pointer released **inside** element | +| `OnReleasedOuter` | pointer released **outside** (but was pressed inside) | +| `OnReleasedOther` | pointer released with no relevant press history | +| `OnDragStart` | drag begins (element must have `enableDragging` set) | +| `OnDragging` | pointer moves while dragging | +| `OnDragEnd` | drag ends | +| `OnEnter` | pointer enters the element bounds | +| `OnExit` | pointer leaves the element bounds | +| `OnMoved` | pointer moves while inside (or while dragging) | +| `OnWheelMoved` | scroll wheel moves while pointer is inside element | +| `OnSizeChanged` | `setDualDim` or `size` called | +| `OnPositionChanged` | `setDualDim` or `move` called | +| `OnDestroy` | element is about to be destroyed | +| `OnCreated` | element was created (forwarded from `gui.Events.OnCreated`) | +| `OnReturn` | (text boxes only) Enter/Return key pressed | +| `OnFontUpdated` | (text elements only) font changed via `setFont` | +| `OnVideoFinished` | (video elements only) video reaches its end | +| `OnLeftStickUp/Down/Left/Right` | gamepad left-stick events | +| `OnRightStickUp/Down/Left/Right` | gamepad right-stick events | + +#### Hierarchy Mode + +By default events fire if another element is not on top. Call: + +```lua +element:respectHierarchy(false) -- events will fire regardless +``` + +to make `OnPressed`, `OnReleased`, `OnEnter`, and `OnMoved` skip when the element is covered by a sibling. + +--- + +### Hot Keys + +Register a keyboard shortcut that fires a connection: + +```lua +local conn = element:setHotKey({"lctrl", "s"}) -- returns a connection +conn(function(ref) print("Ctrl+S on", ref) end) +``` + +You may pass an existing connection as the second argument to reuse it. + +#### Built-in Hot Keys + +| Hot Key | Trigger | +|---|---| +| `gui.HotKeys.OnSelectAll` | Ctrl+A | +| `gui.HotKeys.OnCopy` | Ctrl+C | +| `gui.HotKeys.OnPaste` | Ctrl+V | +| `gui.HotKeys.OnCut` | Ctrl+X | +| `gui.HotKeys.OnUndo` | Ctrl+Z | +| `gui.HotKeys.OnRedo` | Ctrl+Y / Ctrl+Shift+Z | + +These are already wired to the currently-focused text box for standard editing operations. + +--- + +## Element Methods + +### Positioning & Sizing + +| Method | Description | +|---|---| +| `el:getAbsolutes([transform])` | Returns `x, y, w, h` in screen space. Optional `transform` function is applied to each value. | +| `el:setDualDim(x,y,w,h,sx,sy,sw,sh)` | Set layout, fires `OnSizeChanged`. | +| `el:rawSetDualDim(...)` | Set layout, no event. | +| `el:getDualDim()` | Returns all 8 dual-dim components. | +| `el:move(dx, dy)` | Translate by delta, fires `OnPositionChanged`. | +| `el:size(dw, dh)` | Resize by delta, fires `OnSizeChanged`. | +| `el:moveInBounds(dx, dy)` | Translate while keeping element inside parent. | +| `el:fullFrame()` | Fill parent entirely. | +| `el:centerX(bool)` | Auto-center horizontally. | +| `el:centerY(bool)` | Auto-center vertically. | +| `el:getLocalCords(mx, my)` | Convert screen coordinates to element-local coordinates. | + +### Visual Properties + +| Property | Type | Default | Description | +|---|---|---|---| +| `color` | `{r,g,b}` | `{0.6, 0.6, 0.6}` | Background fill color | +| `borderColor` | `{r,g,b}` | black | Border color | +| `drawBorder` | boolean | `true` | Whether to draw the border | +| `visibility` | number | `1` | Background alpha (0–1) | +| `rotation` | number | `0` | Rotation in degrees | +| `active` | boolean | `true` | When `false`, element and all descendants ignore input | +| `visible` | boolean | `true` | Controls `getAllChildren` visibility filter | +| `ignore` | boolean | — | When `true`, element is skipped in coverage tests | + +Set color (also sets `visibility` if a 4th component is present): + +```lua +element:setColor("color", {1, 0, 0, 0.8}) +element:setColor("borderColor", {0, 0, 0}) +``` + +Apply a LÖVE shader: + +```lua +element.shader = love.graphics.newShader(...) +``` + +Apply an effect wrapper (called around the draw call): + +```lua +element.effect = function(drawFunc) + love.graphics.push() + -- setup + drawFunc() + love.graphics.pop() +end +``` + +Apply a post-draw hook: + +```lua +element.post = function(self) + -- called after drawing, inside the same scissor/shader state +end +``` + +### Hierarchy & Parenting + +| Method | Description | +|---|---| +| `el:setParent(newParent)` | Re-parent element. Pass `nil` to detach. | +| `el:getChildren()` | Returns direct children table. | +| `el:getAllChildren([includeHidden])` | Returns all visible descendants recursively. | +| `el:isDescendantOf(obj)` | Returns `true` if `obj` is an ancestor of `el`. | +| `el:topStack()` | Draw on top of siblings. | +| `el:bottomStack()` | Draw behind siblings. | +| `el:destroy()` | Destroy element, its children, and all connections. | +| `el:removeChildren()` | Destroy all children but leave element itself. | +| `el:isActive()` | `true` if `active` and not parented under `gui.virtual`. | +| `el:isOffScreen()` | `true` if element rect is entirely outside screen bounds. | + +### Utilities + +| Method | Description | +|---|---| +| `el:hasType(t)` | Bitmask type test. | +| `el:canPress(mx, my)` | `true` if point is inside element (respects clip area). | +| `el:isBeingCovered(mx, my)` | `true` if a sibling is in front of this element at the given point. | +| `el:intersecpt(x, y, w, h)` | Returns intersection rect with a given AABB. | +| `el:newThread(func)` | Spawn a coroutine-style thread scoped to this element. | +| `el:getObjectFocus()` | Returns the currently focused element. | +| `el:getProcessor()` | Returns the internal updater processor. | + +--- + +## Text Elements + +All text elements (`newTextLabel`, `newTextButton`, `newTextBox`) inherit from `newTextBase`. + +### Properties + +| Property | Type | Default | Description | +|---|---|---|---| +| `text` | string | — | Displayed string | +| `textColor` | `{r,g,b}` | black | Text color | +| `font` | Font | 12px default | LÖVE Font object | +| `align` | constant | `ALIGN_LEFT` | `gui.ALIGN_LEFT`, `ALIGN_CENTER`, `ALIGN_RIGHT` | +| `textOffsetX/Y` | number | `0` | Additional pixel offset for text drawing | +| `textScaleX/Y` | number | `1` | Scale applied to text rendering | +| `textShearingFactorX/Y` | number | `0` | Shearing factor for text transform | +| `textVisibility` | number | `1` | Text alpha (0–1) | + +### Font Management + +```lua +-- By size (default font) +element:setFont(14) + +-- By path and size +element:setFont("fonts/myfont.ttf", 18) + +-- By LÖVE font object +element:setFont(love.graphics.newFont("fonts/myfont.ttf", 18)) +``` + +Automatically resize font to fill element bounds: + +```lua +-- Binary-search fit between min and max size +element:fitFont(minSize, maxSize, {scale = 1}) +-- Returns bestFont, bestSize +``` + +Center text vertically inside the element: + +```lua +element:centerFont(y_offset) +``` + +Calculate where the top and bottom of rendered text actually are (pixel offsets within element): + +```lua +local top, bottom = element:calculateFontOffset(font, adjust) +``` + +### Text Box Internals + +| Property | Description | +|---|---| +| `cur_pos` | Integer cursor position (0 = before first character) | +| `selection` | `{start, stop}` character indices (may be reversed) | +| `bar_show` | `true` when the cursor bar should be visible (blinks via internal thread) | +| `doSelection` | `true` while a drag-selection is in progress | + +Methods: + +```lua +box:HasSelection() -- returns true/false +box:GetSelection() -- returns start, stop (always start ≤ stop) +box:GetSelectedText() -- returns selected substring +box:ClearSelection() -- clear selection state +``` + +--- + +## Image Elements + +All image elements (`newImageLabel`, `newImageButton`) inherit from `newImageBase`. + +### `setImage` + +```lua +-- From a file path (PNG, JPG, etc.) +element:setImage("path/to/image.png") + +-- GIF animation (auto-detected by extension) +element:setImage("path/to/anim.gif") + +-- From a LÖVE Image object +element:setImage(loveImageObject) +``` + +### Properties + +| Property | Description | +|---|---| +| `imageColor` | Tint color applied when drawing | +| `imageVisibility` | Image alpha (0–1) | +| `scaleX / scaleY` | Flip/scale. Negative values flip the axis. | +| `quad` | LÖVE Quad used for rendering (sub-region) | + +### Flipping + +```lua +element:flip(false) -- flip horizontally +element:flip(true) -- flip vertically +``` + +### Gradient + +Apply a gradient as the image of any element: + +```lua +element:applyGradient("horizontal", {r,g,b,a}, {r,g,b,a}, ...) +element:applyGradient("vertical", {r,g,b,a}, {r,g,b,a}, ...) +``` + +### Image Caching + +```lua +-- Pre-load a single image into the cache +gui.cacheImage(gui, "path/to/img.png") + +-- Pre-load multiple images; reports progress via OnStatus +gui.cacheImage(gui, {"img1.png", "img2.png"}) + +-- Tile helper: returns imagedata and quad +local imgdata, quad = gui:getTile("sheet.png", tileX, tileY, tileW, tileH) +``` + +--- + +## Clipping & Scissor + +Clipping is set on a **parent** and affects all descendants: + +```lua +parent.clipDescendants = true +``` + +During each draw pass, the parent propagates its screen-space rectangle to each child's `__variables.clip`. Children then apply LÖVE's scissor test to avoid drawing outside the parent. + +--- + +## Roundness & Shape + +```lua +-- Rounded corners +element:setRoundness(rx, ry, segments, side) +-- rx, ry: x/y radius (default 5) +-- segments: arc segments (default 30) +-- side: "top", "bottom", or true (all corners) + +-- Directional override +element:setRoundnessDirection(horizontal, vertical) +``` + +Circle and arc shapes are set at creation time: + +```lua +-- Circle +element:makeCircle(x, y, radius, sx, sy, sr, segments) + +-- Arc +element:makeArc(arcType, x, y, radius, sx, sy, sr, startAngle, endAngle, segments) +-- arcType: "open", "closed", or "pie" (passed to love.graphics.arc) +-- Angles in radians +``` + +--- + +## Aspect Ratio & Resize Handling + +Lock the root GUI to a design resolution: + +```lua +gui:setAspectSize(1920, 1080) -- set design resolution +gui.aspect_ratio = true -- enable aspect-ratio mode +``` + +When the window resizes, the library calculates letterbox/pillarbox offsets and adjusts `gui.x`, `gui.y`, `gui.w`, `gui.h` (and the same on `gui.virtual`) so all elements remain proportional. + +Disable it: + +```lua +gui:setAspectSize(nil, nil) +gui.aspect_ratio = false +``` + +Utility to compute the scaled size manually: + +```lua +local nw, nh, offsetX, offsetY = gui:GetSizeAdjustedToAspectRatio(windowW, windowH) +``` + +--- + +## The `apply` Helper + +`gui.apply` is a batch property setter that inspects each field name for a prefix: + +| Prefix | Meaning | +|---|---| +| `C_` | Connect to the named connection (value = handler function) | +| `I_` | Invoke the named method with args from a table | +| *(none)* | Direct assignment or smart detection (connection vs function vs value) | + +```lua +gui.apply({ + color = {1, 0, 0}, + C_OnPressed = function(self) print("pressed") end, + I_setFont = {"fonts/bold.ttf", 16}, +}, buttonA, buttonB, buttonC) +``` + +--- + +## Tagging System + +Arbitrary string tags can be attached to any element: + +```lua +element:setTag("draggable") +element:setTag("ui-panel") + +element:hasTag("draggable") -- true / false (direct tag) +element:parentHasTag("ui-panel") -- true if any ancestor has the tag +``` + +The built-in `"visual"` tag suppresses all mouse event connections: + +```lua +local deco = parent:newVisualFrame(...) -- automatically gets "visual" tag +``` + +--- + +## Cloning Elements + +Deep-copy an element and optionally its connection handlers: + +```lua +local copy = element:clone({ + copyTo = targetParent, -- parent for the clone (default: gui.virtual) + connections = true, -- also copy connection handlers +}) +``` + +`clone` recurses through all children. Connection handlers from the original are **bound** (not moved) to the clone's connections, so both elements remain independently connected. + +--- + +## Processors & Threading + +The library uses two internal processors from the `multi` library: + +| Processor | Purpose | +|---|---| +| `updater` | Input hooks, hot keys, text-box blink, video completion, image loading | +| `drawer` | Per-frame draw loop, virtual element position pass | + +Create a new processor that participates in `gui.update`: + +```lua +local proc = gui:newProcessor("MyProcessor") +-- proc is a multi Processor; attach tasks/loops to it normally +``` + +Spawn a coroutine thread scoped to an element: + +```lua +element:newThread(function(self, thread) + while true do + thread.sleep(1) + print("tick", self.text) + end +end) +``` + +Attach a per-frame update callback (called every update loop): + +```lua +gui:OnUpdate(function(self, dt) + -- called every frame +end) + +element:OnUpdate(function(self, dt) + -- called every frame with element as self +end) +``` + +Create a one-shot or reusable function that runs asynchronously: + +```lua +local fn = gui.newFunction(function(arg1, arg2) + -- runs in updater context +end) +fn(arg1, arg2) +``` + +--- + +## Drawing Internals + +The draw loop iterates `gui:getAllChildren()` each frame and calls `draw_handler` on each element in order (back-to-front). + +`draw_handler` does, in order: + +1. Compute and cache `child.x/y/w/h` via `getAbsolutes`. +2. Propagate clip rects to descendants if `clipDescendants` is set. +3. Activate shader if present. +4. Apply LÖVE scissor (clip or roundness-based). +5. Fill background with `child.color` and `child.visibility`. +6. Draw border with `child.borderColor`. +7. Handle special roundness sides ("top"/"bottom"). +8. Dispatch to type-specific draw functions (video → image → text → box cursor/selection). +9. Call `child:post()` if defined. +10. Remove scissor and shader. + +`gui.draw_handler` is exposed publicly so custom renderers can call it directly. + +--- + +## Virtual GUI + +`gui.virtual` is a root node whose children are never rendered on screen but still participate in the layout pass (absolute positions are computed). Use it to keep pre-built off-screen components ready to be re-parented: + +```lua +-- Create off-screen +local popup = gui.virtual:newFrame(0, 0, 400, 300) + +-- Show it by re-parenting +popup:setParent(gui) + +-- Hide it again +popup:setParent(gui.virtual) +``` + +`gui.virtual` shares the same screen dimensions as `gui`, so positions remain correct when an element moves between them. diff --git a/gui/docs/yaml-based-elements.yaml b/gui/docs/yaml-based-elements.yaml new file mode 100644 index 0000000..a648f5a --- /dev/null +++ b/gui/docs/yaml-based-elements.yaml @@ -0,0 +1,186 @@ +# ── Full GUI YAML reference ───────────────────────────────────────── + +# Every element shares these fields: +type: frame # frame | virtual-frame | visual-frame | + # label | button | textbox | + # image | image-button | video + +# ── Position & size (dual-dimension system) ───────────────────────── +# Offset (pixels): +x: 10 +y: 10 +w: 200 +h: 50 +# Or as lists: +pos: [10, 10] +size: [200, 50] + +# Scale (0.0–1.0 of parent): +sx: 0.0 # scale pos x +sy: 0.0 # scale pos y +sw: 0.5 # scale size x — 50% of parent width +sh: 1.0 # scale size y — 100% of parent height +# Or as lists: +scale-pos: [0.0, 0.0] +scale-size: [0.5, 1.0] + +# Shorthand for "fill parent completely": +full-frame: true + +# ── Appearance ─────────────────────────────────────────────────────── +color: "#3a7bd5" # hex string +color: [58, 123, 213] # RGB 0-255 +color: [0.23, 0.48, 0.84] # RGB 0-1 +border-color: "#000000" +draw-border: true +visible: true +active: true +visibility: 1.0 # 0.0–1.0 alpha for the background rect +rotation: 45 # degrees + +# ── Form factor ────────────────────────────────────────────────────── +form: rectangle # default — no extra fields needed +form: circle + radius: 40 # optional; derived from w if omitted + segments: 32 + +form: arc + radius: 60 + arc-type: open # open | closed | pie + angle-start: 0 + angle-end: 3.14159 + segments: 32 + +# ── Roundness ──────────────────────────────────────────────────────── +roundness: 8 # uniform rx/ry +roundness: [8, 8, 30] # rx, ry, segments +roundness: top # "top" or "bottom" special mode +roundness: + side: top # full table form + +# ── Tags ───────────────────────────────────────────────────────────── +tag: "my-element" # single fast-lookup tag (gui:tag) +tags: # multi-tag (gui:setTag) + - draggable-panel + - visual + +# ── Stack order ────────────────────────────────────────────────────── +stack: top # bring to front +stack: bottom # send to back + +# ── Centering ──────────────────────────────────────────────────────── +center-x: true # horizontally center within parent +center-y: true # vertically center within parent + +# ── Dragging ───────────────────────────────────────────────────────── +draggable: 1 # mouse button (1=primary, 2=secondary, 3=middle) + # false/omit to disable + +# ── Hierarchy & clipping ───────────────────────────────────────────── +respect-hierarchy: true # blocks presses when covered by sibling +clip-descendants: true # scissor-clips all children to this rect + +# ── Square locking ─────────────────────────────────────────────────── +square: w # force height = width +square: h # force width = height + +# ── Text element fields (label, button, textbox) ───────────────────── +text: "Hello, world!" +align: left # left | center | right +text-color: "#ffffff" +text-visibility: 1.0 +text-scale: [1.0, 1.0] # [scaleX, scaleY] +text-offset: [0, 0] # [offsetX, offsetY] +text-shear: [0, 0] # [shearX, shearY] + +font: 16 # size, uses default font +font: "fonts/Roboto-Regular.ttf" # path, uses font-size below +font-size: 18 +font: + file: "fonts/Roboto-Regular.ttf" + size: 18 + +fit-font: true # auto-fit font to element bounds +fit-font: + min: 8 + max: 200 + scale: 0.95 # shrink slightly from computed best + +center-font: true # vertically center glyphs in box +center-font: 10 # with y_offset + +# ── Image element fields (image, image-button) ─────────────────────── +source: "assets/logo.png" +tile: [0, 0, 64, 64] # sub-quad [x, y, w, h] +scale-x: 1.0 +scale-y: 1.0 +flip: horizontal # horizontal | vertical | both +image-color: "#ffffff" +image-visibility: 1.0 + +# Gradient (replaces solid image with a generated gradient image): +gradient: + direction: vertical # vertical | horizontal + colors: + - [255, 80, 80, 255] + - [80, 80, 255, 255] + +# ── Video element fields ────────────────────────────────────────────── +source: "assets/intro.ogv" +volume: 0.8 +autoplay: true +video-color: "#ffffff" +video-visibility: 1.0 + +# ── Events ─────────────────────────────────────────────────────────── +# Value can be a global function name (string) or inline Lua source. + +on-pressed: "myPressHandler" +on-released: "myReleaseHandler" +on-released-outer: "myOuterRelease" +on-pressed-outer: "myOuterPress" +on-enter: "onHoverEnter" +on-exit: "onHoverExit" +on-moved: "onMouseMoved" +on-drag-start: "onDragStart" +on-dragging: "onDragging" +on-drag-end: "onDragEnd" +on-wheel: "onWheel" +on-size-changed: "onResized" +on-position-changed: "onMoved" +on-destroy: "onDestroy" +on-load: "onLoaded" +on-return: "onSubmit" # textbox only +on-update: "onUpdate" # called every frame + +# Inline Lua (multi-line string): +on-pressed: | + print("pressed!", self.text) + +# Per-element hotkeys: +hotkeys: + - keys: [lctrl, s] + action: "saveDocument" + - keys: [escape] + action: "closeDialog" + +# ── Children ───────────────────────────────────────────────────────── +children: + - type: label + text: "I am a child" + x: 10 + y: 10 + w: 180 + h: 30 + color: "#2a2a2a" + text-color: "#ffffff" + align: center + + - type: button + text: "Click me" + x: 10 + y: 50 + w: 100 + h: 36 + on-pressed: "handleClick" + children: [] # buttons can also have children \ No newline at end of file diff --git a/gui/elements/init.lua b/gui/elements/init.lua new file mode 100755 index 0000000..caf5822 --- /dev/null +++ b/gui/elements/init.lua @@ -0,0 +1,69 @@ +local gui = require("gui") +local color = require("gui.core.color") +local theme = require("gui.core.theme") +local transition = require("gui.elements.transitions") + +function gui:newMenu(title, sx, position, trans) + if not title then multi.error("Argument 1 string('title') is required") end + if not sx then multi.error("Argument 2 number('sx') is required") end + + local position = position or gui.ALIGN_LEFT + local trans = trans or transition.glide + + local menu, to, tc, open + if position == gui.ALIGN_LEFT then + menu = self:newFrame(0, 0, 0, 0, -sx, 0, sx, 1) + to = trans(-sx, 0, .25) + tc = trans(0, -sx, .25) + elseif position == gui.ALIGN_CENTER then + menu = self:newFrame(0, 0, 0, 0, .5 -sx/2, 1.1, sx, 1) + to = trans(1.1, 0, .35) + tc = trans(0, 1.1, .35) + elseif position == gui.ALIGN_RIGHT then + menu = self:newFrame(0, 0, 0, 0, 1, 0, sx, 1) + to = trans(1, 1 - sx, .25) + tc = trans(1 - sx, 1, .25) + end + + function menu:isOpen() + return open + end + + function menu:Open(show) + if show then + if not menu.lock then + menu.lock = true + local t = to() + t.OnStop(function() + open = true + menu.lock = false + end) + t.OnStep(function(p) + if position == gui.ALIGN_CENTER then + menu:setDualDim(nil, nil, nil, nil, nil, p) + else + menu:setDualDim(nil, nil, nil, nil, p) + end + end) + end + else + if not menu.lock then + menu.lock = true + local t = tc() + t.OnStop(function() + open = false + menu.lock = false + end) + t.OnStep(function(p) + if position == gui.ALIGN_CENTER then + menu:setDualDim(nil, nil, nil, nil, nil, p) + else + menu:setDualDim(nil, nil, nil, nil, p) + end + end) + end + end + end + + return menu +end diff --git a/gui/elements/transitions.lua b/gui/elements/transitions.lua new file mode 100755 index 0000000..53de95f --- /dev/null +++ b/gui/elements/transitions.lua @@ -0,0 +1,78 @@ +local gui = require("gui") +local multi, thread = require("multi"):init() +local processor = gui:newProcessor("Transistion Processor") +local transition = {} + +local width, height, flags = love.window.getMode() +local fps = 60 + +if flags.refreshrate > 0 then + fps = flags.refreshrate +end + +transition.__index = transition +transition.__call = function(t, start, stop, time, ...) + local args = {...} + return function(st, sp, ti) -- allow these values to be overridden + if not (st or start) or not (sp or stop) then return multi.error("start and stop must be supplied") end + if start == stop then + local temp = { + OnStep = function() end, + OnStop = multi:newConnection() + } + proc:newTask(function() + temp.OnStop:Fire() + end) + return temp + end + local handle = t.func(t, start, stop, (ti or time) or 1, unpack(args)) + return { + OnStep = handle.OnStatus, + OnStop = handle.OnReturn + handle.OnError, + Kill = t.Kill + } + end +end + +function transition:newTransition(func) + local c = {} + setmetatable(c, self) + + c.fps = fps + c.func = processor:newFunction(func) + c.OnStop = multi:newConnection() + c.kill = false + + function c:SetFPS(fps) + self.fps = fps + end + + function c:GetFPS(fps) + return self.fps + end + + function c:Kill() + if c.running then + c.kill = true + end + end + + return c +end + +transition.glide = transition:newTransition(function(t, start, stop, time, ...) + local steps = t.fps*time + local piece = time/steps + local split = stop-start + t.running = true + for i = 0, steps do + if not(t.kill) then + thread.sleep(piece) + thread.pushStatus(start + i*(split/steps),piece*i) + end + end + t.running = false + t.kill = false +end) + +return transition \ No newline at end of file diff --git a/gui/init.lua b/gui/init.lua new file mode 100755 index 0000000..c5c7959 --- /dev/null +++ b/gui/init.lua @@ -0,0 +1,1960 @@ +local utf8 = require("utf8") +local multi, thread = require("multi"):init() +local GLOBAL, THREAD = require("multi.integration.loveManager"):init() +local color = require("gui.core.color") +local gif = require("gui.addons.gifloader") +local gui = {} +local updater = multi:newProcessor("UpdateManager", true) + +local drawer = multi:newProcessor("DrawManager", true) + +local bit = require("bit") +local band, bor = bit.band, bit.bor +local cursor_hand = love.mouse.getSystemCursor("hand") +local clips = {} +local max, min, abs, rad, floor, ceil = math.max, math.min, math.abs, math.rad, + math.floor, math.ceil +local frame, image, text, box, video, button, anim = 0, 1, 2, 4, 8, 16, 32 +local global_drag +local object_focus = gui +local first_loop = false + +-- Types +gui.TYPE_FRAME = frame +gui.TYPE_IMAGE = image +gui.TYPE_TEXT = text +gui.TYPE_BOX = box +gui.TYPE_VIDEO = video +gui.TYPE_BUTTON = button +gui.TYPE_ANIM = anim + +-- Form Factor +gui.FORM_RECTANGLE = 1 +gui.FORM_CIRCLE = 2 +gui.FORM_ARC = 3 + +-- Variables + +gui.__index = gui +gui.MOUSE_PRIMARY = 1 +gui.MOUSE_SECONDARY = 2 +gui.MOUSE_MIDDLE = 3 + +gui.ALIGN_CENTER = 0 +gui.ALIGN_LEFT = 1 +gui.ALIGN_RIGHT = 2 + +-- Connections +gui.Events = {} -- We are using fastmode for all connection objects. +gui.Events.OnQuit = multi:newConnection() +gui.Events.OnDirectoryDropped = multi:newConnection() +gui.Events.OnDisplayRotated = multi:newConnection() +gui.Events.OnFilesDropped = multi:newConnection() +gui.Events.OnFocus = multi:newConnection() +gui.Events.OnMouseFocus = multi:newConnection() +gui.Events.OnResized = multi:newConnection() +gui.Events.OnVisible = multi:newConnection() +gui.Events.OnKeyPressed = multi:newConnection() +gui.Events.OnKeyReleased = multi:newConnection() +gui.Events.OnTextEdited = multi:newConnection() +gui.Events.OnTextInputed = multi:newConnection() +gui.Events.OnMouseMoved = multi:newConnection() +gui.Events.OnMousePressed = multi:newConnection() +gui.Events.OnMouseReleased = multi:newConnection() +gui.Events.OnWheelMoved = multi:newConnection() +gui.Events.OnTouchMoved = multi:newConnection() +gui.Events.OnTouchPressed = multi:newConnection() +gui.Events.OnTouchReleased = multi:newConnection() + +-- Joysticks and gamepads +gui.Events.OnGamepadPressed = multi:newConnection() +gui.Events.OnGamepadReleased = multi:newConnection() +gui.Events.OnGamepadAxis = multi:newConnection() +gui.Events.OnJoystickAdded = multi:newConnection() +gui.Events.OnJoystickHat = multi:newConnection() +gui.Events.OnJoystickPressed = multi:newConnection() +gui.Events.OnJoystickReleased = multi:newConnection() +gui.Events.OnJoystickRemoved = multi:newConnection() + +-- Internal Connections +gui.Events.OnCreated = multi:newConnection() +gui.Events.OnObjectFocusChanged = multi:newConnection() + +-- Virtual gui init +gui.virtual = {} + +-- Hooks + +local function Hook(funcname, func) + if love[funcname] then + local cache = love[funcname] + love[funcname] = function(...) + cache(...) + func({}, ...) + end + else + love[funcname] = function(...) func({}, ...) end + end +end + +-- Incase you define one of these methods, we need to process this after that +updater:newTask(function() + -- System + Hook("quit", gui.Events.OnQuit.Fire) + Hook("directorydropped", gui.Events.OnDirectoryDropped.Fire) + Hook("displayrotated", gui.Events.OnDisplayRotated.Fire) + Hook("filedropped", gui.Events.OnFilesDropped.Fire) + Hook("focus", gui.Events.OnFocus.Fire) + Hook("resize", gui.Events.OnResized.Fire) + Hook("visible", gui.Events.OnVisible.Fire) + + -- Mouse + Hook("mousefocus", gui.Events.OnMouseFocus.Fire) + Hook("keypressed", gui.Events.OnKeyPressed.Fire) + Hook("keyreleased", gui.Events.OnKeyReleased.Fire) + Hook("mousemoved", gui.Events.OnMouseMoved.Fire) + Hook("mousepressed", gui.Events.OnMousePressed.Fire) + Hook("mousereleased", gui.Events.OnMouseReleased.Fire) + Hook("wheelmoved", gui.Events.OnWheelMoved.Fire) + + -- Keyboard + Hook("textedited", gui.Events.OnTextEdited.Fire) + Hook("textinput", gui.Events.OnTextInputed.Fire) + + -- Touchscreen + Hook("touchmoved", gui.Events.OnTouchMoved.Fire) + Hook("touchpressed", gui.Events.OnTouchPressed.Fire) + Hook("touchreleased", gui.Events.OnTouchReleased.Fire) + + -- Joystick/Gamepad + Hook("gamepadpressed", gui.Events.OnGamepadPressed.Fire) + Hook("gamepadaxis", gui.Events.OnGamepadAxis.Fire) + Hook("gamepadreleased", gui.Events.OnGamepadReleased.Fire) + Hook("joystickpressed", gui.Events.OnJoystickPressed.Fire) + Hook("joystickreleased", gui.Events.OnJoystickReleased.Fire) + Hook("joystickhat", gui.Events.OnJoystickHat.Fire) + Hook("joystickremoved", gui.Events.OnJoystickRemoved.Fire) + Hook("joystickadded", gui.Events.OnJoystickAdded.Fire) +end) + +-- Hotkeys + +local has_hotkey = false +local hot_keys = {} + +-- Wait for keys to release to reset +local unPress = updater:newFunction(function(keys) + local check = function() + for key = 1, #keys["Keys"] do + if not love.keyboard.isDown(keys["Keys"][key]) then + keys.isBusy = false + return true + end + end + end + thread.hold(check) +end) + +updater:newThread("GUI Hotkey Manager", function() + local check = function() return has_hotkey end + while true do + thread.hold(check) + for i = 1, #hot_keys do + local good = true + for key = 1, #hot_keys[i]["Keys"] do + if not love.keyboard.isDown(hot_keys[i]["Keys"][key]) then + good = false + break + end + end + if good and not hot_keys[i].isBusy then + hot_keys[i]["Connection"]:Fire(hot_keys[i]["Ref"]) + hot_keys[i].isBusy = true + unPress(hot_keys[i]) + end + end + thread.sleep(.001) + end +end) + +function gui:setHotKey(keys, conn) + has_hotkey = true + local conn = conn or multi:newConnection() + table.insert(hot_keys, + {Ref = self, Connection = conn, Keys = {unpack(keys)}}) + return conn +end + +-- Default HotKeys +gui.HotKeys = {} + +-- Connections can be added together to create an OR logic to them, they can be multiplied together to create an AND logic to them +gui.HotKeys.OnSelectAll = gui:setHotKey({"lctrl", "a"}) + + gui:setHotKey({"rctrl", "a"}) + +gui.HotKeys.OnCopy = gui:setHotKey({"lctrl", "c"}) + + gui:setHotKey({"rctrl", "c"}) + +gui.HotKeys.OnPaste = gui:setHotKey({"lctrl", "v"}) + + gui:setHotKey({"rctrl", "v"}) + +gui.HotKeys.OnCut = gui:setHotKey({"lctrl", "x"}) + + gui:setHotKey({"rctrl", "x"}) + +gui.HotKeys.OnUndo = gui:setHotKey({"lctrl", "z"}) + + gui:setHotKey({"rctrl", "z"}) + +gui.HotKeys.OnRedo = gui:setHotKey({"lctrl", "y"}) + + gui:setHotKey({"rctrl", "y"}) + + gui:setHotKey({"lctrl", "lshift", "z"}) + + gui:setHotKey({"rctrl", "lshift", "z"}) + + gui:setHotKey({"lctrl", "rshift", "z"}) + + gui:setHotKey({"rctrl", "rshift", "z"}) + +-- Utils + +function gui:tag(tag) + self.__tag = tag + return self +end + +function gui:getTag() + return self.__tag +end + +--[[ +C_ prefix = connect function to a connection +I_ prefix = invoke function args should be wrapped in a table +]] +local function handleConnection(object,field,value) + if field == "OnUpdate" then + object[field](object,value) + else + object[field](value) + end +end + +local function handleFunction(object,field,value) + if type(value) ~= "table" then return end + object[field](object,unpack(value)) +end + +function gui.apply(apply, ...) + for field, value in pairs(apply) do + for _, object in pairs({...}) do + local cmd = field:sub(1,2) + local handle = field:sub(3,-1) + local tp = type(object[field]) + if cmd == "C_" then + handleConnection(object,handle,value) + elseif cmd == "I_" then + handleFunction(object,handle,value) + elseif tp == "table" and object[field].Type == multi.registerType("connector", "connections") then + handleConnection(object,field,value) + elseif tp == "function" then + handleFunction(object,field,value) + else + object[field] = value + end + end + end +end + +gui.newFunction = updater.newFunction + +function gui:getProcessor() return updater end + +function gui:getObjectFocus() return object_focus end + +function gui:hasType(t) return band(self.type, t) == t end + +function gui:move(x, y) + self.dualDim.offset.pos.x = self.dualDim.offset.pos.x + x + self.dualDim.offset.pos.y = self.dualDim.offset.pos.y + y + self.OnPositionChanged:Fire(self, x, y) +end + +function gui:size(x,y) + self.dualDim.offset.size.x = self.dualDim.offset.size.x + x + self.dualDim.offset.size.y = self.dualDim.offset.size.y + y + self.OnSizeChanged:Fire(self, x, y) +end + +function gui:moveInBounds(dx, dy) + local x, y, w, h = self:getAbsolutes() + local x1, y1, w1, h1 = self.parent:getAbsolutes() + if (x + dx >= x1 or dx > 0) and (x + w + dx <= x1 + w1 or dx < 0) and + (y + dy >= y1 or dy > 0) and (y + h + dy <= y1 + h1 or dy < 0) then + self:move(dx, dy) + end +end + +local function intersecpt(x1, y1, x2, y2, x3, y3, x4, y4) + + local x5 = max(x1, x3) + local y5 = max(y1, y3) + local x6 = min(x2, x4) + local y6 = min(y2, y4) + + -- no intersection + if x5 > x6 or y5 > y6 then + return 0, 0, 0, 0 -- Return a no + end + + local x7 = x5 + local y7 = y6 + local x8 = x6 + local y8 = y5 + + return x7, y7, abs(x7 - x8), abs(y7 - y8) +end + +local function toCoordPoints(x, y, w, h) return x, y, x + w, y + h end + +function gui:intersecpt(x, y, w, h) + local x1, y1, x2, y2 = toCoordPoints(self:getAbsolutes()) + local x3, y3, x4, y4 = toCoordPoints(x, y, w, h) + + return intersecpt(x1, y1, x2, y2, x3, y3, x4, y4) +end + +function gui:isDescendantOf(obj) + local parent = self.parent + while parent ~= gui do + if parent == obj then return true end + parent = parent.parent + end + return false +end + +function gui:getChildren() return self.children end + +function gui:offsetToScale() + local children = self:getAllChildren() + for i = 1, #children do + local child = children[i] + local x, y = child:getAbsolutes() + local _, __, w, h = child.parent:getAbsolutes() + local _, __, w, h = child:getAbsolutes() + local _, __, pw, ph = child.parent:getAbsolutes() + end +end + +function gui:getAbsolutes(transform) -- returns x, y, w, h + local x,y,w,h + if transform then + x, y, w, h = transform((self.parent.w * self.dualDim.scale.pos.x) + + self.dualDim.offset.pos.x + self.parent.x), + transform((self.parent.h * self.dualDim.scale.pos.y) + + self.dualDim.offset.pos.y + self.parent.y), transform((self.parent.w * + self.dualDim.scale.size.x) + self.dualDim.offset.size.x), + transform((self.parent.h * self.dualDim.scale.size.y) + + self.dualDim.offset.size.y) + else + x, y, w, h = (self.parent.w * self.dualDim.scale.pos.x) + + self.dualDim.offset.pos.x + self.parent.x, + (self.parent.h * self.dualDim.scale.pos.y) + + self.dualDim.offset.pos.y + self.parent.y, (self.parent.w * + self.dualDim.scale.size.x) + self.dualDim.offset.size.x, + (self.parent.h * self.dualDim.scale.size.y) + + self.dualDim.offset.size.y + end + if self.square == "w" then + h = w + elseif self.square == "h" then + w = h + end + return x, y, w, h +end + +function gui:getAllChildren(vis) + local children = self:getChildren() + local allChildren = {} + for i, child in ipairs(children) do + if not (vis) and child.visible == true then + allChildren[#allChildren + 1] = child + local grandChildren = child:getAllChildren() + for j, grandChild in ipairs(grandChildren) do + allChildren[#allChildren + 1] = grandChild + end + end + end + return allChildren +end + +function gui:newThread(func) + return updater:newThread("ThreadHandler<" .. self.type .. ">", func, self, thread) +end + +function gui:setDualDim(x, y, w, h, sx, sy, sw, sh) + --[[ + dd.offset.pos = {x = x or 0, y = y or 0} + self.dualDim.offset.size = {x = w or 0, y = h or 0} + self.dualDim.scale.pos = {x = sx or 0, y = sy or 0} + self.dualDim.scale.size = {x = sw or 0, y = sh or 0} + ]] + self.dualDim = self:newDualDim( + x or self.dualDim.offset.pos.x, + y or self.dualDim.offset.pos.y, + w or self.dualDim.offset.size.x, + h or self.dualDim.offset.size.y, + sx or self.dualDim.scale.pos.x, + sy or self.dualDim.scale.pos.y, + sw or self.dualDim.scale.size.x, + sh or self.dualDim.scale.size.y) + self.OnSizeChanged:Fire(self, x, y, w, h, sx, sy, sw, sh) +end + +function gui:rawSetDualDim(x, y, w, h, sx, sy, sw, sh) + self.dualDim = self:newDualDim( + x or self.dualDim.offset.pos.x, + y or self.dualDim.offset.pos.y, + w or self.dualDim.offset.size.x, + h or self.dualDim.offset.size.y, + sx or self.dualDim.scale.pos.x, + sy or self.dualDim.scale.pos.y, + sw or self.dualDim.scale.size.x, + sh or self.dualDim.scale.size.y) +end + +local image_cache = {} +function gui:getTile(i, x, y, w, h) -- returns imagedata + local tw, wh + if i == nil then return end + if type(i) == "string" then i = image_cache[i] or i end + if type(i) == "string" then + i = love.image.newImageData(i) + image_cache[i] = i + elseif type(i) == "userdata" then + -- do nothing + elseif self:hasType(image) then + i, x, y, w, h = self.image, i, x, y, w + else + error("getTile invalid args!!! Usage: ImageElement:getTile(x,y,w,h) or gui:getTile(imagedata,x,y,w,h)") + end + return i, love.graphics.newQuad(x, y, w, h, i:getWidth(), i:getHeight()) +end + +function gui:topStack() + local siblings = self.parent.children + for i = 1, #siblings do + if siblings[i] == self then + table.remove(siblings, i) + break + end + end + siblings[#siblings + 1] = self +end + +function gui:bottomStack() + local siblings = self.parent.children + for i = 1, #siblings do + if siblings[i] == self then + table.remove(siblings, i) + break + end + end + table.insert(siblings, 1, self) +end + +local mainupdater = updater:newLoop() +mainupdater:setName("GUI Update Handler") + +function gui:OnUpdate(func) -- Not crazy about this approach, will probably rework this + if type(self) == "function" then + func = self + end + + mainupdater.OnLoop(function(_,_,dt) + func(self, dt) + end) +end + +function gui:canPress(mx, my) -- Get the intersection of the clip area and the self then test with the clip, otherwise test as normal + local x, y, w, h + if self.__variables.clip[1] then + local clip = self.__variables.clip + x, y, w, h = self:intersecpt(clip[2], clip[3], clip[4], clip[5]) + return mx < x + w and mx > x and my + h < y + h and my + h > y + else + x, y, w, h = self:getAbsolutes() + end + return not (mx > x + w or mx < x or my > y + h or my < y) +end + +function gui:isBeingCovered(mx, my) + local children = gui:getAllChildren() + for i = #children, 1, -1 do + if children[i] == self then + return false + elseif children[i]:canPress(mx, my) and not (children[i] == self) and + not (children[i].ignore) then + return true + end + end + return false +end + +function gui:getLocalCords(mx, my) + x, y, w, h = self:getAbsolutes() + return mx - x, my - y +end + +function gui:setParent(parent) + local temp = self.parent:getChildren() + for i = 1, #temp do + if temp[i] == self then + table.remove(self.parent.children, i) + break + end + end + if parent then + table.insert(parent.children, self) + self.parent = parent + end +end + +local function processDo(ref) ref.Do[1]() end + +function gui:clone(opt) + --[[ + { + copyTo: Who to set the parent to + connections: Do we copy connections? (true/false) + } + ]] + + local temp + local u = self:getUniques() + if self.type == frame then + temp = gui:newFrame(self:getDualDim()) + elseif self.type == text + box then + temp = gui:newTextBox(self.text, self:getDualDim()) + elseif self.type == text + button then + temp = gui:newTextButton(self.text, self:getDualDim()) + elseif self.type == text then + temp = gui:newTextLabel(self.text, self:getDualDim()) + elseif self.type == image + button then + temp = gui:newImageButton(u.Do[2], self:getDualDim()) + elseif self.type == image then + temp = gui:newImageLabel(u.Do[2], self:getDualDim()) + else -- We are dealing with a complex object + temp = processDo(u) + end + + for i, v in pairs(u) do temp[i] = v end + + local conn + if opt then + temp:setParent(opt.copyTo or gui.virtual) + if opt.connections then + conn = true + for i, v in pairs(self) do + if type(v) == "table" and v.Type == "connector" then + -- We want to copy the connection functions from the original object and bind them to the new one + if not temp[i] then + -- Incase we are dealing with a custom object, create a connection if the custom objects unique declearation didn't + temp[i] = multi:newConnection() + end + temp[i]:Bind(v:getConnections()) + end + end + end + end + + -- This recursively clones and sets the parent to the temp + for i, v in pairs(self:getChildren()) do + v:clone({copyTo = temp, connections = conn}) + end + + return temp +end + +function gui:isActive() + return self.active and not (self:isDescendantOf(gui.virtual)) +end + +function gui:isOnScreen() + + return +end + +-- Base get uniques +function gui:getUniques(tab) + local base = { + active = self.active, + visible = self.visible, + visibility = self.visibility, + color = self.color, + borderColor = self.borderColor, + drawBorder = self.drawborder, + rotation = self.rotation + } + + if tab then for i, v in pairs(tab) do base[i] = tab[i] end end + return base +end + +function gui:setTag(tag) + self.tags[tag] = true +end + +function gui:hasTag(tag) + return self.tags[tag] +end + +function gui:parentHasTag(tag) + local parent = self.parent + while parent do + if parent.tags and parent.tags[tag] then return true end + parent = parent.parent + if parent == gui.virtual or parent == gui then return false end + end + return false +end + +local function testVisual(c, x, y, button, istouch, presses) + return not(c:hasTag("visual") or c:parentHasTag("visual")) +end + +-- Base Library +function gui:newBase(typ, x, y, w, h, sx, sy, sw, sh, virtual) + local c = {} + c.tags = {} + local buildBackBetter + local centerX = false + local centerY = false + local centering = false + local dragbutton = 2 + local draggable = false + local hierarchy = false + + local function testHierarchy(c, x, y, button, istouch, presses) + if hierarchy then + return not (global_drag or c:isBeingCovered(x, y)) + end + return true + end + + local function defaultCheck(...) + if not c:isActive() then return false end + local x, y = love.mouse.getPosition() + if c:canPress(x, y) then + return c, ... + end + return false + end + + local function creationCheck(self) + return self:isDescendantOf(c) + end + + setmetatable(c, gui) + c.__variables = {clip = {false, 0, 0, 0, 0}} + c.focus = false + c.active = true + c.type = typ + c.dualDim = self:newDualDim(x, y, w, h, sx, sy, sw, sh) + c.children = {} + c.visible = true + c.visibility = 1 + c.color = {.6, .6, .6} + c.borderColor = color.black + c.drawBorder = true + c.rotation = 0 + c.formFactor = gui.FORM_RECTANGLE + + c.OnLoad = multi:newConnection() + + c.OnPressed = testVisual .. (testHierarchy .. multi:newConnection()) + c.OnPressedOuter = testVisual .. multi:newConnection() + c.OnReleased = testVisual .. (testHierarchy .. multi:newConnection()) + c.OnReleasedOuter = testVisual .. multi:newConnection() + c.OnReleasedOther = testVisual .. multi:newConnection() + + c.OnDragStart = testVisual .. multi:newConnection() + c.OnDragging = testVisual .. multi:newConnection() + c.OnDragEnd = testVisual .. multi:newConnection() + + c.OnEnter = (testHierarchy .. multi:newConnection()) + c.OnExit = testVisual .. multi:newConnection() + + c.OnMoved = testVisual .. (testHierarchy .. multi:newConnection()) + c.OnWheelMoved = testVisual .. (defaultCheck / gui.Events.OnWheelMoved) + + c.OnSizeChanged = testVisual .. multi:newConnection() + c.OnPositionChanged = testVisual .. multi:newConnection() + + c.OnLeftStickUp = testVisual .. multi:newConnection() + c.OnLeftStickDown = testVisual .. multi:newConnection() + c.OnLeftStickLeft = testVisual .. multi:newConnection() + c.OnLeftStickRight = testVisual .. multi:newConnection() + c.OnRightStickUp = testVisual .. multi:newConnection() + c.OnRightStickDown = testVisual .. multi:newConnection() + c.OnRightStickLeft = testVisual .. multi:newConnection() + c.OnRightStickRight = testVisual .. multi:newConnection() + + c.OnDestroy = multi:newConnection() + + c.OnCreated = creationCheck .. multi:newConnection() + local _forwardedRef = multi.forwardConnection(gui.Events.OnCreated,c.OnCreated) + local dragging = false + local entered = false + local moved = false + local pressed = false + + local _mouseMoveRef = gui.Events.OnMouseMoved(function(x, y, dx, dy, istouch) + if not c:isActive() then return end + if c:canPress(x, y) or dragging then + c.OnMoved:Fire(c, x, y, dx, dy, istouch) + if entered == false then + c.OnEnter:Fire(c, x, y) + entered = true + end + if dragging then + c.OnDragging:Fire(c, dx, dy, x, y, istouch) + end + elseif entered then + entered = false + c.OnExit:Fire(c, x, y) + end + end) + + local _mouseRelRef = gui.Events.OnMouseReleased(function(x, y, button, istouch, presses) + if not c:isActive() then return end + if c:canPress(x, y) then + c.OnReleased:Fire(c, x, y, button, istouch, presses) + elseif pressed then + c.OnReleasedOuter:Fire(c, x, y, button, istouch, presses) + else + c.OnReleasedOther:Fire(c, x, y, button, istouch, presses) + end + pressed = false + if dragging and button == dragbutton then + dragging = false + global_drag = false + c.OnDragEnd:Fire(c, dx, dy, x, y, istouch, presses) + end + end) + + local _mousePressRef = gui.Events.OnMousePressed(function(x, y, button, istouch, presses) + if not c:isActive() then return end + if c:canPress(x, y) or dragging then + c.OnPressed:Fire(c, x, y, dx, dy, istouch) + pressed = true + + -- Only change and trigger the event if it is a different object + if c ~= object_focus then + gui.Events.OnObjectFocusChanged:Fire(object_focus, c) + object_focus = c + end + + if draggable and button == dragbutton and not c:isBeingCovered(x, y) and + not global_drag then + dragging = true + global_drag = true + c.OnDragStart:Fire(c, dx, dy, x, y, istouch) + end + else + c.OnPressedOuter:Fire(c, x, y, button, istouch, presses) + end + end) + + function c:setColor(key,col) + if col[4] then + self.visibility = col[4] + end + self[key] = col + end + + function c:isOffScreen() + local x, y, w, h = self:getAbsolutes() + return y + h < 0 or y > gui.h or x + w < 0 or x > gui.w + end + + function c:setRoundness(rx, ry, seg, side) + self.roundness = side or true + self.__rx, self.__ry, self.__segments = rx or 5, ry or 5, seg or 30 + end + + function c:setRoundnessDirection(hori, vert) + self.__rhori = hori + self.__rvert = vert + end + + function c:makeCircle(x, y, r, sx, sy, sr, segments) + self.formFactor = gui.FORM_CIRCLE + self.segments = segments + self.__radius = r + self:setDualDim(x, y, 2*r, 2*r, sx, sy, sr) + return self + end + + function c:makeArc(tp, x, y, r, sx, sy, sr, angle1, angle2, segments) + self.arcType = tp + self:setDualDim(x, y, 2*r, 2*r, sx, sy, sr) + self.__angleS = angle1 + self.__angleE = angle2 + self.__radius = r + self.segments = segments + self.formFactor = gui.FORM_ARC + return self + end + + function c:respectHierarchy(bool) hierarchy = bool end + + local function centerthread() + if centerX or centerY then + local x, y, w, h = c:getAbsolutes() + if centerX then + c:rawSetDualDim(-w / 2, nil, nil, nil, .5) + end + if centerY then + c:rawSetDualDim(nil, -h / 2, nil, nil, nil, .5) + end + end + end + + function c:enableDragging(but) + if not but then + draggable = false + return + end + dragbutton = but or dragbutton + draggable = true + end + + function c:centerX(bool) + centerX = bool + if centering then return end + centering = true + self.OnSizeChanged(centerthread) + self.OnPositionChanged(centerthread) + updater:newLoop(centerthread) + end + + function c:centerY(bool) + centerY = bool + if centering then return end + centering = true + self.OnSizeChanged(centerthread) + self.OnPositionChanged(centerthread) + updater:newLoop(centerthread) + end + + function c:fullFrame() + self:setDualDim(0,0,0,0,0,0,1,1) + end + + function c:destroy() + -- Find and remove self from parent's children list + local children = self.parent and self.parent.children + if not children then return end + + local foundIdx + for i, v in ipairs(children) do + if v == self then + foundIdx = i + break + end + end + if not foundIdx then return end + + -- Fire OnDestroy before teardown so listeners still work during the callback + self.OnDestroy:Fire(self) + + -- Recursively destroy all children first + for _, child in pairs(self.children) do + if type(child.destroy) == "function" then + child:destroy() + end + end + self.children = {} + + -- Disconnect the global connections + gui.Events.OnMouseMoved:Unconnect(_mouseMoveRef) + gui.Events.OnMouseReleased:Unconnect(_mouseRelRef) + gui.Events.OnMousePressed:Unconnect(_mousePressRef) + gui.Events.OnCreated:Unconnect(_forwardedRef) + self.OnWheelMoved:Destroy() + + -- Destroy all connection objects on self (OnPressed, OnReleased, etc.) + for key, value in pairs(self) do + if type(value) == "table" and + value.Type == multi.registerType("connector", "connections") then + value:Destroy() + end + end + + -- Remove from parent + table.remove(children, foundIdx) + self.parent = nil + end + + function c:removeChildren() + for _, child in pairs(self.children) do + if type(child.destroy) == "function" then + child:destroy() -- recursive, disconnects gui.Events listeners + end + end + self.children = {} + end + + -- Add to the parents children table + if virtual then + c.parent = gui.virtual + table.insert(gui.virtual.children, c) + else + c.parent = self + table.insert(self.children, c) + end + local a = 0 + if typ == frame then + gui.Events.OnCreated:Fire(c) -- Trigger frame types instantly + end + return c +end + +function gui:newDualDim(x, y, w, h, sx, sy, sw, sh) + local dd = {} + dd.offset = {} + dd.scale = {} + dd.offset.pos = {x = x or 0, y = y or 0} + dd.offset.size = {x = w or 0, y = h or 0} + dd.scale.pos = {x = sx or 0, y = sy or 0} + dd.scale.size = {x = sw or 0, y = sh or 0} + return dd +end + +function gui:getDualDim() + local dd = self.dualDim + return dd.offset.pos.x, dd.offset.pos.y, dd.offset.size.x, dd.offset.size.y, + dd.scale.pos.x, dd.scale.pos.y, dd.scale.size.x, dd.scale.size.y +end + +-- Frames +function gui:newFrame(x, y, w, h, sx, sy, sw, sh) + return self:newBase(frame, x, y, w, h, sx, sy, sw, sh) +end + +function gui:newVirtualFrame(x, y, w, h, sx, sy, sw, sh) + return self:newBase(frame, x, y, w, h, sx, sy, sw, sh, true) +end + +function gui:newVisualFrame(x, y, w, h, sx, sy, sw, sh) + local visual = self:newBase(frame, x, y, w, h, sx, sy, sw, sh) + visual:setTag("visual") + return visual +end + +local function anyToString(value) + local t = type(value) + if t == "table" then + local parts = {} + for k, v in pairs(value) do + parts[#parts + 1] = tostring(k) .. "=" .. tostring(v) + end + return "{" .. table.concat(parts, ", ") .. "}" + end + return tostring(value) +end + +local testIMG +-- Texts +function gui:newTextBase(typ, txt, x, y, w, h, sx, sy, sw, sh) + local c = self:newBase(text + typ, x, y, w, h, sx, sy, sw, sh) + c.text = txt + c.align = gui.ALIGN_LEFT + c.adjust = 0 + c.textScaleX = 1 + c.textScaleY = 1 + c.textOffsetX = 0 + c.textOffsetY = 0 + c.textShearingFactorX = 0 + c.textShearingFactorY = 0 + c.textVisibility = 1 + c.font = love.graphics.newFont(12) + c.textColor = color.black + c.OnFontUpdated = testVisual .. multi:newConnection() + + function c:calculateFontOffset(font, adjust) + local adjust = adjust or 20 + local x, y, width, height = self:getAbsolutes() + local top = height + adjust + local bottom = 0 + local canvas = love.graphics.newCanvas(width, height + adjust) + love.graphics.setCanvas(canvas) + love.graphics.clear(0, 0, 0, .5, false, false) + love.graphics.setColor(1, 1, 1, 1) + love.graphics.setFont(font) + love.graphics.printf(self.text, 0, adjust / 2, width, "left", + self.rotation, self.textScaleX, self.textScaleY, 0, + 0, self.textShearingFactorX, + self.textShearingFactorY) + love.graphics.setCanvas() + local data = canvas:newImageData() + local f_top, f_bot = false, false + for yy = 0, height - 1 do + for xx = 0, width - 1 do + local r, g, b, a = data:getPixel(xx, yy) + if r ~= 0 or g ~= 0 or b ~= 0 then + if yy < top and not f_top then + top = yy + f_top = true + break + end + end + end + end + for yy = height - 1, 0, -1 do + for xx = 0, width - 1 do + local r, g, b, a = data:getPixel(xx, yy) + if r ~= 0 or g ~= 0 or b ~= 0 then + if yy > bottom and not f_bot then + bottom = yy + f_bot = false + break + end + end + end + end + return top - adjust, bottom - adjust + end + + function c:setFont(font, size) + if type(font) == "number" then + self.font = love.graphics.newFont(font) + elseif type(font) == "string" then + self.fontFile = font + self.font = love.graphics.newFont(font, size) + else + self.font = font + end + self.OnFontUpdated:Fire(self) + end + + local cache = {} + function c:fitFont(minSize, maxSize, opt) + local _,_,w,h = self:getAbsolutes() + local sw, sh = love.graphics.getDimensions() + local index = self.text .. tostring(w) .. tostring(h) .. tostring(sw) .. tostring(sh) + if cache[index] then + self:setFont(cache[index][1]) + return unpack(cache[index]) + end + if opt == nil then + opt = {scale=1} + end + local font + local x, y, boxWidth, boxHeight = self:getAbsolutes() + + if self.fontFile then + if self.fontFile:match("ttf") then + font = function(n) + return love.graphics.newFont(self.fontFile, n, "normal") + end + else + font = function(n) + return love.graphics.newFont(self.fontFile, n) + end + end + else + font = function(n) return love.graphics.setNewFont(n) end + end + + minSize = minSize or 8 + maxSize = maxSize or 200 + + local bestSize = minSize + local bestFont + + local low = minSize + local high = maxSize + local text = self.text + local mid + while low <= high do + mid = math.floor((low + high) / 2) + local testFont = font(mid) + local width = testFont:getWidth(text) + local height = testFont:getHeight() + + if width <= boxWidth and height <= boxHeight then + -- Font fits, try larger + bestSize = mid + bestFont = testFont + low = mid + 1 + else + -- Font too big, try smaller + high = mid - 1 + end + end + if type(opt) == "table" and opt.scale ~= 0 then + bestFont = font(mid*opt.scale) + else + bestFont = font(mid - 1) + end + self:setFont(bestFont) + cache[index] = {bestFont, bestSize} + return bestFont, bestSize + end + + function c:centerFont(y_offset) + local x, y, width, height = self:getAbsolutes() + local top, bottom = self:calculateFontOffset(self.font, y_offset or 0) + self.textOffsetY = floor(((height - bottom) - top) / 2) + self.OnFontUpdated:Fire(self) + end + + function c:getUniques() + return gui.getUniques(c, { + text = c.text, + align = c.align, + textScaleX = c.textScaleX, + textScaleY = c.textScaleY, + textOffsetX = c.textOffsetX, + textOffsetY = c.textOffsetY, + textShearingFactorX = c.textShearingFactorX, + textShearingFactorY = c.textShearingFactorY, + textVisibility = c.textVisibility, + font = c.font, + textColor = c.textColor + }) + end + return c +end + +function gui:newTextButton(txt, x, y, w, h, sx, sy, sw, sh) + local c = self:newTextBase(button, txt, x, y, w, h, sx, sy, sw, sh) + c:respectHierarchy(true) + + c.OnEnter(function(c, x, y, dx, dy, istouch) + love.mouse.setCursor(cursor_hand) + end) + + c.OnExit(function(c, x, y, dx, dy, istouch) love.mouse.setCursor() end) + gui.Events.OnCreated:Fire(c) + return c +end + +function gui:newTextLabel(txt, x, y, w, h, sx, sy, sw, sh) + local c = self:newTextBase(frame, txt, x, y, w, h, sx, sy, sw, sh) + gui.Events.OnCreated:Fire(c) + return c +end + +-- local val used when drawing + +local function getTextPosition(text, self, mx, my, exact) + -- Initialize variables + local pos = 0 + local font = love.graphics.getFont() + local width = 0 + local height = font:getHeight() + -- Loop through each character in the string + for i = 1, #text do + + local _w = font:getWidth(text:sub(i, i)) + local x, y, w, h = math.floor(width + self.adjust + self.textOffsetX), + 0, _w, height + + width = width + _w + + if not (mx > x + w or mx < x or my > y + h or my < y) then + if not (exact) and + (_w - + (width - (mx - math.floor(self.adjust + self.textOffsetX))) < + _w / 2 and i >= 1) then + return i - 1 + else + return i + end + elseif i == #text and mx > x + w then + return #text + end + end + return pos +end + +local cur = love.mouse.getCursor() +function gui:newTextBox(txt, x, y, w, h, sx, sy, sw, sh) + local c = self:newTextBase(box, txt, x, y, w, h, sx, sy, sw, sh) + c:respectHierarchy(true) + c.doSelection = false + + c.OnReturn = testVisual .. multi:newConnection() + + c.cur_pos = 0 + c.selection = {0, 0} + + function c:getUniques() + return gui.getUniques(c, { + doSelection = c.doSelection, + cur_pos = c.cur_pos, + adjust = c.adjust + }) + end + + function c:HasSelection() + return c.selection[1] ~= 0 and c.selection[2] ~= 0 + end + + function c:GetSelection() + local start, stop = c.selection[1], c.selection[2] + if start > stop then start, stop = stop, start end + return start, stop + end + + function c:GetSelectedText() + if not c:HasSelection() then return "" end + local sta, sto = c.selection[1], c.selection[2] + if sta > sto then sta, sto = sto, sta end + return c.text:sub(sta, sto) + end + + function c:ClearSelection() + c.doSelection = false + c.selection = {0, 0} + end + + c.OnEnter(function(c, x, y, dx, dy, istouch) + love.mouse.setCursor(love.mouse.getSystemCursor("ibeam")) + end) + + c.OnExit(function(c, x, y, dx, dy, istouch) love.mouse.setCursor(cur) end) + + c.OnPressed(function(c, x, y, dx, dy, istouch) + object_focus.bar_show = true + c.cur_pos = getTextPosition(c.text, c, c:getLocalCords(x, y)) + c.selection[1] = c.cur_pos + c.doSelection = true + end) + + c.OnMoved(function(c, x, y, dx, dy, istouch) + if c.doSelection then + local xx, yy = c:getLocalCords(x, y) + c.selection[2] = getTextPosition(c.text, c, xx, yy, true) + end + end); -- Needed to keep next line from being treated like a function call + + -- Connect to both events + (c.OnReleased + c.OnReleasedOuter)(function(c, x, y, dx, dy, istouch) + c.doSelection = false + end); + + -- ReleasedOther is different than ReleasedOuter (Other/Outer) + (c.OnReleasedOther + c.OnPressedOuter)(function() + c.doSelection = false + c.selection = {0, 0} + end) + + c.OnPressedOuter(function() c.bar_show = false end) + gui.Events.OnCreated:Fire(c) + return c +end + +local function textBoxThread() + updater:newThread("Textbox Handler", function() + local check = function() return object_focus:hasType(box) end + while true do + -- Do nothing if we aren't dealing with a textbox + thread.hold(check) + local ref = object_focus + ref.bar_show = true + thread.sleep(.5) + ref.bar_show = false + thread.sleep(.5) + end + end).OnError(textBoxThread) +end +textBoxThread() + +local function insert(obj, n_text) + if obj:HasSelection() then + local start, stop = obj:GetSelection() + obj.text = obj.text:sub(1, start - 1) .. n_text .. + obj.text:sub(stop + 1, -1) + obj:ClearSelection() + obj.cur_pos = start + if #n_text > 1 then obj.cur_pos = start + #n_text end + else + obj.text = obj.text:sub(1, obj.cur_pos) .. n_text .. + obj.text:sub(obj.cur_pos + 1, -1) + obj.cur_pos = obj.cur_pos + 1 + if #n_text > 1 then obj.cur_pos = obj.cur_pos + #n_text end + end +end + +local function delete(obj, cmd) + if obj:HasSelection() then + local start, stop = obj:GetSelection() + obj.text = obj.text:sub(1, start - 1) .. obj.text:sub(stop + 1, -1) + obj:ClearSelection() + obj.cur_pos = start - 1 + else + if cmd == "delete" then + obj.text = obj.text:sub(1, obj.cur_pos) .. + obj.text:sub(obj.cur_pos + 2, -1) + else + obj.text = obj.text:sub(1, obj.cur_pos - 1) .. + obj.text:sub(obj.cur_pos + 1, -1) + object_focus.cur_pos = object_focus.cur_pos - 1 + if object_focus.cur_pos == 0 then + object_focus.cur_pos = 1 + end + end + end +end + +gui.HotKeys.OnSelectAll(function() + if object_focus:hasType(box) then + object_focus.selection = {1, #object_focus.text} + end +end) + +gui.Events.OnTextInputed(function(text) + if object_focus:hasType(box) then insert(object_focus, text) end +end) + +gui.HotKeys.OnCopy(function() + if object_focus:hasType(box) then + love.system.setClipboardText(object_focus:GetSelectedText()) + end +end) + +gui.HotKeys.OnPaste(function() + if object_focus:hasType(box) then + insert(object_focus, love.system.getClipboardText()) + end +end) + +gui.HotKeys.OnCut(function() + if object_focus:hasType(box) and object_focus:HasSelection() then + love.system.setClipboardText(object_focus:GetSelectedText()) + delete(object_focus, "backspace") + end +end) + +gui.Events.OnKeyPressed(function(key, scancode, isrepeat) + -- Don't process if we aren't dealing with a textbox + if not object_focus:hasType(box) then return end + if key == "left" then + object_focus.cur_pos = object_focus.cur_pos - 1 + object_focus.bar_show = true + elseif key == "right" then + object_focus.cur_pos = object_focus.cur_pos + 1 + object_focus.bar_show = true + elseif key == "return" then + object_focus.OnReturn:Fire(object_focus, object_focus.text) + elseif key == "backspace" then + delete(object_focus, "backspace") + elseif key == "delete" then + delete(object_focus, "delete") + end +end) + +-- Images + +local load_image = THREAD:newFunction(function(path) + require("love.image") + return love.image.newImageData(path) +end) + +local load_images = THREAD:newFunction(function(paths) + require("love.image") + local images = #paths + for i = 1, #paths do + _G.THREAD.pushStatus(i, images, love.image.newImageData(paths[i])) + end +end) + +-- Loads a resource and adds it to the cache +gui.cacheImage = updater:newFunction(function(self, path_or_paths) + if type(path_or_paths) == "string" then + -- runs thread to load image then cache it for faster loading + load_image(path_or_paths).OnReturn(function(img) + image_cache[path_or_paths] = img + end) + -- table of paths + elseif type(path_or_paths) == "table" then + local handler = load_images(path_or_paths) + handler.OnStatus(function(part, whole, img) + image_cache[path_or_paths[part]] = img + thread.pushStatus(part, whole, image_cache[path_or_paths[part]]) + end) + end +end) + +function gui:applyGradient(direction, ...) + local colors = {...} + if direction == "horizontal" then + direction = true + elseif direction == "vertical" then + direction = false + else + error("Invalid direction '" .. tostring(direction) .. "' for gradient. Horizontal or vertical expected.") + end + local result = love.image.newImageData(direction and 1 or #colors, direction and #colors or 1) + for i, color in ipairs(colors) do + local x, y + if direction then + x, y = 0, i - 1 + else + x, y = i - 1, 0 + end + result:setPixel(x, y, color[1], color[2], color[3], color[4] or 255) + end + + local img = love.graphics.newImage(result) + img:setFilter('linear', 'linear') + local x, y, w, h = self:getAbsolutes() + self.imageColor = color.white + self.imageVisibility = 1 + self.image = img + self.image:setWrap("repeat", "repeat") + self.imageHeight = img:getHeight() + self.imageWidth = img:getWidth() + self.quad = love.graphics.newQuad(0, 0, self.imageWidth, self.imageHeight, self.imageWidth, self.imageHeight) + + if not (band(self.type, image) == image) then + self.type = self.type + image + end +end + +function gui:newImageBase(typ, x, y, w, h, sx, sy, sw, sh) + local c = self:newBase(image + typ, x, y, w, h, sx, sy, sw, sh) + c.color = color.white + c.visibility = 0 + c.scaleX = 1 + c.scaleY = 1 + + local IMAGE + + function c:getUniques() + return gui.getUniques(c, { + -- Recreating the image object using set image is the way to go + DO = {[[setImage]], c.image or IMAGE} + }) + end + + function c:flip(vert) + if vert then + c.scaleY = c.scaleY * -1 + else + c.scaleX = c.scaleX * -1 + end + end + + function c:getSource() + return IMAGE + end + + local img + + c.setImage = function(self, i, x, y, w, h) + if i == nil then return end + + if type(i) == "string" and i:match(".gif") then + img = gif.load(i) + + gif.Updater(img, drawer) + c.OnDestroy(function() + img.kill = true -- trigger the gif thread to terminate + end) + + IMAGE = i + c.__isGif = true + elseif type(i) == "string" then + img = love.image.newImageData(i) + img = love.graphics.newImage(img) + IMAGE = i + end + + if type(i) == "string" then i = image_cache[i] or i end + + if i and x then + c.imageHeight = h + c.imageWidth = w + + if type(i) == "string" and not c.__isGif then + image_cache[i] = img + i = image_cache[i] + end + + c.image = i + if not c.__isGif then + c.image:setWrap("repeat", "repeat") + end + c.imageColor = color.white + c.quad = love.graphics.newQuad(x, y, w, h, c.image:getWidth(), c.image:getHeight()) + c.imageVisibility = 1 + + return + end + + if type(i) == "userdata" and i:type() == "Image" then + img = i + end + + local x, y, w, h = c:getAbsolutes() + c.imageColor = color.white + c.imageVisibility = 1 + c.image = img + if not self.__isGif then + c.image:setWrap("repeat", "repeat") + end + c.imageHeight = img:getHeight() + c.imageWidth = img:getWidth() + c.quad = love.graphics.newQuad(0, 0, c.imageWidth, c.imageHeight, c.imageWidth, c.imageHeight) + end + return c +end + +function gui:newImageLabel(source, x, y, w, h, sx, sy, sw, sh) + local c = self:newImageBase(frame, x, y, w, h, sx, sy, sw, sh) + c:setImage(source) + gui.Events.OnCreated:Fire(c) + return c +end + +function gui:newImageButton(source, x, y, w, h, sx, sy, sw, sh) + local c = self:newImageBase(frame, x, y, w, h, sx, sy, sw, sh) + c:respectHierarchy(true) + c:setImage(source) + + c.OnEnter(function(c, x, y, dx, dy, istouch) + love.mouse.setCursor(cursor_hand) + end) + + c.OnExit(function(c, x, y, dx, dy, istouch) love.mouse.setCursor() end) + gui.Events.OnCreated:Fire(c) + return c +end + +-- Video +function gui:newVideo(source, x, y, w, h, sx, sy, sw, sh) + local c = self:newImageBase(video, x, y, w, h, sx, sy, sw, sh) + c.OnVideoFinished = multi:newConnection() + c.playing = false + + function c:setVideo(v) + if type(v) == "string" then + c.video = love.graphics.newVideo(v) + elseif v then + c.video = v + end + c.audiosource = c.video:getSource() + if c.audiosource then c.audioLength = c.audiosource:getDuration() end + c.videoHeigth = c.video:getHeight() + c.videoWidth = c.video:getWidth() + c.quad = love.graphics.newQuad(0, 0, w, h, c.videoWidth, c.videoHeigth) + end + + function c:getDuration() + return c.audioLength + end + + function c:getVideo() return self.video end + + if type(source) == "string" then c:setVideo(source) end + + function c:play() + c.playing = true + c.video:play() + end + + function c:setVolume(vol) + if self.audiosource then self.audiosource:setVolume(vol) end + end + + function c:pause() c.video:pause() end + + function c:stop() + c.playing = false + c.video:pause() + c.video:rewind() + end + + function c:rewind() c.video:rewind() end + + function c:seek(n) c.video:seek(n) end + + function c:tell() return c.video:tell() end + + updater:newThread("Video Handler",function() + + local testCompletion = function() -- More intensive test + if c.video:tell() == 0 then + c.OnVideoFinished:Fire(c) + return true + end + end + + local isplaying = function() -- Less intensive test + return c.video:isPlaying() + end + + while true do thread.chain(isplaying, testCompletion) end + + end) + + c.videoVisibility = 1 + c.videoColor = color.white + gui.Events.OnCreated:Fire(c) + return c +end + +-- Draw Function + +-- local label, image, text, button, box, video, animation (spritesheet) +local drawtypes = { + [0] = function(child, x, y, w, h) end, + [1] = function(child, x, y, w, h) + if child.image then + love.graphics.setColor(child.imageColor[1], child.imageColor[2], child.imageColor[3], child.imageVisibility) + if child.__isGif then + if child.scaleX < 0 or child.scaleY < 0 then + local sx, sy = child.scaleX, child.scaleY + local adjustX, adjustY = child.scaleX * w, child.scaleY * h + if sx < 0 and sy < 0 then + love.graphics.draw(child.image.frames[child.image.currentFrame], child.quad, x - adjustX, y - adjustY, rad(child.rotation), (w / child.imageWidth) * child.scaleX, (h / child.imageHeight) * child.scaleY) + elseif sx < 0 then + love.graphics.draw(child.image.frames[child.image.currentFrame], child.quad, x - adjustX, y, rad(child.rotation), (w / child.imageWidth) * child.scaleX, h / child.imageHeight) + else + love.graphics.draw(child.image.frames[child.image.currentFrame], child.quad, x, y - adjustY, rad(child.rotation), w / child.imageWidth, (h / child.imageHeight) * child.scaleY) + end + else + if child.image.frames[child.image.currentFrame] then + love.graphics.draw(child.image.frames[child.image.currentFrame], child.quad, x, y, rad(child.rotation), w / child.imageWidth, h / child.imageHeight) + end + end + else + if child.scaleX < 0 or child.scaleY < 0 then + local sx, sy = child.scaleX, child.scaleY + local adjustX, adjustY = child.scaleX * w, child.scaleY * h + if sx < 0 and sy < 0 then + love.graphics.draw(child.image, child.quad, x - adjustX, y - adjustY, rad(child.rotation), (w / child.imageWidth) * child.scaleX, (h / child.imageHeight) * child.scaleY) + elseif sx < 0 then + love.graphics.draw(child.image, child.quad, x - adjustX, y, rad(child.rotation), (w / child.imageWidth) * child.scaleX, h / child.imageHeight) + else + love.graphics.draw(child.image, child.quad, x, y - adjustY, rad(child.rotation), w / child.imageWidth, (h / child.imageHeight) * child.scaleY) + end + else + love.graphics.draw(child.image, child.quad, x, y, rad(child.rotation), w / child.imageWidth, h / child.imageHeight) + end + end + end + end, + [2] = function(child, x, y, w, h) + love.graphics.setColor(child.textColor[1], child.textColor[2], + child.textColor[3], child.textVisibility) + love.graphics.setFont(child.font) + if child.align == gui.ALIGN_LEFT then + child.adjust = 0 + elseif child.align == gui.ALIGN_CENTER then + local fw = child.font:getWidth(child.text) + child.adjust = (w - fw) / 2 + elseif child.align == gui.ALIGN_RIGHT then + local fw = child.font:getWidth(child.text) + child.adjust = w - fw - 4 + end + local mul = 1 + if (child.formFactor == gui.FORM_ARC) or (child.formFactor == gui.FORM_CIRCLE) then + mul = 2 + end + love.graphics.printf(child.text, child.adjust + x + child.textOffsetX, + y + child.textOffsetY, w*mul, "left", child.rotation, + child.textScaleX, child.textScaleY, 0, 0, + child.textShearingFactorX, + child.textShearingFactorY) + end, + [4] = function(child, x, y, w, h) + if child.bar_show then + local lw = love.graphics.getLineWidth() + love.graphics.setLineWidth(1) + local font = child.font + local fh = font:getHeight() + local fw = font:getWidth(child.text:sub(1, child.cur_pos)) + love.graphics.line(child.textOffsetX + child.adjust + x + fw, y + 4, + child.textOffsetX + child.adjust + x + fw, + y + fh - 2) + love.graphics.setLineWidth(lw) + end + if child:HasSelection() then + local blue = color.highlighter_blue + local start, stop = child.selection[1], child.selection[2] + if start > stop then start, stop = stop, start end + local x1, y1 = child.font:getWidth(child.text:sub(1, start - 1)), 0 + local x2, y2 = child.font:getWidth(child.text:sub(1, stop)), h + love.graphics.setColor(blue[1], blue[2], blue[3], .5) + draw_factor(child,"fill", x + x1 + child.adjust, y + y1, x2 - x1, y2 - y1) + --love.graphics.rectangle("fill", x + x1 + child.adjust, y + y1,x2 - x1, y2 - y1) + end + end, + [8] = function(child, x, y, w, h) + if child.video and child.playing then + love.graphics.setColor(child.videoColor[1], child.videoColor[2], + child.videoColor[3], child.videoVisibility) + if w ~= child.imageWidth and h ~= child.imageHeight then + love.graphics.draw(child.video, x, y, rad(child.rotation), + w / child.videoWidth, h / child.videoHeigth) + else + love.graphics.draw(child.video, child.quad, x, y, + rad(child.rotation), w / child.videoWidth, + h / child.videoHeigth) + end + end + end, + [16] = function(child, x, y, w, h) + -- + end +} + +local draw_factor = function(child, mode, x, y, w, h, rx, ry, as, ae, seg) + if child.formFactor == gui.FORM_RECTANGLE then + love.graphics.rectangle(mode, x, y, w, h, rx, ry, seg) + elseif child.formFactor == gui.FORM_CIRCLE then + love.graphics.circle(mode, x+child.__radius, y+child.__radius, child.__radius, seg) + elseif child.formFactor == gui.FORM_ARC then + love.graphics.arc(mode, child.arcType, x+child.__radius, y+child.__radius, child.__radius, child.__angleS, child.__angleE, seg) + else + error("Invalid form factor selected: ".. tostring(child.formFactor)) + end +end + +local draw_handler = function(child, no_draw, dt) + local bg = child.color + local bbg = child.borderColor + local ctype = child.type + local vis = child.visibility + local x, y, w, h = child:getAbsolutes() + local roundness = child.roundness + local rx, ry, segments = child.__rx or 0, child.__ry or 0, + child.__segments or child.segments or 0 + child.x = x + child.y = y + child.w = w + child.h = h + + if no_draw then return end + + if child.clipDescendants then + local children = child:getAllChildren() + for c = 1, #children do -- Tell the children to clip themselves + local clip = children[c].__variables.clip + clip[1] = true + clip[2] = x + clip[3] = y + clip[4] = w + clip[5] = h + end + end + + if child.shader and band(ctype, image) == 2 then + love.graphics.setShader(child.shader) + end + + if child.__variables.clip[1] then + local clip = child.__variables.clip + love.graphics.setScissor(clip[2], clip[3], clip[4], clip[5]) + elseif type(roundness) == "string" then + love.graphics.setScissor(x - 1, y - 2, w + 2, h + 3) + end + + local drawB = child.drawBorder + + love.graphics.setColor(bg[1], bg[2], bg[3], vis) + draw_factor(child,"fill", x, y, w, h, rx, ry, nil, nil, segments) + + love.graphics.setLineStyle("smooth") + love.graphics.setLineWidth(1) + if drawB then + love.graphics.setColor(bbg[1], bbg[2], bbg[3], vis) + draw_factor(child,"line", x, y, w, h, rx, ry, nil, nil, segments) + end + + if drawB then + if roundness == "top" then + draw_factor(child,"fill", x, y + ry / 2, w, h - ry / 2 + 1) + --love.graphics.rectangle("fill", x, y + ry / 2, w, h - ry / 2 + 1) + love.graphics.setLineStyle("rough") + love.graphics.setColor(bbg[1], bbg[2], bbg[3], 1) + love.graphics.setLineWidth(1) + love.graphics.line(x, y + ry, x, y + h + 1, x + 1 + w, y + h + 1, + x + 1 + w, y + ry) + love.graphics.line(x, y + h, x + 1 + w, y + h) + + love.graphics.setScissor() + love.graphics.setColor(bbg[1], bbg[2], bbg[3], .6) + love.graphics.line(x - 1, y + ry / 2 + 2, x - 1, y + h + 2) + love.graphics.line(x + w + 2, y + ry / 2 + 2, x + w + 2, y + h + 2) + elseif roundness == "bottom" then + draw_factor(child,"fill", x, y, w, h - ry + 2) + --love.graphics.rectangle("fill", x, y, w, h - ry + 2) + love.graphics.setLineStyle("rough") + love.graphics.setColor(bbg[1], bbg[2], bbg[3], 1) + love.graphics.setLineWidth(2) + love.graphics.line(x - 1, y + ry + 1, x - 1, y - 1, x + w + 1, y - 1, + x + w + 1, y + ry + 1) + love.graphics.setScissor() + love.graphics.line(x - 1, y - 1, x + w + 1, y - 1) + + love.graphics.setColor(bbg[1], bbg[2], bbg[3], .6) + love.graphics.setLineWidth(2) + love.graphics.line(x - 1, y + 2, x - 1, y + h - 4 - ry / 2) + love.graphics.line(x + w + 1, y + 2, x + w + 1, y + h - 4 - ry / 2) + end + end + + -- Start object specific stuff + drawtypes[band(ctype, video)](child, x, y, w, h) + drawtypes[band(ctype, image)](child, x, y, w, h) + drawtypes[band(ctype, text)](child, x, y, w, h) + drawtypes[band(ctype, box)](child, x, y, w, h) + + if child.post then child:post() end + + if child.__variables.clip[1] then + love.graphics.setScissor() -- Remove the scissor + end + + if child.shader then + love.graphics.setShader() + end +end + +gui.draw_handler = draw_handler + +local draw_loop = drawer:newLoop(function(self, dt) + local children = gui:getAllChildren() + for i = 1, #children do + local child = children[i] + if child.effect then + child.effect(function() draw_handler(child, nil, dt) end) + else + draw_handler(child,nil,dt) + end + end + first_loop = true + love.graphics.setColor(1, 1, 1, 1) +end) +draw_loop:setName("GUI Draw Handler") + +drawer:newThread("Draw Handler",function() + while true do + thread.sleep(.01) + local children = gui.virtual:getAllChildren() + for i = 1, #children do + local child = children[i] + if child.effect then + child.effect(function() draw_handler(child, true, 0) end) + else + draw_handler(child, true, 0) + end + end + first_loop = true + end +end) + +local processors = { + updater.run +} + +-- Drawing and Updating +gui.draw = drawer.run +gui.update = function(dt) + for i = 1, #processors do + processors[i](dt) + end +end + +function gui:newProcessor(name) + local proc = multi:newProcessor(name or "UnNamedProcess_"..multi.randomString(4), true) + table.insert(processors, proc.run) + return proc +end + +-- Virtual gui +gui.virtual.type = frame +gui.virtual.children = {} +gui.virtual.dualDim = gui:newDualDim() +gui.virtual.x = 0 +gui.virtual.y = 0 +setmetatable(gui.virtual, gui) + +local w, h = love.graphics.getDimensions() + +gui.virtual.dualDim.offset.size.x = w +gui.virtual.dualDim.offset.size.y = h +gui.virtual.w = w +gui.virtual.h = h +gui.virtual.parent = gui.virtual + +-- Root gui +gui.parent = gui +gui.type = frame +gui.children = {} +gui.dualDim = gui:newDualDim() +gui.x = 0 +gui.y = 0 + +local w, h = love.graphics.getDimensions() +gui.dualDim.offset.size.x = w +gui.dualDim.offset.size.y = h +gui.w = w +gui.h = h + +function gui:GetSizeAdjustedToAspectRatio(dWidth, dHeight) + + local newHeight = 0 + local newWidth = 0 + + if self.g_width / self.g_height > dWidth / dHeight then + newHeight = dWidth * self.g_height / self.g_width + newWidth = dWidth + else + newWidth = dHeight * self.g_width / self.g_height + newHeight = dHeight + end + return newWidth, newHeight, (dWidth-newWidth)/2, (dHeight-newHeight)/2 +end + +--gui.GetSizeAdjustedToAspectRatio = GetSizeAdjustedToAspectRatio + +function gui:setAspectSize(w, h) + if w and h then + self.g_width, self.g_height = w, h + else + self.g_width, self.g_height = 0, 0 + end +end + +gui.Events.OnResized(function(w, h) + if gui.aspect_ratio then + local nw, nh, xt, yt = gui:GetSizeAdjustedToAspectRatio(w, h) + gui.x = xt + gui.y = yt + gui.dualDim.offset.size.x = nw + gui.dualDim.offset.size.y = nh + gui.w = nw + gui.h = nh + + gui.virtual.x = xt + gui.virtual.y = yt + gui.virtual.dualDim.offset.size.x = nw + gui.virtual.dualDim.offset.size.y = nh + gui.virtual.w = nw + gui.virtual.h = nh + else + gui.dualDim.offset.size.x = w + gui.dualDim.offset.size.y = h + gui.w = w + gui.h = h + + gui.virtual.dualDim.offset.size.x = w + gui.virtual.dualDim.offset.size.y = h + gui.virtual.w = w + gui.virtual.h = h + end +end) + +return gui diff --git a/gui/yaml/builder.lua b/gui/yaml/builder.lua new file mode 100644 index 0000000..84bb80f --- /dev/null +++ b/gui/yaml/builder.lua @@ -0,0 +1,430 @@ +-- gui_yaml.lua +-- Parses a YAML-like table (pre-parsed by a YAML lib) into GUI elements. +-- Usage: local yaml = require("tinyyaml") (or lyaml, etc.) +-- local def = yaml.parse(yaml_string) +-- local root = gui_yaml.build(gui, def) + +local gui_yaml = {} + +-- ───────────────────────────────────────────── +-- Helpers +-- ───────────────────────────────────────────── + +local function parseColor(v) + if type(v) == "string" then + return require("gui.core.color").new(v) + elseif type(v) == "table" then + -- {r, g, b} or {r, g, b, a} — values 0-255 or 0-1 + local r, g, b, a = v[1], v[2], v[3], v[4] or 255 + -- normalise if in 0-255 range + if r > 1 or g > 1 or b > 1 then + r, g, b = r/255, g/255, b/255 + if a > 1 then a = a/255 end + end + return {r, g, b, a} + end + return nil +end + +local function parseDualDim(def) + --[[ + YAML formats accepted: + pos: [x, y] offset + size: [w, h] offset + scale-pos: [sx, sy] scale (0-1) + scale-size: [sw, sh] scale (0-1) + + Or shorthand flat: + x, y, w, h, sx, sy, sw, sh + ]] + local px = def.x or (def.pos and def.pos[1]) or 0 + local py = def.y or (def.pos and def.pos[2]) or 0 + local pw = def.w or def.width or (def.size and def.size[1]) or 0 + local ph = def.h or def.height or (def.size and def.size[2]) or 0 + local sx = def.sx or (def["scale-pos"] and def["scale-pos"][1]) or 0 + local sy = def.sy or (def["scale-pos"] and def["scale-pos"][2]) or 0 + local sw = def.sw or (def["scale-size"] and def["scale-size"][1]) or 0 + local sh = def.sh or (def["scale-size"] and def["scale-size"][2]) or 0 + return px, py, pw, ph, sx, sy, sw, sh +end + +local function applyShared(parent, obj, def) + -- Color / border + if def.color then obj.color = parseColor(def.color) end + if def["border-color"] then obj.borderColor = parseColor(def["border-color"]) end + if def["draw-border"] ~= nil then obj.drawBorder = def["draw-border"] end + + -- Visibility + if def.visible ~= nil then obj.visible = def.visible end + if def.active ~= nil then obj.active = def.active end + if def.visibility ~= nil then obj.visibility = def.visibility end + + -- Rotation + if def.rotation then obj.rotation = def.rotation end + + -- Tag + if def.tag then obj:tag(def.tag) end + + -- Tags (multi) + if def.tags then + for _, t in ipairs(def.tags) do obj:setTag(t) end + end + + -- Form factor + if def.form then + local f = def.form + if f == "circle" then + local x, y, w, h, sx, sy, sw = parseDualDim(def) + local r = def.radius or (w / 2) + obj:makeCircle(x, y, r, sx, sy, sw, def.segments) + elseif f == "arc" then + local x, y, w, h, sx, sy, sw = parseDualDim(def) + local r = def.radius or (w / 2) + obj:makeArc( + def["arc-type"] or "open", + x, y, r, sx, sy, sw, + def["angle-start"] or 0, + def["angle-end"] or math.pi * 2, + def.segments + ) + end + end + + -- Roundness + if def.roundness then + local r = def.roundness + if type(r) == "table" then + obj:setRoundness(r[1], r[2], r[3], r.side) + elseif type(r) == "string" then + -- "top" | "bottom" shorthand + obj:setRoundness(5, 5, 30, r) + else + obj:setRoundness(r, r, 30) + end + end + + -- Centering + if def["center-x"] then obj:centerX(def["center-x"]) end + if def["center-y"] then obj:centerY(def["center-y"]) end + + -- Full frame shorthand + if def["full-frame"] then obj:fullFrame() end + + -- Square lock + if def.square then obj.square = def.square end + + -- Dragging + if def.draggable then + obj:enableDragging( + type(def.draggable) == "number" and def.draggable or 1 + ) + end + + -- Hierarchy + if def["respect-hierarchy"] ~= nil then + obj:respectHierarchy(def["respect-hierarchy"]) + end + + -- Clip descendants + if def["clip-descendants"] ~= nil then + obj.clipDescendants = def["clip-descendants"] + end + + -- Effects (function reference by name — looked up via _G or a registry) + if def.effect then + local fn = type(def.effect) == "function" + and def.effect + or _G[def.effect] + if fn then obj.effect = fn end + end + + -- Shader (name → looked up in _G) + if def.shader then + obj.shader = type(def.shader) == "userdata" + and def.shader + or _G[def.shader] + end + + -- Position on stack + if def.stack then + if def.stack == "top" then obj:topStack() end + if def.stack == "bottom" then obj:bottomStack() end + end +end + +local function applyEvents(obj, def, env) + --[[ + Events in YAML can be: + on-pressed: "myFunction" -- looks up _G or env + on-pressed: | + print("hello") -- raw Lua string, loaded as chunk + ]] + local function resolve(v) + if type(v) == "function" then return v end + if type(v) == "string" then + -- Try global lookup first + if _G[v] and type(_G[v]) == "function" then return _G[v] end + -- Otherwise treat as Lua source + if env then + return env[v] + end + end + end + + local map = { + ["on-pressed"] = "OnPressed", + ["on-released"] = "OnReleased", + ["on-released-outer"] = "OnReleasedOuter", + ["on-pressed-outer"] = "OnPressedOuter", + ["on-enter"] = "OnEnter", + ["on-exit"] = "OnExit", + ["on-moved"] = "OnMoved", + ["on-drag-start"] = "OnDragStart", + ["on-dragging"] = "OnDragging", + ["on-drag-end"] = "OnDragEnd", + ["on-wheel"] = "OnWheelMoved", + ["on-size-changed"] = "OnSizeChanged", + ["on-position-changed"] = "OnPositionChanged", + ["on-destroy"] = "OnDestroy", + ["on-load"] = "OnLoad", + ["on-return"] = "OnReturn", -- textbox only + } + + for yaml_key, conn_key in pairs(map) do + if def[yaml_key] and obj[conn_key] then + local fn = resolve(def[yaml_key]) + if fn then obj[conn_key](fn) end + end + end + + -- on-update is special (not a connection) + if def["on-update"] then + local fn = resolve(def["on-update"]) + if fn then obj:OnUpdate(fn) end + end + + -- Hotkeys + if def.hotkeys then + for _, hk in ipairs(def.hotkeys) do + -- {keys: [lctrl, s], action: "mySaveFunction"} + local fn = resolve(hk.action) + if fn then obj:setHotKey(hk.keys)(fn) end + end + end +end + +local function applyTextProps(obj, def) + if def.text then obj.text = tostring(def.text) end + if def["text-color"] then obj.textColor = parseColor(def["text-color"]) end + if def["text-visibility"] then obj.textVisibility = def["text-visibility"] end + if def["text-scale"] then + obj.textScaleX = def["text-scale"][1] or 1 + obj.textScaleY = def["text-scale"][2] or 1 + end + if def["text-offset"] then + obj.textOffsetX = def["text-offset"][1] or 0 + obj.textOffsetY = def["text-offset"][2] or 0 + end + if def["text-shear"] then + obj.textShearingFactorX = def["text-shear"][1] or 0 + obj.textShearingFactorY = def["text-shear"][2] or 0 + end + + -- Alignment + local alignMap = {left = 1, center = 0, right = 2} + if def.align then + obj.align = alignMap[def.align] or 1 + end + + -- Font + if def.font then + local f = def.font + if type(f) == "number" then + obj:setFont(f) + elseif type(f) == "string" then + obj:setFont(f, def["font-size"]) + elseif type(f) == "table" then + -- {file: "fonts/roboto.ttf", size: 18} + obj:setFont(f.file or f[1], f.size or f[2]) + end + end + + -- fit-font: true | {min: 8, max: 200, scale: 1} + if def["fit-font"] then + local ff = def["fit-font"] + if ff == true then + obj:fitFont() + elseif type(ff) == "table" then + obj:fitFont(ff.min, ff.max, ff.scale and {scale=ff.scale} or nil) + end + end + + -- center-font: true | offset + if def["center-font"] then + local cf = def["center-font"] + obj:centerFont(type(cf) == "number" and cf or nil) + end +end + +local function applyImageProps(obj, def) + -- source: "path/to/image.png" + -- tile: [x, y, w, h] (optional sub-quad) + if def.source then + if def.tile then + local t = def.tile + obj:setImage(def.source, t[1], t[2], t[3], t[4]) + else + obj:setImage(def.source) + end + end + + if def["scale-x"] then obj.scaleX = def["scale-x"] end + if def["scale-y"] then obj.scaleY = def["scale-y"] end + if def["image-color"] then obj.imageColor = parseColor(def["image-color"]) end + if def["image-visibility"] then obj.imageVisibility = def["image-visibility"] end + + -- flip: "horizontal" | "vertical" | "both" + if def.flip then + local fl = def.flip + if fl == "horizontal" or fl == "both" then obj:flip(false) end + if fl == "vertical" or fl == "both" then obj:flip(true) end + end + + -- gradient shorthand + if def.gradient then + local g = def.gradient + -- {direction: "vertical", colors: [[r,g,b,a], ...]} + local colors = {} + for _, c in ipairs(g.colors) do + colors[#colors+1] = parseColor(c) + end + obj:applyGradient(g.direction or "vertical", table.unpack(colors)) + end +end + +local function applyVideoProps(obj, def) + if def.source then obj:setVideo(def.source) end + if def.volume then obj:setVolume(def.volume) end + if def.autoplay and def.autoplay then obj:play() end + if def["video-color"] then obj.videoColor = parseColor(def["video-color"]) end + if def["video-visibility"] then obj.videoVisibility = def["video-visibility"] end +end + +-- ───────────────────────────────────────────── +-- Core builder +-- ───────────────────────────────────────────── + +local builders -- forward ref for recursion + +builders = { + ["frame"] = function(parent, def) + local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) + return parent:newFrame(x,y,w,h,sx,sy,sw,sh) + end, + ["virtual-frame"] = function(parent, def) + local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) + return parent:newVirtualFrame(x,y,w,h,sx,sy,sw,sh) + end, + ["visual-frame"] = function(parent, def) + local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) + return parent:newVisualFrame(x,y,w,h,sx,sy,sw,sh) + end, + ["label"] = function(parent, def) + local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) + return parent:newTextLabel(def.text or "", x,y,w,h,sx,sy,sw,sh) + end, + ["button"] = function(parent, def) + local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) + return parent:newTextButton(def.text or "", x,y,w,h,sx,sy,sw,sh) + end, + ["textbox"] = function(parent, def) + local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) + return parent:newTextBox(def.text or "", x,y,w,h,sx,sy,sw,sh) + end, + ["image"] = function(parent, def) + local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) + return parent:newImageLabel(def.source, x,y,w,h,sx,sy,sw,sh) + end, + ["image-button"] = function(parent, def) + local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) + return parent:newImageButton(def.source, x,y,w,h,sx,sy,sw,sh) + end, + ["video"] = function(parent, def) + local x,y,w,h,sx,sy,sw,sh = parseDualDim(def) + return parent:newVideo(def.source, x,y,w,h,sx,sy,sw,sh) + end, +} + +local TEXT_TYPES = {label=true, button=true, textbox=true} +local IMAGE_TYPES = {image=true, ["image-button"]=true} +local VIDEO_TYPES = {video=true} + +function gui_yaml.build(parent, def, env) + --[[ + parent : gui element or gui root + def : parsed YAML table (one element) + env : optional Lua env table for event string resolution + + Returns the created object (or nil on unknown type). + ]] + local typ = def.type + if not typ then + error("gui_yaml: element missing 'type' field") + end + + local builder = builders[typ] + if not builder then + error("gui_yaml: unknown element type '" .. tostring(typ) .. "'") + end + + local obj = builder(parent, def) + + -- Apply shared properties + applyShared(parent, obj, def) + + -- Apply type-specific properties + if TEXT_TYPES[typ] then applyTextProps(obj, def) end + if IMAGE_TYPES[typ] then applyImageProps(obj, def) end + if VIDEO_TYPES[typ] then applyVideoProps(obj, def) end + + -- Events + applyEvents(obj, def, env) + + -- Recurse into children + if def.children then + for _, child_def in ipairs(def.children) do + gui_yaml.build(obj, child_def, env) + end + end + + return obj +end + +function gui_yaml.buildMany(parent, defs, env) + local results = {} + for _, def in ipairs(defs) do + results[#results+1] = gui_yaml.build(parent, def, env) + end + return results +end + +-- Convenience: parse a YAML string and build in one call. +-- Requires a YAML library. Tries tinyyaml, then lyaml. +function gui_yaml.fromString(parent, yaml_str, env) + local ok, yaml = pcall(require, "gui.yaml.tinyyaml") + if not ok then + ok, yaml = pcall(require, "lyaml") + if not ok then + error("gui_yaml.fromString: no YAML library found (tried tinyyaml, lyaml)") + end + end + local def = yaml.parse(yaml_str) + -- Support both single-element and list-of-elements at root + if def.type then + return gui_yaml.build(parent, def, env) + else + return gui_yaml.buildMany(parent, def, env) + end +end + +return gui_yaml \ No newline at end of file diff --git a/gui/yaml/tinyyaml.lua b/gui/yaml/tinyyaml.lua new file mode 100644 index 0000000..6d4bcba --- /dev/null +++ b/gui/yaml/tinyyaml.lua @@ -0,0 +1,776 @@ +------------------------------------------------------------------------------- +-- tinyyaml - YAML subset parser +------------------------------------------------------------------------------- + +local table = table +local string = string +local schar = string.char +local ssub, gsub = string.sub, string.gsub +local sfind, smatch = string.find, string.match +local tinsert, tremove = table.insert, table.remove +local setmetatable = setmetatable +local pairs = pairs +local type = type +local tonumber = tonumber +local math = math +local getmetatable = getmetatable +local error = error + +local UNESCAPES = { + ['0'] = "\x00", z = "\x00", N = "\x85", + a = "\x07", b = "\x08", t = "\x09", + n = "\x0a", v = "\x0b", f = "\x0c", + r = "\x0d", e = "\x1b", ['\\'] = '\\', +}; + +------------------------------------------------------------------------------- +-- utils +local function select(list, pred) + local selected = {} + for i = 0, #list do + local v = list[i] + if v and pred(v, i) then + tinsert(selected, v) + end + end + return selected +end + +local function startswith(haystack, needle) + return ssub(haystack, 1, #needle) == needle +end + +local function ltrim(str) + return smatch(str, "^%s*(.-)$") +end + +local function rtrim(str) + return smatch(str, "^(.-)%s*$") +end + +------------------------------------------------------------------------------- +-- Implementation. +-- +local class = {__meta={}} +function class.__meta.__call(cls, ...) + local self = setmetatable({}, cls) + if cls.__init then + cls.__init(self, ...) + end + return self +end + +function class.def(base, typ, cls) + base = base or class + local mt = {__metatable=base, __index=base} + for k, v in pairs(base.__meta) do mt[k] = v end + cls = setmetatable(cls or {}, mt) + cls.__index = cls + cls.__metatable = cls + cls.__type = typ + cls.__meta = mt + return cls +end + + +local types = { + null = class:def('null'), + map = class:def('map'), + omap = class:def('omap'), + pairs = class:def('pairs'), + set = class:def('set'), + seq = class:def('seq'), + timestamp = class:def('timestamp'), +} + +local Null = types.null +function Null.__tostring() return 'yaml.null' end +function Null.isnull(v) + if v == nil then return true end + if type(v) == 'table' and getmetatable(v) == Null then return true end + return false +end +local null = Null() + +function types.timestamp:__init(y, m, d, h, i, s, f, z) + self.year = tonumber(y) + self.month = tonumber(m) + self.day = tonumber(d) + self.hour = tonumber(h or 0) + self.minute = tonumber(i or 0) + self.second = tonumber(s or 0) + if type(f) == 'string' and sfind(f, '^%d+$') then + self.fraction = tonumber(f) * math.pow(10, 3 - #f) + elseif f then + self.fraction = f + else + self.fraction = 0 + end + self.timezone = z +end + +function types.timestamp:__tostring() + return string.format( + '%04d-%02d-%02dT%02d:%02d:%02d.%03d%s', + self.year, self.month, self.day, + self.hour, self.minute, self.second, self.fraction, + self:gettz()) +end + +function types.timestamp:gettz() + if not self.timezone then + return '' + end + if self.timezone == 0 then + return 'Z' + end + local sign = self.timezone > 0 + local z = sign and self.timezone or -self.timezone + local zh = math.floor(z) + local zi = (z - zh) * 60 + return string.format( + '%s%02d:%02d', sign and '+' or '-', zh, zi) +end + + +local function countindent(line) + local _, j = sfind(line, '^%s+') + if not j then + return 0, line + end + return j, ssub(line, j+1) +end + +local function parsestring(line, stopper) + stopper = stopper or '' + local q = ssub(line, 1, 1) + if q == ' ' or q == '\t' then + return parsestring(ssub(line, 2)) + end + if q == "'" then + local i = sfind(line, "'", 2, true) + if not i then + return nil, line + end + return ssub(line, 2, i-1), ssub(line, i+1) + end + if q == '"' then + local i, buf = 2, '' + while i < #line do + local c = ssub(line, i, i) + if c == '\\' then + local n = ssub(line, i+1, i+1) + if UNESCAPES[n] ~= nil then + buf = buf..UNESCAPES[n] + elseif n == 'x' then + local h = ssub(i+2,i+3) + if sfind(h, '^[0-9a-fA-F]$') then + buf = buf..schar(tonumber(h, 16)) + i = i + 2 + else + buf = buf..'x' + end + else + buf = buf..n + end + i = i + 1 + elseif c == q then + break + else + buf = buf..c + end + i = i + 1 + end + return buf, ssub(line, i+1) + end + if q == '{' or q == '[' then -- flow style + return nil, line + end + if q == '|' or q == '>' then -- block + return nil, line + end + if q == '-' or q == ':' then + if ssub(line, 2, 2) == ' ' or #line == 1 then + return nil, line + end + end + local buf = '' + while #line > 0 do + local c = ssub(line, 1, 1) + if sfind(stopper, c, 1, true) then + break + elseif c == ':' and (ssub(line, 2, 2) == ' ' or #line == 1) then + break + elseif c == '#' and (ssub(buf, #buf, #buf) == ' ') then + break + else + buf = buf..c + end + line = ssub(line, 2) + end + return rtrim(buf), line +end + +local function isemptyline(line) + return line == '' or sfind(line, '^%s*$') or sfind(line, '^%s*#') +end + +local function equalsline(line, needle) + return startswith(line, needle) and isemptyline(ssub(line, #needle+1)) +end + +local function checkdupekey(map, key) + if map[key] ~= nil then + -- print("found a duplicate key '"..key.."' in line: "..line) + local suffix = 1 + while map[key..'_'..suffix] do + suffix = suffix + 1 + end + key = key ..'_'..suffix + end + return key +end + +local function parseflowstyle(line, lines) + local stack = {} + while true do + if #line == 0 then + if #lines == 0 then + break + else + line = tremove(lines, 1) + end + end + local c = ssub(line, 1, 1) + if c == '#' then + line = '' + elseif c == ' ' or c == '\t' or c == '\r' or c == '\n' then + line = ssub(line, 2) + elseif c == '{' or c == '[' then + tinsert(stack, {v={},t=c}) + line = ssub(line, 2) + elseif c == ':' then + local s = tremove(stack) + tinsert(stack, {v=s.v, t=':'}) + line = ssub(line, 2) + elseif c == ',' then + local value = tremove(stack) + if value.t == ':' or value.t == '{' or value.t == '[' then error() end + if stack[#stack].t == ':' then + -- map + local key = tremove(stack) + key.v = checkdupekey(stack[#stack].v, key.v) + stack[#stack].v[key.v] = value.v + elseif stack[#stack].t == '{' then + -- set + stack[#stack].v[value.v] = true + elseif stack[#stack].t == '[' then + -- seq + tinsert(stack[#stack].v, value.v) + end + line = ssub(line, 2) + elseif c == '}' then + if stack[#stack].t == '{' then + if #stack == 1 then break end + stack[#stack].t = '}' + line = ssub(line, 2) + else + line = ','..line + end + elseif c == ']' then + if stack[#stack].t == '[' then + if #stack == 1 then break end + stack[#stack].t = ']' + line = ssub(line, 2) + else + line = ','..line + end + else + local s, rest = parsestring(line, ',{}[]') + if not s then + error('invalid flowstyle line: '..line) + end + tinsert(stack, {v=s, t='s'}) + line = rest + end + end + return stack[1].v, line +end + +local function parseblockstylestring(line, lines, indent) + if #lines == 0 then + error("failed to find multi-line scalar content") + end + local s = {} + local firstindent = -1 + local endline = -1 + for i = 1, #lines do + local ln = lines[i] + local idt = countindent(ln) + if idt <= indent then + break + end + if ln == '' then + tinsert(s, '') + else + if firstindent == -1 then + firstindent = idt + elseif idt < firstindent then + break + end + tinsert(s, ssub(ln, firstindent + 1)) + end + endline = i + end + + local striptrailing = true + local sep = '\n' + local newlineatend = true + if line == '|' then + striptrailing = true + sep = '\n' + newlineatend = true + elseif line == '|+' then + striptrailing = false + sep = '\n' + newlineatend = true + elseif line == '|-' then + striptrailing = true + sep = '\n' + newlineatend = false + elseif line == '>' then + striptrailing = true + sep = ' ' + newlineatend = true + elseif line == '>+' then + striptrailing = false + sep = ' ' + newlineatend = true + elseif line == '>-' then + striptrailing = true + sep = ' ' + newlineatend = false + else + error('invalid blockstyle string:'..line) + end + local eonl = 0 + for i = #s, 1, -1 do + if s[i] == '' then + tremove(s, i) + eonl = eonl + 1 + end + end + if striptrailing then + eonl = 0 + end + if newlineatend then + eonl = eonl + 1 + end + for i = endline, 1, -1 do + tremove(lines, i) + end + return table.concat(s, sep)..string.rep('\n', eonl) +end + +local function parsetimestamp(line) + local _, p1, y, m, d = sfind(line, '^(%d%d%d%d)%-(%d%d)%-(%d%d)') + if not p1 then + return nil, line + end + if p1 == #line then + return types.timestamp(y, m, d), '' + end + local _, p2, h, i, s = sfind(line, '^[Tt ](%d+):(%d+):(%d+)', p1+1) + if not p2 then + return types.timestamp(y, m, d), ssub(line, p1+1) + end + if p2 == #line then + return types.timestamp(y, m, d, h, i, s), '' + end + local _, p3, f = sfind(line, '^%.(%d+)', p2+1) + if not p3 then + p3 = p2 + f = 0 + end + local zc = ssub(line, p3+1, p3+1) + local _, p4, zs, z = sfind(line, '^ ?([%+%-])(%d+)', p3+1) + if p4 then + z = tonumber(z) + local _, p5, zi = sfind(line, '^:(%d+)', p4+1) + if p5 then + z = z + tonumber(zi) / 60 + end + z = zs == '-' and -tonumber(z) or tonumber(z) + elseif zc == 'Z' then + p4 = p3 + 1 + z = 0 + else + p4 = p3 + z = false + end + return types.timestamp(y, m, d, h, i, s, f, z), ssub(line, p4+1) +end + +local function parsescalar(line, lines, indent) + line = ltrim(line) + line = gsub(line, '^%s*#.*$', '') -- comment only -> '' + line = gsub(line, '^%s*', '') -- trim head spaces + + if line == '' or line == '~' then + return null + end + + local ts, _ = parsetimestamp(line) + if ts then + return ts + end + + local s, _ = parsestring(line) + -- startswith quote ... string + -- not startswith quote ... maybe string + if s and (startswith(line, '"') or startswith(line, "'")) then + return s + end + + if startswith('!', line) then -- unexpected tagchar + error('unsupported line: '..line) + end + + if equalsline(line, '{}') then + return {} + end + if equalsline(line, '[]') then + return {} + end + + if startswith(line, '{') or startswith(line, '[') then + return parseflowstyle(line, lines) + end + + if startswith(line, '|') or startswith(line, '>') then + return parseblockstylestring(line, lines, indent) + end + + -- Regular unquoted string + line = gsub(line, '%s*#.*$', '') -- trim tail comment + local v = line + if v == 'null' or v == 'Null' or v == 'NULL'then + return null + elseif v == 'true' or v == 'True' or v == 'TRUE' then + return true + elseif v == 'false' or v == 'False' or v == 'FALSE' then + return false + elseif v == '.inf' or v == '.Inf' or v == '.INF' then + return math.huge + elseif v == '+.inf' or v == '+.Inf' or v == '+.INF' then + return math.huge + elseif v == '-.inf' or v == '-.Inf' or v == '-.INF' then + return -math.huge + elseif v == '.nan' or v == '.NaN' or v == '.NAN' then + return 0 / 0 + elseif sfind(v, '^[%+%-]?[0-9]+$') or sfind(v, '^[%+%-]?[0-9]+%.$')then + return tonumber(v) -- : int + elseif sfind(v, '^[%+%-]?[0-9]+%.[0-9]+$') then + return tonumber(v) + end + return s or v +end + +local parsemap; -- : func + +local function parseseq(line, lines, indent) + local seq = setmetatable({}, types.seq) + if line ~= '' then + error() + end + while #lines > 0 do + -- Check for a new document + line = lines[1] + if startswith(line, '---') then + while #lines > 0 and not startswith(lines, '---') do + tremove(lines, 1) + end + return seq + end + + -- Check the indent level + local level = countindent(line) + if level < indent then + return seq + elseif level > indent then + error("found bad indenting in line: ".. line) + end + + local i, j = sfind(line, '%-%s+') + if not i then + i, j = sfind(line, '%-$') + if not i then + return seq + end + end + local rest = ssub(line, j+1) + + if sfind(rest, '^[^\'\"%s]*:') then + -- Inline nested hash + local indent2 = j + lines[1] = string.rep(' ', indent2)..rest + tinsert(seq, parsemap('', lines, indent2)) + elseif sfind(rest, '^%-%s+') then + -- Inline nested seq + local indent2 = j + lines[1] = string.rep(' ', indent2)..rest + tinsert(seq, parseseq('', lines, indent2)) + elseif isemptyline(rest) then + tremove(lines, 1) + if #lines == 0 then + tinsert(seq, null) + return seq + end + if sfind(lines[1], '^%s*%-') then + local nextline = lines[1] + local indent2 = countindent(nextline) + if indent2 == indent then + -- Null seqay entry + tinsert(seq, null) + else + tinsert(seq, parseseq('', lines, indent2)) + end + else + -- - # comment + -- key: value + local nextline = lines[1] + local indent2 = countindent(nextline) + tinsert(seq, parsemap('', lines, indent2)) + end + elseif rest then + -- Array entry with a value + tremove(lines, 1) + tinsert(seq, parsescalar(rest, lines)) + end + end + return seq +end + +local function parseset(line, lines, indent) + if not isemptyline(line) then + error('not seq line: '..line) + end + local set = setmetatable({}, types.set) + while #lines > 0 do + -- Check for a new document + line = lines[1] + if startswith(line, '---') then + while #lines > 0 and not startswith(lines, '---') do + tremove(lines, 1) + end + return set + end + + -- Check the indent level + local level = countindent(line) + if level < indent then + return set + elseif level > indent then + error("found bad indenting in line: ".. line) + end + + local i, j = sfind(line, '%?%s+') + if not i then + i, j = sfind(line, '%?$') + if not i then + return set + end + end + local rest = ssub(line, j+1) + + if sfind(rest, '^[^\'\"%s]*:') then + -- Inline nested hash + local indent2 = j + lines[1] = string.rep(' ', indent2)..rest + set[parsemap('', lines, indent2)] = true + elseif sfind(rest, '^%s+$') then + tremove(lines, 1) + if #lines == 0 then + tinsert(set, null) + return set + end + if sfind(lines[1], '^%s*%?') then + local indent2 = countindent(lines[1]) + if indent2 == indent then + -- Null array entry + set[null] = true + else + set[parseseq('', lines, indent2)] = true + end + end + + elseif rest then + tremove(lines, 1) + set[parsescalar(rest, lines)] = true + else + error("failed to classify line: "..line) + end + end + return set +end + +function parsemap(line, lines, indent) + if not isemptyline(line) then + error('not map line: '..line) + end + local map = setmetatable({}, types.map) + while #lines > 0 do + -- Check for a new document + line = lines[1] + if startswith(line, '---') then + while #lines > 0 and not startswith(lines, '---') do + tremove(lines, 1) + end + return map + end + + -- Check the indent level + local level, _ = countindent(line) + if level < indent then + return map + elseif level > indent then + error("found bad indenting in line: ".. line) + end + + -- Find the key + local key + local s, rest = parsestring(line) + + -- Quoted keys + if s and startswith(rest, ':') then + local sc = parsescalar(s, {}, 0) + if sc and type(sc) ~= 'string' then + key = sc + else + key = s + end + line = ssub(rest, 2) + else + error("failed to classify line: "..line) + end + + key = checkdupekey(map, key) + line = ltrim(line) + + if ssub(line, 1, 1) == '!' then + -- ignore type + local rh = ltrim(ssub(line, 3)) + local typename = smatch(rh, '^!?[^%s]+') + line = ltrim(ssub(rh, #typename+1)) + end + + if not isemptyline(line) then + tremove(lines, 1) + line = ltrim(line) + map[key] = parsescalar(line, lines, indent) + else + -- An indent + tremove(lines, 1) + if #lines == 0 then + map[key] = null + return map; + end + if sfind(lines[1], '^%s*%-') then + local indent2 = countindent(lines[1]) + map[key] = parseseq('', lines, indent2) + elseif sfind(lines[1], '^%s*%?') then + local indent2 = countindent(lines[1]) + map[key] = parseset('', lines, indent2) + else + local indent2 = countindent(lines[1]) + if indent >= indent2 then + -- Null hash entry + map[key] = null + else + map[key] = parsemap('', lines, indent2) + end + end + end + end + return map +end + + +-- : (list)->dict +local function parsedocuments(lines) + lines = select(lines, function(s) return not isemptyline(s) end) + + if sfind(lines[1], '^%%YAML') then tremove(lines, 1) end + + local root = {} + local in_document = false + while #lines > 0 do + local line = lines[1] + -- Do we have a document header? + local docright; + if sfind(line, '^%-%-%-') then + -- Handle scalar documents + docright = ssub(line, 4) + tremove(lines, 1) + in_document = true + end + if docright then + if (not sfind(docright, '^%s+$') and + not sfind(docright, '^%s+#')) then + tinsert(root, parsescalar(docright, lines)) + end + elseif #lines == 0 or startswith(line, '---') then + -- A naked document + tinsert(root, null) + while #lines > 0 and not sfind(lines[1], '---') do + tremove(lines, 1) + end + in_document = false + -- XXX The final '-+$' is to look for -- which ends up being an + -- error later. + elseif not in_document and #root > 0 then + -- only the first document can be explicit + error('parse error: '..line) + elseif sfind(line, '^%s*%-') then + -- An array at the root + tinsert(root, parseseq('', lines, 0)) + elseif sfind(line, '^%s*[^%s]') then + -- A hash at the root + local level = countindent(line) + tinsert(root, parsemap('', lines, level)) + else + -- Shouldn't get here. @lines have whitespace-only lines + -- stripped, and previous match is a line with any + -- non-whitespace. So this clause should only be reachable via + -- a perlbug where \s is not symmetric with \S + + -- uncoverable statement + error('parse error: '..line) + end + end + if #root > 1 and Null.isnull(root[1]) then + tremove(root, 1) + return root + end + return root +end + +--- Parse yaml string into table. +local function parse(source) + local lines = {} + for line in string.gmatch(source .. '\n', '(.-)\r?\n') do + tinsert(lines, line) + end + + local docs = parsedocuments(lines) + if #docs == 1 then + return docs[1] + end + + return docs +end + +return { + version = 0.1, + parse = parse, +} \ No newline at end of file diff --git a/gui/yaml/toyaml.lua b/gui/yaml/toyaml.lua new file mode 100644 index 0000000..d157fd5 --- /dev/null +++ b/gui/yaml/toyaml.lua @@ -0,0 +1,362 @@ +local function colorToYaml(c) + if not c then return nil end + -- Check if it's a color object with hex method, otherwise use raw values + if type(c) == "table" then + if c.toHex then return c:toHex() end + -- Normalise to 0-255 for readability + local r = c[1] or 0 + local g = c[2] or 0 + local b = c[3] or 0 + local a = c[4] + if r <= 1 and g <= 1 and b <= 1 then + r, g, b = math.floor(r*255), math.floor(g*255), math.floor(b*255) + if a then a = math.floor(a*255) end + end + if a and a < 255 then + return string.format("[%d, %d, %d, %d]", r, g, b, a) + end + return string.format("[%d, %d, %d]", r, g, b) + end + return nil +end + +local function dualDimToYaml(obj) + local dd = obj.dualDim + local fields = {} + local op = dd.offset.pos + local os = dd.offset.size + local sp = dd.scale.pos + local ss = dd.scale.size + + if op.x ~= 0 then fields[#fields+1] = {"x", op.x} end + if op.y ~= 0 then fields[#fields+1] = {"y", op.y} end + if os.x ~= 0 then fields[#fields+1] = {"w", os.x} end + if os.y ~= 0 then fields[#fields+1] = {"h", os.y} end + if sp.x ~= 0 then fields[#fields+1] = {"sx", sp.x} end + if sp.y ~= 0 then fields[#fields+1] = {"sy", sp.y} end + if ss.x ~= 0 then fields[#fields+1] = {"sw", ss.x} end + if ss.y ~= 0 then fields[#fields+1] = {"sh", ss.y} end + return fields +end + +local bit = require("bit") +local band = bit.band +local frame, image, text, box, video, button, anim = 0, 1, 2, 4, 8, 16, 32 + +local function resolveTypeName(typ) + -- Match in specificity order (combined types first) + if typ == text + box then return "textbox" end + if typ == text + button then return "button" end + if typ == text + frame then return "label" end + if typ == image + frame then return "image" end + -- image+button uses frame internally in this lib + -- (newImageButton sets type = image+frame but adds cursor behaviour) + -- We detect image buttons by checking for cursor handler presence; + -- as a fallback we use "image" and the loader will still reconstruct it. + if band(typ, video) == video then return "video" end + if band(typ, image) == image then return "image" end + if typ == frame then return "frame" end + return "frame" -- safe fallback +end + +local alignNames = {[0]="center", [1]="left", [2]="right"} +local formNames = { + [1] = "rectangle", + [2] = "circle", + [3] = "arc", +} + +-- YAML emitter — produces clean, human-readable YAML without a library dep. +local function emit(val, indent, visited) + indent = indent or 0 + visited = visited or {} + local pad = string.rep(" ", indent) + local t = type(val) + + if t == "boolean" then return tostring(val) end + if t == "number" then + -- Avoid scientific notation for small floats + if val == math.floor(val) then return string.format("%d", val) end + return string.format("%.6g", val) + end + if t == "string" then + -- Quote if contains special YAML chars or is empty + if val == "" or val:match("^[%s#&*!|>'\"%[%]{},?:-]") or val:match("[\n\r]") then + -- Escape inner quotes, wrap in double quotes + return '"' .. val:gsub('"', '\\"'):gsub("\n", "\\n") .. '"' + end + return val + end + if t ~= "table" then return tostring(val) end + + -- Cycle guard + if visited[val] then return '""' end + visited[val] = true + + -- Detect plain array (sequential integer keys starting at 1) + local isArray = true + local maxN = 0 + for k, _ in pairs(val) do + if type(k) ~= "number" or k ~= math.floor(k) or k < 1 then + isArray = false + break + end + if k > maxN then maxN = k end + end + if isArray and maxN ~= #val then isArray = false end + + local lines = {} + + if isArray then + -- Inline short numeric/string arrays on one line + local allScalar = true + for _, v in ipairs(val) do + if type(v) == "table" then allScalar = false; break end + end + if allScalar and #val <= 6 then + local parts = {} + for _, v in ipairs(val) do parts[#parts+1] = emit(v, 0, visited) end + visited[val] = nil + return "[" .. table.concat(parts, ", ") .. "]" + end + for _, v in ipairs(val) do + local rendered = emit(v, indent + 1, visited) + if type(v) == "table" then + lines[#lines+1] = pad .. "-\n" .. rendered + else + lines[#lines+1] = pad .. "- " .. rendered + end + end + else + for _, pair in ipairs(val) do + local k, v = pair[1], pair[2] + local rendered = emit(v, indent + 1, visited) + if type(v) == "table" and #v > 0 and type(v[1]) == "table" then + -- Nested block (list of pairs = mapping, or list of items) + lines[#lines+1] = pad .. k .. ":\n" .. rendered + elseif type(v) == "table" and type(v[1]) ~= "table" then + -- Inline array + lines[#lines+1] = pad .. k .. ": " .. rendered + else + lines[#lines+1] = pad .. k .. ": " .. rendered + end + end + end + + visited[val] = nil + return table.concat(lines, "\n") +end + +-- ───────────────────────────────────────────────────────────────────────────── +-- Main export function +-- ───────────────────────────────────────────────────────────────────────────── + +local function guiToYaml(obj, opts) + --[[ + opts = { + indent = 0, -- starting indent level + skipDefaults = true, -- omit fields that equal their default values + includeChildren = true, -- recurse into children + eventNames = {}, -- map of connection object -> string name + -- e.g. {[obj.OnPressed] = "handlePress"} + } + --]] + opts = opts or {} + local skipDef = opts.skipDefaults ~= false -- default true + local inclChild = opts.includeChildren ~= false -- default true + local indent = opts.indent or 0 + local evNames = opts.eventNames or {} + + -- Ordered list of {key, value} pairs — order controls YAML output order + local fields = {} + local function add(k, v) + if v == nil then return end + if skipDef then + -- Skip booleans that match common defaults + if k == "visible" and v == true then return end + if k == "active" and v == true then return end + if k == "visibility" and v == 1 then return end + if k == "draw-border" and v == true then return end + if k == "rotation" and v == 0 then return end + if k == "align" and v == "left" then return end + if k == "text-visibility" and v == 1 then return end + if k == "image-visibility" and v == 1 then return end + if k == "video-visibility" and v == 1 then return end + if k == "scale-x" and v == 1 then return end + if k == "scale-y" and v == 1 then return end + end + fields[#fields+1] = {k, v} + end + + -- ── Type ────────────────────────────────────────────────────────── + add("type", resolveTypeName(obj.type)) + + -- ── Dual dimensions ─────────────────────────────────────────────── + for _, pair in ipairs(dualDimToYaml(obj)) do + add(pair[1], pair[2]) + end + + -- ── Shared appearance ───────────────────────────────────────────── + local col = colorToYaml(obj.color) + if col and col ~= "[142, 141, 141]" then -- skip library default grey + add("color", col) + end + local bcol = colorToYaml(obj.borderColor) + if bcol and bcol ~= "[0, 0, 0]" then + add("border-color", bcol) + end + add("draw-border", obj.drawBorder) + add("visible", obj.visible) + add("active", obj.active) + if obj.visibility ~= 1 then add("visibility", obj.visibility) end + if obj.rotation ~= 0 then add("rotation", obj.rotation) end + + -- ── Tag / tags ──────────────────────────────────────────────────── + if obj.__tag then add("tag", obj.__tag) end + if obj.tags then + local tagList = {} + for t, _ in pairs(obj.tags) do tagList[#tagList+1] = t end + if #tagList > 0 then add("tags", tagList) end + end + + -- ── Form factor ─────────────────────────────────────────────────── + local ff = obj.formFactor or 1 + if ff ~= 1 then -- skip default "rectangle" + add("form", formNames[ff] or "rectangle") + if obj.__radius then add("radius", obj.__radius) end + if obj.segments then add("segments", obj.segments) end + if ff == 3 then + add("arc-type", obj.arcType or "open") + add("angle-start", obj.__angleS) + add("angle-end", obj.__angleE) + end + end + + -- ── Roundness ───────────────────────────────────────────────────── + if obj.roundness then + local r = obj.roundness + if r == true then + -- generic — emit the rx/ry/segments triple + add("roundness", {obj.__rx or 5, obj.__ry or 5, obj.__segments or 30}) + elseif type(r) == "string" then + add("roundness", r) -- "top" or "bottom" + end + end + + -- ── Behaviour flags ─────────────────────────────────────────────── + if obj.clipDescendants then add("clip-descendants", true) end + if obj.square then add("square", obj.square) end + + -- ── Text-type fields ────────────────────────────────────────────── + if band(obj.type, text) == text then + if obj.text and obj.text ~= "" then add("text", obj.text) end + + local al = alignNames[obj.align] + add("align", al) + + local tc = colorToYaml(obj.textColor) + if tc and tc ~= "[0, 0, 0]" then add("text-color", tc) end + if obj.textVisibility ~= 1 then add("text-visibility", obj.textVisibility) end + + if obj.textScaleX ~= 1 or obj.textScaleY ~= 1 then + add("text-scale", {obj.textScaleX, obj.textScaleY}) + end + if obj.textOffsetX ~= 0 or obj.textOffsetY ~= 0 then + add("text-offset", {obj.textOffsetX, obj.textOffsetY}) + end + if obj.textShearingFactorX ~= 0 or obj.textShearingFactorY ~= 0 then + add("text-shear", {obj.textShearingFactorX, obj.textShearingFactorY}) + end + + -- Font: emit as {file, size} when a file path is known + if obj.font then + if obj.fontFile then + add("font", {{"file", obj.fontFile}, {"size", obj.font:getHeight()}}) + else + add("font", obj.font:getHeight()) + end + end + end + + -- ── Image-type fields ───────────────────────────────────────────── + if band(obj.type, image) == image and band(obj.type, video) ~= video then + -- Source path is stored via getSource() + local src = obj:getSource and obj:getSource() + if src then add("source", src) end + + if obj.scaleX ~= 1 then add("scale-x", obj.scaleX) end + if obj.scaleY ~= 1 then add("scale-y", obj.scaleY) end + + local ic = colorToYaml(obj.imageColor) + if ic and ic ~= "[255, 255, 255]" then add("image-color", ic) end + if obj.imageVisibility and obj.imageVisibility ~= 1 then + add("image-visibility", obj.imageVisibility) + end + end + + -- ── Video-type fields ───────────────────────────────────────────── + if band(obj.type, video) == video then + local src = obj:getSource and obj:getSource() + if src then add("source", src) end + + local vc = colorToYaml(obj.videoColor) + if vc and vc ~= "[255, 255, 255]" then add("video-color", vc) end + if obj.videoVisibility and obj.videoVisibility ~= 1 then + add("video-visibility", obj.videoVisibility) + end + if obj.audiosource then + add("volume", obj.audiosource:getVolume()) + end + if obj.playing then add("autoplay", true) end + end + + -- ── Events ──────────────────────────────────────────────────────── + -- We can only serialise events when the caller supplies a name map. + -- Otherwise we silently skip them (can't decompile closures). + local connMap = { + ["on-pressed"] = obj.OnPressed, + ["on-released"] = obj.OnReleased, + ["on-released-outer"] = obj.OnReleasedOuter, + ["on-pressed-outer"] = obj.OnPressedOuter, + ["on-enter"] = obj.OnEnter, + ["on-exit"] = obj.OnExit, + ["on-moved"] = obj.OnMoved, + ["on-drag-start"] = obj.OnDragStart, + ["on-dragging"] = obj.OnDragging, + ["on-drag-end"] = obj.OnDragEnd, + ["on-wheel"] = obj.OnWheelMoved, + ["on-size-changed"] = obj.OnSizeChanged, + ["on-position-changed"] = obj.OnPositionChanged, + ["on-destroy"] = obj.OnDestroy, + ["on-load"] = obj.OnLoad, + ["on-return"] = obj.OnReturn, + } + for yamlKey, conn in pairs(connMap) do + if conn and evNames[conn] then + add(yamlKey, evNames[conn]) + end + end + + -- ── Children ────────────────────────────────────────────────────── + if inclChild and obj.children and #obj.children > 0 then + local childDefs = {} + for _, child in ipairs(obj.children) do + -- Recurse, collect as ordered-pair tables for the emitter + local childFields = guiToYaml(child, { + skipDefaults = opts.skipDefaults, + includeChildren = opts.includeChildren, + eventNames = opts.eventNames, + _returnRaw = true, -- internal: return fields table, not string + }) + childDefs[#childDefs+1] = childFields + end + fields[#fields+1] = {"children", childDefs} + end + + -- Internal mode: return the raw ordered-pair table for parent to embed + if opts._returnRaw then return fields end + + return emit(fields, indent) +end + +return guiToYaml \ No newline at end of file diff --git a/loader.lua b/loader.lua new file mode 100644 index 0000000..0233b26 --- /dev/null +++ b/loader.lua @@ -0,0 +1,34 @@ +local yaml = require("yaml") +local loader = {} +loader.__index = loader + +function loader:new(path_or_file) + local c = {} + setmetatable(c, loader) + c.source = path_or_file + local file = io.open(path_or_file .. "/index.yaml","r") + if not file then + error("Unable to load file: ".. path_or_file .."/index.yaml") + end + c.index = yaml.parse(file:read("*a")) + for i,v in pairs(c.index.categories) do + local link = io.open(path_or_file .. "/" .. v.name .. ".yaml") + if not link then + print("Error! Cannot find file: " .. path_or_file .."/" .. v.name .. ".yaml") + else + local category = yaml.parse(link:read("*a")) + c.index.categories[i].questions = category.questions + end + end + return c +end + +function loader:getCategories() + return self.index.categories +end + +function loader:getSettings() + return self.index.settings +end + +return loader \ No newline at end of file diff --git a/login_dialog.yaml b/login_dialog.yaml new file mode 100644 index 0000000..71324cf --- /dev/null +++ b/login_dialog.yaml @@ -0,0 +1,59 @@ +# login_dialog.yaml +type: frame +x: 0 +y: 0 +scale-size: [0.4, 0.25] +scale-pos: [0.3, 0.2] +color: "#1e1e2e" +border-color: "#4a4a6a" +roundness: 12 + +children: + - type: label + text: "Sign In" + x: 0 + y: 20 + scale-size: [1.0, 0.0] + h: 48 + align: center + font: 28 + text-color: "#cdd6f4" + + - type: textbox + x: 30 + y: 90 + scale-size: [0.85, 0.0] + h: 36 + text: "" + color: "#313244" + border-color: "#6c7086" + text-color: "#cdd6f4" + font: 14 + on-return: "focusPassword" + + - type: textbox + x: 30 + y: 140 + scale-size: [0.85, 0.0] + h: 36 + text: "" + color: "#313244" + border-color: "#6c7086" + text-color: "#cdd6f4" + font: 14 + on-return: "submitLogin" + + - type: button + text: "Log In" + align: "center" + fit-font: true + x: 30 + y: 200 + scale-size: [0.2, 0.0] + h: 40 + color: "#89b4fa" + text-color: "#1e1e2e" + roundness: 8 + on-pressed: "submitLogin" + on-enter: "highlightButton" + on-exit: "unhighlightButton" \ No newline at end of file diff --git a/main.lua b/main.lua new file mode 100755 index 0000000..570bf39 --- /dev/null +++ b/main.lua @@ -0,0 +1,258 @@ +local gui, color, theme, utils, board, yaml, loader, system, elements, scoreUpdater + +local activePlayer + +function GetActivePlayer() + return activePlayer.link +end + +function love.filedropped(file) + file:open("r") + local data = file:read() + print("Load file? " .. file:getFilename()) +end + + +function init() + multi, thread = require("multi"):init({priority=true}) + multi.setClock(require("socket").gettime) -- When on linux os.clock doesn't reture actual seconds the program has elapsed for + GLOBAL, THREAD = require("multi.integration.loveManager"):init() + + gui = require("gui") + color = require("gui.core.color") + theme = require("gui.core.theme") + utils = require("utils") + board = require("board") + yaml = require("yaml") + loader = require("loader") + system = require("gui.addons.system") + elements = require("gui.elements") + + scoreUpdater = gui:getProcessor():newProcessor("score-updater") + scoreUpdater.Start() +end + +function ScoreBoard(frame, x, y, w, h, sx, sy, sw, sh) + local scoreboard = {} + + -- Colors + local C_BG_PANEL = color.new("#1a1a2e") + local C_BG_HEADER = color.new("#16213e") + local C_ACCENT = color.new("#e94560") + local C_ROW_TOP = color.new("#1c2641") + local C_ROW_NORM = color.new("#121226") + local C_BORDER_TOP = color.new("#323c6e") + local C_BORDER_NRM = color.new("#1c1c37") + local C_BAR_EMPTY = color.new("#232341") + local C_TEXT_MUTED = color.new("#786e96") + local C_WHITE = color.new("#ffffff") + local C_GOLD = color.new("#ffd700") + local C_SILVER = color.new("#C0C0C0") + local C_BRONZE = color.new("#cd7f32") + + -- Config stuff + local LEADER_HEIGHT_SCALE = .06 + local HEIGHT_SCALE = .03 + local PLAYER_HEIGHT = .05 + -- + + local rankColors = { C_GOLD, C_SILVER, C_BRONZE } + + local leaderboard = frame:newFrame(x, y, w, h, sx, sy, sw, sh) + leaderboard.color = C_BORDER_NRM + + local headernum = leaderboard:newTextLabel("#",0,0,0,0,0,LEADER_HEIGHT_SCALE,1/6,HEIGHT_SCALE) + headernum.align = gui.ALIGN_CENTER + + local headerplayer = leaderboard:newTextLabel("PLAYER",0,0,0,0,1/6,LEADER_HEIGHT_SCALE,3/6,HEIGHT_SCALE) + headerplayer.align = gui.ALIGN_LEFT + + local headerscore = leaderboard:newTextLabel("SCORE",0,0,0,0,4/6,LEADER_HEIGHT_SCALE,2/6,HEIGHT_SCALE) + headerscore.align = gui.ALIGN_RIGHT + + local header = leaderboard:newTextLabel("LEADERBOARD",0,0,0,0,0,0,1,LEADER_HEIGHT_SCALE) + header.borderColor = C_ACCENT + header.textColor = C_ACCENT + header.color = C_BG_HEADER + header.align = gui.ALIGN_CENTER + + local BASE_HEIGHT = LEADER_HEIGHT_SCALE + HEIGHT_SCALE + + gui.apply({ + drawBorder = false, + color = color.new("#0f2a60"), + textColor = C_TEXT_MUTED, + }, headernum, headerplayer, headerscore) + + local updateList = {header, headernum, headerplayer, headerscore} + local playerList = {} + local function ScoreResize() + scoreUpdater:newThread(function() + thread.skip(2) + + for _,object in pairs(updateList) do + object:fitFont(nil, nil, {scale = 5/6}) + object:centerFont() + end + + for _,object in pairs(playerList) do + if type(object) == "table" and object.Ref then + for i, player in pairs(object.Ref) do + if player:hasType(gui.TYPE_TEXT) then + player:fitFont(nil, nil, {scale = 5/6}) + player:centerFont() + end + end + end + end + end) + end + + function scoreboard:AddPlayer(name, score, icon) + local player = { + Name = name, + Score = score, + Icon = icon, + Add = function(self, amt) + self.Score = tostring(tonumber(self.Score) + amt) + table.sort(playerList, function(a, b) + if a.Score == b.Score then + return a.Name < b.Name + end + return tonumber(a.Score) > tonumber(b.Score) + end) + scoreboard:RenderPlayer(playerList) + ScoreResize() + end, + } + table.insert(playerList, player) + scoreboard:RenderPlayer(playerList) + ScoreResize() + return player + end + + local colors = {C_GOLD, C_SILVER, C_BRONZE} + local function MapColor(index) + if index <=3 and index > 0 then + return colors[index] + else + return C_WHITE + end + end + + function scoreboard:RenderPlayer(list) + for index, player in ipairs(list) do + if player.Ref then + player.Ref[1].text = player.Name or "" + player.Ref[2].text = player.Score or "" + player.Ref[3].text = tostring(index) + player.Ref.Frame:setDualDim(nil,5*index,nil,nil,nil,BASE_HEIGHT + (index-1) * PLAYER_HEIGHT) + player.Ref.Frame.link = player + + gui.apply({ + visibility = 0, + drawBorder = false, + textColor = MapColor(index) + }, unpack(player.Ref)) + + if activePlayer == nil then + activePlayer = player.Ref.Frame + end + + if player.Ref.Frame == activePlayer then + player.Ref.Frame.borderColor = C_BORDER_NRM + player.Ref.Frame.color = C_ROW_NORM + else + player.Ref.Frame.borderColor = C_BORDER_TOP + player.Ref.Frame.color = C_ROW_TOP + end + else + local playernum, playerName, playerIcon, playerScore, playerLine + local playerFrame = leaderboard:newFrame(5,5*index,-10,0,0,BASE_HEIGHT + (index-1) * PLAYER_HEIGHT,1,PLAYER_HEIGHT) + + playerFrame:OnReleased(function(self) + activePlayer = self + scoreboard:RenderPlayer(playerList) + end) + + playerFrame:respectHierarchy(false) + + playerFrame.borderColor = C_BORDER_TOP + playerFrame.color = C_ROW_TOP + + playernum = playerFrame:newTextLabel(index,0,0,0,0,.015,.1,1/8,.8) + playernum.align = gui.ALIGN_CENTER + + if player.Icon ~= nil then + playerIcon = playerFrame:newImageLabel(player.Icon,0,0,0,0,.16,.1,.1,.8) + playerIcon.square = "h" -- When working with scales squaring is trickier. (h/w) to switch on width or height + + playerName = playerFrame:newTextLabel(player.Name,0,0,0,0,.3,0,2/5,.8) + playerName.align = gui.ALIGN_LEFT + + playerLine = playerFrame:newFrame(0,0,0,0,.3,.8,.69,.07) + playerLine.color = C_GOLD + else + playerName = playerFrame:newTextLabel(player.Name,0,0,0,0,.16,0,7/13,.8) + playerName.align = gui.ALIGN_LEFT + + playerLine = playerFrame:newFrame(0,0,0,0,.16,.8,7/13 + 2/7,.07) + playerLine.color = C_GOLD + end + + playerScore = playerFrame:newTextLabel(player.Score,0,0,0,0,.71,0,2/7,.8) + playerScore.align = gui.ALIGN_CENTER + playerLine.drawBorder = false + + gui.apply({ + visibility = 0, + drawBorder = false, + textColor = MapColor(index) + },playernum, playerName, playerScore, playerIcon, playerLine) + + player.Ref = {playerName, playerScore, playernum, playerIcon, playerLine, Frame = playerFrame} + end + end + end + + gui.Events.OnResized(ScoreResize) + + return scoreboard +end + + +require("gui.addons.players") +-- local webp = require("webp") +function love.load() + init() + gui:setAspectSize(1920, 1080) + gui.aspect_ratio = true + local bg = gui:newFrame() + bg:fullFrame() + bg.color = color.new("#242f9b") + + local qframe = bg:newFrame(0, 0, 0, 0, .2, .05, .75, .9) + qframe.color = color.new("#060ee9") + local scoreboard = ScoreBoard(bg, 0, 0, 0, 0, .015, .05, .170, .9) + + p1 = scoreboard:AddPlayer("Epicknex", "0") + p2 = scoreboard:AddPlayer("Bob", "0") + p3 = scoreboard:AddPlayer("John", "0") + p4 = scoreboard:AddPlayer("Billy", "0") + + board.buildBoard(qframe, "anime") + + -- gui:newVideoPlayer("test.ogv",0,0,428,240) + -- local img = webp.load("test.webp") + -- gui:newImageLabel(img,0,0,735,1041) +end + +function love.update(dt) + gui.update(dt) + multi:uManager(dt) +end + +function love.draw() + gui.draw() +end + diff --git a/multi b/multi new file mode 160000 index 0000000..4a52cd5 --- /dev/null +++ b/multi @@ -0,0 +1 @@ +Subproject commit 4a52cd5e14abfc409bc82f6427d0cf79f8d2a894 diff --git a/server_test/server.lua b/server_test/server.lua new file mode 100644 index 0000000..6ba0e67 --- /dev/null +++ b/server_test/server.lua @@ -0,0 +1,423 @@ +-- server.lua +-- Simple HTTP server in Lua using LuaSocket +-- Install dependency: luarocks install luasocket + +local socket = require("socket") + +-- ─── Configuration ──────────────────────────────────────────────────────────── +local HOST = "127.0.0.1" +local PORT = 8080 + +-- ─── In-memory data store ───────────────────────────────────────────────────── +local messages = {} -- stores messages sent from the client + +-- ─── Helper: parse the request body from raw HTTP request ───────────────────── +local function parse_body(raw) + -- Body follows the blank line after headers + local body = raw:match("\r\n\r\n(.*)") + return body or "" +end + +-- ─── Helper: URL-decode a string ────────────────────────────────────────────── +local function url_decode(str) + str = str:gsub("+", " ") + str = str:gsub("%%(%x%x)", function(h) + return string.char(tonumber(h, 16)) + end) + return str +end + +-- ─── Helper: parse application/x-www-form-urlencoded body ──────────────────── +local function parse_form(body) + local params = {} + for key, value in body:gmatch("([^&=]+)=([^&=]*)") do + params[url_decode(key)] = url_decode(value) + end + return params +end + +-- ─── Helper: encode a Lua table as a simple JSON object ─────────────────────── +local function to_json(tbl) + local parts = {} + for k, v in pairs(tbl) do + local val + if type(v) == "string" then + val = string.format('"%s"', v:gsub('"', '\\"')) + elseif type(v) == "number" then + val = tostring(v) + elseif type(v) == "boolean" then + val = tostring(v) + else + val = '"[unsupported]"' + end + table.insert(parts, string.format('"%s": %s', k, val)) + end + return "{ " .. table.concat(parts, ", ") .. " }" +end + +-- ─── Helper: encode a list of messages as a JSON array ──────────────────────── +local function messages_to_json() + local items = {} + for _, msg in ipairs(messages) do + table.insert(items, to_json(msg)) + end + return "[" .. table.concat(items, ", ") .. "]" +end + +-- ─── HTTP response builders ──────────────────────────────────────────────────── +local function http_response(status, content_type, body) + return string.format( + "HTTP/1.1 %s\r\nContent-Type: %s\r\nContent-Length: %d\r\nAccess-Control-Allow-Origin: *\r\nConnection: close\r\n\r\n%s", + status, content_type, #body, body + ) +end + +local function ok_json(body) + return http_response("200 OK", "application/json", body) +end + +local function ok_html(body) + return http_response("200 OK", "text/html", body) +end + +local function not_found() + return http_response("404 Not Found", "text/plain", "404 Not Found") +end + +-- ─── HTML page served to the browser ───────────────────────────────────────── +local HTML = [[ + + + + + Lua Server Demo + + + +
+

▶ Lua HTTP Server

+

Bidirectional client ↔ server communication demo

+
+ + +
+

Send a Message

+ + + +
+
+ + +
+

Server Message Log

+
  • Loading…
+
+ + +
+

Server Info

+
Fetching…
+
+ + + + +]] + +-- ─── Route handler ──────────────────────────────────────────────────────────── +local start_time = os.time() +local request_count = 0 + +local function handle(raw_request) + request_count = request_count + 1 + + -- Parse method and path from the request line + local method, path = raw_request:match("^(%u+) (/[^ ]*) HTTP") + method = method or "GET" + path = path or "/" + + -- GET / → serve the HTML page + if method == "GET" and path == "/" then + return ok_html(HTML) + + -- GET /messages → return all stored messages as JSON + elseif method == "GET" and path == "/messages" then + return ok_json(messages_to_json()) + + -- GET /info → return server metadata as JSON + elseif method == "GET" and path == "/info" then + local info = { + host = HOST, + port = tostring(PORT), + uptime = tostring(os.time() - start_time), + requests = tostring(request_count), + lua_version = _VERSION, + } + return ok_json(to_json(info)) + + -- POST /send → accept a message from the client + elseif method == "POST" and path == "/send" then + local body = parse_body(raw_request) + local params = parse_form(body) + + local name = params["name"] or "Anonymous" + local text = params["message"] or "" + + if text == "" then + return ok_json('{ "ok": false, "error": "empty message" }') + end + + -- Store the message + table.insert(messages, { + name = name, + text = text, + time = os.date("%H:%M:%S"), + }) + + print(string.format("[POST /send] %s: %s", name, text)) + return ok_json('{ "ok": true }') + + else + return not_found() + end +end + +-- ─── Main server loop ───────────────────────────────────────────────────────── +local server = assert(socket.bind(HOST, PORT)) +server:settimeout(0) -- non-blocking accept + +print(string.format("Lua HTTP server running at http://%s:%d/", HOST, PORT)) +print("Press Ctrl-C to stop.\n") + +while true do + local client = server:accept() + if client then + client:settimeout(5) + + -- Read the full request (stop at blank line for GET, read body for POST) + local raw = {} + local content_length = 0 + + -- Read headers + while true do + local line, err = client:receive("*l") + if not line or line == "" then break end + table.insert(raw, line) + + -- Capture Content-Length so we can read the body + local cl = line:match("^[Cc]ontent%-[Ll]ength:%s*(%d+)") + if cl then content_length = tonumber(cl) end + end + + local header_str = table.concat(raw, "\r\n") .. "\r\n\r\n" + + -- Read body if present + local body_str = "" + if content_length > 0 then + body_str = client:receive(content_length) or "" + end + + local full_request = header_str .. body_str + local response = handle(full_request) + + client:send(response) + client:close() + end + + socket.sleep(0.01) -- prevent busy-loop +end diff --git a/skills-lock.json b/skills-lock.json new file mode 100644 index 0000000..3f59996 --- /dev/null +++ b/skills-lock.json @@ -0,0 +1,10 @@ +{ + "version": 1, + "skills": { + "filesystem": { + "source": "oimiragieo/agent-studio", + "sourceType": "github", + "computedHash": "70b2b09227ca549eac68abeefa5ca2bfba0d158da5faec7be5de875282fc7d28" + } + } +} diff --git a/templates/one-image.lua b/templates/one-image.lua new file mode 100755 index 0000000..9964fb1 --- /dev/null +++ b/templates/one-image.lua @@ -0,0 +1,56 @@ +--[[ Constants + * (all lua builtins that don't allow io/executing code) + color (interface) + gui (interface) + multi (interface bound to a processor) no thread module + + callback true/false correct/wrong +]] +local label +local imageHolder + +local function index(window, q, callback) + frame = window:newFrame(0,0,0,-200,0,.2,1,.8) + frame.visibility = 0 + label = window:newTextLabel(" " ..q.title.. " ",0,0,0,0,0,0,1,.2) + label.align = ALIGN_CENTER + label.textColor = color.white + label.color = color.new("#060ce9") + if not q.image or q.image == "" then + error("Missing 'image' field for question!") + end + imageHolder = frame:newImageLabel(q.image) + imageHolder:setAspectSize(imageHolder.imageWidth,imageHolder.imageHeight) + imageHolder:centerX(true) + imageHolder:centerY(true) + imageHolder:setDualDim(0,0,imageHolder.imageWidth,imageHolder.imageHeight) + local correct = window:newTextButton("Correct",0,-200,0,100,0,1,.5) + correct.color = color.new("#52b11b") + local wrong = window:newTextButton("Wrong",0,-200,0,100,.5,1,.5) + wrong.color = color.new("#bd2626") + local skip = window:newTextButton("Skip",0,-100,0,100,.25,1,.5) + skip.color = color.new("#5d5d5d") + window.apply({ + fitFont={}, + align=window.ALIGN_CENTER, + OnReleased=function(self) + if self.text == "Skip" then + callback() + return + end + callback(self.text == "Correct") + end, + },correct,wrong,skip) +end + +local function update(dt) -- time in seconds that has passed since + _,_,w,h = imageHolder.parent:getAbsolutes() + local x,y,w,h = imageHolder:GetSizeAdjustedToAspectRatio(w,h) + imageHolder:setDualDim(w,h,x,y) + label:fitFont() +end + +return { + index = index, + update = update +} diff --git a/templates/whatis.lua b/templates/whatis.lua new file mode 100755 index 0000000..b4a3202 --- /dev/null +++ b/templates/whatis.lua @@ -0,0 +1,44 @@ +--[[ Constants + * (all lua builtins that don't allow io/executing code) + color (interface) + gui (interface) + multi (interface bound to a processor) no thread module + + callback true/false correct/wrong +]] +local label + +local function index(window, q, callback) + label = window:newTextLabel(" " ..q.title.. " ") + label.align = ALIGN_CENTER + label:fullFrame() + label.textColor = color.white + label.color = color.new("#060ce9") + local correct = window:newTextButton("Correct",0,-200,0,100,0,1,.5) + correct.color = color.new("#52b11b") + local wrong = window:newTextButton("Wrong",0,-200,0,100,.5,1,.5) + wrong.color = color.new("#bd2626") + local skip = window:newTextButton("Skip",0,-100,0,100,.25,1,.5) + skip.color = color.new("#5d5d5d") + gui.apply({ + fitFont={}, + align=gui.ALIGN_CENTER, + OnReleased=function(self) + if self.text == "Skip" then + callback() + return + end + callback(self.text == "Correct") + end, + },correct,wrong,skip) +end + +local function update(dt) -- time in seconds that has passed since + -- label:centerFont() + label:fitFont() +end + +return { + index = index, + update = update +} diff --git a/test.ogv b/test.ogv new file mode 100644 index 0000000..e5bf380 Binary files /dev/null and b/test.ogv differ diff --git a/test.webp b/test.webp new file mode 100644 index 0000000..0aed682 Binary files /dev/null and b/test.webp differ diff --git a/timesup.mp3 b/timesup.mp3 new file mode 100644 index 0000000..0992341 Binary files /dev/null and b/timesup.mp3 differ diff --git a/utils.lua b/utils.lua new file mode 100755 index 0000000..cce8378 --- /dev/null +++ b/utils.lua @@ -0,0 +1,108 @@ +local gui = require("gui") +local color = require("gui.core.color") +local transition = require("gui.elements.transitions") +local multi = require("multi"):init() + +local function startTimer(opt) + local default = { + duration = 30, + autoText = true, + autoColor = true, + autoCleanup = false, + startColor = color.green, + textColor = color.black, + warnColor = color.yellow, + timeColor = color.red, + finegrained = false, + visibility = 1 + } + + if type(opt) == "number" then + local d = opt + opt = default + opt.duration = d + elseif type(opt) ~= "table" then + opt = default + elseif type(opt) == "table" then + for i,v in pairs(opt) do + default[i] = v + end + opt = default + end + + local timeRemaining = opt.duration or 30 + local timer = transition.glide(3.5,1.5,5) + transition.glide:SetFPS(120) + local handle = timer(3.5,1.5,timeRemaining) + + local tpie = gui:newFrame():makeArc("pie",-200, 0, 100,1 ,0 ,0 ,1.5*math.pi, 3.5*math.pi, 360) + local tlabel = tpie:newTextLabel("") + tlabel.textColor = opt.textColor + tlabel.align = gui.ALIGN_CENTER + tlabel:fullFrame() + tlabel.visibility = 0 + tpie.color = opt.startColor + tpie.visibility = opt.visibility + + local tm, num + local func = function(p,t) + if num ~= timeRemaining-math.floor(t) then + num = timeRemaining-math.floor(t) + end + + if opt.autoColor then + tpie.color = opt.startColor + if num <= timeRemaining/3 then + tpie.color = opt.timeColor + elseif num <= timeRemaining/2 then + tpie.color = opt.warnColor + end + end + + tpie:makeArc("pie", -200, 0, 100, 1, 0, 0, 1.5 * math.pi, p * math.pi, 360) + + if opt.autoText then + tlabel.text = num + tlabel:fitFont(nil, nil, {scale=1/2}) + tlabel:centerFont() + end + + if num == 0 then + thread:newThread("Pie Timer",function() + thread.yield() + if opt.autoText then + tlabel.text = "Time" + tlabel:fitFont(nil, nil, {scale=1/2}) + tlabel:centerFont() + end + tpie:makeArc("pie", -200, 0, 100, 1, 0, 0, 1.5 * math.pi, 3.5 * math.pi, 360) + tm.OnStop:Fire(tm) + if opt.autoCleanup then + tm:Cleanup() + end + end) + end + + return tm, num, t + end + + tm = { + Duration = timeRemaining, + Cleanup = function() handle:Kill() tpie:destroy() tm.Cleanup = function() end end, + SetText = function(str) tlabel.text = str end, + SetColor = function(c) tpie.color = c end, + OnStop = multi:newConnection() + } + + if opt.finegrained then + tm.OnTime = func % handle.OnStep + else + tm.OnTime = function(self, sec, t) return math.floor(t) == t, self, sec end / (func % handle.OnStep) + end + + return tm +end + +return { + startTimer = startTimer +} \ No newline at end of file diff --git a/webp-old.lua b/webp-old.lua new file mode 100644 index 0000000..7f01cdf --- /dev/null +++ b/webp-old.lua @@ -0,0 +1,602 @@ +-- webp.lua — Pure Lua VP8L (lossless WebP) decoder for Love2D (LuaJIT) +-- Supports: VP8L (lossless) only. VP8 (lossy) is not feasible in pure Lua. +-- Usage: local WebP = require("webp"); local img = WebP.load("sprite.webp") + +local WebP = {} + +local bit = require("bit") +local band = bit.band +local bor = bit.bor +local lshift = bit.lshift +local rshift = bit.rshift +local tobit = bit.tobit + +-- ─── Bitstream reader ──────────────────────────────────────────────────────── +-- VP8L is LSB-first. We maintain a 32-bit window since LuaJIT bit ops are 32-bit. + +local function newBitReader(data) + local r = { + data = data, + pos = 1, -- next byte to load (1-based) + window = 0, -- current bit window (32-bit) + bits = 0, -- valid bits in window + } + + function r:fill() + while self.bits <= 24 and self.pos <= #self.data do + local byte = self.data:byte(self.pos) + self.window = bor(self.window, lshift(byte, self.bits)) + self.bits = self.bits + 8 + self.pos = self.pos + 1 + end + end + + function r:read(n) + if self.bits < n then self:fill() end + local v = band(self.window, lshift(1, n) - 1) + self.window = rshift(self.window, n) + self.bits = self.bits - n + return v + end + + function r:readBool() + return self:read(1) == 1 + end + + r:fill() + return r +end + +-- ─── Huffman trees ─────────────────────────────────────────────────────────── + +local function buildHuffmanTable(codeLengths) + local n = #codeLengths + local counts = {} + for i = 0, 15 do counts[i] = 0 end + for _, cl in ipairs(codeLengths) do + if cl > 0 then counts[cl] = counts[cl] + 1 end + end + + local nextCode = {} + local code = 0 + counts[0] = 0 + for bits = 1, 15 do + code = lshift(code + counts[bits - 1], 1) + nextCode[bits] = code + end + + local htable = {} + for i = 1, n do + local len = codeLengths[i] + if len > 0 then + htable[nextCode[len]] = { sym = i - 1, len = len } + nextCode[len] = nextCode[len] + 1 + end + end + return htable +end + +local function decodeHuffman(br, htable) + local code = 0 + for len = 1, 15 do + code = bor(lshift(code, 1), br:read(1)) + local entry = htable[code] + if entry and entry.len == len then + return entry.sym + end + end + error("Invalid Huffman code") +end + +-- ─── Code length decoding ──────────────────────────────────────────────────── + +local CODE_LENGTH_ORDER = {17,18,0,1,2,3,4,5,16,6,7,8,9,10,11,12,13,14,15} + +local function readCodeLengths(br, n) + local numCLCodes = br:read(4) + 4 + local clLengths = {} + for i = 1, 19 do clLengths[i] = 0 end + for i = 1, numCLCodes do + clLengths[CODE_LENGTH_ORDER[i] + 1] = br:read(3) + end + local clTable = buildHuffmanTable(clLengths) + + local lengths = {} + local prev = 8 + while #lengths < n do + local sym = decodeHuffman(br, clTable) + if sym <= 15 then + lengths[#lengths + 1] = sym + if sym ~= 0 then prev = sym end + elseif sym == 16 then + local rep = br:read(2) + 3 + for _ = 1, rep do lengths[#lengths + 1] = prev end + elseif sym == 17 then + local rep = br:read(3) + 3 + for _ = 1, rep do lengths[#lengths + 1] = 0 end + elseif sym == 18 then + local rep = br:read(7) + 11 + for _ = 1, rep do lengths[#lengths + 1] = 0 end + end + end + return lengths +end + +-- ─── Prefix code reading ───────────────────────────────────────────────────── + +local function readPrefixCode(br, alphabetSize) + local simpleCode = br:read(1) + if simpleCode == 1 then + local numSyms = br:read(1) + 1 + local firstSym = br:read(1) + local sym1 = br:read(firstSym == 1 and 8 or 1) + local lengths = {} + for i = 1, alphabetSize do lengths[i] = 0 end + lengths[sym1 + 1] = 1 + if numSyms == 2 then + local sym2 = br:read(8) + lengths[sym2 + 1] = 1 + end + return buildHuffmanTable(lengths) + else + local lengths = readCodeLengths(br, alphabetSize) + return buildHuffmanTable(lengths) + end +end + +-- ─── Color cache ───────────────────────────────────────────────────────────── + +local function newColorCache(bits) + local size = lshift(1, bits) + local cache = {} + for i = 0, size - 1 do cache[i] = 0 end + return { + size = size, + data = cache, + insert = function(self, color) + -- hash: (0x1e35a7bd * color) >> (32 - bits), masked to cache size + local hash = tobit(0x1e35a7bd * color) + local idx = band(rshift(hash, 32 - bits), size - 1) + self.data[idx] = color + end, + lookup = function(self, idx) + return self.data[idx] + end, + } +end + +-- ─── Transform type constants ──────────────────────────────────────────────── + +local TRANSFORM_PREDICTOR = 0 +local TRANSFORM_COLOR = 1 +local TRANSFORM_SUBTRACT_GREEN = 2 +local TRANSFORM_COLOR_INDEXING = 3 + +-- ─── Prefix length/distance tables ────────────────────────────────────────── + +local PREFIX_EXTRA_BITS = { + 0,0,0,0, 1,1,2,2, 3,3,4,4, 5,5,6,6, 7,7,8,8, 9,9,10,10, 11,11,12,12, 13,13 +} +local PREFIX_OFFSET = { + 0,1,2,3, 4,6,8,12, 16,24,32,48, 64,96,128,192, 256,384,512,768, + 1024,1536,2048,3072, 4096,6144,8192,12288, 16384,24576 +} + +local function prefixToValue(br, code) + if code < 4 then return code end + local extra = PREFIX_EXTRA_BITS[code + 1] or 0 + local offset = PREFIX_OFFSET[code + 1] or 0 + return offset + br:read(extra) +end + +-- ─── VP8L distance offset table (120 entries) ──────────────────────────────── + +local DIST_MAP = { + {0,1},{1,0},{1,1},{-1,1},{0,2},{2,0},{1,2},{-1,2}, + {2,1},{-2,1},{2,2},{-2,2},{0,3},{3,0},{1,3},{-1,3}, + {3,1},{-3,1},{2,3},{-2,3},{3,2},{-3,2},{0,4},{4,0}, + {1,4},{-1,4},{4,1},{-4,1},{3,3},{-3,3},{2,4},{-2,4}, + {4,2},{-4,2},{0,5},{3,4},{-3,4},{4,3},{-4,3},{5,0}, + {1,5},{-1,5},{5,1},{-5,1},{2,5},{-2,5},{5,2},{-5,2}, + {4,4},{-4,4},{3,5},{-3,5},{5,3},{-5,3},{0,6},{6,0}, + {1,6},{-1,6},{6,1},{-6,1},{2,6},{-2,6},{6,2},{-6,2}, + {4,5},{-4,5},{5,4},{-5,4},{3,6},{-3,6},{6,3},{-6,3}, + {0,7},{7,0},{1,7},{-1,7},{5,5},{-5,5},{7,1},{-7,1}, + {4,6},{-4,6},{6,4},{-6,4},{2,7},{-2,7},{7,2},{-7,2}, + {3,7},{-3,7},{7,3},{-7,3},{5,6},{-5,6},{6,5},{-6,5}, + {8,0},{4,7},{-4,7},{7,4},{-7,4},{8,1},{8,2},{6,6}, + {-6,6},{8,3},{5,7},{-5,7},{7,5},{-7,5},{8,4},{6,7}, + {-6,7},{7,6},{-7,6},{8,5},{8,6},{7,7},{-7,7},{8,7}, +} + +-- ─── Main VP8L decode (recursive for transform sub-images) ─────────────────── + +local function decodeVP8L(br, width, height) + + -- color cache + local hasCCache = br:readBool() + local ccache = nil + local ccacheBits = 0 + if hasCCache then + ccacheBits = br:read(4) + ccache = newColorCache(ccacheBits) + end + + -- collect transforms (applied in reverse after decode) + local transforms = {} + while br:readBool() do + local ttype = br:read(2) + local t = { ttype = ttype } + if ttype == TRANSFORM_PREDICTOR or ttype == TRANSFORM_COLOR then + t.sizeBits = br:read(3) + 2 + local tw = math.floor((width + lshift(1, t.sizeBits) - 1) / lshift(1, t.sizeBits)) + local th = math.floor((height + lshift(1, t.sizeBits) - 1) / lshift(1, t.sizeBits)) + t.data = decodeVP8L(br, tw, th) + elseif ttype == TRANSFORM_COLOR_INDEXING then + t.numColors = br:read(8) + 1 + t.colors = decodeVP8L(br, t.numColors, 1) + end + -- SUBTRACT_GREEN has no extra data + transforms[#transforms + 1] = t + end + + -- entropy/meta-Huffman image + local groupBits = 0 + local groupImage = nil + local numGroups = 1 + if br:readBool() then + groupBits = br:read(3) + 2 + local gw = math.floor((width + lshift(1, groupBits) - 1) / lshift(1, groupBits)) + local gh = math.floor((height + lshift(1, groupBits) - 1) / lshift(1, groupBits)) + groupImage = decodeVP8L(br, gw, gh) + local maxG = 0 + for _, v in ipairs(groupImage) do + local g = band(rshift(v, 8), 0xffff) + if g > maxG then maxG = g end + end + numGroups = maxG + 1 + end + + -- alphabet sizes + local ccSize = hasCCache and lshift(1, ccacheBits) or 0 + local alphabetG = 256 + 24 + ccSize + + -- read Huffman tables for each group (G, R, B, A + distance) + local huffGroups = {} + for g = 1, numGroups do + huffGroups[g] = { + G = readPrefixCode(br, alphabetG), + R = readPrefixCode(br, 256), + B = readPrefixCode(br, 256), + A = readPrefixCode(br, 256), + dist = readPrefixCode(br, 40), + } + end + + -- decode pixels + local pixels = {} + local numPixels = width * height + local px, py = 0, 0 + + local function getGroup() + if not groupImage then return huffGroups[1] end + local gx = rshift(px, groupBits) + local gy = rshift(py, groupBits) + local gw = math.floor((width + lshift(1, groupBits) - 1) / lshift(1, groupBits)) + local idx = gy * gw + gx + 1 + local g = band(rshift(groupImage[idx] or 0, 8), 0xffff) + return huffGroups[g + 1] or huffGroups[1] + end + + while #pixels < numPixels do + local hg = getGroup() + local code = decodeHuffman(br, hg.G) + + if code < 256 then + -- literal ARGB (green first, then R, B, A) + local g = code + local r = decodeHuffman(br, hg.R) + local b = decodeHuffman(br, hg.B) + local a = decodeHuffman(br, hg.A) + local color = bor(bor(bor(lshift(a, 24), lshift(r, 16)), lshift(g, 8)), b) + pixels[#pixels + 1] = color + if ccache then ccache:insert(color) end + + elseif code < 256 + 24 then + -- LZ77 back-reference + local lenCode = code - 256 + local length = prefixToValue(br, lenCode) + 1 + local distCode = decodeHuffman(br, hg.dist) + local dist = prefixToValue(br, distCode) + 1 + + -- remap small distances through VP8L spatial table + if dist <= 120 then + local d = DIST_MAP[dist] + local srcX = px - d[1] + local srcY = py - d[2] + dist = py * width + px - (srcY * width + srcX) + end + + local src = #pixels - dist + 1 + for i = 0, length - 1 do + local c = pixels[src + i] or 0 + pixels[#pixels + 1] = c + if ccache then ccache:insert(c) end + end + + else + -- color cache reference + local cacheIdx = code - 256 - 24 + pixels[#pixels + 1] = ccache:lookup(cacheIdx) + end + + px = px + 1 + if px >= width then px = 0; py = py + 1 end + end + + -- ─── Apply transforms in reverse order ─────────────────────────────────── + + for i = #transforms, 1, -1 do + local t = transforms[i] + + -- SUBTRACT_GREEN: R += G, B += G (mod 256) + if t.ttype == TRANSFORM_SUBTRACT_GREEN then + for idx = 1, #pixels do + local c = pixels[idx] + local a = band(rshift(c, 24), 0xff) + local r = band(rshift(c, 16), 0xff) + local g = band(rshift(c, 8), 0xff) + local b = band(c, 0xff) + r = band(r + g, 0xff) + b = band(b + g, 0xff) + pixels[idx] = bor(bor(bor(lshift(a, 24), lshift(r, 16)), lshift(g, 8)), b) + end + + -- COLOR_INDEXING: replace each pixel's green channel with palette entry + elseif t.ttype == TRANSFORM_COLOR_INDEXING then + local bpp = 8 + if t.numColors <= 2 then bpp = 1 + elseif t.numColors <= 4 then bpp = 2 + elseif t.numColors <= 16 then bpp = 4 end + + local newPixels = {} + if bpp == 8 then + for _, c in ipairs(pixels) do + local idx = band(rshift(c, 8), 0xff) + newPixels[#newPixels + 1] = t.colors[idx + 1] or 0 + end + else + local pxPerByte = 8 / bpp + local mask = lshift(1, bpp) - 1 + for _, c in ipairs(pixels) do + local packed = band(rshift(c, 8), 0xff) + for p = 0, pxPerByte - 1 do + if #newPixels < width * height then + local idx = band(rshift(packed, p * bpp), mask) + newPixels[#newPixels + 1] = t.colors[idx + 1] or 0 + end + end + end + end + pixels = newPixels + + -- PREDICTOR: undo per-block spatial prediction + elseif t.ttype == TRANSFORM_PREDICTOR then + local sb = t.sizeBits + local tw = math.floor((width + lshift(1, sb) - 1) / lshift(1, sb)) + + local function getpx(x, y) + if x < 0 then x = 0 end + if y < 0 then return tobit(0xff000000) end + return pixels[y * width + x + 1] or 0 + end + + local function addARGB(a, b) + local aa = band(rshift(a, 24), 0xff) + local ar = band(rshift(a, 16), 0xff) + local ag = band(rshift(a, 8), 0xff) + local ab = band(a, 0xff) + local ba = band(rshift(b, 24), 0xff) + local br_ = band(rshift(b, 16), 0xff) + local bg = band(rshift(b, 8), 0xff) + local bb = band(b, 0xff) + return bor(bor(bor( + lshift(band(aa + ba, 0xff), 24), + lshift(band(ar + br_, 0xff), 16)), + lshift(band(ag + bg, 0xff), 8)), + band(ab + bb, 0xff)) + end + + local function avgARGB(a, b) + local aa = band(rshift(a, 24), 0xff) + local ar = band(rshift(a, 16), 0xff) + local ag = band(rshift(a, 8), 0xff) + local ab = band(a, 0xff) + local ba = band(rshift(b, 24), 0xff) + local br_ = band(rshift(b, 16), 0xff) + local bg = band(rshift(b, 8), 0xff) + local bb = band(b, 0xff) + return bor(bor(bor( + lshift(rshift(aa + ba, 1), 24), + lshift(rshift(ar + br_, 1), 16)), + lshift(rshift(ag + bg, 1), 8)), + rshift(ab + bb, 1)) + end + + local function clampByte(v) + return math.max(0, math.min(255, v)) + end + + local function clampAddARGB(a, b) + local aa = band(rshift(a, 24), 0xff) + local ar = band(rshift(a, 16), 0xff) + local ag = band(rshift(a, 8), 0xff) + local ab = band(a, 0xff) + local ba = band(rshift(b, 24), 0xff) + local br_ = band(rshift(b, 16), 0xff) + local bg = band(rshift(b, 8), 0xff) + local bb = band(b, 0xff) + return bor(bor(bor( + lshift(clampByte(aa + ba), 24), + lshift(clampByte(ar + br_), 16)), + lshift(clampByte(ag + bg), 8)), + clampByte(ab + bb)) + end + + local function subARGB(a, b) + local aa = band(rshift(a, 24), 0xff) + local ar = band(rshift(a, 16), 0xff) + local ag = band(rshift(a, 8), 0xff) + local ab = band(a, 0xff) + local ba = band(rshift(b, 24), 0xff) + local br_ = band(rshift(b, 16), 0xff) + local bg = band(rshift(b, 8), 0xff) + local bb = band(b, 0xff) + return bor(bor(bor( + lshift(band(aa - ba, 0xff), 24), + lshift(band(ar - br_, 0xff), 16)), + lshift(band(ag - bg, 0xff), 8)), + band(ab - bb, 0xff)) + end + + local function halfSubARGB(a, b) + local aa = band(rshift(a, 24), 0xff) + local ar = band(rshift(a, 16), 0xff) + local ag = band(rshift(a, 8), 0xff) + local ab = band(a, 0xff) + local ba = band(rshift(b, 24), 0xff) + local br_ = band(rshift(b, 16), 0xff) + local bg = band(rshift(b, 8), 0xff) + local bb = band(b, 0xff) + return bor(bor(bor( + lshift(rshift(aa - ba, 1), 24), + lshift(rshift(ar - br_, 1), 16)), + lshift(rshift(ag - bg, 1), 8)), + rshift(ab - bb, 1)) + end + + local function selectARGB(l, tp, tl) + local function absdiff(x, y, s) + return math.abs(band(rshift(x, s), 0xff) - band(rshift(y, s), 0xff)) + end + local function dist(x, y) + return absdiff(x,y,24) + absdiff(x,y,16) + absdiff(x,y,8) + absdiff(x,y,0) + end + return dist(l, tl) <= dist(tp, tl) and l or tp + end + + for iy = 0, height - 1 do + for ix = 0, width - 1 do + if not (ix == 0 and iy == 0) then + local pidx = iy * width + ix + 1 + local L = getpx(ix - 1, iy) + local T = getpx(ix, iy - 1) + local TL = getpx(ix - 1, iy - 1) + local TR = getpx(ix + 1, iy - 1) + local tx = rshift(ix, sb) + local ty = rshift(iy, sb) + local mode = band(t.data[ty * tw + tx + 1] or 0, 0xff) + + local pred + if mode == 0 then pred = tobit(0xff000000) + elseif mode == 1 then pred = L + elseif mode == 2 then pred = T + elseif mode == 3 then pred = TR + elseif mode == 4 then pred = TL + elseif mode == 5 then pred = avgARGB(avgARGB(L, TR), T) + elseif mode == 6 then pred = avgARGB(L, TL) + elseif mode == 7 then pred = avgARGB(L, T) + elseif mode == 8 then pred = avgARGB(TL, T) + elseif mode == 9 then pred = avgARGB(T, TR) + elseif mode == 10 then pred = avgARGB(avgARGB(L, TL), avgARGB(T, TR)) + elseif mode == 11 then pred = selectARGB(L, T, TL) + elseif mode == 12 then pred = clampAddARGB(L, subARGB(T, TL)) + elseif mode == 13 then + local avg = avgARGB(L, T) + pred = clampAddARGB(avg, halfSubARGB(avg, TL)) + else pred = L end + + pixels[pidx] = addARGB(pixels[pidx], pred) + end + end + end + + -- COLOR: undo green/red channel correlations + elseif t.ttype == TRANSFORM_COLOR then + local sb = t.sizeBits + local tw = math.floor((width + lshift(1, sb) - 1) / lshift(1, sb)) + for iy = 0, height - 1 do + for ix = 0, width - 1 do + local pidx = iy * width + ix + 1 + local c = pixels[pidx] or 0 + local tx = rshift(ix, sb) + local ty = rshift(iy, sb) + local m = t.data[ty * tw + tx + 1] or 0 + + -- unpack signed bytes from transform pixel (stored as ARGB) + local g2r = band(rshift(m, 16), 0xff) + local r2b = band(rshift(m, 8), 0xff) + local g2b = band(m, 0xff) + if g2r >= 128 then g2r = g2r - 256 end + if r2b >= 128 then r2b = r2b - 256 end + if g2b >= 128 then g2b = g2b - 256 end + + local a = band(rshift(c, 24), 0xff) + local r = band(rshift(c, 16), 0xff) + local g = band(rshift(c, 8), 0xff) + local b = band(c, 0xff) + + r = band(r + math.floor(g2r * g / 256), 0xff) + b = band(b + math.floor(g2b * g / 256) + math.floor(r2b * r / 256), 0xff) + + pixels[pidx] = bor(bor(bor(lshift(a, 24), lshift(r, 16)), lshift(g, 8)), b) + end + end + end + end + + return pixels +end + +-- ─── Public API ────────────────────────────────────────────────────────────── + +function WebP.decode(data) + assert(data:sub(1, 4) == "RIFF", "Not a RIFF file") + assert(data:sub(9, 12) == "WEBP", "Not a WEBP file") + + local fourCC = data:sub(13, 16) + assert(fourCC == "VP8L", "Only lossless VP8L WebP is supported (got: " .. fourCC .. ")") + + -- byte 21 = VP8L signature (0x2f), bitstream starts at byte 22 + assert(data:byte(21) == 0x2f, "Invalid VP8L signature byte") + local br = newBitReader(data:sub(22)) + + local width = br:read(14) + 1 + local height = br:read(14) + 1 + br:readBool() -- alpha hint flag, unused + local version = br:read(3) + assert(version == 0, "Unsupported VP8L version: " .. version) + + local pixels = decodeVP8L(br, width, height) + + -- VP8L stores ARGB; Love2D mapPixel expects normalised RGBA floats + local imageData = love.image.newImageData(width, height, "rgba8") + imageData:mapPixel(function(x, y) + local c = pixels[y * width + x + 1] or 0 + local a = band(rshift(c, 24), 0xff) + local r = band(rshift(c, 16), 0xff) + local g = band(rshift(c, 8), 0xff) + local b = band(c, 0xff) + return r / 255, g / 255, b / 255, a / 255 + end) + + return love.graphics.newImage(imageData) +end + +function WebP.load(path) + local data = love.filesystem.read(path) + assert(data, "Could not read file: " .. path) + return WebP.decode(data) +end + +return WebP diff --git a/webp.lua b/webp.lua new file mode 100644 index 0000000..757619c --- /dev/null +++ b/webp.lua @@ -0,0 +1,1626 @@ +-- webp.lua — Pure Lua WebP decoder for Love2D (LuaJIT) +-- Supports: VP8 (lossy), VP8L (lossless), VP8X (extended container) +-- Usage: local WebP = require("webp"); local img = WebP.load("sprite.webp") + +local WebP = {} + +local bit = require("bit") +local band = bit.band +local bor = bit.bor +local lshift = bit.lshift +local rshift = bit.rshift +local tobit = bit.tobit + +-- ─── Bitstream reader ──────────────────────────────────────────────────────── +-- VP8L is LSB-first. We maintain a 32-bit window since LuaJIT bit ops are 32-bit. + +-- ─── Bitstream reader (unchanged, still LSB‑first) ─────────────────────────── + +local function newBitReader(data) + local r = { + data = data, + pos = 1, + window = 0, + bits = 0, + } + + function r:fill() + while self.bits <= 24 and self.pos <= #self.data do + local byte = self.data:byte(self.pos) + self.window = bor(self.window, lshift(byte, self.bits)) + self.bits = self.bits + 8 + self.pos = self.pos + 1 + end + end + + function r:read(n) + if self.bits < n then self:fill() end + local v = band(self.window, lshift(1, n) - 1) + self.window = rshift(self.window, n) + self.bits = self.bits - n + return v + end + + function r:readBool() + return self:read(1) == 1 + end + + r:fill() + return r +end + +-- ─── Helper: reverse bits for LSB‑first Huffman codes ─────────────────────── + +local function reverseBits(code, len) + local r = 0 + for _ = 1, len do + r = lshift(r, 1) + band(code, 1) + code = rshift(code, 1) + end + return r +end + +-- ─── Huffman trees (fixed for LSB‑first) ──────────────────────────────────── + +local function buildHuffmanTable(codeLengths) + local n = #codeLengths + local counts = {} + for i = 0, 15 do counts[i] = 0 end + for _, cl in ipairs(codeLengths) do + if cl > 0 then counts[cl] = counts[cl] + 1 end + end + + local nextCode = {} + local code = 0 + counts[0] = 0 + for bits = 1, 15 do + code = lshift(code + counts[bits - 1], 1) + nextCode[bits] = code + end + + local htable = {} + for i = 1, n do + local len = codeLengths[i] + if len > 0 then + local c = nextCode[len] + -- IMPORTANT: reverse bits because we read LSB‑first + local rev = reverseBits(c, len) + htable[rev] = { sym = i - 1, len = len } + nextCode[len] = c + 1 + end + end + return htable +end + +local function decodeHuffman(br, htable) + local code = 0 + for len = 1, 15 do + -- we read one bit at a time, LSB‑first + local bit = br:read(1) + code = bor(code, lshift(bit, len - 1)) + local entry = htable[code] + if entry and entry.len == len then + return entry.sym + end + end + error("Invalid Huffman code") +end + +-- ─── Code length decoding ──────────────────────────────────────────────────── + +local CODE_LENGTH_ORDER = {17,18,0,1,2,3,4,5,16,6,7,8,9,10,11,12,13,14,15} + +local function readCodeLengths(br, n) + local numCLCodes = br:read(4) + 4 + local clLengths = {} + for i = 1, 19 do clLengths[i] = 0 end + for i = 1, numCLCodes do + clLengths[CODE_LENGTH_ORDER[i] + 1] = br:read(3) + end + local clTable = buildHuffmanTable(clLengths) + + local lengths = {} + local prev = 8 + while #lengths < n do + local sym = decodeHuffman(br, clTable) + if sym <= 15 then + lengths[#lengths + 1] = sym + if sym ~= 0 then prev = sym end + elseif sym == 16 then + local rep = br:read(2) + 3 + for _ = 1, rep do lengths[#lengths + 1] = prev end + elseif sym == 17 then + local rep = br:read(3) + 3 + for _ = 1, rep do lengths[#lengths + 1] = 0 end + elseif sym == 18 then + local rep = br:read(7) + 11 + for _ = 1, rep do lengths[#lengths + 1] = 0 end + end + end + return lengths +end + +-- ─── Prefix code reading ───────────────────────────────────────────────────── + +local function readPrefixCode(br, alphabetSize) + local simpleCode = br:read(1) + if simpleCode == 1 then + local numSyms = br:read(1) + 1 + local firstSym = br:read(1) + local sym1 = br:read(firstSym == 1 and 8 or 1) + local lengths = {} + for i = 1, alphabetSize do lengths[i] = 0 end + lengths[sym1 + 1] = 1 + if numSyms == 2 then + local sym2 = br:read(8) + lengths[sym2 + 1] = 1 + end + return buildHuffmanTable(lengths) + else + local lengths = readCodeLengths(br, alphabetSize) + return buildHuffmanTable(lengths) + end +end + +-- ─── Color cache ───────────────────────────────────────────────────────────── + +local function newColorCache(bits) + local size = lshift(1, bits) + local cache = {} + for i = 0, size - 1 do cache[i] = 0 end + return { + size = size, + data = cache, + insert = function(self, color) + -- hash: (0x1e35a7bd * color) >> (32 - bits), masked to cache size + local hash = tobit(0x1e35a7bd * color) + local idx = band(rshift(hash, 32 - bits), size - 1) + self.data[idx] = color + end, + lookup = function(self, idx) + return self.data[idx] + end, + } +end + +-- ─── Transform type constants ──────────────────────────────────────────────── + +local TRANSFORM_PREDICTOR = 0 +local TRANSFORM_COLOR = 1 +local TRANSFORM_SUBTRACT_GREEN = 2 +local TRANSFORM_COLOR_INDEXING = 3 + +-- ─── Prefix length/distance tables ────────────────────────────────────────── + +local PREFIX_EXTRA_BITS = { + 0,0,0,0, 1,1,2,2, 3,3,4,4, 5,5,6,6, 7,7,8,8, 9,9,10,10, 11,11,12,12, 13,13 +} +local PREFIX_OFFSET = { + 0,1,2,3, 4,6,8,12, 16,24,32,48, 64,96,128,192, 256,384,512,768, + 1024,1536,2048,3072, 4096,6144,8192,12288, 16384,24576 +} + +local function prefixToValue(br, code) + if code < 4 then return code end + local extra = PREFIX_EXTRA_BITS[code + 1] or 0 + local offset = PREFIX_OFFSET[code + 1] or 0 + return offset + br:read(extra) +end + +-- ─── VP8L distance offset table (120 entries) ──────────────────────────────── + +local DIST_MAP = { + {0,1},{1,0},{1,1},{-1,1},{0,2},{2,0},{1,2},{-1,2}, + {2,1},{-2,1},{2,2},{-2,2},{0,3},{3,0},{1,3},{-1,3}, + {3,1},{-3,1},{2,3},{-2,3},{3,2},{-3,2},{0,4},{4,0}, + {1,4},{-1,4},{4,1},{-4,1},{3,3},{-3,3},{2,4},{-2,4}, + {4,2},{-4,2},{0,5},{3,4},{-3,4},{4,3},{-4,3},{5,0}, + {1,5},{-1,5},{5,1},{-5,1},{2,5},{-2,5},{5,2},{-5,2}, + {4,4},{-4,4},{3,5},{-3,5},{5,3},{-5,3},{0,6},{6,0}, + {1,6},{-1,6},{6,1},{-6,1},{2,6},{-2,6},{6,2},{-6,2}, + {4,5},{-4,5},{5,4},{-5,4},{3,6},{-3,6},{6,3},{-6,3}, + {0,7},{7,0},{1,7},{-1,7},{5,5},{-5,5},{7,1},{-7,1}, + {4,6},{-4,6},{6,4},{-6,4},{2,7},{-2,7},{7,2},{-7,2}, + {3,7},{-3,7},{7,3},{-7,3},{5,6},{-5,6},{6,5},{-6,5}, + {8,0},{4,7},{-4,7},{7,4},{-7,4},{8,1},{8,2},{6,6}, + {-6,6},{8,3},{5,7},{-5,7},{7,5},{-7,5},{8,4},{6,7}, + {-6,7},{7,6},{-7,6},{8,5},{8,6},{7,7},{-7,7},{8,7}, +} + +-- ─── Main VP8L decode (recursive for transform sub-images) ─────────────────── + +local function decodeVP8L(br, width, height) + + -- color cache + local hasCCache = br:readBool() + local ccache = nil + local ccacheBits = 0 + if hasCCache then + ccacheBits = br:read(4) + ccache = newColorCache(ccacheBits) + end + + -- collect transforms (applied in reverse after decode) + local transforms = {} + while br:readBool() do + local ttype = br:read(2) + local t = { ttype = ttype } + if ttype == TRANSFORM_PREDICTOR or ttype == TRANSFORM_COLOR then + t.sizeBits = br:read(3) + 2 + local tw = math.floor((width + lshift(1, t.sizeBits) - 1) / lshift(1, t.sizeBits)) + local th = math.floor((height + lshift(1, t.sizeBits) - 1) / lshift(1, t.sizeBits)) + t.data = decodeVP8L(br, tw, th) + elseif ttype == TRANSFORM_COLOR_INDEXING then + t.numColors = br:read(8) + 1 + t.colors = decodeVP8L(br, t.numColors, 1) + end + -- SUBTRACT_GREEN has no extra data + transforms[#transforms + 1] = t + end + + -- entropy/meta-Huffman image + local groupBits = 0 + local groupImage = nil + local numGroups = 1 + if br:readBool() then + groupBits = br:read(3) + 2 + local gw = math.floor((width + lshift(1, groupBits) - 1) / lshift(1, groupBits)) + local gh = math.floor((height + lshift(1, groupBits) - 1) / lshift(1, groupBits)) + groupImage = decodeVP8L(br, gw, gh) + local maxG = 0 + for _, v in ipairs(groupImage) do + local g = band(rshift(v, 8), 0xffff) + if g > maxG then maxG = g end + end + numGroups = maxG + 1 + end + + -- alphabet sizes + local ccSize = hasCCache and lshift(1, ccacheBits) or 0 + local alphabetG = 256 + 24 + ccSize + + -- read Huffman tables for each group (G, R, B, A + distance) + local huffGroups = {} + for g = 1, numGroups do + huffGroups[g] = { + G = readPrefixCode(br, alphabetG), + R = readPrefixCode(br, 256), + B = readPrefixCode(br, 256), + A = readPrefixCode(br, 256), + dist = readPrefixCode(br, 40), + } + end + + -- decode pixels + local pixels = {} + local numPixels = width * height + local px, py = 0, 0 + + local function getGroup() + if not groupImage then return huffGroups[1] end + local gx = rshift(px, groupBits) + local gy = rshift(py, groupBits) + local gw = math.floor((width + lshift(1, groupBits) - 1) / lshift(1, groupBits)) + local idx = gy * gw + gx + 1 + local g = band(rshift(groupImage[idx] or 0, 8), 0xffff) + return huffGroups[g + 1] or huffGroups[1] + end + + while #pixels < numPixels do + local hg = getGroup() + local code = decodeHuffman(br, hg.G) + + if code < 256 then + -- literal ARGB (green first, then R, B, A) + local g = code + local r = decodeHuffman(br, hg.R) + local b = decodeHuffman(br, hg.B) + local a = decodeHuffman(br, hg.A) + local color = bor(bor(bor(lshift(a, 24), lshift(r, 16)), lshift(g, 8)), b) + pixels[#pixels + 1] = color + if ccache then ccache:insert(color) end + + elseif code < 256 + 24 then + -- LZ77 back-reference + local lenCode = code - 256 + local length = prefixToValue(br, lenCode) + 1 + local distCode = decodeHuffman(br, hg.dist) + local dist = prefixToValue(br, distCode) + 1 + + -- remap small distances through VP8L spatial table + if dist <= 120 then + local d = DIST_MAP[dist] + local srcX = px - d[1] + local srcY = py - d[2] + dist = py * width + px - (srcY * width + srcX) + end + + local src = #pixels - dist + 1 + for i = 0, length - 1 do + local c = pixels[src + i] or 0 + pixels[#pixels + 1] = c + if ccache then ccache:insert(c) end + end + + else + -- color cache reference + local cacheIdx = code - 256 - 24 + pixels[#pixels + 1] = ccache:lookup(cacheIdx) + end + + px = px + 1 + if px >= width then px = 0; py = py + 1 end + end + + -- ─── Apply transforms in reverse order ─────────────────────────────────── + + for i = #transforms, 1, -1 do + local t = transforms[i] + + -- SUBTRACT_GREEN: R += G, B += G (mod 256) + if t.ttype == TRANSFORM_SUBTRACT_GREEN then + for idx = 1, #pixels do + local c = pixels[idx] + local a = band(rshift(c, 24), 0xff) + local r = band(rshift(c, 16), 0xff) + local g = band(rshift(c, 8), 0xff) + local b = band(c, 0xff) + r = band(r + g, 0xff) + b = band(b + g, 0xff) + pixels[idx] = bor(bor(bor(lshift(a, 24), lshift(r, 16)), lshift(g, 8)), b) + end + + -- COLOR_INDEXING: replace each pixel's green channel with palette entry + elseif t.ttype == TRANSFORM_COLOR_INDEXING then + local bpp = 8 + if t.numColors <= 2 then bpp = 1 + elseif t.numColors <= 4 then bpp = 2 + elseif t.numColors <= 16 then bpp = 4 end + + local newPixels = {} + if bpp == 8 then + for _, c in ipairs(pixels) do + local idx = band(rshift(c, 8), 0xff) + newPixels[#newPixels + 1] = t.colors[idx + 1] or 0 + end + else + local pxPerByte = 8 / bpp + local mask = lshift(1, bpp) - 1 + for _, c in ipairs(pixels) do + local packed = band(rshift(c, 8), 0xff) + for p = 0, pxPerByte - 1 do + if #newPixels < width * height then + local idx = band(rshift(packed, p * bpp), mask) + newPixels[#newPixels + 1] = t.colors[idx + 1] or 0 + end + end + end + end + pixels = newPixels + + -- PREDICTOR: undo per-block spatial prediction + elseif t.ttype == TRANSFORM_PREDICTOR then + local sb = t.sizeBits + local tw = math.floor((width + lshift(1, sb) - 1) / lshift(1, sb)) + + local function getpx(x, y) + if x < 0 then x = 0 end + if y < 0 then return tobit(0xff000000) end + return pixels[y * width + x + 1] or 0 + end + + local function addARGB(a, b) + local aa = band(rshift(a, 24), 0xff) + local ar = band(rshift(a, 16), 0xff) + local ag = band(rshift(a, 8), 0xff) + local ab = band(a, 0xff) + local ba = band(rshift(b, 24), 0xff) + local br_ = band(rshift(b, 16), 0xff) + local bg = band(rshift(b, 8), 0xff) + local bb = band(b, 0xff) + return bor(bor(bor( + lshift(band(aa + ba, 0xff), 24), + lshift(band(ar + br_, 0xff), 16)), + lshift(band(ag + bg, 0xff), 8)), + band(ab + bb, 0xff)) + end + + local function avgARGB(a, b) + local aa = band(rshift(a, 24), 0xff) + local ar = band(rshift(a, 16), 0xff) + local ag = band(rshift(a, 8), 0xff) + local ab = band(a, 0xff) + local ba = band(rshift(b, 24), 0xff) + local br_ = band(rshift(b, 16), 0xff) + local bg = band(rshift(b, 8), 0xff) + local bb = band(b, 0xff) + return bor(bor(bor( + lshift(rshift(aa + ba, 1), 24), + lshift(rshift(ar + br_, 1), 16)), + lshift(rshift(ag + bg, 1), 8)), + rshift(ab + bb, 1)) + end + + local function clampByte(v) + return math.max(0, math.min(255, v)) + end + + local function clampAddARGB(a, b) + local aa = band(rshift(a, 24), 0xff) + local ar = band(rshift(a, 16), 0xff) + local ag = band(rshift(a, 8), 0xff) + local ab = band(a, 0xff) + local ba = band(rshift(b, 24), 0xff) + local br_ = band(rshift(b, 16), 0xff) + local bg = band(rshift(b, 8), 0xff) + local bb = band(b, 0xff) + return bor(bor(bor( + lshift(clampByte(aa + ba), 24), + lshift(clampByte(ar + br_), 16)), + lshift(clampByte(ag + bg), 8)), + clampByte(ab + bb)) + end + + local function subARGB(a, b) + local aa = band(rshift(a, 24), 0xff) + local ar = band(rshift(a, 16), 0xff) + local ag = band(rshift(a, 8), 0xff) + local ab = band(a, 0xff) + local ba = band(rshift(b, 24), 0xff) + local br_ = band(rshift(b, 16), 0xff) + local bg = band(rshift(b, 8), 0xff) + local bb = band(b, 0xff) + return bor(bor(bor( + lshift(band(aa - ba, 0xff), 24), + lshift(band(ar - br_, 0xff), 16)), + lshift(band(ag - bg, 0xff), 8)), + band(ab - bb, 0xff)) + end + + local function halfSubARGB(a, b) + local aa = band(rshift(a, 24), 0xff) + local ar = band(rshift(a, 16), 0xff) + local ag = band(rshift(a, 8), 0xff) + local ab = band(a, 0xff) + local ba = band(rshift(b, 24), 0xff) + local br_ = band(rshift(b, 16), 0xff) + local bg = band(rshift(b, 8), 0xff) + local bb = band(b, 0xff) + return bor(bor(bor( + lshift(rshift(aa - ba, 1), 24), + lshift(rshift(ar - br_, 1), 16)), + lshift(rshift(ag - bg, 1), 8)), + rshift(ab - bb, 1)) + end + + local function selectARGB(l, tp, tl) + local function absdiff(x, y, s) + return math.abs(band(rshift(x, s), 0xff) - band(rshift(y, s), 0xff)) + end + local function dist(x, y) + return absdiff(x,y,24) + absdiff(x,y,16) + absdiff(x,y,8) + absdiff(x,y,0) + end + return dist(l, tl) <= dist(tp, tl) and l or tp + end + + for iy = 0, height - 1 do + for ix = 0, width - 1 do + if not (ix == 0 and iy == 0) then + local pidx = iy * width + ix + 1 + local L = getpx(ix - 1, iy) + local T = getpx(ix, iy - 1) + local TL = getpx(ix - 1, iy - 1) + local TR = getpx(ix + 1, iy - 1) + local tx = rshift(ix, sb) + local ty = rshift(iy, sb) + local mode = band(t.data[ty * tw + tx + 1] or 0, 0xff) + + local pred + if mode == 0 then pred = tobit(0xff000000) + elseif mode == 1 then pred = L + elseif mode == 2 then pred = T + elseif mode == 3 then pred = TR + elseif mode == 4 then pred = TL + elseif mode == 5 then pred = avgARGB(avgARGB(L, TR), T) + elseif mode == 6 then pred = avgARGB(L, TL) + elseif mode == 7 then pred = avgARGB(L, T) + elseif mode == 8 then pred = avgARGB(TL, T) + elseif mode == 9 then pred = avgARGB(T, TR) + elseif mode == 10 then pred = avgARGB(avgARGB(L, TL), avgARGB(T, TR)) + elseif mode == 11 then pred = selectARGB(L, T, TL) + elseif mode == 12 then pred = clampAddARGB(L, subARGB(T, TL)) + elseif mode == 13 then + local avg = avgARGB(L, T) + pred = clampAddARGB(avg, halfSubARGB(avg, TL)) + else pred = L end + + pixels[pidx] = addARGB(pixels[pidx], pred) + end + end + end + + -- COLOR: undo green/red channel correlations + elseif t.ttype == TRANSFORM_COLOR then + local sb = t.sizeBits + local tw = math.floor((width + lshift(1, sb) - 1) / lshift(1, sb)) + for iy = 0, height - 1 do + for ix = 0, width - 1 do + local pidx = iy * width + ix + 1 + local c = pixels[pidx] or 0 + local tx = rshift(ix, sb) + local ty = rshift(iy, sb) + local m = t.data[ty * tw + tx + 1] or 0 + + -- unpack signed bytes from transform pixel (stored as ARGB) + local g2r = band(rshift(m, 16), 0xff) + local r2b = band(rshift(m, 8), 0xff) + local g2b = band(m, 0xff) + if g2r >= 128 then g2r = g2r - 256 end + if r2b >= 128 then r2b = r2b - 256 end + if g2b >= 128 then g2b = g2b - 256 end + + local a = band(rshift(c, 24), 0xff) + local r = band(rshift(c, 16), 0xff) + local g = band(rshift(c, 8), 0xff) + local b = band(c, 0xff) + + r = band(r + math.floor(g2r * g / 256), 0xff) + b = band(b + math.floor(g2b * g / 256) + math.floor(r2b * r / 256), 0xff) + + pixels[pidx] = bor(bor(bor(lshift(a, 24), lshift(r, 16)), lshift(g, 8)), b) + end + end + end + end + + return pixels +end + +-- ═══════════════════════════════════════════════════════════════════════════ +-- VP8 (LOSSY) DECODER +-- ═══════════════════════════════════════════════════════════════════════════ + +-- ─── Boolean arithmetic decoder ────────────────────────────────────────────── +-- VP8 uses a range coder (not Huffman). Each symbol is decoded against a +-- probability in [0,255]. The range is maintained in [128,255]<= bigsplit then + retval = 1 + b.range = b.range - split + b.value = b.value - bigsplit + else + retval = 0 + b.range = split + end + -- renormalise + while b.range < 128 do + b.range = lshift(b.range, 1) + b.value = lshift(b.value, 1) + b.bits = b.bits + 1 + if b.bits == 8 then + local byte = b.data:byte(b.pos) or 0 + b.pos = b.pos + 1 + b.value = bor(b.value, byte) + b.bits = 0 + end + end + return retval +end + +local function boolReadLit(b, n) + local v = 0 + for _ = 1, n do + v = lshift(v, 1) + boolRead(b, 128) + end + return v +end + +local function boolReadSigned(b, n) + local v = boolReadLit(b, n) + if boolRead(b, 128) == 1 then v = -v end + return v +end + +-- ─── VP8 probability tables ─────────────────────────────────────────────────── + +-- Coefficient probability table (384 entries, from the VP8 spec Annex B) +-- Indexed as [band][ctx][node] but flattened here for brevity. +-- This is the default/initial probability table before any updates. +local VP8_COEF_PROBS = { + -- block type 0: Y after DC (4x4 intra) + { -- band 0 + {128,128,128,128,128,128,128,128,128,128,128}, + {128,128,128,128,128,128,128,128,128,128,128}, + {128,128,128,128,128,128,128,128,128,128,128}, + }, + { -- band 1 + {253,136,254,255,228,219,128,128,128,128,128}, + {189,129,242,255,227,213,255,219,128,128,128}, + {106,126,227,252,214,209,255,255,128,128,128}, + }, + { -- band 2 + { 1, 98,248,255,236,226,255,255,128,128,128}, + {181,133,238,254,221,234,255,154,128,128,128}, + { 78,134,202,247,198,180,255,219,128,128,128}, + }, + { -- band 3 + { 1,185,249,255,243,255,128,128,128,128,128}, + {184,150,247,255,236,224,128,128,128,128,128}, + { 77,110,216,255,236,230,128,128,128,128,128}, + }, + { -- band 4 + { 1,101,251,255,241,255,128,128,128,128,128}, + {170,139,241,252,236,209,255,255,128,128,128}, + { 37, 116,196,243,228,255,255,255,128,128,128}, + }, + { -- band 5 + { 1,204,254,255,245,255,128,128,128,128,128}, + {207,160,250,255,238,128,128,128,128,128,128}, + {102,103,231,255,211,171,128,128,128,128,128}, + }, + { -- band 6 + { 1,152,252,255,240,255,128,128,128,128,128}, + {177,135,243,255,234,225,128,128,128,128,128}, + { 80,129,211,255,194,224,128,128,128,128,128}, + }, + { -- band 7 + { 1, 1,255,128,128,128,128,128,128,128,128}, + {246, 1,255,128,128,128,128,128,128,128,128}, + {255,128,128,128,128,128,128,128,128,128,128}, + }, +} + +-- Intra mode probabilities (from VP8 spec section 11) +local VP8_KEYFRAME_YMODE_PROB = {145, 156, 163, 128} -- DC, V, H, TM +local VP8_KEYFRAME_UVMODE_PROB = {142, 114, 183} + +-- Default MV probabilities (from spec) +local VP8_MV_UPDATE_PROBS = { + {237,246,253,253,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254}, + {231,243,245,253,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254}, +} +local VP8_MV_DEFAULT_PROBS = { + {162,128,225,146,172,147,214, 39,156,128,129,132, 75,145,178,206,239,254,254}, + {164,128,204,170,119,235,140,230,228,128,130,130, 74,148,180,203,236,254,254}, +} + +-- ─── Zigzag scan order for 4x4 DCT block ───────────────────────────────────── +local ZIGZAG = {0,1,4,8,5,2,3,6,9,12,13,10,7,11,14,15} + +-- ─── Quantizer table ────────────────────────────────────────────────────────── +local function dcQ(q) + if q == 0 then return 4 end + return q < 134 and (q * 2 + 3 > 132 and 132 or q * 2 + 3) + or (5 * q + 77 > 2040 and 2040 or 5 * q + 77) +end +local function acQ(q) + return q < 6 and 8 or + q < 10 and (6 * q - 12) or + q < 126 and (q * 2 + 3 > 255 and 255 or q * 2 + 3) or + (5 * q - 370 > 2040 and 2040 or 5 * q - 370) +end + +-- From VP8 spec Table 14 (DC/AC quantizer index tables) +local VP8_DC_QLOOKUP = {} +local VP8_AC_QLOOKUP = {} +do + -- spec tables verbatim + local dc = {4,5,6,7,8,9,10,10,11,12,13,14,15,16,17,17,18,19,20,20,21,21,22,22,23,23,24,25,25,26,27,28,29,30,31,32,33,34,35,36,37,37,38,39,40,41,42,43,44,45,46,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,76,77,78,79,80,81,82,83,84,85,86,87,88,89,91,93,95,96,97,98,100,101,102,104,106,108,110,112,114,116,118,122,124,126,128,130,132,134,136,138,140,143,145,148,151,154,157} + local ac = {4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,60,62,64,66,68,70,72,74,76,79,81,84,87,90,93,96,99,102,105,108,111,114,117,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,205,210,215,220,225,230,235,240,245,249,254,254} + for i,v in ipairs(dc) do VP8_DC_QLOOKUP[i-1] = v end + for i,v in ipairs(ac) do VP8_AC_QLOOKUP[i-1] = v end +end + +-- ─── Inverse DCT (4x4, in-place on a 16-element array, 1-based) ────────────── +local function idct4x4(c) + -- rows + for i = 0, 3 do + local a0 = c[i*4+1] + c[i*4+3] + local a1 = c[i*4+1] - c[i*4+3] + local a2 = rshift(c[i*4+2], 1) - c[i*4+4] + local a3 = c[i*4+2] + rshift(c[i*4+4], 1) + c[i*4+1] = a0 + a3 + c[i*4+2] = a1 + a2 + c[i*4+3] = a1 - a2 + c[i*4+4] = a0 - a3 + end + -- columns + for i = 0, 3 do + local a0 = c[i+1] + c[i+9] + local a1 = c[i+1] - c[i+9] + local a2 = rshift(c[i+5], 1) - c[i+13] + local a3 = c[i+5] + rshift(c[i+13], 1) + c[i+1] = rshift(a0 + a3 + 4, 3) + c[i+5] = rshift(a1 + a2 + 4, 3) + c[i+9] = rshift(a1 - a2 + 4, 3) + c[i+13] = rshift(a0 - a3 + 4, 3) + end +end + +-- WHT (Walsh-Hadamard, 4x4 for DC coefficients) +local function iwht4x4(c) + for i = 0, 3 do + local a0 = c[i*4+1] + c[i*4+3] + local a1 = c[i*4+2] + c[i*4+4] + local a2 = c[i*4+2] - c[i*4+4] + local a3 = c[i*4+1] - c[i*4+3] + c[i*4+1] = a0 + a1 + c[i*4+2] = a3 + a2 + c[i*4+3] = a0 - a1 + c[i*4+4] = a3 - a2 + end + for i = 0, 3 do + local a0 = c[i+1] + c[i+9] + local a1 = c[i+5] + c[i+13] + local a2 = c[i+5] - c[i+13] + local a3 = c[i+1] - c[i+9] + c[i+1] = rshift(a0 + a1, 3) + c[i+5] = rshift(a3 + a2, 3) + c[i+9] = rshift(a0 - a1, 3) + c[i+13] = rshift(a3 - a2, 3) + end +end + +-- ─── Coefficient decoding ───────────────────────────────────────────────────── +-- Decode one 4x4 block of DCT coefficients from the bool decoder. +-- plane: 0=Y 1=UV, blockCtx: context from neighbouring blocks +local function decodeCoefficients(bool, probs, firstCoef, lastCoef, blockCtx) + local coeffs = {} + for i = 0, 15 do coeffs[i] = 0 end + + local ctx = blockCtx + local i = firstCoef + + while i <= lastCoef do + local p = probs[i + 1] or probs[1] + -- EOB or nonzero? + if boolRead(bool, p[ctx + 1] and p[ctx+1][1] or 128) == 0 then + break -- end of block + end + -- skip zeros + while boolRead(bool, (p[ctx+1] and p[ctx+1][2]) or 128) == 0 do + i = i + 1 + if i > lastCoef then return coeffs, 0 end + ctx = 0 + p = probs[i + 1] or probs[1] + end + -- read coefficient value + local v + local pp = p[ctx+1] or {} + if boolRead(bool, pp[3] or 128) == 0 then + v = 1 + elseif boolRead(bool, pp[4] or 128) == 0 then + if boolRead(bool, pp[5] or 128) == 0 then + v = 2 + else + v = 3 + boolRead(bool, pp[6] or 128) + end + elseif boolRead(bool, pp[7] or 128) == 0 then + if boolRead(bool, pp[8] or 128) == 0 then + v = 5 + boolRead(bool, 159) + else + v = 7 + 2 * boolRead(bool, 165) + boolRead(bool, 145) + end + elseif boolRead(bool, pp[9] or 128) == 0 then + local cat3 = {pp[10] or 128, 165, 145} + v = 11 + for _, pr in ipairs(cat3) do v = v * 2 + boolRead(bool, pr) end + elseif boolRead(bool, pp[10] or 128) == 0 then + local cat4 = {pp[11] or 128, 165, 145, 128} + v = 19 + for _, pr in ipairs(cat4) do v = v * 2 + boolRead(bool, pr) end + else + -- cat5 or cat6 + if boolRead(bool, 128) == 0 then + v = 35 + for _, pr in ipairs({165,145,128,128,128}) do + v = v * 2 + boolRead(bool, pr) + end + else + v = 67 + for _, pr in ipairs({145,128,128,128,128,128}) do + v = v * 2 + boolRead(bool, pr) + end + end + end + -- sign bit + if boolRead(bool, 128) == 1 then v = -v end + coeffs[ZIGZAG[i + 1]] = v + ctx = (math.abs(v) == 1) and 1 or 2 + i = i + 1 + end + + local nzCount = 0 + for _, v in ipairs(coeffs) do if v ~= 0 then nzCount = nzCount + 1 end end + return coeffs, (nzCount > 0 and 1 or 0) +end + +-- ─── Intra prediction modes ─────────────────────────────────────────────────── +-- B_PRED sub-modes for 4x4 luma intra prediction +local B_DC_PRED, B_TM_PRED, B_VE_PRED, B_HE_PRED = 0,1,2,3 +local B_LD_PRED, B_RD_PRED, B_VR_PRED, B_VL_PRED = 4,5,6,7 +local B_HD_PRED, B_HU_PRED = 8,9 + +-- Clamp to [0,255] +local function clamp8(v) return math.max(0, math.min(255, v)) end + +-- Fill a 4x4 block in a flat array (stride = mbw*4) using intra prediction. +-- out: flat Y/U/V plane array (1-based, row-major) +-- x4,y4: top-left pixel coords in the plane +-- stride: row stride +-- mode: prediction mode +-- above: 8 pixels above (indices 0..7, may be nil for top-of-image) +-- left: 4 pixels to left (indices 0..3, may be nil for left-of-image) +local function predictBlock4x4(out, x4, y4, stride, mode, above, left, aboveLeft) + local function set(px, py, v) + out[(y4 + py) * stride + x4 + px + 1] = v + end + local A = above or {127,127,127,127,127,127,127,127} + local L = left or {129,129,129,129} + local TL = aboveLeft or 127 + + if mode == B_DC_PRED then + local sum, n = 0, 0 + if above then for i=0,3 do sum=sum+A[i+1]; n=n+1 end end + if left then for i=0,3 do sum=sum+L[i+1]; n=n+1 end end + local dc = n > 0 and rshift(sum + rshift(n, 1), n == 8 and 3 or 2) or 128 + for py=0,3 do for px=0,3 do set(px,py,dc) end end + + elseif mode == B_TM_PRED then + for py=0,3 do for px=0,3 do + set(px, py, clamp8(A[px+1] + L[py+1] - TL)) + end end + + elseif mode == B_VE_PRED then + for py=0,3 do for px=0,3 do set(px,py,A[px+1]) end end + + elseif mode == B_HE_PRED then + for py=0,3 do for px=0,3 do set(px,py,L[py+1]) end end + + elseif mode == B_LD_PRED then + local a = {A[1],A[2],A[3],A[4],A[5],A[6],A[7],A[8]} + local function ld(i) + local x0 = i < 7 and a[i+1] or a[8] + local x1 = i < 7 and a[i+2] or a[8] + local x2 = i < 6 and a[i+3] or a[8] + return rshift(x0 + 2*x1 + x2 + 2, 2) + end + for py=0,3 do for px=0,3 do set(px,py,ld(px+py)) end end + + elseif mode == B_RD_PRED then + local a = {TL,A[1],A[2],A[3],A[4],L[1],L[2],L[3],L[4]} + local function rd(i) -- i in 0..6 + local x0 = a[i+1] or a[1] + local x1 = a[i+2] or a[1] + local x2 = a[i+3] or a[1] + return rshift(x0 + 2*x1 + x2 + 2, 2) + end + for py=0,3 do for px=0,3 do + local d = px - py + set(px, py, rd(d + 4)) + end end + + elseif mode == B_VR_PRED then + local a = {TL,A[1],A[2],A[3],A[4],L[1],L[2],L[3]} + for py=0,3 do for px=0,3 do + local x = 2*px - py + local v + if x >= 0 then + local ai = x / 2 + 1 + if x % 2 == 0 then + v = rshift((a[ai] or 127) + (a[ai+1] or 127) + 1, 1) + else + v = rshift((a[ai] or 127) + 2*(a[ai+1] or 127) + (a[ai+2] or 127) + 2, 2) + end + else + v = rshift(L[-x] + 2*(L[-x+1] or L[4]) + (L[-x+2] or L[4]) + 2, 2) + end + set(px, py, clamp8(v)) + end end + + elseif mode == B_VL_PRED then + for py=0,3 do for px=0,3 do + local x = px + rshift(py, 1) + local v + if py % 2 == 0 then + v = rshift((A[x+1] or A[8]) + (A[x+2] or A[8]) + 1, 1) + else + v = rshift((A[x+1] or A[8]) + 2*(A[x+2] or A[8]) + (A[x+3] or A[8]) + 2, 2) + end + set(px, py, clamp8(v)) + end end + + elseif mode == B_HD_PRED then + local a = {L[4],L[3],L[2],L[1],TL,A[1],A[2],A[3],A[4]} + for py=0,3 do for px=0,3 do + local x = 2*py - px + local v + if x >= 0 then + local ai = rshift(x, 1) + 1 + if x % 2 == 0 then + v = rshift((a[ai] or 127) + (a[ai+1] or 127) + 1, 1) + else + v = rshift((a[ai] or 127) + 2*(a[ai+1] or 127) + (a[ai+2] or 127) + 2, 2) + end + else + v = rshift(A[-x] + 2*(A[-x+1] or A[8]) + (A[-x+2] or A[8]) + 2, 2) + end + set(px, py, clamp8(v)) + end end + + elseif mode == B_HU_PRED then + for py=0,3 do for px=0,3 do + local x = px + 2*py + local v + if x + 1 < 4 then + v = rshift(L[x+1] + L[x+2] + 1, 1) + elseif x == 6 then + v = rshift(L[4] + 3*L[4] + 2, 2) + else + v = L[4] + end + set(px, py, clamp8(v)) + end end + end +end + +-- 16x16 luma intra prediction (DC, V, H, TM) +local function predictMB16(plane, mbx, mby, mbw, mode, aboveRow, leftCol) + local base_x = mbx * 16 + local base_y = mby * 16 + local stride = mbw * 16 + + if mode == 0 then -- DC_PRED + local sum, n = 0, 0 + if aboveRow then for i=0,15 do sum=sum+aboveRow[i+1]; n=n+1 end end + if leftCol then for i=0,15 do sum=sum+leftCol[i+1]; n=n+1 end end + local dc = n > 0 and rshift(sum + rshift(n, 1), n == 32 and 5 or 4) or 128 + for py=0,15 do for px=0,15 do + plane[(base_y+py)*stride + base_x+px + 1] = dc + end end + + elseif mode == 1 then -- V_PRED + local src = aboveRow or {} + for py=0,15 do for px=0,15 do + plane[(base_y+py)*stride + base_x+px + 1] = src[px+1] or 127 + end end + + elseif mode == 2 then -- H_PRED + local src = leftCol or {} + for py=0,15 do for px=0,15 do + plane[(base_y+py)*stride + base_x+px + 1] = src[py+1] or 129 + end end + + elseif mode == 3 then -- TM_PRED + local tl = (aboveRow and leftCol) and 127 or 128 + if aboveRow and leftCol then + -- tl is above-left of macroblock + tl = 127 -- approximation; proper value from frame buffer + end + local A = aboveRow or {} + local L = leftCol or {} + for py=0,15 do for px=0,15 do + plane[(base_y+py)*stride + base_x+px + 1] = + clamp8((A[px+1] or 127) + (L[py+1] or 129) - tl) + end end + end +end + +-- 8x8 chroma intra prediction +local function predictMB8(plane, mbx, mby, mbw, mode, aboveRow, leftCol) + local base_x = mbx * 8 + local base_y = mby * 8 + local stride = mbw * 8 + + if mode == 0 then -- DC + local sum, n = 0, 0 + if aboveRow then for i=0,7 do sum=sum+aboveRow[i+1]; n=n+1 end end + if leftCol then for i=0,7 do sum=sum+leftCol[i+1]; n=n+1 end end + local dc = n > 0 and rshift(sum + rshift(n, 1), n == 16 and 4 or 3) or 128 + for py=0,7 do for px=0,7 do + plane[(base_y+py)*stride + base_x+px + 1] = dc + end end + elseif mode == 1 then -- V + local src = aboveRow or {} + for py=0,7 do for px=0,7 do + plane[(base_y+py)*stride + base_x+px + 1] = src[px+1] or 127 + end end + elseif mode == 2 then -- H + local src = leftCol or {} + for py=0,7 do for px=0,7 do + plane[(base_y+py)*stride + base_x+px + 1] = src[py+1] or 129 + end end + elseif mode == 3 then -- TM + local A = aboveRow or {} + local L = leftCol or {} + for py=0,7 do for px=0,7 do + plane[(base_y+py)*stride + base_x+px + 1] = clamp8((A[px+1] or 127) + (L[py+1] or 129) - 127) + end end + end +end + +-- ─── Add residuals to prediction ───────────────────────────────────────────── +local function addResiduals(plane, base_x, base_y, stride, coeffs) + for py = 0, 3 do + for px = 0, 3 do + local idx = (base_y + py) * stride + base_x + px + 1 + plane[idx] = clamp8((plane[idx] or 0) + (coeffs[py * 4 + px + 1] or 0)) + end + end +end + +-- ─── Simple loop filter ─────────────────────────────────────────────────────── +local function filterSimple(plane, stride, w, h) + -- horizontal edges + for y = 1, h - 1 do + for x = 0, w - 1 do + local p1 = plane[(y-1)*stride + x + 1] or 128 + local q1 = plane[ y *stride + x + 1] or 128 + local d = rshift(3*(q1 - p1) + 4, 3) + d = math.max(-4, math.min(4, d)) + plane[(y-1)*stride+x+1] = clamp8(p1 + d) + plane[ y *stride+x+1] = clamp8(q1 - d) + end + end + -- vertical edges + for y = 0, h - 1 do + for x = 1, w - 1 do + local p1 = plane[y*stride + x] or 128 + local q1 = plane[y*stride + x + 1] or 128 + local d = rshift(3*(q1 - p1) + 4, 3) + d = math.max(-4, math.min(4, d)) + plane[y*stride+x] = clamp8(p1 + d) + plane[y*stride+x+1] = clamp8(q1 - d) + end + end +end + +-- ─── YCbCr → RGB conversion ─────────────────────────────────────────────────── +-- VP8 uses BT.601 studio swing: +-- R = Y + 1.402*(Cr-128) +-- G = Y - 0.344*(Cb-128) - 0.714*(Cr-128) +-- B = Y + 1.772*(Cb-128) +local function yuvToRgb(y, cb, cr) + local r = clamp8(math.floor(y + 1.402 * (cr - 128) + 0.5)) + local g = clamp8(math.floor(y - 0.34414 * (cb - 128) - 0.71414 * (cr - 128) + 0.5)) + local b = clamp8(math.floor(y + 1.772 * (cb - 128) + 0.5)) + return r, g, b +end + +-- ─── Main VP8 decode ────────────────────────────────────────────────────────── +local function decodeVP8(data, offset, length) + offset = offset or 1 + -- Frame tag (3 bytes) + local b0 = data:byte(offset) + local b1 = data:byte(offset + 1) + local b2 = data:byte(offset + 2) + local keyFrame = band(b0, 1) == 0 + local version = band(rshift(b0, 1), 7) + local showFrame = band(rshift(b0, 4), 1) == 1 + local firstPartSize = bor(rshift(b0, 5), bor(lshift(b1, 3), lshift(b2, 11))) + + assert(keyFrame, "VP8 inter frames not supported (not a keyframe)") + + -- Start code (3 bytes: 0x9d 0x01 0x2a) + assert(data:byte(offset+3) == 0x9d and + data:byte(offset+4) == 0x01 and + data:byte(offset+5) == 0x2a, "Invalid VP8 start code") + + local w_raw = bor(data:byte(offset+6), lshift(data:byte(offset+7), 8)) + local h_raw = bor(data:byte(offset+8), lshift(data:byte(offset+9), 8)) + local width = band(w_raw, 0x3fff) + local height = band(h_raw, 0x3fff) + local hscale = rshift(w_raw, 14) + local vscale = rshift(h_raw, 14) + + -- macroblock dimensions + local mbw = rshift(width + 15, 4) + local mbh = rshift(height + 15, 4) + + -- First partition starts at offset+3 (after frame tag) + local bool = newBoolDecoder(data, offset + 3) + + -- ── Frame header ── + -- color space and clamping + local colorSpace = boolRead(bool, 128) + local clampType = boolRead(bool, 128) + + -- segmentation + local segmentEnabled = boolRead(bool, 128) + local segmentAbsDelta = false + local segQuant = {0,0,0,0} + local segFilter = {0,0,0,0} + if segmentEnabled == 1 then + local updateMap = boolRead(bool, 128) + local updateData = boolRead(bool, 128) + if updateData == 1 then + segmentAbsDelta = boolRead(bool, 128) == 1 + for i = 1, 4 do + if boolRead(bool, 128) == 1 then + segQuant[i] = boolReadSigned(bool, 7) + end + end + for i = 1, 4 do + if boolRead(bool, 128) == 1 then + segFilter[i] = boolReadSigned(bool, 6) + end + end + end + if updateMap == 1 then + -- read 3 probabilities (skip for keyframe - all MBs resend) + for _ = 1, 3 do + if boolRead(bool, 128) == 1 then boolReadLit(bool, 8) end + end + end + end + + -- loop filter + local filterType = boolRead(bool, 128) -- 0=normal, 1=simple + local filterLevel = boolReadLit(bool, 6) + local filterSharp = boolReadLit(bool, 3) + local lfAdjEnable = boolRead(bool, 128) + if lfAdjEnable == 1 then + if boolRead(bool, 128) == 1 then -- mode_ref_lf_delta_update + for _ = 1, 4 do -- ref_frame deltas + if boolRead(bool, 128) == 1 then boolReadSigned(bool, 6) end + end + for _ = 1, 4 do -- mb mode deltas + if boolRead(bool, 128) == 1 then boolReadSigned(bool, 6) end + end + end + end + + -- partition count + local log2Partitions = boolReadLit(bool, 2) + local numPartitions = lshift(1, log2Partitions) + + -- quantizer indices + local baseQ = boolReadLit(bool, 7) + local dqY1dc = boolRead(bool,128)==1 and boolReadSigned(bool,4) or 0 + local dqY2dc = boolRead(bool,128)==1 and boolReadSigned(bool,4) or 0 + local dqY2ac = boolRead(bool,128)==1 and boolReadSigned(bool,4) or 0 + local dqUVdc = boolRead(bool,128)==1 and boolReadSigned(bool,4) or 0 + local dqUVac = boolRead(bool,128)==1 and boolReadSigned(bool,4) or 0 + + -- build quantizer tables per segment + local function qi(base, delta, isAbs) + local q = isAbs and delta or base + delta + return math.max(0, math.min(127, q)) + end + + -- coefficient probability updates + -- (use default table; read updates from bitstream) + local coefProbs = {} + for t = 1, 4 do + coefProbs[t] = {} + for b = 1, 8 do + coefProbs[t][b] = {} + for c = 1, 3 do + coefProbs[t][b][c] = {} + for n = 1, 11 do + coefProbs[t][b][c][n] = VP8_COEF_PROBS[b] and + VP8_COEF_PROBS[b][c] and VP8_COEF_PROBS[b][c][n] or 128 + end + end + end + end + -- read coefficient probability updates + for t = 1, 4 do + for b = 1, 8 do + for c = 1, 3 do + for n = 1, 11 do + if boolRead(bool, 255) == 1 then + coefProbs[t][b][c][n] = boolReadLit(bool, 8) + end + end + end + end + end + + -- skip probability + local mbSkipEnabled = boolRead(bool, 128) + local skipProb = mbSkipEnabled == 1 and boolReadLit(bool, 8) or 0 + + -- intra mode probabilities for key frames are fixed (not transmitted) + -- (use VP8_KEYFRAME_YMODE_PROB / VP8_KEYFRAME_UVMODE_PROB) + + -- ── Locate second partition(s) ── + -- The first partition ends at offset+3+firstPartSize + -- Immediately after are (numPartitions-1) * 3 bytes of partition sizes, + -- then the partition data. + local firstPartEnd = offset + 3 + firstPartSize + local partSizeBase = firstPartEnd + local partDataBase = partSizeBase + (numPartitions - 1) * 3 + local partOffsets = {} + local acc = 0 + for p = 1, numPartitions - 1 do + local o = partSizeBase + (p-1)*3 + local sz = bor(data:byte(o) or 0, + bor(lshift(data:byte(o+1) or 0, 8), + lshift(data:byte(o+2) or 0, 16))) + partOffsets[p] = partDataBase + acc + acc = acc + sz + end + partOffsets[numPartitions] = partDataBase + acc + + -- create bool decoders for each residual partition + local partBools = {} + for p = 1, numPartitions do + partBools[p] = newBoolDecoder(data, partOffsets[p]) + end + + -- ── Allocate planes ── + local Yplane = {} + local Uplane = {} + local Vplane = {} + local Ystride = mbw * 16 + local Cstride = mbw * 8 + for i = 1, Ystride * mbh * 16 do Yplane[i] = 128 end + for i = 1, Cstride * mbh * 8 do Uplane[i] = 128; Vplane[i] = 128 end + + -- ── Per-macroblock decode ── + local partIdx = 1 + + for mby = 0, mbh - 1 do + for mbx = 0, mbw - 1 do + -- select residual partition (round-robin) + local pb = partBools[partIdx] + partIdx = (partIdx % numPartitions) + 1 + + -- skip flag + local mbSkip = false + if mbSkipEnabled == 1 then + mbSkip = boolRead(bool, skipProb) == 1 + end + + -- intra prediction mode for luma (16x16 or B_PRED / 4x4) + local lumaMode = 0 -- DC default + -- read y_mode + local yMode + if boolRead(bool, 145) == 0 then + -- B_PRED: 4x4 sub-modes per sub-block + yMode = 4 -- B_PRED sentinel + else + if boolRead(bool, 156) == 0 then + if boolRead(bool, 163) == 0 then + yMode = 1 -- V_PRED + else + yMode = 2 -- H_PRED + end + else + if boolRead(bool, 128) == 0 then + yMode = 0 -- DC_PRED + else + yMode = 3 -- TM_PRED + end + end + end + + -- UV mode + local uvMode + if boolRead(bool, 142) == 0 then + uvMode = 0 -- DC + elseif boolRead(bool, 114) == 0 then + uvMode = 1 -- V + elseif boolRead(bool, 183) == 0 then + uvMode = 2 -- H + else + uvMode = 3 -- TM + end + + -- read 4x4 sub-modes if B_PRED + local subModes = {} + if yMode == 4 then + local B_PROB = { + {231,120, 48, 89,115,113,120,152,112}, + {152,179, 64,126,170,118, 46, 70, 95}, + {175, 69,143, 80, 85, 82, 72,155, 64}, + {212,188,128, 97,151,195, 9, 41, 15}, + { 3, 9, 1, 7, 3, 3, 5, 1, 16}, + } + for _ = 0, 15 do + -- simplified: use DC_PRED as default for all sub-blocks + -- full implementation would use context-adaptive probs + local m = 0 + if boolRead(bool, 128) == 0 then m = 0 + elseif boolRead(bool, 156) == 0 then m = 1 + elseif boolRead(bool, 163) == 0 then m = 2 + elseif boolRead(bool, 128) == 0 then m = 3 + elseif boolRead(bool, 128) == 0 then m = 4 + elseif boolRead(bool, 128) == 0 then m = 5 + elseif boolRead(bool, 128) == 0 then m = 6 + elseif boolRead(bool, 128) == 0 then m = 7 + elseif boolRead(bool, 128) == 0 then m = 8 + else m = 9 end + subModes[#subModes + 1] = m + end + end + + -- ── Intra prediction (luma) ── + local base_x = mbx * 16 + local base_y = mby * 16 + + -- gather above/left context + local function getAbove16(plane, bx, by, stride) + if by == 0 then return nil end + local row = {} + for i = 0, 15 do + row[i+1] = plane[(by-1)*stride + bx + i + 1] or 127 + end + return row + end + local function getLeft16(plane, bx, by, stride) + if bx == 0 then return nil end + local col = {} + for i = 0, 15 do + col[i+1] = plane[by*stride + bx - 1 + (i * stride) - (stride-1)] or 129 + end + return col + end + + if yMode ~= 4 then + -- whole-MB prediction + local abv = getAbove16(Yplane, base_x, base_y, Ystride) + local lft = nil + if mbx > 0 then + lft = {} + for i = 0, 15 do + lft[i+1] = Yplane[(base_y+i)*Ystride + base_x] or 129 + end + end + predictMB16(Yplane, mbx, mby, mbw, yMode, abv, lft) + else + -- B_PRED: 4x4 sub-block prediction + for si = 0, 15 do + local sx = band(si, 3) * 4 + local sy = rshift(si, 2) * 4 + local px = base_x + sx + local py = base_y + sy + + local above4 = {} + local left4 = {} + local tl + + for i = 0, 7 do + above4[i+1] = py > 0 and (Yplane[(py-1)*Ystride + px + i + 1] or 127) or 127 + end + for i = 0, 3 do + left4[i+1] = px > 0 and (Yplane[(py+i)*Ystride + px] or 129) or 129 + end + tl = (py > 0 and px > 0) and (Yplane[(py-1)*Ystride + px] or 127) or 127 + + predictBlock4x4(Yplane, px, py, Ystride, subModes[si+1] or 0, + above4, left4, tl) + end + end + + -- UV prediction + do + local ubx = mbx * 8 + local uby = mby * 8 + local function above8(plane) + if mby == 0 then return nil end + local r = {} + for i=0,7 do r[i+1]=plane[(uby-1)*Cstride+ubx+i+1] or 127 end + return r + end + local function left8(plane) + if mbx == 0 then return nil end + local c = {} + for i=0,7 do c[i+1]=plane[(uby+i)*Cstride+ubx] or 129 end + return c + end + predictMB8(Uplane, mbx, mby, mbw, uvMode, above8(Uplane), left8(Uplane)) + predictMB8(Vplane, mbx, mby, mbw, uvMode, above8(Vplane), left8(Vplane)) + end + + -- ── Residuals ── + if not mbSkip then + local q = math.max(0, math.min(127, baseQ)) + local yDCq = VP8_DC_QLOOKUP[math.max(0,math.min(127, q + dqY1dc))] + local yACq = VP8_AC_QLOOKUP[q] + local y2DCq= VP8_DC_QLOOKUP[math.max(0,math.min(127, q + dqY2dc))] + local y2ACq= VP8_AC_QLOOKUP[math.max(0,math.min(127, q + dqY2ac))] + local uvDCq= VP8_DC_QLOOKUP[math.max(0,math.min(127, q + dqUVdc))] + local uvACq= VP8_AC_QLOOKUP[math.max(0,math.min(127, q + dqUVac))] + + -- Y2 (DC only for 16x16 modes) + local y2coeffs = nil + if yMode ~= 4 then + local c16 = {} + for i=1,16 do c16[i] = 0 end + -- decode 4x4 WHT block for Y2 + local rawC, nz = decodeCoefficients(pb, coefProbs[1], 0, 15, 0) + if nz > 0 then + for k=0,15 do rawC[k] = (rawC[k] or 0) * (k==0 and y2DCq or y2ACq) end + local wht = {} + for k=1,16 do wht[k] = rawC[k-1] or 0 end + iwht4x4(wht) + y2coeffs = wht + end + end + + -- 16 Y sub-blocks + for si = 0, 15 do + local sx = band(si, 3) * 4 + local sy = rshift(si, 2) * 4 + local px = base_x + sx + local py = base_y + sy + local firstCoef = yMode ~= 4 and 1 or 0 + local rawC, nz = decodeCoefficients(pb, coefProbs[yMode~=4 and 2 or 1], + firstCoef, 15, 0) + -- inject Y2 DC if present + if y2coeffs then + rawC[0] = y2coeffs[si + 1] or 0 + end + -- dequantize + for k = 0, 15 do + rawC[k] = (rawC[k] or 0) * (k == 0 and yDCq or yACq) + end + -- inverse DCT + local block = {} + for k = 1, 16 do block[k] = rawC[k-1] or 0 end + idct4x4(block) + addResiduals(Yplane, px, py, Ystride, block) + end + + -- 4 U + 4 V sub-blocks + for _, plane in ipairs({Uplane, Vplane}) do + for si = 0, 3 do + local sx = band(si, 1) * 4 + local sy = rshift(si, 1) * 4 + local px = mbx * 8 + sx + local py = mby * 8 + sy + local rawC, nz = decodeCoefficients(pb, coefProbs[3], 0, 15, 0) + for k = 0, 15 do + rawC[k] = (rawC[k] or 0) * (k == 0 and uvDCq or uvACq) + end + local block = {} + for k = 1, 16 do block[k] = rawC[k-1] or 0 end + idct4x4(block) + addResiduals(plane, px, py, Cstride, block) + end + end + end + + end -- mbx + end -- mby + + -- optional simple loop filter + if filterType == 1 and filterLevel > 0 then + filterSimple(Yplane, Ystride, mbw*16, mbh*16) + filterSimple(Uplane, Cstride, mbw*8, mbh*8) + filterSimple(Vplane, Cstride, mbw*8, mbh*8) + end + + -- ── Assemble RGBA image ── + local imageData = love.image.newImageData(width, height, "rgba8") + imageData:mapPixel(function(px, py) + local yi = py * Ystride + px + 1 + local cy = rshift(py, 1) + local cx = rshift(px, 1) + local ci = cy * Cstride + cx + 1 + local Y = Yplane[yi] or 128 + local Cb = Uplane[ci] or 128 + local Cr = Vplane[ci] or 128 + local r, g, b = yuvToRgb(Y, Cb, Cr) + return r/255, g/255, b/255, 1 + end) + + return love.graphics.newImage(imageData) +end + +-- ═══════════════════════════════════════════════════════════════════════════ +-- VP8L → Love2D image (unchanged, just wrapped for the dispatcher) +-- ═══════════════════════════════════════════════════════════════════════════ + +local function decodeVP8L_image(data, offset) + offset = offset or 1 + assert(data:byte(offset) == 0x2f, "Invalid VP8L signature byte") + local br = newBitReader(data:sub(offset + 1)) + local width = br:read(14) + 1 + local height = br:read(14) + 1 + br:readBool() -- alpha hint + local version = br:read(3) + assert(version == 0, "Unsupported VP8L version: " .. version) + + local pixels = decodeVP8L(br, width, height) + + local imageData = love.image.newImageData(width, height, "rgba8") + imageData:mapPixel(function(x, y) + local c = pixels[y * width + x + 1] or 0 + local a = band(rshift(c, 24), 0xff) + local r = band(rshift(c, 16), 0xff) + local g = band(rshift(c, 8), 0xff) + local b = band(c, 0xff) + return r/255, g/255, b/255, a/255 + end) + + return love.graphics.newImage(imageData) +end + +-- ═══════════════════════════════════════════════════════════════════════════ +-- PUBLIC API +-- ═══════════════════════════════════════════════════════════════════════════ + +-- Read a little-endian uint32 from data at pos (1-based) +local function readU32LE(data, pos) + return bor(data:byte(pos), + bor(lshift(data:byte(pos+1), 8), + bor(lshift(data:byte(pos+2), 16), + lshift(data:byte(pos+3), 24)))) +end + +function WebP.decode(data) + assert(data:sub(1, 4) == "RIFF", "Not a RIFF file") + assert(data:sub(9, 12) == "WEBP", "Not a WEBP file") + + local fourCC = data:sub(13, 16) + + if fourCC == "VP8 " then + -- Simple lossy: VP8 bitstream starts after the 8-byte chunk header + -- bytes 13-16: "VP8 ", 17-20: chunk size (LE), 21+: VP8 bitstream + return decodeVP8(data, 21) + + elseif fourCC == "VP8L" then + -- Simple lossless: VP8L bitstream starts at byte 21 + return decodeVP8L_image(data, 21) + + elseif fourCC == "VP8X" then + -- Extended format: parse chunks after the VP8X header + -- VP8X chunk: 4cc(4) + size(4) + flags(4) + canvas_w-1(3) + canvas_h-1(3) = 18 bytes + local pos = 13 -- start of VP8X chunk + local chunkSize = readU32LE(data, pos + 4) + pos = pos + 8 + chunkSize -- skip past VP8X chunk data + + -- walk remaining chunks + while pos + 8 <= #data do + local cc = data:sub(pos, pos + 3) + local sz = readU32LE(data, pos + 4) + local dOff = pos + 8 -- chunk data offset (1-based) + + if cc == "VP8 " then + return decodeVP8(data, dOff) + elseif cc == "VP8L" then + return decodeVP8L_image(data, dOff) + elseif cc == "ANIM" or cc == "ANMF" then + error("Animated WebP is not supported") + end + + pos = pos + 8 + sz + if band(sz, 1) == 1 then pos = pos + 1 end -- padding byte + end + error("VP8X: no VP8 or VP8L chunk found") + else + error("Unsupported WebP chunk type: " .. fourCC) + end +end + +function WebP.load(path) + local data = love.filesystem.read(path) + assert(data, "Could not read file: " .. path) + return WebP.decode(data) +end + +return WebP diff --git a/yaml.lua b/yaml.lua new file mode 100755 index 0000000..36c105a --- /dev/null +++ b/yaml.lua @@ -0,0 +1,776 @@ +------------------------------------------------------------------------------- +-- tinyyaml - YAML subset parser +------------------------------------------------------------------------------- + +local table = table +local string = string +local schar = string.char +local ssub, gsub = string.sub, string.gsub +local sfind, smatch = string.find, string.match +local tinsert, tremove = table.insert, table.remove +local setmetatable = setmetatable +local pairs = pairs +local type = type +local tonumber = tonumber +local math = math +local getmetatable = getmetatable +local error = error + +local UNESCAPES = { + ['0'] = "\x00", z = "\x00", N = "\x85", + a = "\x07", b = "\x08", t = "\x09", + n = "\x0a", v = "\x0b", f = "\x0c", + r = "\x0d", e = "\x1b", ['\\'] = '\\', +}; + +------------------------------------------------------------------------------- +-- utils +local function select(list, pred) + local selected = {} + for i = 0, #list do + local v = list[i] + if v and pred(v, i) then + tinsert(selected, v) + end + end + return selected +end + +local function startswith(haystack, needle) + return ssub(haystack, 1, #needle) == needle +end + +local function ltrim(str) + return smatch(str, "^%s*(.-)$") +end + +local function rtrim(str) + return smatch(str, "^(.-)%s*$") +end + +------------------------------------------------------------------------------- +-- Implementation. +-- +local class = {__meta={}} +function class.__meta.__call(cls, ...) + local self = setmetatable({}, cls) + if cls.__init then + cls.__init(self, ...) + end + return self +end + +function class.def(base, typ, cls) + base = base or class + local mt = {__metatable=base, __index=base} + for k, v in pairs(base.__meta) do mt[k] = v end + cls = setmetatable(cls or {}, mt) + cls.__index = cls + cls.__metatable = cls + cls.__type = typ + cls.__meta = mt + return cls +end + + +local types = { + null = class:def('null'), + map = class:def('map'), + omap = class:def('omap'), + pairs = class:def('pairs'), + set = class:def('set'), + seq = class:def('seq'), + timestamp = class:def('timestamp'), +} + +local Null = types.null +function Null.__tostring() return 'yaml.null' end +function Null.isnull(v) + if v == nil then return true end + if type(v) == 'table' and getmetatable(v) == Null then return true end + return false +end +local null = Null() + +function types.timestamp:__init(y, m, d, h, i, s, f, z) + self.year = tonumber(y) + self.month = tonumber(m) + self.day = tonumber(d) + self.hour = tonumber(h or 0) + self.minute = tonumber(i or 0) + self.second = tonumber(s or 0) + if type(f) == 'string' and sfind(f, '^%d+$') then + self.fraction = tonumber(f) * math.pow(10, 3 - #f) + elseif f then + self.fraction = f + else + self.fraction = 0 + end + self.timezone = z +end + +function types.timestamp:__tostring() + return string.format( + '%04d-%02d-%02dT%02d:%02d:%02d.%03d%s', + self.year, self.month, self.day, + self.hour, self.minute, self.second, self.fraction, + self:gettz()) +end + +function types.timestamp:gettz() + if not self.timezone then + return '' + end + if self.timezone == 0 then + return 'Z' + end + local sign = self.timezone > 0 + local z = sign and self.timezone or -self.timezone + local zh = math.floor(z) + local zi = (z - zh) * 60 + return string.format( + '%s%02d:%02d', sign and '+' or '-', zh, zi) +end + + +local function countindent(line) + local _, j = sfind(line, '^%s+') + if not j then + return 0, line + end + return j, ssub(line, j+1) +end + +local function parsestring(line, stopper) + stopper = stopper or '' + local q = ssub(line, 1, 1) + if q == ' ' or q == '\t' then + return parsestring(ssub(line, 2)) + end + if q == "'" then + local i = sfind(line, "'", 2, true) + if not i then + return nil, line + end + return ssub(line, 2, i-1), ssub(line, i+1) + end + if q == '"' then + local i, buf = 2, '' + while i < #line do + local c = ssub(line, i, i) + if c == '\\' then + local n = ssub(line, i+1, i+1) + if UNESCAPES[n] ~= nil then + buf = buf..UNESCAPES[n] + elseif n == 'x' then + local h = ssub(i+2,i+3) + if sfind(h, '^[0-9a-fA-F]$') then + buf = buf..schar(tonumber(h, 16)) + i = i + 2 + else + buf = buf..'x' + end + else + buf = buf..n + end + i = i + 1 + elseif c == q then + break + else + buf = buf..c + end + i = i + 1 + end + return buf, ssub(line, i+1) + end + if q == '{' or q == '[' then -- flow style + return nil, line + end + if q == '|' or q == '>' then -- block + return nil, line + end + if q == '-' or q == ':' then + if ssub(line, 2, 2) == ' ' or #line == 1 then + return nil, line + end + end + local buf = '' + while #line > 0 do + local c = ssub(line, 1, 1) + if sfind(stopper, c, 1, true) then + break + elseif c == ':' and (ssub(line, 2, 2) == ' ' or #line == 1) then + break + elseif c == '#' and (ssub(buf, #buf, #buf) == ' ') then + break + else + buf = buf..c + end + line = ssub(line, 2) + end + return rtrim(buf), line +end + +local function isemptyline(line) + return line == '' or sfind(line, '^%s*$') or sfind(line, '^%s*#') +end + +local function equalsline(line, needle) + return startswith(line, needle) and isemptyline(ssub(line, #needle+1)) +end + +local function checkdupekey(map, key) + if map[key] ~= nil then + -- print("found a duplicate key '"..key.."' in line: "..line) + local suffix = 1 + while map[key..'_'..suffix] do + suffix = suffix + 1 + end + key = key ..'_'..suffix + end + return key +end + +local function parseflowstyle(line, lines) + local stack = {} + while true do + if #line == 0 then + if #lines == 0 then + break + else + line = tremove(lines, 1) + end + end + local c = ssub(line, 1, 1) + if c == '#' then + line = '' + elseif c == ' ' or c == '\t' or c == '\r' or c == '\n' then + line = ssub(line, 2) + elseif c == '{' or c == '[' then + tinsert(stack, {v={},t=c}) + line = ssub(line, 2) + elseif c == ':' then + local s = tremove(stack) + tinsert(stack, {v=s.v, t=':'}) + line = ssub(line, 2) + elseif c == ',' then + local value = tremove(stack) + if value.t == ':' or value.t == '{' or value.t == '[' then error() end + if stack[#stack].t == ':' then + -- map + local key = tremove(stack) + key.v = checkdupekey(stack[#stack].v, key.v) + stack[#stack].v[key.v] = value.v + elseif stack[#stack].t == '{' then + -- set + stack[#stack].v[value.v] = true + elseif stack[#stack].t == '[' then + -- seq + tinsert(stack[#stack].v, value.v) + end + line = ssub(line, 2) + elseif c == '}' then + if stack[#stack].t == '{' then + if #stack == 1 then break end + stack[#stack].t = '}' + line = ssub(line, 2) + else + line = ','..line + end + elseif c == ']' then + if stack[#stack].t == '[' then + if #stack == 1 then break end + stack[#stack].t = ']' + line = ssub(line, 2) + else + line = ','..line + end + else + local s, rest = parsestring(line, ',{}[]') + if not s then + error('invalid flowstyle line: '..line) + end + tinsert(stack, {v=s, t='s'}) + line = rest + end + end + return stack[1].v, line +end + +local function parseblockstylestring(line, lines, indent) + if #lines == 0 then + error("failed to find multi-line scalar content") + end + local s = {} + local firstindent = -1 + local endline = -1 + for i = 1, #lines do + local ln = lines[i] + local idt = countindent(ln) + if idt <= indent then + break + end + if ln == '' then + tinsert(s, '') + else + if firstindent == -1 then + firstindent = idt + elseif idt < firstindent then + break + end + tinsert(s, ssub(ln, firstindent + 1)) + end + endline = i + end + + local striptrailing = true + local sep = '\n' + local newlineatend = true + if line == '|' then + striptrailing = true + sep = '\n' + newlineatend = true + elseif line == '|+' then + striptrailing = false + sep = '\n' + newlineatend = true + elseif line == '|-' then + striptrailing = true + sep = '\n' + newlineatend = false + elseif line == '>' then + striptrailing = true + sep = ' ' + newlineatend = true + elseif line == '>+' then + striptrailing = false + sep = ' ' + newlineatend = true + elseif line == '>-' then + striptrailing = true + sep = ' ' + newlineatend = false + else + error('invalid blockstyle string:'..line) + end + local eonl = 0 + for i = #s, 1, -1 do + if s[i] == '' then + tremove(s, i) + eonl = eonl + 1 + end + end + if striptrailing then + eonl = 0 + end + if newlineatend then + eonl = eonl + 1 + end + for i = endline, 1, -1 do + tremove(lines, i) + end + return table.concat(s, sep)..string.rep('\n', eonl) +end + +local function parsetimestamp(line) + local _, p1, y, m, d = sfind(line, '^(%d%d%d%d)%-(%d%d)%-(%d%d)') + if not p1 then + return nil, line + end + if p1 == #line then + return types.timestamp(y, m, d), '' + end + local _, p2, h, i, s = sfind(line, '^[Tt ](%d+):(%d+):(%d+)', p1+1) + if not p2 then + return types.timestamp(y, m, d), ssub(line, p1+1) + end + if p2 == #line then + return types.timestamp(y, m, d, h, i, s), '' + end + local _, p3, f = sfind(line, '^%.(%d+)', p2+1) + if not p3 then + p3 = p2 + f = 0 + end + local zc = ssub(line, p3+1, p3+1) + local _, p4, zs, z = sfind(line, '^ ?([%+%-])(%d+)', p3+1) + if p4 then + z = tonumber(z) + local _, p5, zi = sfind(line, '^:(%d+)', p4+1) + if p5 then + z = z + tonumber(zi) / 60 + end + z = zs == '-' and -tonumber(z) or tonumber(z) + elseif zc == 'Z' then + p4 = p3 + 1 + z = 0 + else + p4 = p3 + z = false + end + return types.timestamp(y, m, d, h, i, s, f, z), ssub(line, p4+1) +end + +local function parsescalar(line, lines, indent) + line = ltrim(line) + line = gsub(line, '^%s*#.*$', '') -- comment only -> '' + line = gsub(line, '^%s*', '') -- trim head spaces + + if line == '' or line == '~' then + return null + end + + local ts, _ = parsetimestamp(line) + if ts then + return ts + end + + local s, _ = parsestring(line) + -- startswith quote ... string + -- not startswith quote ... maybe string + if s and (startswith(line, '"') or startswith(line, "'")) then + return s + end + + if startswith('!', line) then -- unexpected tagchar + error('unsupported line: '..line) + end + + if equalsline(line, '{}') then + return {} + end + if equalsline(line, '[]') then + return {} + end + + if startswith(line, '{') or startswith(line, '[') then + return parseflowstyle(line, lines) + end + + if startswith(line, '|') or startswith(line, '>') then + return parseblockstylestring(line, lines, indent) + end + + -- Regular unquoted string + line = gsub(line, '%s*#.*$', '') -- trim tail comment + local v = line + if v == 'null' or v == 'Null' or v == 'NULL'then + return null + elseif v == 'true' or v == 'True' or v == 'TRUE' then + return true + elseif v == 'false' or v == 'False' or v == 'FALSE' then + return false + elseif v == '.inf' or v == '.Inf' or v == '.INF' then + return math.huge + elseif v == '+.inf' or v == '+.Inf' or v == '+.INF' then + return math.huge + elseif v == '-.inf' or v == '-.Inf' or v == '-.INF' then + return -math.huge + elseif v == '.nan' or v == '.NaN' or v == '.NAN' then + return 0 / 0 + elseif sfind(v, '^[%+%-]?[0-9]+$') or sfind(v, '^[%+%-]?[0-9]+%.$')then + return tonumber(v) -- : int + elseif sfind(v, '^[%+%-]?[0-9]+%.[0-9]+$') then + return tonumber(v) + end + return s or v +end + +local parsemap; -- : func + +local function parseseq(line, lines, indent) + local seq = setmetatable({}, types.seq) + if line ~= '' then + error() + end + while #lines > 0 do + -- Check for a new document + line = lines[1] + if startswith(line, '---') then + while #lines > 0 and not startswith(lines, '---') do + tremove(lines, 1) + end + return seq + end + + -- Check the indent level + local level = countindent(line) + if level < indent then + return seq + elseif level > indent then + error("found bad indenting in line: ".. line) + end + + local i, j = sfind(line, '%-%s+') + if not i then + i, j = sfind(line, '%-$') + if not i then + return seq + end + end + local rest = ssub(line, j+1) + + if sfind(rest, '^[^\'\"%s]*:') then + -- Inline nested hash + local indent2 = j + lines[1] = string.rep(' ', indent2)..rest + tinsert(seq, parsemap('', lines, indent2)) + elseif sfind(rest, '^%-%s+') then + -- Inline nested seq + local indent2 = j + lines[1] = string.rep(' ', indent2)..rest + tinsert(seq, parseseq('', lines, indent2)) + elseif isemptyline(rest) then + tremove(lines, 1) + if #lines == 0 then + tinsert(seq, null) + return seq + end + if sfind(lines[1], '^%s*%-') then + local nextline = lines[1] + local indent2 = countindent(nextline) + if indent2 == indent then + -- Null seqay entry + tinsert(seq, null) + else + tinsert(seq, parseseq('', lines, indent2)) + end + else + -- - # comment + -- key: value + local nextline = lines[1] + local indent2 = countindent(nextline) + tinsert(seq, parsemap('', lines, indent2)) + end + elseif rest then + -- Array entry with a value + tremove(lines, 1) + tinsert(seq, parsescalar(rest, lines)) + end + end + return seq +end + +local function parseset(line, lines, indent) + if not isemptyline(line) then + error('not seq line: '..line) + end + local set = setmetatable({}, types.set) + while #lines > 0 do + -- Check for a new document + line = lines[1] + if startswith(line, '---') then + while #lines > 0 and not startswith(lines, '---') do + tremove(lines, 1) + end + return set + end + + -- Check the indent level + local level = countindent(line) + if level < indent then + return set + elseif level > indent then + error("found bad indenting in line: ".. line) + end + + local i, j = sfind(line, '%?%s+') + if not i then + i, j = sfind(line, '%?$') + if not i then + return set + end + end + local rest = ssub(line, j+1) + + if sfind(rest, '^[^\'\"%s]*:') then + -- Inline nested hash + local indent2 = j + lines[1] = string.rep(' ', indent2)..rest + set[parsemap('', lines, indent2)] = true + elseif sfind(rest, '^%s+$') then + tremove(lines, 1) + if #lines == 0 then + tinsert(set, null) + return set + end + if sfind(lines[1], '^%s*%?') then + local indent2 = countindent(lines[1]) + if indent2 == indent then + -- Null array entry + set[null] = true + else + set[parseseq('', lines, indent2)] = true + end + end + + elseif rest then + tremove(lines, 1) + set[parsescalar(rest, lines)] = true + else + error("failed to classify line: "..line) + end + end + return set +end + +function parsemap(line, lines, indent) + if not isemptyline(line) then + error('not map line: '..line) + end + local map = setmetatable({}, types.map) + while #lines > 0 do + -- Check for a new document + line = lines[1] + if startswith(line, '---') then + while #lines > 0 and not startswith(lines, '---') do + tremove(lines, 1) + end + return map + end + + -- Check the indent level + local level, _ = countindent(line) + if level < indent then + return map + elseif level > indent then + error("found bad indenting in line: ".. line) + end + + -- Find the key + local key + local s, rest = parsestring(line) + + -- Quoted keys + if s and startswith(rest, ':') then + local sc = parsescalar(s, {}, 0) + if sc and type(sc) ~= 'string' then + key = sc + else + key = s + end + line = ssub(rest, 2) + else + error("failed to classify line: "..line) + end + + key = checkdupekey(map, key) + line = ltrim(line) + + if ssub(line, 1, 1) == '!' then + -- ignore type + local rh = ltrim(ssub(line, 3)) + local typename = smatch(rh, '^!?[^%s]+') + line = ltrim(ssub(rh, #typename+1)) + end + + if not isemptyline(line) then + tremove(lines, 1) + line = ltrim(line) + map[key] = parsescalar(line, lines, indent) + else + -- An indent + tremove(lines, 1) + if #lines == 0 then + map[key] = null + return map; + end + if sfind(lines[1], '^%s*%-') then + local indent2 = countindent(lines[1]) + map[key] = parseseq('', lines, indent2) + elseif sfind(lines[1], '^%s*%?') then + local indent2 = countindent(lines[1]) + map[key] = parseset('', lines, indent2) + else + local indent2 = countindent(lines[1]) + if indent >= indent2 then + -- Null hash entry + map[key] = null + else + map[key] = parsemap('', lines, indent2) + end + end + end + end + return map +end + + +-- : (list)->dict +local function parsedocuments(lines) + lines = select(lines, function(s) return not isemptyline(s) end) + + if sfind(lines[1], '^%%YAML') then tremove(lines, 1) end + + local root = {} + local in_document = false + while #lines > 0 do + local line = lines[1] + -- Do we have a document header? + local docright; + if sfind(line, '^%-%-%-') then + -- Handle scalar documents + docright = ssub(line, 4) + tremove(lines, 1) + in_document = true + end + if docright then + if (not sfind(docright, '^%s+$') and + not sfind(docright, '^%s+#')) then + tinsert(root, parsescalar(docright, lines)) + end + elseif #lines == 0 or startswith(line, '---') then + -- A naked document + tinsert(root, null) + while #lines > 0 and not sfind(lines[1], '---') do + tremove(lines, 1) + end + in_document = false + -- XXX The final '-+$' is to look for -- which ends up being an + -- error later. + elseif not in_document and #root > 0 then + -- only the first document can be explicit + error('parse error: '..line) + elseif sfind(line, '^%s*%-') then + -- An array at the root + tinsert(root, parseseq('', lines, 0)) + elseif sfind(line, '^%s*[^%s]') then + -- A hash at the root + local level = countindent(line) + tinsert(root, parsemap('', lines, level)) + else + -- Shouldn't get here. @lines have whitespace-only lines + -- stripped, and previous match is a line with any + -- non-whitespace. So this clause should only be reachable via + -- a perlbug where \s is not symmetric with \S + + -- uncoverable statement + error('parse error: '..line) + end + end + if #root > 1 and Null.isnull(root[1]) then + tremove(root, 1) + return root + end + return root +end + +--- Parse yaml string into table. +local function parse(source) + local lines = {} + for line in string.gmatch(source .. '\n', '(.-)\r?\n') do + tinsert(lines, line) + end + + local docs = parsedocuments(lines) + if #docs == 1 then + return docs[1] + end + + return docs +end + +return { + version = 0.1, + parse = parse, +} \ No newline at end of file