fixed drift in timer

This commit is contained in:
Ryan Ward 2026-05-12 21:10:14 -07:00
parent c3496cfdbe
commit 2d74ca0745
21 changed files with 3718 additions and 3773 deletions

View File

@ -9,7 +9,7 @@ settings:
categories: # the name of each category should correspond to a yaml file with the same name 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 - 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 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 image: "assets/14018-3193093789.gif" # if set will display image instead of name, categories are referenced by name which is required
- name: openings - name: openings
displayName: "Openings" # if blank will use name displayName: "Openings" # if blank will use name
- name: sadness - name: sadness

View File

@ -150,7 +150,7 @@ local function buildBoard(frame, path)
t.index = tier t.index = tier
t.price = start + inc*(tier-1) t.price = start + inc*(tier-1)
t:OnReleased(boardUpdater:newFunction(function(self) t:OnReleased(boardUpdater:newFunction(function(self)
if self.text == "" then return end if self.text == "" or GetActivePlayer() == nil then return end
if dd_enabled then if dd_enabled then
applyDD() -- check and run DD if conditions meet applyDD() -- check and run DD if conditions meet
end end

View File

@ -37,4 +37,36 @@ Events:
- Other Events - Other Events
- ~~OnUpdate~~ ✔️ - ~~OnUpdate~~ ✔️
- ~~OnDraw~~ ✔️ - ~~OnDraw~~ ✔️
Polish:
- ~~gui:newCheckbox()~~
- ~~gui:isOnScreen()~~
- ~~gui:newRadioGroup()~~
- gui:newSlider()
- ~~gui:newProgressBar()~~
- gui:newTooltip()
- gui:newTextArea()
- TODO: selection, hotkeys
- gui:newListFrame()
- gui:newGridFrame()
Better Transistions:
- ease.easeIn(start, stop, time) -- accelerates from start
- ease.easeOut(start, stop, time) -- decelerates into stop
- ease.easeInOut(start, stop, time) -- smooth S-curve
- ease.easeInCubic(...) -- more aggressive acceleration
- ease.easeOutCubic(...) -- more aggressive deceleration
- ease.bounce(start, stop, time) -- bounces at the end
- ease.elastic(start, stop, time) -- overshoots then settles
- ease.back(start, stop, time) -- slight pull-back before moving
Focus Management:
- gui.focus.set(element) -- programmatically set focus
- gui.focus.get() -- returns currently focused element (or nil)
- gui.focus.clear() -- clear focus (no element focused)
- gui.focus.setTabOrder(list) -- set a list of elements for Tab navigation
- gui.focus.tabNext() -- focus the next element in the tab order
- gui.focus.tabPrev() -- focus the previous element
Z-index Enhancments:
- gui:setLayer(n)
- gui:getLayer()

167
gui/addons/extensions.lua Normal file
View File

@ -0,0 +1,167 @@
local gui = require("gui")
local theme = require("gui.core.theme")
local color = require("gui.core.color")
local multi, thread = require("multi"):init()
local mediaProc = gui:newProcessor()
local function noOf(sx,sy,sw,sh)
return nil,nil,nil,nil,sx,sy,sw,sh
end
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()
mediaProc:newThread(function()
while true do
thread.yield()
seeker:setDualDim(nil,nil,nil,nil,nil,nil,video:tell()/length)
end
end)
-- print()
end
function gui:newCheckbox(label, x, y, size, sx, sy, checked)
local checkbox = self:newFrame(x, y, size, size, sx, sy)
checkbox.color = color.black
local border = checkbox:newVisualFrame(noOf(.1,.1,.8,.8))
border.color = color.white
local toggle = border:newFrame(noOf(.3,.3,.4,.4))
toggle.color = color.black
toggle.visible = false
checkbox:OnReleased(function()
checkbox:check(not toggle.visible)
end)
if label ~= "" then
local text = checkbox:newTextLabel(label, noOf(1.25,0,15,1))
text:OnUpdate(function()
text:centerFont()
end)
text:setFont(size-2)
text.visibility = 0
end
function checkbox:check(value)
toggle.visible = value
self.OnChanged:Fire(value)
end
function checkbox:isChecked()
return toggle.visible
end
function checkbox:getLabel()
return label or ""
end
checkbox.OnChanged = multi:newConnection()
return checkbox
end
function gui:newRadioGroup(options, x, y, sx, sy, size)
local group = {}
local rg = self:newFrame()
local selected
rg.OnSelectionChanged = multi:newConnection()
for i,v in ipairs(options or {}) do
table.insert(group,self:newCheckbox(tostring(v),x,y+((i-1)*size+((options.padding or 0)*(i-1))),size,sx,sy))
end
gui.apply({
OnReleased=function(self)
gui.apply({check={false}},unpack(group))
self:check(true)
if selected ~= self then
rg.OnSelectionChanged:Fire(rg, self)
end
selected = self
end,
},unpack(group))
function rg:getSelectedOption()
return selected
end
return rg
end
function gui:newProgressBar(x, y, w, h, sx, sy, sw, sh, count, value)
local value = value or 0
local progressbar = self:newFrame(x,y,w,h,sx,sy,sw,sh)
local fillframe = progressbar:newFrame(noOf(.025, .1, .95, .8))
local fill = fillframe:newFrame(noOf(0, 0, 1, 1))
fillframe.visibility = 0
progressbar.color = color.new("#000000")
fill.color = color.new("#ffffff")
progressbar.fillframe = fillframe
progressbar.fill = fill
function progressbar:update(value)
if value > count then value = count end
if value < 0 then value = 0 end
local percent = value/count
fill:setDualDim(noOf(nil,nil,percent))
end
function progressbar:add(n)
if value >= count then
return
end
value = value + n
self:update(value)
end
function progressbar:sub(n)
if value <= 0 then
return
end
value = value - n
self:update(value)
end
function progressbar:max()
value = count
self:update(value)
end
function progressbar:min()
value = 0
self:update(value)
end
progressbar:update(value)
-- to change colors and modify main components
return progressbar, fill, fillframe
end

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,3 @@
-- Addons modify the gui interface directly and do not return anything.
require("gui.addons.extensions")
require("gui.addons.system")

View File

@ -1,43 +0,0 @@
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

View File

@ -155,149 +155,6 @@ local function collectTasks()
return rows return rows
end 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) function gui:newScrollFrame(x, y, w, h, sx, sy, sw, sh)
local viewport = self:newFrame(x, y, w, h, sx, sy, sw, sh) local viewport = self:newFrame(x, y, w, h, sx, sy, sw, sh)
viewport.clipDescendants = true viewport.clipDescendants = true
@ -453,6 +310,148 @@ function gui:newScrollFrame(x, y, w, h, sx, sy, sw, sh)
return content return content
end 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
-- ── row pool ────────────────────────────────────────────────────────────────── -- ── row pool ──────────────────────────────────────────────────────────────────
local COLOR_PROC_ROW = TM_THEME.colorPrimaryDark local COLOR_PROC_ROW = TM_THEME.colorPrimaryDark
local COLOR_ROW_EVEN = TM_THEME.colorPrimary local COLOR_ROW_EVEN = TM_THEME.colorPrimary
@ -896,7 +895,7 @@ function gui:showTaskManager()
-- ── load probe ──────────────────────────────────────────────────────────── -- ── load probe ────────────────────────────────────────────────────────────
-- Install once. getLoad() is now non-blocking — just reads the EMA state. -- Install once. getLoad() is now non-blocking — just reads the EMA state.
local schedulerProbe = require("gui.addons.probe") local schedulerProbe = require("gui.core.probe")
schedulerProbe:install(multi) schedulerProbe:install(multi)
-- ── main-thread update ──────────────────────────────────────────────────── -- ── main-thread update ────────────────────────────────────────────────────

View File

@ -1,22 +1,22 @@
local color={} local color={}
local mt = { local mt = {
__add = function (c1,c2) __add = function (c1,c2)
return color.new(c1[1]+c2[1],c1[2]+c2[2],c1[2]+c2[2]) return color.new(c1[1]+c2[1],c1[2]+c2[2],c1[3]+c2[3])
end, end,
__sub = function (c1,c2) __sub = function (c1,c2)
return color.new(c1[1]-c2[1],c1[2]-c2[2],c1[2]-c2[2]) return color.new(c1[1]-c2[1],c1[2]-c2[2],c1[3]-c2[3])
end, end,
__mul = function (c1,c2) __mul = function (c1,c2)
return color.new(c1[1]*c2[1],c1[2]*c2[2],c1[2]*c2[2]) return color.new(c1[1]*c2[1],c1[2]*c2[2],c1[3]*c2[3])
end, end,
__div = function (c1,c2) __div = function (c1,c2)
return color.new(c1[1]/c2[1],c1[2]/c2[2],c1[2]/c2[2]) return color.new(c1[1]/c2[1],c1[2]/c2[2],c1[3]/c2[3])
end, end,
__mod = function (c1,c2) __mod = function (c1,c2)
return color.new(c1[1]%c2[1],c1[2]%c2[2],c1[2]%c2[2]) return color.new(c1[1]%c2[1],c1[2]%c2[2],c1[3]%c2[3])
end, end,
__pow = function (c1,c2) __pow = function (c1,c2)
return color.new(c1[1]^c2[1],c1[2]^c2[2],c1[2]^c2[2]) return color.new(c1[1]^c2[1],c1[2]^c2[2],c1[3]^c2[3])
end, end,
__unm = function (c1) __unm = function (c1)
return color.new(-c1[1],-c1[2],-c1[2]) return color.new(-c1[1],-c1[2],-c1[2])
@ -25,13 +25,13 @@ local mt = {
return "("..c[1]..","..c[2]..","..c[3]..",".. (c[4] or "1") ..")" return "("..c[1]..","..c[2]..","..c[3]..",".. (c[4] or "1") ..")"
end, end,
__eq = function (c1,c2) __eq = function (c1,c2)
return (c1[1]==c2[1] and c1[2]==c2[2] and c1[2]==c2[2]) return (c1[1]==c2[1] and c1[2]==c2[2] and c1[3]==c2[3])
end, end,
__lt = function (c1,c2) __lt = function (c1,c2)
return (c1[1]<c2[1] and c1[2]<c2[2] and c1[2]<c2[2]) return (c1[1]<c2[1] and c1[2]<c2[2] and c1[3]<c2[3])
end, end,
__le = function (c1,c2) __le = function (c1,c2)
return (c1[1]<=c2[1] and c1[2]<=c2[2] and c1[2]<=c2[2]) return (c1[1]<=c2[1] and c1[2]<=c2[2] and c1[3]<=c2[3])
end end
} }

View File

@ -1,458 +1,437 @@
-- GIF Loader for Love2D with LZW Decompression -- GIF Loader for Love2D with LZW Decompression
-- Note: love.data.compress/decompress don't support LZW, so we implement it -- Note: love.data.compress/decompress don't support LZW, so we implement it
local GifLoader = {} local GifLoader = {}
-- Pure Lua LZW decompression for GIF -- Pure Lua LZW decompression for GIF
local function decompressLZW(data, minCodeSize) local function decompressLZW(data, minCodeSize)
local clearCode = 2 ^ minCodeSize local clearCode = 2 ^ minCodeSize
local endCode = clearCode + 1 local endCode = clearCode + 1
local nextCode = endCode + 1 local nextCode = endCode + 1
local codeSize = minCodeSize + 1 local codeSize = minCodeSize + 1
local dict = {} local dict = {}
for i = 0, clearCode - 1 do for i = 0, clearCode - 1 do
dict[i] = {string.byte(string.char(i))} dict[i] = {string.byte(string.char(i))}
end end
local output = {} local output = {}
local bits = 0 local bits = 0
local bitBuffer = 0 local bitBuffer = 0
local pos = 1 local pos = 1
local prevCode = nil local prevCode = nil
local function readCode() local function readCode()
while bits < codeSize do while bits < codeSize do
if pos > #data then return nil end if pos > #data then return nil end
bitBuffer = bitBuffer + bit.lshift(string.byte(data, pos), bits) bitBuffer = bitBuffer + bit.lshift(string.byte(data, pos), bits)
bits = bits + 8 bits = bits + 8
pos = pos + 1 pos = pos + 1
end end
local code = bit.band(bitBuffer, bit.lshift(1, codeSize) - 1) local code = bit.band(bitBuffer, bit.lshift(1, codeSize) - 1)
bitBuffer = bit.rshift(bitBuffer, codeSize) bitBuffer = bit.rshift(bitBuffer, codeSize)
bits = bits - codeSize bits = bits - codeSize
return code return code
end end
local first = true local first = true
while true do while true do
local code = readCode() local code = readCode()
if not code or code == endCode then break end if not code or code == endCode then break end
if code == clearCode then if code == clearCode then
dict = {} dict = {}
for i = 0, clearCode - 1 do for i = 0, clearCode - 1 do
dict[i] = {string.byte(string.char(i))} dict[i] = {string.byte(string.char(i))}
end end
nextCode = endCode + 1 nextCode = endCode + 1
codeSize = minCodeSize + 1 codeSize = minCodeSize + 1
prevCode = nil prevCode = nil
first = true first = true
else else
local entry local entry
if dict[code] then if dict[code] then
entry = dict[code] entry = dict[code]
elseif code == nextCode and prevCode then elseif code == nextCode and prevCode then
-- Special case: code not in dict yet -- Special case: code not in dict yet
entry = {} entry = {}
for i = 1, #dict[prevCode] do for i = 1, #dict[prevCode] do
entry[i] = dict[prevCode][i] entry[i] = dict[prevCode][i]
end end
entry[#entry + 1] = dict[prevCode][1] entry[#entry + 1] = dict[prevCode][1]
else else
-- Invalid code, stop -- Invalid code, stop
break break
end end
-- Output the entry -- Output the entry
for i = 1, #entry do for i = 1, #entry do
table.insert(output, entry[i]) table.insert(output, entry[i])
end end
-- Add new entry to dictionary -- Add new entry to dictionary
if not first and prevCode and nextCode < 4096 then if not first and prevCode and nextCode < 4096 then
local newEntry = {} local newEntry = {}
for i = 1, #dict[prevCode] do for i = 1, #dict[prevCode] do
newEntry[i] = dict[prevCode][i] newEntry[i] = dict[prevCode][i]
end end
newEntry[#newEntry + 1] = entry[1] newEntry[#newEntry + 1] = entry[1]
dict[nextCode] = newEntry dict[nextCode] = newEntry
nextCode = nextCode + 1 nextCode = nextCode + 1
-- Increase code size when needed -- Increase code size when needed
if nextCode >= bit.lshift(1, codeSize) and codeSize < 12 then if nextCode >= bit.lshift(1, codeSize) and codeSize < 12 then
codeSize = codeSize + 1 codeSize = codeSize + 1
end end
end end
prevCode = code prevCode = code
first = false first = false
end end
end end
-- Convert output bytes to string -- Convert output bytes to string
local result = {} local result = {}
for i = 1, #output do for i = 1, #output do
result[i] = string.char(output[i]) result[i] = string.char(output[i])
end end
return table.concat(result) return table.concat(result)
end end
function GifLoader.load(filepath) function GifLoader.load(filepath)
local fileData = love.filesystem.read(filepath) local fileData = love.filesystem.read(filepath)
if not fileData then if not fileData then
error("Could not read GIF file: " .. filepath) error("Could not read GIF file: " .. filepath)
end end
local gif = { local gif = {
frames = {}, frames = {},
frameData = {}, -- Store ImageData for frame composition frameData = {}, -- Store ImageData for frame composition
delays = {}, delays = {},
currentFrame = 1, currentFrame = 1,
timer = 0, timer = 0,
width = 0, width = 0,
height = 0, height = 0,
playing = true, playing = true,
loop = true, loop = true,
getWidth = function(self) getWidth = function(self)
return self.width return self.width
end, end,
getHeight = function(self) getHeight = function(self)
return self.height return self.height
end, end,
} }
-- Parse GIF header -- Parse GIF header
local header = fileData:sub(1, 6) local header = fileData:sub(1, 6)
if header ~= "GIF87a" and header ~= "GIF89a" then if header ~= "GIF87a" and header ~= "GIF89a" then
error("Not a valid GIF file") error("Not a valid GIF file")
end end
-- Read logical screen descriptor -- Read logical screen descriptor
local pos = 7 local pos = 7
gif.width = string.byte(fileData, pos) + string.byte(fileData, pos + 1) * 256 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 gif.height = string.byte(fileData, pos + 2) + string.byte(fileData, pos + 3) * 256
local packed = string.byte(fileData, pos + 4) local packed = string.byte(fileData, pos + 4)
local hasGlobalColorTable = bit.band(packed, 0x80) ~= 0 local hasGlobalColorTable = bit.band(packed, 0x80) ~= 0
local backgroundColorIndex = string.byte(fileData, pos + 5) local backgroundColorIndex = string.byte(fileData, pos + 5)
pos = pos + 7 pos = pos + 7
-- Read global color table -- Read global color table
local globalColorTable = {} local globalColorTable = {}
if hasGlobalColorTable then if hasGlobalColorTable then
local size = 2 ^ (bit.band(packed, 0x07) + 1) local size = 2 ^ (bit.band(packed, 0x07) + 1)
for i = 1, size do for i = 1, size do
local r = string.byte(fileData, pos) / 255 local r = string.byte(fileData, pos) / 255
local g = string.byte(fileData, pos + 1) / 255 local g = string.byte(fileData, pos + 1) / 255
local b = string.byte(fileData, pos + 2) / 255 local b = string.byte(fileData, pos + 2) / 255
table.insert(globalColorTable, {r, g, b, 1}) table.insert(globalColorTable, {r, g, b, 1})
pos = pos + 3 pos = pos + 3
end end
end end
-- Parse blocks -- Parse blocks
local delay = 0.1 local delay = 0.1
local transparentIndex = nil local transparentIndex = nil
local disposalMethod = 0 local disposalMethod = 0
local delayForNextFrame = 0.1 -- Track delay for next frame local delayForNextFrame = 0.1 -- Track delay for next frame
while pos <= #fileData do while pos <= #fileData do
local separator = string.byte(fileData, pos) local separator = string.byte(fileData, pos)
if separator == 0x21 then -- Extension if separator == 0x21 then -- Extension
local label = string.byte(fileData, pos + 1) local label = string.byte(fileData, pos + 1)
pos = pos + 2 pos = pos + 2
if label == 0xF9 then -- Graphic Control Extension if label == 0xF9 then -- Graphic Control Extension
local blockSize = string.byte(fileData, pos) local blockSize = string.byte(fileData, pos)
pos = pos + 1 pos = pos + 1
local flags = string.byte(fileData, pos) local flags = string.byte(fileData, pos)
disposalMethod = bit.rshift(bit.band(flags, 0x1C), 2) disposalMethod = bit.rshift(bit.band(flags, 0x1C), 2)
local hasTransparency = bit.band(flags, 0x01) ~= 0 local hasTransparency = bit.band(flags, 0x01) ~= 0
local delayTime = string.byte(fileData, pos + 1) + string.byte(fileData, pos + 2) * 256 local delayTime = string.byte(fileData, pos + 1) + string.byte(fileData, pos + 2) * 256
-- GIF delay is in hundredths of a second, convert to seconds -- GIF delay is in hundredths of a second, convert to seconds
-- Many GIFs use 0 or very small delays, set a minimum -- Many GIFs use 0 or very small delays, set a minimum
if delayTime == 0 then if delayTime == 0 then
delayForNextFrame = 0.1 -- Default 100ms delayForNextFrame = 0.1 -- Default 100ms
elseif delayTime <= 2 then elseif delayTime <= 2 then
delayForNextFrame = 0.02 -- Minimum 20ms for very fast animations delayForNextFrame = 0.02 -- Minimum 20ms for very fast animations
else else
delayForNextFrame = delayTime / 100 delayForNextFrame = delayTime / 100
end end
if hasTransparency then if hasTransparency then
transparentIndex = string.byte(fileData, pos + 3) transparentIndex = string.byte(fileData, pos + 3)
else else
transparentIndex = nil transparentIndex = nil
end end
pos = pos + blockSize + 1 pos = pos + blockSize + 1
else else
-- Skip other extensions -- Skip other extensions
repeat repeat
local blockSize = string.byte(fileData, pos) local blockSize = string.byte(fileData, pos)
pos = pos + 1 pos = pos + 1
if blockSize > 0 then if blockSize > 0 then
pos = pos + blockSize pos = pos + blockSize
end end
until blockSize == 0 until blockSize == 0
end end
elseif separator == 0x2C then -- Image Descriptor elseif separator == 0x2C then -- Image Descriptor
pos = pos + 1 pos = pos + 1
local left = string.byte(fileData, pos) + string.byte(fileData, pos + 1) * 256 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 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 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 height = string.byte(fileData, pos + 6) + string.byte(fileData, pos + 7) * 256
local imgPacked = string.byte(fileData, pos + 8) local imgPacked = string.byte(fileData, pos + 8)
local hasLocalColorTable = bit.band(imgPacked, 0x80) ~= 0 local hasLocalColorTable = bit.band(imgPacked, 0x80) ~= 0
local interlaced = bit.band(imgPacked, 0x40) ~= 0 local interlaced = bit.band(imgPacked, 0x40) ~= 0
pos = pos + 9 pos = pos + 9
local colorTable = globalColorTable local colorTable = globalColorTable
if hasLocalColorTable then if hasLocalColorTable then
local size = 2 ^ (bit.band(imgPacked, 0x07) + 1) local size = 2 ^ (bit.band(imgPacked, 0x07) + 1)
colorTable = {} colorTable = {}
for i = 1, size do for i = 1, size do
local r = string.byte(fileData, pos) / 255 local r = string.byte(fileData, pos) / 255
local g = string.byte(fileData, pos + 1) / 255 local g = string.byte(fileData, pos + 1) / 255
local b = string.byte(fileData, pos + 2) / 255 local b = string.byte(fileData, pos + 2) / 255
table.insert(colorTable, {r, g, b, 1}) table.insert(colorTable, {r, g, b, 1})
pos = pos + 3 pos = pos + 3
end end
end end
-- Read LZW minimum code size -- Read LZW minimum code size
local minCodeSize = string.byte(fileData, pos) local minCodeSize = string.byte(fileData, pos)
pos = pos + 1 pos = pos + 1
-- Read compressed image data blocks -- Read compressed image data blocks
local compressedData = {} local compressedData = {}
while true do while true do
local blockSize = string.byte(fileData, pos) local blockSize = string.byte(fileData, pos)
pos = pos + 1 pos = pos + 1
if blockSize == 0 then break end if blockSize == 0 then break end
table.insert(compressedData, fileData:sub(pos, pos + blockSize - 1)) table.insert(compressedData, fileData:sub(pos, pos + blockSize - 1))
pos = pos + blockSize pos = pos + blockSize
end end
-- Decompress image data -- Decompress image data
local indexStream = decompressLZW(table.concat(compressedData), minCodeSize) local indexStream = decompressLZW(table.concat(compressedData), minCodeSize)
-- Create image data -- Create image data
local imageData = love.image.newImageData(gif.width, gif.height) local imageData = love.image.newImageData(gif.width, gif.height)
-- Fill with background if first frame -- Fill with background if first frame
if #gif.frames == 0 and backgroundColorIndex and globalColorTable[backgroundColorIndex + 1] then if #gif.frames == 0 and backgroundColorIndex and globalColorTable[backgroundColorIndex + 1] then
local bg = globalColorTable[backgroundColorIndex + 1] local bg = globalColorTable[backgroundColorIndex + 1]
for y = 0, gif.height - 1 do for y = 0, gif.height - 1 do
for x = 0, gif.width - 1 do for x = 0, gif.width - 1 do
imageData:setPixel(x, y, bg[1], bg[2], bg[3], bg[4]) imageData:setPixel(x, y, bg[1], bg[2], bg[3], bg[4])
end end
end end
elseif #gif.frames > 0 then elseif #gif.frames > 0 then
-- Copy previous frame if needed -- Copy previous frame if needed
local prevData = gif.frameData[#gif.frameData] local prevData = gif.frameData[#gif.frameData]
imageData:paste(prevData, 0, 0, 0, 0, gif.width, gif.height) imageData:paste(prevData, 0, 0, 0, 0, gif.width, gif.height)
end end
-- Draw current frame -- Draw current frame
if indexStream and #indexStream > 0 then if indexStream and #indexStream > 0 then
local idx = 1 local idx = 1
-- Use mapPixel for faster pixel operations -- Use mapPixel for faster pixel operations
local function setPixels(x, y, r, g, b, a) local function setPixels(x, y, r, g, b, a)
if x >= left and x < left + width and y >= top and y < top + height then if x >= left and x < left + width and y >= top and y < top + height then
local pixelIdx = (y - top) * width + (x - left) + 1 local pixelIdx = (y - top) * width + (x - left) + 1
if pixelIdx <= #indexStream then if pixelIdx <= #indexStream then
local colorIndex = string.byte(indexStream, pixelIdx) local colorIndex = string.byte(indexStream, pixelIdx)
-- Skip transparent pixels -- Skip transparent pixels
if transparentIndex == nil or colorIndex ~= transparentIndex then if transparentIndex == nil or colorIndex ~= transparentIndex then
if colorTable[colorIndex + 1] then if colorTable[colorIndex + 1] then
local color = colorTable[colorIndex + 1] local color = colorTable[colorIndex + 1]
return color[1], color[2], color[3], color[4] return color[1], color[2], color[3], color[4]
end end
end end
end end
end end
return r, g, b, a return r, g, b, a
end end
-- Only update the region where the frame is located -- Only update the region where the frame is located
for y = top, top + height - 1 do for y = top, top + height - 1 do
for x = left, left + width - 1 do for x = left, left + width - 1 do
local pixelIdx = (y - top) * width + (x - left) + 1 local pixelIdx = (y - top) * width + (x - left) + 1
if pixelIdx <= #indexStream then if pixelIdx <= #indexStream then
local colorIndex = string.byte(indexStream, pixelIdx) local colorIndex = string.byte(indexStream, pixelIdx)
-- Skip transparent pixels -- Skip transparent pixels
if transparentIndex == nil or colorIndex ~= transparentIndex then if transparentIndex == nil or colorIndex ~= transparentIndex then
if colorTable[colorIndex + 1] then if colorTable[colorIndex + 1] then
local color = colorTable[colorIndex + 1] local color = colorTable[colorIndex + 1]
imageData:setPixel(x, y, color[1], color[2], color[3], color[4]) imageData:setPixel(x, y, color[1], color[2], color[3], color[4])
end end
end end
end end
end end
end end
end end
table.insert(gif.frameData, imageData) table.insert(gif.frameData, imageData)
table.insert(gif.frames, love.graphics.newImage(imageData)) table.insert(gif.frames, love.graphics.newImage(imageData))
table.insert(gif.delays, delayForNextFrame) table.insert(gif.delays, delayForNextFrame)
-- Reset delay for next frame -- Reset delay for next frame
delayForNextFrame = 0.1 delayForNextFrame = 0.1
elseif separator == 0x3B then -- Trailer elseif separator == 0x3B then -- Trailer
break break
else else
pos = pos + 1 pos = pos + 1
end end
end end
if #gif.frames == 0 then if #gif.frames == 0 then
error("No frames found in GIF") error("No frames found in GIF")
end end
-- Ensure all frames have valid delays -- Ensure all frames have valid delays
for i = 1, #gif.delays do for i = 1, #gif.delays do
if gif.delays[i] <= 0 or gif.delays[i] ~= gif.delays[i] then -- check for 0 or NaN if gif.delays[i] <= 0 or gif.delays[i] ~= gif.delays[i] then -- check for 0 or NaN
gif.delays[i] = 0.1 gif.delays[i] = 0.1
end end
end end
return gif return gif
end end
function GifLoader.Updater(gif, proc) function GifLoader.Updater(gif, proc)
local wait = function() local wait = function()
return gif.playing or gif.kill return gif.playing or gif.kill
end end
proc:newThread("Gif Handler",function() proc:newThread("Gif Handler",function()
while true do while true do
-- Only run if not paused -- Only run if not paused
if gif.kill then -- When we want to clean up if gif.kill then -- When we want to clean up
thread.kill() thread.kill()
end end
thread.hold(wait) thread.hold(wait)
thread.sleep(gif.delays[gif.currentFrame] * 4) thread.sleep(gif.delays[gif.currentFrame] * 4)
gif.currentFrame = gif.currentFrame + 1 gif.currentFrame = gif.currentFrame + 1
if gif.currentFrame > #gif.frames then if gif.currentFrame > #gif.frames then
if gif.loop then if gif.loop then
gif.currentFrame = 1 gif.currentFrame = 1
else else
gif.currentFrame = #gif.frames gif.currentFrame = #gif.frames
gif.playing = false gif.playing = false
gif.timer = 0 gif.timer = 0
end end
end end
end end
end) end)
end end
function GifLoader.update(gif, dt) function GifLoader.update(gif, dt)
if not gif.playing or #gif.frames <= 1 then return end if not gif.playing or #gif.frames <= 1 then return end
gif.timer = gif.timer + dt gif.timer = gif.timer + dt
-- Simple, accurate frame advancement -- Simple, accurate frame advancement
if gif.timer >= gif.delays[gif.currentFrame] then if gif.timer >= gif.delays[gif.currentFrame] then
-- Subtract the current frame's delay -- Subtract the current frame's delay
gif.timer = gif.timer - gif.delays[gif.currentFrame] gif.timer = gif.timer - gif.delays[gif.currentFrame]
-- Move to next frame -- Move to next frame
gif.currentFrame = gif.currentFrame + 1 gif.currentFrame = gif.currentFrame + 1
if gif.currentFrame > #gif.frames then if gif.currentFrame > #gif.frames then
if gif.loop then if gif.loop then
gif.currentFrame = 1 gif.currentFrame = 1
else else
gif.currentFrame = #gif.frames gif.currentFrame = #gif.frames
gif.playing = false gif.playing = false
gif.timer = 0 gif.timer = 0
end end
end end
-- If we've accumulated too much time (lag spike), cap it -- If we've accumulated too much time (lag spike), cap it
if gif.timer > 0.5 then if gif.timer > 0.5 then
gif.timer = 0 gif.timer = 0
end end
end end
end end
function GifLoader.draw(gif, x, y, r, sx, sy, ox, oy) function GifLoader.draw(gif, x, y, r, sx, sy, ox, oy)
if gif.frames[gif.currentFrame] then 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) 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
end end
function GifLoader.play(gif) function GifLoader.play(gif)
gif.playing = true gif.playing = true
end end
function GifLoader.pause(gif) function GifLoader.pause(gif)
gif.playing = false gif.playing = false
end end
function GifLoader.reset(gif) function GifLoader.reset(gif)
gif.currentFrame = 1 gif.currentFrame = 1
gif.timer = 0 gif.timer = 0
end end
function GifLoader.setFixedFramerate(gif, fps) function GifLoader.setFixedFramerate(gif, fps)
local delay = 1 / fps local delay = 1 / fps
for i = 1, #gif.delays do for i = 1, #gif.delays do
gif.delays[i] = delay gif.delays[i] = delay
end end
end end
function GifLoader.getInfo(gif) function GifLoader.getInfo(gif)
return { return {
width = gif.width, width = gif.width,
height = gif.height, height = gif.height,
frameCount = #gif.frames, frameCount = #gif.frames,
delays = gif.delays, -- Show actual delays for debugging delays = gif.delays, -- Show actual delays for debugging
totalDuration = (function() totalDuration = (function()
local total = 0 local total = 0
for i = 1, #gif.delays do for i = 1, #gif.delays do
total = total + gif.delays[i] total = total + gif.delays[i]
end end
return total return total
end)() end)()
} }
end end
return GifLoader 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
]]

View File

@ -1,6 +1,6 @@
local gui = require("gui") local gui = require("gui")
local multi, thread = require("multi"):init() local multi, thread = require("multi"):init()
local transition = require("gui.elements.transitions") local transition = require("gui.core.transitions")
-- Triggers press then release -- Triggers press then release
local function getPosition(obj, x, y) local function getPosition(obj, x, y)

View File

@ -1,78 +1,78 @@
local gui = require("gui") local gui = require("gui")
local multi, thread = require("multi"):init() local multi, thread = require("multi"):init()
local processor = gui:newProcessor("Transistion Processor") local processor = gui:newProcessor("Transistion Processor")
local transition = {} local transition = {}
local width, height, flags = love.window.getMode() local width, height, flags = love.window.getMode()
local fps = 60 local fps = 60
if flags.refreshrate > 0 then if flags.refreshrate > 0 then
fps = flags.refreshrate fps = flags.refreshrate
end end
transition.__index = transition transition.__index = transition
transition.__call = function(t, start, stop, time, ...) transition.__call = function(t, start, stop, time, ...)
local args = {...} local args = {...}
return function(st, sp, ti) -- allow these values to be overridden 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 not (st or start) or not (sp or stop) then return multi.error("start and stop must be supplied") end
if start == stop then if start == stop then
local temp = { local temp = {
OnStep = function() end, OnStep = function() end,
OnStop = multi:newConnection() OnStop = multi:newConnection()
} }
proc:newTask(function() proc:newTask(function()
temp.OnStop:Fire() temp.OnStop:Fire()
end) end)
return temp return temp
end end
local handle = t.func(t, start, stop, (ti or time) or 1, unpack(args)) local handle = t.func(t, start, stop, (ti or time) or 1, unpack(args))
return { return {
OnStep = handle.OnStatus, OnStep = handle.OnStatus,
OnStop = handle.OnReturn + handle.OnError, OnStop = handle.OnReturn + handle.OnError,
Kill = t.Kill Kill = t.Kill
} }
end end
end end
function transition:newTransition(func) function transition:newTransition(func)
local c = {} local c = {}
setmetatable(c, self) setmetatable(c, self)
c.fps = fps c.fps = fps
c.func = processor:newFunction(func) c.func = processor:newFunction(func)
c.OnStop = multi:newConnection() c.OnStop = multi:newConnection()
c.kill = false c.kill = false
function c:SetFPS(fps) function c:SetFPS(fps)
self.fps = fps self.fps = fps
end end
function c:GetFPS(fps) function c:GetFPS(fps)
return self.fps return self.fps
end end
function c:Kill() function c:Kill()
if c.running then if c.running then
c.kill = true c.kill = true
end end
end end
return c return c
end end
transition.glide = transition:newTransition(function(t, start, stop, time, ...) transition.glide = transition:newTransition(function(t, start, stop, time, ...)
local steps = t.fps*time local steps = t.fps*time
local piece = time/steps local piece = time/steps
local split = stop-start local split = stop-start
t.running = true t.running = true
for i = 0, steps do for i = 0, steps do
if not(t.kill) then if not(t.kill) then
thread.sleep(piece) thread.sleep(piece)
thread.pushStatus(start + i*(split/steps),piece*i) thread.pushStatus(start + i*(split/steps),piece*i)
end end
end end
t.running = false t.running = false
t.kill = false t.kill = false
end) end)
return transition return transition

File diff suppressed because it is too large Load Diff

View File

@ -1,69 +0,0 @@
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

View File

@ -2,10 +2,9 @@ local utf8 = require("utf8")
local multi, thread = require("multi"):init() local multi, thread = require("multi"):init()
local GLOBAL, THREAD = require("multi.integration.loveManager"):init() local GLOBAL, THREAD = require("multi.integration.loveManager"):init()
local color = require("gui.core.color") local color = require("gui.core.color")
local gif = require("gui.addons.gifloader") local gif = require("gui.core.gifloader")
local gui = {} local gui = {}
local updater = multi:newProcessor("UpdateManager", true) local updater = multi:newProcessor("UpdateManager", true)
local drawer = multi:newProcessor("DrawManager", true) local drawer = multi:newProcessor("DrawManager", true)
local bit = require("bit") local bit = require("bit")
@ -139,6 +138,10 @@ end)
-- Hotkeys -- Hotkeys
local function noOf(sx,sy,sw,sh)
return nil,nil,nil,nil,sx,sy,sw,sh
end
local has_hotkey = false local has_hotkey = false
local hot_keys = {} local hot_keys = {}
@ -575,8 +578,7 @@ function gui:isActive()
end end
function gui:isOnScreen() function gui:isOnScreen()
return not self:isOffScreen()
return
end end
-- Base get uniques -- Base get uniques
@ -1123,6 +1125,302 @@ function gui:newTextBase(typ, txt, x, y, w, h, sx, sy, sw, sh)
return c return c
end end
function gui:newTextArea(initialText, x, y, w, h, sx, sy, sw, sh)
-- Outer viewport (clips content)
local viewport = self:newFrame(x, y, w or 0, h or 0, sx, sy, sw, sh)
viewport.clipDescendants = true
viewport.color = color.new("#f9f9f9")
viewport:setRoundness(3, 3)
-- Inner content frame (scrolled by offsetting its y)
local content = viewport:newFrame(2, 2, -4, -4, 0, 0, 1, 0)
content.drawBorder = false
content.color = {0, 0, 0, 0}
content.visibility = 0
-- Cursor line rendering happens via a separate frame
local cursorBar = viewport:newFrame(0, 0, 1, 0)
cursorBar.color = color.new("#222222")
cursorBar.drawBorder = false
cursorBar.ignore = true
cursorBar.visibility = 0
local lines = {}
local lineObjs = {} -- TextLabel per line
local LINE_H = 18
local scrollY = 0
local cursorLine = 1
local cursorCol = 0
local blinkOn = true
local blinkTimer = 0
local BLINK_RATE = 0.5
local focused = false
viewport.OnChanged = multi:newConnection()
viewport.readOnly = false
-- Split a string into lines
local function splitLines(s)
local result = {}
local pos = 1
while true do
local nl = s:find("\n", pos, true)
if nl then
result[#result + 1] = s:sub(pos, nl - 1)
pos = nl + 1
else
result[#result + 1] = s:sub(pos)
break
end
end
return result
end
-- Join lines back to a single string
local function joinLines()
return table.concat(lines, "\n")
end
-- Rebuild all line label objects
local function rebuildLabels()
for _, obj in ipairs(lineObjs) do
obj:destroy()
end
lineObjs = {}
for i, lineText in ipairs(lines) do
local lbl = content:newTextLabel(lineText, 0, (i-1)*LINE_H, 0, LINE_H, 0, 0, 1)
lbl.drawBorder = false
lbl.color = {0, 0, 0, 0}
lbl.visibility = 0
lbl.textColor = color.new("#222222")
lbl.align = gui.ALIGN_LEFT
lbl.ignore = true
lbl:setFont(13)
lineObjs[i] = lbl
end
-- Resize content frame to fit all lines
local totalH = #lines * LINE_H + 4
content:setDualDim(nil, nil, nil, totalH)
end
-- Apply vertical scroll so the cursor stays visible
local function applyScroll()
local _, _, _, vh = viewport:getAbsolutes()
local contentH = #lines * LINE_H + 4
local maxScroll = math.max(0, contentH - vh)
scrollY = math.max(0, math.min(scrollY, maxScroll))
content:rawSetDualDim(2, 2 - scrollY)
end
local function ensureCursorVisible()
local _, _, _, vh = viewport:getAbsolutes()
local cursorY = (cursorLine - 1) * LINE_H
if cursorY < scrollY then
scrollY = cursorY
elseif cursorY + LINE_H > scrollY + vh then
scrollY = cursorY + LINE_H - vh
end
applyScroll()
end
-- Update the cursor bar position
local function updateCursor()
if not focused then
cursorBar.visibility = 0
return
end
local ax, ay = viewport:getAbsolutes()
local lineText = lines[cursorLine] or ""
local font = love.graphics.newFont(13)
local cx = 2 + font:getWidth(lineText:sub(1, cursorCol))
local cy = 2 + (cursorLine - 1) * LINE_H - scrollY
cursorBar:rawSetDualDim(cx, cy, 1, LINE_H)
cursorBar.visibility = blinkOn and 1 or 0
end
local function setText(s)
lines = splitLines(s or "")
if #lines == 0 then lines = {""} end
rebuildLabels()
cursorLine = math.min(cursorLine, #lines)
cursorCol = math.min(cursorCol, #lines[cursorLine])
applyScroll()
updateCursor()
end
function viewport:getText()
return joinLines()
end
function viewport:setText(s)
setText(s)
self.OnChanged:Fire(self, joinLines())
end
function viewport:appendLine(s)
lines[#lines + 1] = s
rebuildLabels()
applyScroll()
end
function viewport:scrollToBottom()
scrollY = math.huge
applyScroll()
end
-- Insert text at cursor
local function insertText(s)
if viewport.readOnly then return end
local line = lines[cursorLine] or ""
-- Handle newlines in inserted text
if s == "\n" then
local before = line:sub(1, cursorCol)
local after = line:sub(cursorCol + 1)
lines[cursorLine] = before
table.insert(lines, cursorLine + 1, after)
cursorLine = cursorLine + 1
cursorCol = 0
else
lines[cursorLine] = line:sub(1, cursorCol) .. s .. line:sub(cursorCol + 1)
cursorCol = cursorCol + #s
end
rebuildLabels()
ensureCursorVisible()
updateCursor()
viewport.OnChanged:Fire(viewport, joinLines())
end
local function deleteBack()
if viewport.readOnly then return end
if cursorCol > 0 then
local line = lines[cursorLine]
lines[cursorLine] = line:sub(1, cursorCol - 1) .. line:sub(cursorCol + 1)
cursorCol = cursorCol - 1
elseif cursorLine > 1 then
-- merge with previous line
local prevLine = lines[cursorLine - 1]
cursorCol = #prevLine
lines[cursorLine - 1] = prevLine .. lines[cursorLine]
table.remove(lines, cursorLine)
cursorLine = cursorLine - 1
end
rebuildLabels()
ensureCursorVisible()
updateCursor()
viewport.OnChanged:Fire(viewport, joinLines())
end
local function deleteForward()
if viewport.readOnly then return end
local line = lines[cursorLine]
if cursorCol < #line then
lines[cursorLine] = line:sub(1, cursorCol) .. line:sub(cursorCol + 2)
elseif cursorLine < #lines then
lines[cursorLine] = line .. lines[cursorLine + 1]
table.remove(lines, cursorLine + 1)
end
rebuildLabels()
updateCursor()
viewport.OnChanged:Fire(viewport, joinLines())
end
-- Mouse click to position cursor
viewport.OnPressed(function(self, mx, my)
focused = true
local _, vy = viewport:getAbsolutes()
local relY = my - vy + scrollY - 2
cursorLine = math.max(1, math.min(#lines, math.floor(relY / LINE_H) + 1))
local lineText = lines[cursorLine] or ""
local font = love.graphics.newFont(13)
local _, vx = viewport:getAbsolutes()
local relX = mx - vx - 2
-- binary-search for cursor column
local col = 0
for i = 1, #lineText do
local w = font:getWidth(lineText:sub(1, i))
if w > relX then break end
col = i
end
cursorCol = col
updateCursor()
end)
viewport.OnPressedOuter(function()
focused = false
updateCursor()
end)
-- Keyboard input (only when focused)
gui.Events.OnTextInputed(function(t)
if not focused then return end
insertText(t)
end)
gui.Events.OnKeyPressed(function(key)
if not focused then return end
if key == "return" or key == "kpenter" then
insertText("\n")
elseif key == "backspace" then
deleteBack()
elseif key == "delete" then
deleteForward()
elseif key == "up" then
cursorLine = math.max(1, cursorLine - 1)
cursorCol = math.min(cursorCol, #(lines[cursorLine] or ""))
ensureCursorVisible(); updateCursor()
elseif key == "down" then
cursorLine = math.min(#lines, cursorLine + 1)
cursorCol = math.min(cursorCol, #(lines[cursorLine] or ""))
ensureCursorVisible(); updateCursor()
elseif key == "left" then
if cursorCol > 0 then
cursorCol = cursorCol - 1
elseif cursorLine > 1 then
cursorLine = cursorLine - 1
cursorCol = #lines[cursorLine]
end
ensureCursorVisible(); updateCursor()
elseif key == "right" then
local lineLen = #(lines[cursorLine] or "")
if cursorCol < lineLen then
cursorCol = cursorCol + 1
elseif cursorLine < #lines then
cursorLine = cursorLine + 1
cursorCol = 0
end
ensureCursorVisible(); updateCursor()
elseif key == "home" then
cursorCol = 0; updateCursor()
elseif key == "end" then
cursorCol = #(lines[cursorLine] or ""); updateCursor()
end
end)
-- Scroll wheel
viewport.OnWheelMoved(function(_, dy)
scrollY = scrollY - dy * 30
applyScroll()
updateCursor()
end)
-- Cursor blink
viewport:OnUpdate(function(self, dt)
blinkTimer = blinkTimer + dt
if blinkTimer >= BLINK_RATE then
blinkTimer = 0
blinkOn = not blinkOn
if focused then
cursorBar.visibility = blinkOn and 1 or 0
end
end
end)
setText(initialText or "")
return viewport
end
function gui:newTextButton(txt, x, y, w, h, sx, sy, sw, sh) 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) local c = self:newTextBase(button, txt, x, y, w, h, sx, sy, sw, sh)
c:respectHierarchy(true) c:respectHierarchy(true)

122
main.lua
View File

@ -1,5 +1,4 @@
local gui, color, theme, utils, board, yaml, loader, system, elements, scoreUpdater local gui, color, theme, utils, board, yaml, loader, scoreUpdater
local activePlayer local activePlayer
local playerList = {} local playerList = {}
local playerStaticList = {} local playerStaticList = {}
@ -49,8 +48,7 @@ function init()
board = require("board") board = require("board")
yaml = require("yaml") yaml = require("yaml")
loader = require("loader") loader = require("loader")
system = require("gui.addons.system") require("gui.addons")
elements = require("gui.elements")
scoreUpdater = gui:getProcessor():newProcessor("score-updater") scoreUpdater = gui:getProcessor():newProcessor("score-updater")
scoreUpdater.Start() scoreUpdater.Start()
@ -165,7 +163,44 @@ function ScoreBoard(frame, x, y, w, h, sx, sy, sw, sh)
end end
local add_player = leaderboard:newFrame(5,-5,-10,0,0,1-PLAYER_HEIGHT,1,PLAYER_HEIGHT) local add_player = leaderboard:newFrame(5,-5,-10,0,0,1-PLAYER_HEIGHT,1,PLAYER_HEIGHT)
local remove_player = leaderboard:newTextButton("Remove Selected",5,-10,-10,0,0,1-2*PLAYER_HEIGHT,1,PLAYER_HEIGHT) local edit_player = leaderboard:newFrame(5,-10,-10,0,0,1-2*PLAYER_HEIGHT,1,PLAYER_HEIGHT)
local remove_player = leaderboard:newTextButton("Remove Selected",5,-15,-10,0,0,1-3*PLAYER_HEIGHT,1,PLAYER_HEIGHT)
remove_player:setFont(20)
remove_player.align = gui.ALIGN_CENTER
local embededWatch = {remove_player}
scoreUpdater:newThread(function()
while true do
thread.sleep(.01)
for i,v in pairs(embededWatch) do
v:centerFont()
end
end
end)
local function embedTextEdit(reference, default, but_text, callback)
reference.color = C_BORDER_NRM
local textbox = reference:newTextBox(default,0,0,0,0,.015,.1,.8,.8)
textbox.textColor = C_GOLD
textbox.blink = false
textbox.color = C_BORDER_TOP
textbox.textColor = C_WHITE
textbox:OnPressed(function()
textbox.text = ""
end)
local button = reference:newTextButton(but_text,5,0,-10,0,.815,.1,.185,.8)
button.color = color.new("#7eae5b")
button:OnReleased(function()
callback(textbox)
end)
gui.apply({
setFont = {20},
align = gui.ALIGN_CENTER
},textbox,button,reference)
table.insert(embededWatch,textbox)
table.insert(embededWatch,button)
end
remove_player.color = color.new("#a13a3a") remove_player.color = color.new("#a13a3a")
remove_player:OnReleased(function() remove_player:OnReleased(function()
local player = GetActivePlayer() local player = GetActivePlayer()
@ -185,42 +220,16 @@ function ScoreBoard(frame, x, y, w, h, sx, sy, sw, sh)
scoreboard:RenderPlayer(playerList) scoreboard:RenderPlayer(playerList)
player.Ref.Frame:destroy() player.Ref.Frame:destroy()
end) end)
add_player.color = C_BORDER_NRM
local textbox = add_player:newTextBox("Player name",0,0,0,0,.015,.1,.8,.8) embedTextEdit(add_player, "Player Name", "Add", function(self)
textbox.textColor = C_GOLD scoreboard:AddPlayer(self.text, "0")
textbox.blink = false
textbox.color = C_BORDER_TOP
textbox.textColor = C_WHITE
textbox:OnPressed(function()
textbox.text = ""
end) end)
-- A bit glitchy embedTextEdit(edit_player, "Modify Score", "Edit", function(self)
-- gui:setHotKey({"return"})(function() local player = GetActivePlayer()
-- local object_focus = gui:getObjectFocus() if player then
-- if object_focus:hasType(gui.TYPE_BOX) then player.Score = self.text
-- scoreboard:AddPlayer(textbox.text, "0") scoreboard:RenderPlayer(playerList)
-- end
-- end)
local addbutton = add_player:newTextButton("Add",5,0,-10,0,.815,.1,.185,.8)
addbutton.color = color.new("#7eae5b")
addbutton:OnReleased(function()
scoreboard:AddPlayer(textbox.text, "0")
end)
gui.apply({
setFont = {20},
align = gui.ALIGN_CENTER
},textbox,addbutton,remove_player)
thread:newThread(function()
while true do
thread.sleep(.01)
textbox:centerFont()
addbutton:centerFont()
remove_player:centerFont()
end end
end) end)
@ -305,22 +314,51 @@ function ScoreBoard(frame, x, y, w, h, sx, sy, sw, sh)
end end
require("gui.addons.players")
-- local webp = require("webp") -- local webp = require("webp")
init()
local function noOf(sx,sy,sw,sh)
return nil,nil,nil,nil,sx,sy,sw,sh
end
function love.load() function love.load()
init()
gui:cacheImage({"assets/checked.png","assets/unchecked.png"}) gui:cacheImage({"assets/checked.png","assets/unchecked.png"})
gui:setAspectSize(1920, 1080) gui:setAspectSize(1920, 1080)
gui.aspect_ratio = true gui.aspect_ratio = true
-- local ext = require("gui.addons.extensions")
local bg = gui:newFrame() local bg = gui:newFrame()
bg:fullFrame() bg:fullFrame()
bg.color = color.new("#242f9b") bg.color = color.new("#242f9b")
-- pb = bg:newProgressBar(0, 0, 200, 40, 0, 0, 0, 0, 200, 0)
-- thread:newThread(function()
-- for i=1,200 do
-- thread.sleep(.01)
-- pb:add(1)
-- end
-- end)
-- local group = bg:newRadioGroup({
-- padding = 5,
-- "Option A",
-- "Option B",
-- "Option C"
-- },0,0,0,0, 80)
-- group.OnSelectionChanged(function(group, selection)
-- print(selection:getLabel())
-- end)
local qframe = bg:newFrame(0, 0, 0, 0, .2, .05, .75, .9) local qframe = bg:newFrame(0, 0, 0, 0, .2, .05, .75, .9)
qframe.color = color.new("#060ee9") qframe.color = color.new("#060ee9")
local scoreboard = ScoreBoard(bg, 0, 0, 0, 0, .015, .05, .170, .9) local scoreboard = ScoreBoard(bg, 0, 0, 0, 0, .015, .05, .170, .9)
board.buildBoard(qframe, "ai-anime") board.buildBoard(qframe, "anime")
-- gui:newVideoPlayer("test.ogv",0,0,428,240) -- gui:newVideoPlayer("test.ogv",0,0,428,240)
-- local img = webp.load("test.webp") -- local img = webp.load("test.webp")

BIN
test.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

233
utils.lua
View File

@ -1,7 +1,117 @@
-- local gui = require("gui")
-- local color = require("gui.core.color")
-- local transition = require("gui.core.transitions")
-- local multi = require("multi"):init()
-- local timer = transition.glide(3.5,1.5,5)
-- local function startTimer(opt)
-- local default = {
-- duration = 30,
-- autoText = true,
-- autoColor = true,
-- autoCleanup = true,
-- 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
-- transition.glide:SetFPS(60)
-- 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
-- tm.OnTime:Destroy()
-- tm.OnStop:Destroy()
-- 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
-- }
local gui = require("gui") local gui = require("gui")
local color = require("gui.core.color") local color = require("gui.core.color")
local transition = require("gui.elements.transitions") local multi, thread = require("multi"):init()
local multi = require("multi"):init()
local function startTimer(opt) local function startTimer(opt)
local default = { local default = {
@ -23,19 +133,16 @@ local function startTimer(opt)
opt.duration = d opt.duration = d
elseif type(opt) ~= "table" then elseif type(opt) ~= "table" then
opt = default opt = default
elseif type(opt) == "table" then else
for i,v in pairs(opt) do for i, v in pairs(opt) do
default[i] = v default[i] = v
end end
opt = default opt = default
end end
local timeRemaining = opt.duration or 30 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 tpie = gui:newFrame():makeArc("pie", -200, 0, 100, 1, 0, 0, 1.5*math.pi, 3.5*math.pi, 360)
local tlabel = tpie:newTextLabel("") local tlabel = tpie:newTextLabel("")
tlabel.textColor = opt.textColor tlabel.textColor = opt.textColor
tlabel.align = gui.ALIGN_CENTER tlabel.align = gui.ALIGN_CENTER
@ -44,62 +151,92 @@ local function startTimer(opt)
tpie.color = opt.startColor tpie.color = opt.startColor
tpie.visibility = opt.visibility tpie.visibility = opt.visibility
local tm, num local tm
local func = function(p,t) local stopped = false
if num ~= timeRemaining-math.floor(t) then local onTimeConn = multi:newConnection()
num = timeRemaining-math.floor(t) local onStopConn = multi:newConnection()
end
if opt.autoColor then local function cleanup()
tpie.color = opt.startColor if stopped then return end
if num <= timeRemaining/3 then stopped = true
tpie.color = opt.timeColor if onTimeConn and not onTimeConn.destroyed then
elseif num <= timeRemaining/2 then onTimeConn:Destroy()
tpie.color = opt.warnColor end
if not tpie.destroyed then
tpie:destroy()
end
end
local timerThread = thread:newThread("Pie Timer", function()
local startTime = love.timer.getTime()
local lastSecond = timeRemaining
while not stopped do
thread.sleep(1/60)
local now = love.timer.getTime()
local elapsed = now - startTime
local t = math.min(elapsed, timeRemaining)
local num = timeRemaining - math.floor(t)
local p = 3.5 - (t / timeRemaining) * 2
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 opt.finegrained then
onTimeConn:Fire(tm, num, t)
elseif num < lastSecond then
-- crossed a second boundary
lastSecond = num
onTimeConn:Fire(tm, num, t)
end
if elapsed >= timeRemaining then
break
end end
end end
tpie:makeArc("pie", -200, 0, 100, 1, 0, 0, 1.5 * math.pi, p * math.pi, 360) -- Timer finished
if stopped then return end
if opt.autoText then if opt.autoText then
tlabel.text = num tlabel.text = "Time"
tlabel:fitFont(nil, nil, {scale=1/2}) tlabel:fitFont(nil, nil, {scale=1/2})
tlabel:centerFont() tlabel:centerFont()
end end
tpie:makeArc("pie", -200, 0, 100, 1, 0, 0, 1.5*math.pi, 3.5*math.pi, 360)
if num == 0 then local onStop = onStopConn
thread:newThread("Pie Timer",function() cleanup()
thread.yield() onStop:Fire(tm)
if opt.autoText then end)
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 = { tm = {
Duration = timeRemaining, Duration = timeRemaining,
Cleanup = function() handle:Kill() tpie:destroy() tm.Cleanup = function() end end, OnTime = onTimeConn,
OnStop = onStopConn,
SetText = function(str) tlabel.text = str end, SetText = function(str) tlabel.text = str end,
SetColor = function(c) tpie.color = c end, SetColor = function(c) tpie.color = c end,
OnStop = multi:newConnection() Cleanup = function()
timerThread:Kill()
cleanup()
end,
} }
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 return tm
end end

View File

@ -1,602 +0,0 @@
-- 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

1626
webp.lua

File diff suppressed because it is too large Load Diff