fixed drift in timer
This commit is contained in:
parent
c3496cfdbe
commit
2d74ca0745
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
167
gui/addons/extensions.lua
Normal 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
|
||||||
1376
gui/addons/extensions_old.lua
Normal file
1376
gui/addons/extensions_old.lua
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,3 @@
|
|||||||
|
-- Addons modify the gui interface directly and do not return anything.
|
||||||
|
require("gui.addons.extensions")
|
||||||
|
require("gui.addons.system")
|
||||||
@ -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
|
|
||||||
@ -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 ────────────────────────────────────────────────────
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -435,24 +435,3 @@ function GifLoader.getInfo(gif)
|
|||||||
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
|
|
||||||
]]
|
|
||||||
@ -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)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -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
|
|
||||||
306
gui/init.lua
306
gui/init.lua
@ -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
122
main.lua
@ -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")
|
||||||
|
|||||||
207
utils.lua
207
utils.lua
@ -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,22 +151,45 @@ 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()
|
||||||
|
|
||||||
|
local function cleanup()
|
||||||
|
if stopped then return end
|
||||||
|
stopped = true
|
||||||
|
if onTimeConn and not onTimeConn.destroyed then
|
||||||
|
onTimeConn:Destroy()
|
||||||
end
|
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
|
if opt.autoColor then
|
||||||
tpie.color = opt.startColor
|
tpie.color = opt.startColor
|
||||||
if num <= timeRemaining/3 then
|
if num <= timeRemaining / 3 then
|
||||||
tpie.color = opt.timeColor
|
tpie.color = opt.timeColor
|
||||||
elseif num <= timeRemaining/2 then
|
elseif num <= timeRemaining / 2 then
|
||||||
tpie.color = opt.warnColor
|
tpie.color = opt.warnColor
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
tpie:makeArc("pie", -200, 0, 100, 1, 0, 0, 1.5 * math.pi, p * math.pi, 360)
|
tpie:makeArc("pie", -200, 0, 100, 1, 0, 0, 1.5*math.pi, p*math.pi, 360)
|
||||||
|
|
||||||
if opt.autoText then
|
if opt.autoText then
|
||||||
tlabel.text = num
|
tlabel.text = num
|
||||||
@ -67,39 +197,46 @@ local function startTimer(opt)
|
|||||||
tlabel:centerFont()
|
tlabel:centerFont()
|
||||||
end
|
end
|
||||||
|
|
||||||
if num == 0 then
|
if opt.finegrained then
|
||||||
thread:newThread("Pie Timer",function()
|
onTimeConn:Fire(tm, num, t)
|
||||||
thread.yield()
|
elseif num < lastSecond then
|
||||||
|
-- crossed a second boundary
|
||||||
|
lastSecond = num
|
||||||
|
onTimeConn:Fire(tm, num, t)
|
||||||
|
end
|
||||||
|
|
||||||
|
if elapsed >= timeRemaining then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
-- Timer finished
|
||||||
|
if stopped then return end
|
||||||
|
|
||||||
if opt.autoText then
|
if opt.autoText then
|
||||||
tlabel.text = "Time"
|
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)
|
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
|
local onStop = onStopConn
|
||||||
end
|
cleanup()
|
||||||
|
onStop:Fire(tm)
|
||||||
|
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
|
||||||
|
|
||||||
|
|||||||
602
webp-old.lua
602
webp-old.lua
@ -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
|
|
||||||
Loading…
x
Reference in New Issue
Block a user