From 2d74ca0745ce5eeaf238e65c152e5e381a043ec2 Mon Sep 17 00:00:00 2001 From: Ryan Ward Date: Tue, 12 May 2026 21:10:14 -0700 Subject: [PATCH] fixed drift in timer --- anime/index.yml | 2 +- board.lua | 2 +- gui/README.md | 32 + gui/addons/extensions.lua | 167 +++ gui/addons/extensions_old.lua | 1376 ++++++++++++++++++++ gui/addons/init.lua | 3 + gui/addons/players.lua | 43 - gui/addons/system.lua | 287 +++-- gui/core/color.lua | 18 +- gui/{addons => core}/gifloader.lua | 895 +++++++------ gui/{addons => core}/probe.lua | 0 gui/core/simulate.lua | 2 +- gui/{elements => core}/transitions.lua | 154 +-- gui/docs/gui-library-docs.md | 1552 ++++++++++++---------- gui/elements/init.lua | 69 - gui/init.lua | 306 ++++- main.lua | 122 +- test.webp | Bin 256480 -> 0 bytes utils.lua | 233 +++- webp-old.lua | 602 --------- webp.lua | 1626 ------------------------ 21 files changed, 3718 insertions(+), 3773 deletions(-) create mode 100644 gui/addons/extensions.lua create mode 100644 gui/addons/extensions_old.lua delete mode 100644 gui/addons/players.lua rename gui/{addons => core}/gifloader.lua (94%) rename gui/{addons => core}/probe.lua (100%) rename gui/{elements => core}/transitions.lua (95%) delete mode 100644 gui/elements/init.lua delete mode 100644 test.webp delete mode 100644 webp-old.lua delete mode 100644 webp.lua diff --git a/anime/index.yml b/anime/index.yml index 7a917a0..db1eb6f 100644 --- a/anime/index.yml +++ b/anime/index.yml @@ -9,7 +9,7 @@ settings: categories: # the name of each category should correspond to a yaml file with the same name - name: shounen # name must be alphanumeric, cannot start with a number or contain special characters "-" and "." are ok if they are not at the beginning displayName: "Shounen" # if blank will use name - #image: "assets/14018-3193093789.gif" # if set will display image instead of name, categories are referenced by name which is required + image: "assets/14018-3193093789.gif" # if set will display image instead of name, categories are referenced by name which is required - name: openings displayName: "Openings" # if blank will use name - name: sadness diff --git a/board.lua b/board.lua index 187291c..714b004 100644 --- a/board.lua +++ b/board.lua @@ -150,7 +150,7 @@ local function buildBoard(frame, path) t.index = tier t.price = start + inc*(tier-1) 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 applyDD() -- check and run DD if conditions meet end diff --git a/gui/README.md b/gui/README.md index 149e2f3..761438c 100644 --- a/gui/README.md +++ b/gui/README.md @@ -37,4 +37,36 @@ Events: - Other Events - ~~OnUpdate~~ ✔️ - ~~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() \ No newline at end of file diff --git a/gui/addons/extensions.lua b/gui/addons/extensions.lua new file mode 100644 index 0000000..ca1883c --- /dev/null +++ b/gui/addons/extensions.lua @@ -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 \ No newline at end of file diff --git a/gui/addons/extensions_old.lua b/gui/addons/extensions_old.lua new file mode 100644 index 0000000..3f1b34e --- /dev/null +++ b/gui/addons/extensions_old.lua @@ -0,0 +1,1376 @@ +--[[ + gui_extensions.lua + ------------------ + Drop-in enhancements for the gui library. Patches bugs, fills gaps, + and adds new widgets without modifying any original source files. + + USAGE + require("gui_extensions") -- after requiring "gui" + + CONTENTS + BUG FIXES + 1. color arithmetic blue-channel bug + 2. gui:isOnScreen() stub + + NEW WIDGETS + 3. Checkbox + 4. RadioGroup + 5. Slider (horizontal or vertical) + 6. ProgressBar + 7. Tooltip + 8. TextArea (multiline input) + + LAYOUT CONTAINERS + 9. ListFrame (auto-stacking vertical/horizontal list) + 10. GridFrame (fixed-column grid) + + SCROLL FRAME AUTO-SIZING + 11. Auto-measure patch for newScrollFrame content + + EASING LIBRARY + 12. transition.easings (ease-in/out, cubic, bounce, elastic, back) + + FOCUS MANAGEMENT + 13. gui.focus.* (set/get focus, OnFocusGained, OnFocusLost per element) + + Z-INDEX / LAYER SYSTEM + 14. gui:setLayer(n) / gui:getLayer() +]] + +local gui = require("gui") +local color = require("gui.core.color") +local transition = require("gui.elements.transitions") +local multi, thread = require("multi"):init() + +local ext = {} + +-- ───────────────────────────────────────────────────────────────────────────── +-- 1. BUG FIX: color arithmetic metamethods (blue channel was always c[2]) +-- ───────────────────────────────────────────────────────────────────────────── + +-- We reach into the metatable that color.new sets on every color table and +-- replace the broken metamethods with correct ones. All existing color objects +-- share this metatable so the fix is retroactive. +do + local probe = color.new(1, 0, 0) -- make a throwaway color to get the MT + local mt = getmetatable(probe) + if mt then + mt.__add = function(a, b) return color.new(a[1]+b[1], a[2]+b[2], a[3]+b[3]) end + mt.__sub = function(a, b) return color.new(a[1]-b[1], a[2]-b[2], a[3]-b[3]) end + mt.__mul = function(a, b) return color.new(a[1]*b[1], a[2]*b[2], a[3]*b[3]) end + mt.__div = function(a, b) return color.new(a[1]/b[1], a[2]/b[2], a[3]/b[3]) end + mt.__mod = function(a, b) return color.new(a[1]%b[1], a[2]%b[2], a[3]%b[3]) end + mt.__pow = function(a, b) return color.new(a[1]^b[1], a[2]^b[2], a[3]^b[3]) end + mt.__unm = function(a) return color.new(-a[1], -a[2], -a[3]) end + mt.__eq = function(a, b) return a[1]==b[1] and a[2]==b[2] and a[3]==b[3] end + mt.__lt = function(a, b) return a[1]< b[1] and a[2]< b[2] and a[3]< b[3] end + mt.__le = function(a, b) return a[1]<=b[1] and a[2]<=b[2] and a[3]<=b[3] end + end +end + +-- ───────────────────────────────────────────────────────────────────────────── +-- 2. BUG FIX: gui:isOnScreen() was an empty stub +-- ───────────────────────────────────────────────────────────────────────────── + +function gui:isOnScreen() + return not self:isOffScreen() +end + +-- ───────────────────────────────────────────────────────────────────────────── +-- 3. WIDGET: Checkbox +-- +-- local cb = parent:newCheckbox(label, x, y, size, sx, sy, checked) +-- +-- Properties cb.checked (boolean) +-- Events cb.OnChanged(function(self, checked) end) +-- Methods cb:setValue(bool) cb:toggle() +-- ───────────────────────────────────────────────────────────────────────────── + +function gui:newCheckbox(label, x, y, size, sx, sy, checked) + size = size or 20 + + local container = self:newFrame(x, y, 0, size, sx, sy, 0, 0) + container.drawBorder = false + container.color = {0, 0, 0, 0} + container.visibility = 0 + + -- The clickable box + local box = container:newTextButton("", 0, 0, size, size) + box:setRoundness(3, 3) + box.color = color.new("#dddddd") + + -- The checkmark label (drawn inside the box) + local check = box:newTextLabel("✓", 0, 0, size, size) + check.align = gui.ALIGN_CENTER + check.textColor = color.new("#ffffff") + check.visibility = 0 + check.ignore = true + check:setFont(math.floor(size * 0.7)) + + -- The text label to the right of the box + local lbl = container:newTextLabel(label or "", size + 6, 0, 0, size, 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 + + container.OnChanged = multi:newConnection() + container.checked = checked or false + + local normalColor = color.new("#dddddd") + local checkedColor = color.new("#4a90d9") + + local function refresh() + if container.checked then + box.color = checkedColor + check.visibility = 1 + else + box.color = normalColor + check.visibility = 0 + end + end + + function container:setValue(v) + self.checked = v + refresh() + self.OnChanged:Fire(self, self.checked) + end + + function container:toggle() + self:setValue(not self.checked) + end + + box.OnReleased(function() + container:toggle() + end) + + refresh() + return container +end + +-- ───────────────────────────────────────────────────────────────────────────── +-- 4. WIDGET: RadioGroup +-- +-- local rg = parent:newRadioGroup(options, x, y, w, itemHeight, sx, sy) +-- -- options: {"Option A", "Option B", ...} +-- +-- Properties rg.selected (1-based index of the selected option, or nil) +-- Events rg.OnChanged(function(self, index, label) end) +-- Methods rg:select(index) +-- ───────────────────────────────────────────────────────────────────────────── + +function gui:newRadioGroup(options, x, y, w, itemHeight, sx, sy) + itemHeight = itemHeight or 28 + local totalH = #options * itemHeight + + local container = self:newFrame(x, y, w or 0, totalH, sx, sy, w and 0 or 1) + container.drawBorder = false + container.color = {0, 0, 0, 0} + container.visibility = 0 + + container.OnChanged = multi:newConnection() + container.selected = nil + + local dotSize = math.floor(itemHeight * 0.55) + local dotRadius = math.floor(dotSize / 2) + local normalBorder = color.new("#aaaaaa") + local selectedBorder = color.new("#4a90d9") + local dotColor = color.new("#4a90d9") + + local buttons = {} + + for i, optLabel in ipairs(options) do + local row = container:newFrame(0, (i-1)*itemHeight, 0, itemHeight, 0, 0, 1) + row.drawBorder = false + row.color = {0, 0, 0, 0} + row.visibility = 0 + + -- Outer ring + local ring = row:newTextButton("", 0, math.floor((itemHeight - dotSize)/2), dotSize, dotSize) + ring:makeCircle(0, math.floor((itemHeight - dotSize)/2), dotRadius) + ring.color = {1, 1, 1} + ring.borderColor = normalBorder + + -- Inner filled dot (hidden when unselected) + local innerR = math.floor(dotRadius * 0.5) + local innerOff = dotRadius - innerR + local dot = ring:newFrame(innerOff, innerOff, innerR*2, innerR*2) + dot:makeCircle(innerOff, innerOff, innerR) + dot.color = dotColor + dot.drawBorder = false + dot.visibility = 0 + dot.ignore = true + + local lbl = row:newTextLabel(optLabel, dotSize + 8, 0, 0, itemHeight, 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 + + buttons[i] = {ring = ring, dot = dot, label = optLabel} + + local idx = i + ring.OnReleased(function() + container:select(idx) + end) + end + + function container:select(index) + -- deselect previous + if self.selected and buttons[self.selected] then + local prev = buttons[self.selected] + prev.ring.borderColor = normalBorder + prev.dot.visibility = 0 + end + self.selected = index + if buttons[index] then + local cur = buttons[index] + cur.ring.borderColor = selectedBorder + cur.dot.visibility = 1 + end + self.OnChanged:Fire(self, index, options[index]) + end + + return container +end + +-- ───────────────────────────────────────────────────────────────────────────── +-- 5. WIDGET: Slider +-- +-- local sl = parent:newSlider(min, max, value, x, y, w, h, sx, sy, vertical) +-- +-- Properties sl.value (current value, clamped to [min,max]) +-- Events sl.OnChanged(function(self, value) end) +-- Methods sl:setValue(n) sl:setRange(min, max) +-- ───────────────────────────────────────────────────────────────────────────── + +function gui:newSlider(min, max, value, x, y, w, h, sx, sy, vertical) + min = min or 0 + max = max or 100 + value = value or min + + local container = self:newFrame(x, y, w or 0, h or (vertical and 0 or 20), sx, sy, w and 0 or 1, h and 0 or 0) + container.drawBorder = false + container.color = {0, 0, 0, 0} + container.visibility = 0 + + -- Track + local track = container:newFrame(0, 0, 0, 0, 0, 0, 1, 1) + track.drawBorder = false + track.color = color.new("#cccccc") + if vertical then + track:setDualDim(math.floor((w or 20)/2)-3, 0, 6, 0, 0, 0, 0, 1) + else + track:setDualDim(0, math.floor((h or 20)/2)-3, 0, 6, 0, 0, 1, 0) + end + track:setRoundness(3, 3) + + -- Fill (colored part up to the thumb) + local fill = track:newFrame(0, 0, 0, 0, 0, 0, 0, 1) + fill.drawBorder = false + fill.color = color.new("#4a90d9") + fill:setRoundness(3, 3) + if vertical then + fill:setDualDim(0, 0, 0, 0, 0, 0, 1, 0) -- will be sized by setValue + end + + -- Thumb + local thumbSize = vertical and (w or 20) or (h or 20) + local thumb = container:newFrame(0, 0, thumbSize, thumbSize) + thumb:makeCircle(0, 0, math.floor(thumbSize/2)) + thumb.color = color.new("#ffffff") + thumb.borderColor = color.new("#4a90d9") + thumb:enableDragging(gui.MOUSE_PRIMARY) + thumb:respectHierarchy(true) + + container.OnChanged = multi:newConnection() + container.value = value + container._min = min + container._max = max + + local function normalize(v) + return (v - container._min) / (container._max - container._min) + end + + local function positionThumb(v) + local t = normalize(v) + local ax, ay, aw, ah = container:getAbsolutes() + if vertical then + local travel = ah - thumbSize + local py = travel * (1 - t) -- 0 = top = max + thumb:rawSetDualDim(math.floor((aw - thumbSize)/2), math.floor(py)) + fill:setDualDim(0, math.floor(py + thumbSize/2), 0, 0, 0, 0, 1, 1 - t) + else + local travel = aw - thumbSize + local px = travel * t + thumb:rawSetDualDim(math.floor(px), math.floor((ah - thumbSize)/2)) + fill:setDualDim(0, 0, 0, 0, 0, 0, t, 1) + end + end + + function container:setValue(v) + v = math.max(self._min, math.min(self._max, v)) + self.value = v + positionThumb(v) + self.OnChanged:Fire(self, v) + end + + function container:setRange(newMin, newMax) + self._min = newMin + self._max = newMax + self:setValue(math.max(newMin, math.min(newMax, self.value))) + end + + -- Drag handling + thumb.OnDragging(function(self, dx, dy) + local ax, ay, aw, ah = container:getAbsolutes() + local t + if vertical then + local travel = ah - thumbSize + if travel <= 0 then return end + local _, ty = thumb:getAbsolutes() + local newY = ty - ay + dy + t = 1 - math.max(0, math.min(1, newY / travel)) + else + local travel = aw - thumbSize + if travel <= 0 then return end + local tx, _ = thumb:getAbsolutes() + local newX = tx - ax + dx + t = math.max(0, math.min(1, newX / travel)) + end + local newVal = container._min + t * (container._max - container._min) + container:setValue(newVal) + end) + + -- Click on track to jump + track.OnReleased(function(self, mx, my) + local ax, ay, aw, ah = container:getAbsolutes() + local t + if vertical then + t = 1 - math.max(0, math.min(1, (my - ay) / ah)) + else + t = math.max(0, math.min(1, (mx - ax) / aw)) + end + local newVal = container._min + t * (container._max - container._min) + container:setValue(newVal) + end) + + -- Initial position (after first layout pass) + container.OnSizeChanged(function() positionThumb(container.value) end) + container:OnUpdate(function() + -- only run once to set initial position + positionThumb(container.value) + -- replace with no-op after first run + container.OnUpdate = function() end + end) + + return container +end + +-- ───────────────────────────────────────────────────────────────────────────── +-- 6. WIDGET: ProgressBar +-- +-- local pb = parent:newProgressBar(min, max, value, x, y, w, h, sx, sy) +-- +-- Properties pb.value +-- Methods pb:setValue(n) pb:setRange(min, max) +-- pb:setColors(bg, fill) +-- ───────────────────────────────────────────────────────────────────────────── + +function gui:newProgressBar(min, max, value, x, y, w, h, sx, sy) + min = min or 0 + max = max or 100 + value = value or min + + local bar = self:newFrame(x, y, w or 0, h or 20, sx, sy, w and 0 or 1) + bar.color = color.new("#cccccc") + bar:setRoundness(4, 4) + + local fill = bar:newFrame(0, 0, 0, 0, 0, 0, 0, 1) + fill.drawBorder = false + fill.color = color.new("#4a90d9") + fill:setRoundness(4, 4) + + bar._min = min + bar._max = max + bar.value = value + + local function refresh() + local t = (bar.value - bar._min) / (bar._max - bar._min) + t = math.max(0, math.min(1, t)) + fill:setDualDim(0, 0, 0, 0, 0, 0, t, 1) + end + + function bar:setValue(v) + self.value = math.max(self._min, math.min(self._max, v)) + refresh() + end + + function bar:setRange(newMin, newMax) + self._min = newMin + self._max = newMax + self:setValue(self.value) + end + + function bar:setColors(bg, fg) + if bg then self.color = color.new(bg) end + if fg then fill.color = color.new(fg) end + end + + refresh() + return bar +end + +-- ───────────────────────────────────────────────────────────────────────────── +-- 7. WIDGET: Tooltip +-- +-- local tt = gui:newTooltip(text, delay) +-- tt:attach(someElement) -- show tooltip when hovering someElement +-- +-- Or attach inline: +-- gui:attachTooltip(element, text, delay) +-- +-- The tooltip lives at the root and always renders on top. +-- ───────────────────────────────────────────────────────────────────────────── + +do + -- Shared tooltip panel (one global instance) + local _panel, _label + local _visible = false + local _timer = nil + + local function ensurePanel() + if _panel then return end + _panel = gui:newFrame(0, 0, 0, 0) + _panel.color = color.new("#333333") + _panel.visibility = 0 + _panel.drawBorder = false + _panel:setRoundness(4, 4) + _panel:topStack() + + _label = _panel:newTextLabel("", 6, 4, 0, 0, 0, 0, 1, 1) + _label.textColor = color.new("#ffffff") + _label.drawBorder = false + _label.color = {0, 0, 0, 0} + _label.visibility = 0 + _label.ignore = true + _label:setFont(13) + end + + local function showAt(text, x, y) + ensurePanel() + _label.text = text + local fw = _label.font:getWidth(text) + 12 + local fh = _label.font:getHeight() + 8 + local sw, sh = love.graphics.getDimensions() + -- keep tooltip on screen + local px = math.min(x + 14, sw - fw - 4) + local py = math.min(y + 14, sh - fh - 4) + _panel:rawSetDualDim(px, py, fw, fh) + _panel.visibility = 0.92 + _panel:topStack() + _visible = true + end + + local function hide() + if _panel then _panel.visibility = 0 end + _visible = false + end + + function gui:attachTooltip(element, text, delay) + delay = delay or 0.6 + ensurePanel() + + local hoverTimer = nil + local timerDone = false + + element.OnEnter(function(self, mx, my) + timerDone = false + hoverTimer = 0 + end) + + element.OnMoved(function(self, mx, my) + if not timerDone then + -- show once timer expires; track mouse position + if hoverTimer and hoverTimer >= delay then + showAt(text, mx, my) + timerDone = true + end + end + end) + + element.OnExit(function() + hoverTimer = nil + timerDone = false + hide() + end) + + -- We need a per-frame tick to advance the hover timer. + -- OnUpdate is on the element itself so it cleans up when the element dies. + local elapsed = 0 + element:OnUpdate(function(self, dt) + if hoverTimer ~= nil and not timerDone then + hoverTimer = hoverTimer + dt + if hoverTimer >= delay then + local mx, my = love.mouse.getPosition() + showAt(text, mx, my) + timerDone = true + end + end + end) + + element.OnDestroy(function() hide() end) + end + + -- Standalone tooltip object (attach to multiple targets) + function gui:newTooltip(text, delay) + local tt = { text = text, delay = delay or 0.6 } + function tt:attach(element) + gui:attachTooltip(element, self.text, self.delay) + end + function tt:setText(t) + self.text = t + end + return tt + end +end + +-- ───────────────────────────────────────────────────────────────────────────── +-- 8. WIDGET: TextArea (multiline input) +-- +-- local ta = parent:newTextArea(text, x, y, w, h, sx, sy, sw, sh) +-- +-- Properties ta.text (string, may contain "\n") +-- ta.readOnly (boolean, default false) +-- Events ta.OnChanged(function(self, text) end) +-- Methods ta:getText() ta:setText(s) ta:appendLine(s) +-- ta:scrollToBottom() +-- ───────────────────────────────────────────────────────────────────────────── + +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 + +-- ───────────────────────────────────────────────────────────────────────────── +-- 9. LAYOUT: ListFrame (auto-stacking container) +-- +-- local list = parent:newListFrame(x, y, w, h, sx, sy, sw, sh, opts) +-- opts = { direction = "vertical"|"horizontal", gap = 4, padding = 6 } +-- +-- The list watches OnSizeChanged on each child and re-stacks automatically. +-- Methods list:reflow() list:setGap(n) list:setPadding(n) +-- ───────────────────────────────────────────────────────────────────────────── + +function gui:newListFrame(x, y, w, h, sx, sy, sw, sh, opts) + opts = opts or {} + local direction = opts.direction or "vertical" + local gap = opts.gap or 4 + local padding = opts.padding or 0 + + local frame = self:newFrame(x, y, w or 0, h or 0, sx, sy, sw, sh) + frame.drawBorder = false + frame.color = {0, 0, 0, 0} + frame.visibility = 0 + + -- Track insertion order separately from frame.children (which can be + -- reordered by topStack/bottomStack). + local managed = {} + + local function reflow() + local cursor = padding + for _, child in ipairs(managed) do + if child.visible ~= false then + local _, _, cw, ch = child:getAbsolutes() + if direction == "vertical" then + child:rawSetDualDim(padding, cursor) + cursor = cursor + ch + gap + else + child:rawSetDualDim(cursor, padding) + cursor = cursor + cw + gap + end + end + end + -- Auto-size the frame to fit its content if no fractional scale given + if (sw or 0) == 0 and direction == "vertical" then + -- don't auto-resize width; height grows to fit + frame:rawSetDualDim(nil, nil, nil, cursor + padding - gap) + elseif (sh or 0) == 0 and direction == "horizontal" then + frame:rawSetDualDim(nil, nil, cursor + padding - gap) + end + end + + frame.reflow = reflow + + function frame:setGap(n) + gap = n + reflow() + end + + function frame:setPadding(n) + padding = n + reflow() + end + + -- Override newFrame etc. on this frame so children auto-register + local function wrapChild(child) + managed[#managed + 1] = child + child.OnSizeChanged(reflow) + child.OnDestroy(function() + for i, v in ipairs(managed) do + if v == child then table.remove(managed, i); break end + end + reflow() + end) + reflow() + return child + end + + -- Intercept common constructors so anything added to this list is tracked. + -- We do this by wrapping the frame's methods after creation. + local _newFrame = frame.newFrame + local _newTextButton = frame.newTextButton + local _newTextLabel = frame.newTextLabel + local _newTextBox = frame.newTextBox + local _newImageLabel = frame.newImageLabel + local _newImageButton = frame.newImageButton + local _newProgressBar = frame.newProgressBar + local _newCheckbox = frame.newCheckbox + local _newSlider = frame.newSlider + + function frame:newFrame(...) return wrapChild(_newFrame(self, ...)) end + function frame:newTextButton(...) return wrapChild(_newTextButton(self, ...)) end + function frame:newTextLabel(...) return wrapChild(_newTextLabel(self, ...)) end + function frame:newTextBox(...) return wrapChild(_newTextBox(self, ...)) end + function frame:newImageLabel(...) return wrapChild(_newImageLabel(self, ...)) end + function frame:newImageButton(...) return wrapChild(_newImageButton(self, ...)) end + function frame:newProgressBar(...) return wrapChild(_newProgressBar(self, ...)) end + function frame:newCheckbox(...) return wrapChild(_newCheckbox(self, ...)) end + function frame:newSlider(...) return wrapChild(_newSlider(self, ...)) end + + -- Allow manually registering an externally created child + function frame:register(child) + return wrapChild(child) + end + + reflow() + return frame +end + +-- ───────────────────────────────────────────────────────────────────────────── +-- 10. LAYOUT: GridFrame (fixed-column auto grid) +-- +-- local grid = parent:newGridFrame(cols, cellW, cellH, x, y, sx, sy, gap, padding) +-- +-- Methods grid:reflow() grid:setColumns(n) +-- Children are placed left-to-right, wrapping at `cols` columns. +-- ───────────────────────────────────────────────────────────────────────────── + +function gui:newGridFrame(cols, cellW, cellH, x, y, sx, sy, gap, padding) + cols = cols or 3 + cellW = cellW or 100 + cellH = cellH or 100 + gap = gap or 4 + padding = padding or 0 + + local totalW = cols * cellW + (cols - 1) * gap + padding * 2 + local frame = self:newFrame(x or 0, y or 0, totalW, 0, sx, sy) + frame.drawBorder = false + frame.color = {0, 0, 0, 0} + frame.visibility = 0 + + local managed = {} + + local function reflow() + local row, col = 0, 0 + for _, child in ipairs(managed) do + local px = padding + col * (cellW + gap) + local py = padding + row * (cellH + gap) + child:rawSetDualDim(px, py, cellW, cellH) + col = col + 1 + if col >= cols then + col = 0 + row = row + 1 + end + end + local rows = math.ceil(#managed / cols) + frame:rawSetDualDim(nil, nil, totalW, rows * (cellH + gap) - gap + padding * 2) + end + + frame.reflow = reflow + + function frame:setColumns(n) + cols = n + totalW = cols * cellW + (cols - 1) * gap + padding * 2 + self:rawSetDualDim(nil, nil, totalW) + reflow() + end + + local function wrapChild(child) + managed[#managed + 1] = child + child.OnDestroy(function() + for i, v in ipairs(managed) do + if v == child then table.remove(managed, i); break end + end + reflow() + end) + reflow() + return child + end + + local _newFrame = frame.newFrame + local _newTextButton = frame.newTextButton + local _newTextLabel = frame.newTextLabel + local _newImageLabel = frame.newImageLabel + local _newImageButton = frame.newImageButton + + function frame:newFrame(...) return wrapChild(_newFrame(self, ...)) end + function frame:newTextButton(...) return wrapChild(_newTextButton(self, ...)) end + function frame:newTextLabel(...) return wrapChild(_newTextLabel(self, ...)) end + function frame:newImageLabel(...) return wrapChild(_newImageLabel(self, ...)) end + function frame:newImageButton(...) return wrapChild(_newImageButton(self, ...)) end + + function frame:register(child) return wrapChild(child) end + + return frame +end + +-- ───────────────────────────────────────────────────────────────────────────── +-- 11. SCROLL FRAME AUTO-SIZING PATCH +-- +-- Patches gui:newScrollFrame so the content frame automatically measures its +-- children and grows to fit them. No manual setContentSize() needed. +-- +-- The original newScrollFrame is preserved as gui:newScrollFrameRaw if you +-- need the unpatched version. +-- ───────────────────────────────────────────────────────────────────────────── + +do + -- Store the original (defined in addons/system.lua after require) + -- We wrap it after the module is loaded. Using a post-load hook via + -- the next iteration of the updater is not practical here, so we patch + -- directly. If addons.system hasn't been required yet this is a no-op and + -- the patch is applied lazily on first call. + local _original + + local function wrap() + if _original then return end + _original = gui.newScrollFrame + if not _original then return end -- system addon not loaded yet + + gui.newScrollFrameRaw = _original + + gui.newScrollFrame = function(self, x, y, w, h, sx, sy, sw, sh) + local content = _original(self, x, y, w, h, sx, sy, sw, sh) + + -- After each child is created under content, measure and resize. + local function measure() + local maxH = 0 + local maxW = 0 + for _, child in ipairs(content:getChildren()) do + local cx, cy, cw, ch = child:getAbsolutes() + local _, contentY = content:getAbsolutes() + local relBottom = (cy - contentY) + ch + local relRight = (cx - (select(1, content:getAbsolutes()))) + cw + if relBottom > maxH then maxH = relBottom end + if relRight > maxW then maxW = relRight end + end + -- Only grow, don't shrink (prevents thrash while items are being added) + local _, _, curW, curH = content:getAbsolutes() + if maxH > curH then content:setDualDim(nil, nil, nil, maxH + 4) end + end + + content.OnCreated(function(child) + child.OnSizeChanged(measure) + child.OnDestroy(measure) + measure() + end) + + return content + end + end + + -- Attempt to patch immediately; if the addon isn't loaded yet this becomes + -- a one-shot deferred patch triggered on first call. + wrap() + + -- Deferred fallback: wrap on first call if not already done + local _deferred = gui.newScrollFrame + if _deferred == _original or _original == nil then + gui.newScrollFrame = function(self, ...) + wrap() -- try again now that more modules may be loaded + return gui.newScrollFrame(self, ...) + end + end +end + +-- ───────────────────────────────────────────────────────────────────────────── +-- 12. EASING LIBRARY +-- +-- Extended transition easings to complement transition.glide. +-- +-- USAGE (same pattern as glide): +-- +-- local ease = transition.easings +-- +-- local t = ease.easeInOut(0, 100, 0.4)() +-- t.OnStep(function(v) panel:setDualDim(v) end) +-- t.OnStop(function() print("done") end) +-- +-- Available: +-- 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 +-- ───────────────────────────────────────────────────────────────────────────── + +do + local easings = {} + transition.easings = easings + + -- Helper: build a transition from an easing function f(t) where t is 0→1 + local function makeEasing(f) + return transition:newTransition(function(t, start, stop, time) + local steps = t.fps * time + local piece = time / steps + local range = stop - start + t.running = true + for i = 0, steps do + if not t.kill then + thread.sleep(piece) + local progress = i / steps + thread.pushStatus(start + f(progress) * range) + end + end + t.running = false + t.kill = false + end) + end + + easings.easeIn = makeEasing(function(t) + return t * t + end) + + easings.easeOut = makeEasing(function(t) + return t * (2 - t) + end) + + easings.easeInOut = makeEasing(function(t) + return t < 0.5 and (2 * t * t) or (-1 + (4 - 2*t) * t) + end) + + easings.easeInCubic = makeEasing(function(t) + return t * t * t + end) + + easings.easeOutCubic = makeEasing(function(t) + local u = t - 1 + return u * u * u + 1 + end) + + easings.bounce = makeEasing(function(t) + if t < 1/2.75 then + return 7.5625 * t * t + elseif t < 2/2.75 then + t = t - 1.5/2.75 + return 7.5625 * t * t + 0.75 + elseif t < 2.5/2.75 then + t = t - 2.25/2.75 + return 7.5625 * t * t + 0.9375 + else + t = t - 2.625/2.75 + return 7.5625 * t * t + 0.984375 + end + end) + + easings.elastic = makeEasing(function(t) + if t == 0 or t == 1 then return t end + local p = 0.3 + local s = p / 4 + local u = t - 1 + return -(math.pow(2, 10 * u) * math.sin((u - s) * (2 * math.pi) / p)) + end) + + easings.back = makeEasing(function(t) + local s = 1.70158 + return t * t * ((s + 1) * t - s) + end) +end + +-- ───────────────────────────────────────────────────────────────────────────── +-- 13. 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 +-- +-- Per-element events (added to every element at creation via OnCreated): +-- element.OnFocusGained(function(self) end) +-- element.OnFocusLost(function(self) end) +-- +-- NOTE: gui.focus tracks focus independently of the internal object_focus. +-- It is the public, programmable layer on top. +-- ───────────────────────────────────────────────────────────────────────────── + +do + gui.focus = {} + + local _current = nil + local _tabOrder = {} + + -- Attach focus events to every new element + gui.Events.OnCreated(function(element) + if not element.OnFocusGained then + element.OnFocusGained = multi:newConnection() + end + if not element.OnFocusLost then + element.OnFocusLost = multi:newConnection() + end + end) + + function gui.focus.set(element) + if _current == element then return end + if _current and _current.OnFocusLost then + _current.OnFocusLost:Fire(_current) + end + _current = element + if element and element.OnFocusGained then + element.OnFocusGained:Fire(element) + end + end + + function gui.focus.get() + return _current + end + + function gui.focus.clear() + gui.focus.set(nil) + end + + function gui.focus.setTabOrder(list) + _tabOrder = list + end + + function gui.focus.tabNext() + if #_tabOrder == 0 then return end + local idx = 1 + for i, v in ipairs(_tabOrder) do + if v == _current then idx = i + 1; break end + end + if idx > #_tabOrder then idx = 1 end + gui.focus.set(_tabOrder[idx]) + end + + function gui.focus.tabPrev() + if #_tabOrder == 0 then return end + local idx = #_tabOrder + for i, v in ipairs(_tabOrder) do + if v == _current then idx = i - 1; break end + end + if idx < 1 then idx = #_tabOrder end + gui.focus.set(_tabOrder[idx]) + end + + -- Wire Tab / Shift+Tab globally + gui.Events.OnKeyPressed(function(key) + if key == "tab" then + if love.keyboard.isDown("lshift") or love.keyboard.isDown("rshift") then + gui.focus.tabPrev() + else + gui.focus.tabNext() + end + end + end) + + -- Keep gui.focus in sync with mouse clicks on interactive elements. + -- We listen to the global ObjectFocusChanged which fires whenever any + -- element is pressed. + gui.Events.OnObjectFocusChanged(function(prev, next) + gui.focus.set(next) + end) + + -- Elements that are destroyed should be removed from the tab order + -- and should lose focus if they currently hold it. + gui.Events.OnCreated(function(element) + element.OnDestroy(function(self) + if _current == self then gui.focus.clear() end + for i, v in ipairs(_tabOrder) do + if v == self then table.remove(_tabOrder, i); break end + end + end) + end) +end + +-- ───────────────────────────────────────────────────────────────────────────── +-- 14. Z-INDEX / LAYER SYSTEM +-- +-- obj:setLayer(n) -- set a numeric z-index (higher = drawn on top) +-- obj:getLayer() -- returns current layer (default 0) +-- +-- The draw order within each parent is sorted by layer every frame. +-- Elements with the same layer retain their insertion order (stable sort). +-- This replaces the need to call topStack() in OnUpdate for persistent +-- ordering requirements. +-- +-- NOTE: Sorting the children table every frame is O(n log n) in the number +-- of siblings. For large flat hierarchies, assign layers once and call +-- gui.layers.pause() to skip re-sorting when nothing has changed. +-- ───────────────────────────────────────────────────────────────────────────── + +do + gui.layers = {} + local _dirty = false -- set to true when any layer changes + + function gui:setLayer(n) + self.__layer = n + _dirty = true + end + + function gui:getLayer() + return self.__layer or 0 + end + + -- Stable sort: preserve relative order for equal layers + local function stableSort(t, lt) + -- insertion sort (stable and fast for small-to-medium sibling counts) + for i = 2, #t do + local key = t[i] + local j = i - 1 + while j >= 1 and lt(key, t[j]) do + t[j + 1] = t[j] + j = j - 1 + end + t[j + 1] = key + end + end + + local function sortChildren(parent) + stableSort(parent.children, function(a, b) + return (a.__layer or 0) < (b.__layer or 0) + end) + for _, child in ipairs(parent.children) do + if #child.children > 0 then + sortChildren(child) + end + end + end + + function gui.layers.pause() _dirty = false end + function gui.layers.resume() _dirty = true end + + -- Re-sort the tree when layers change. We piggy-back on OnUpdate so this + -- runs before the draw loop. + gui:OnUpdate(function(_, dt) + if not _dirty then return end + _dirty = false + sortChildren(gui) + end) +end + +-- ───────────────────────────────────────────────────────────────────────────── +-- Return the extension table for optional introspection +-- ───────────────────────────────────────────────────────────────────────────── + +return ext diff --git a/gui/addons/init.lua b/gui/addons/init.lua index e69de29..84cae69 100644 --- a/gui/addons/init.lua +++ b/gui/addons/init.lua @@ -0,0 +1,3 @@ +-- Addons modify the gui interface directly and do not return anything. +require("gui.addons.extensions") +require("gui.addons.system") \ No newline at end of file diff --git a/gui/addons/players.lua b/gui/addons/players.lua deleted file mode 100644 index b8fe61e..0000000 --- a/gui/addons/players.lua +++ /dev/null @@ -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 diff --git a/gui/addons/system.lua b/gui/addons/system.lua index 23450ed..862920c 100644 --- a/gui/addons/system.lua +++ b/gui/addons/system.lua @@ -155,149 +155,6 @@ local function collectTasks() return rows end --- ── window constructor (unchanged from original) ────────────────────────────── -local windowCount = 0 -function gui:newWindow(x, y, w, h, text, draggable, theme) - local process = gui:newProcessor(text or "window_"..windowCount) - windowCount = windowCount + 1 - local parent = self - local pointer = love.mouse.getCursor() - local sizewe = love.mouse.getSystemCursor("sizewe") - local sizens = love.mouse.getSystemCursor("sizens") - local sizenesw = love.mouse.getSystemCursor("sizenesw") - local sizenwse = love.mouse.getSystemCursor("sizenwse") - local theme = theme or default_theme - - local header = self:newFrame(x, y, w, 35) - header:setRoundness(10, 10, nil, "top") - local window = header:newFrame(0, 35, 0, h - 35, 0, 0, 1) - window.clipDescendants = true - local left = window:newFrame(0, -4, 4, 0, 0, 0, 0, 1):tag("left") - local right = window:newFrame(-4, -4, 4, 0, 1, 0, 0, 1):tag("right") - local bottom = window:newFrame(4, -4, -8, 4, 0, 1, 1):tag("bottom") - local bottomleft = window:newFrame(0, -4, 4, 4, 0, 1):tag("bleft") - local bottomright = window:newFrame(-4, -4, 4, 4, 1, 1):tag("bright") - gui.apply({ - visibility = 0, - I_enableDragging = {gui.MOUSE_PRIMARY}, - respectHierarchy = {false}, - OnUpdate = function(self) self:topStack() end, - OnDragging = function(self, dx, dy) - local ox, oy, ow, oh = header:getAbsolutes() - local tag = self:getTag() - if tag == "left" or tag == "bleft" then - window:size(0, dy) - header:move(dx, 0) - header:size(-dx, 0) - else - window:size(0, dy) - header:size(dx, 0) - end - local x, y, w, h = header:getAbsolutes() - if w < 200 and (tag == "left" or tag == "bleft") then - header:setDualDim(ox, nil, 200) - elseif w < 200 then - header:setDualDim(nil, nil, 200) - end - local x, y, w, h = window:getAbsolutes() - if h < 100 then window:setDualDim(nil, nil, nil, 100) end - end, - OnDragEnd = function(self) love.mouse.setCursor(pointer) end, - OnEnter = function(self) - local tag = self:getTag() - if tag == "left" or tag == "right" then - love.mouse.setCursor(sizewe) - elseif tag == "bleft" then - love.mouse.setCursor(sizenesw) - elseif tag == "bright" then - love.mouse.setCursor(sizenwse) - else - love.mouse.setCursor(sizens) - end - end, - OnExit = function(self) love.mouse.setCursor(pointer) end, - }, left, right, bottom, bottomleft, bottomright) - - local title = header:newTextLabel(text or "", 5, 0, w - 35, 35) - title.clipDescendants = true - title.visibility = 0 - title.ignore = true - title:setFont(theme.fontPrimary) - title:fitFont() - - function window:setTitle(t) title.text = t end - - local X = header:newTextButton("", -25, -25, 20, 20, 1, 1) - X:setRoundness(10, 10) - X.align = gui.ALIGN_CENTER - X.color = color.red - local darkenX = color.darken(color.red, .2) - X.OnEnter(function(self) self.color = darkenX end) - X.OnExit(function(self) self.color = color.red end) - - if draggable then - header:enableDragging(gui.MOUSE_PRIMARY) - header:OnDragging(function(self, dx, dy) self:move(dx, dy) end) - header:OnDragEnd(function(self) - local x, y, w, h = self:getAbsolutes() - local width, height = love.graphics.getDimensions() - if x <= 0 then self:setDualDim(0) end - if y <= 0 then self:setDualDim(nil, 0) end - if x + w >= width then self:setDualDim(width - w) end - if y + h >= height then self:setDualDim(nil, height - 35) end - end) - end - - window.OnClose = function() return window end % X.OnPressed - window.OnClose(function() - header:setParent(gui.virtual) - love.mouse.setCursor(pointer) - end) - function window:close() window.OnClose:Fire(self) end - function window:open() header:setParent(parent) end - - function window:setTheme(th) - theme = th - title.textColor = theme.colorPrimaryText - header.color = theme.colorPrimaryDark - window.color = theme.colorPrimary - end - function window:getTheme() return theme end - - process:newThread(function() window:setTheme(theme) end) - - window.OnSizeChanged(function() window:refresh() end) - function window:refresh() window:setTheme(theme) end - - window.process = process - window.OnCreated(function(element) - if element:hasType(gui.TYPE_BUTTON) then - element:setFont(theme.fontButton) - element.color = theme.colorButtonNormal - element.textColor = theme.colorButtonText - if not element.__registeredTheme then - element.OnEnter(function(self) self.color = theme.colorButtonHighlight end) - element.OnExit(function(self) self.color = theme.colorButtonNormal end) - end - element:fitFont() - element.__registeredTheme = true - elseif element:hasType(gui.TYPE_TEXT) then - element.color = theme.colorPrimary - element:setFont(theme.fontPrimary) - element.textColor = theme.colorPrimaryText - element:fitFont() - elseif element:hasType(gui.TYPE_FRAME) then - if element.__isHeader then - element.color = theme.colorPrimaryDark - else - element.color = theme.colorPrimary - end - end - end) - return window -end - --- ── scroll frame (unchanged from original) ──────────────────────────────────── function gui:newScrollFrame(x, y, w, h, sx, sy, sw, sh) local viewport = self:newFrame(x, y, w, h, sx, sy, sw, sh) viewport.clipDescendants = true @@ -453,6 +310,148 @@ function gui:newScrollFrame(x, y, w, h, sx, sy, sw, sh) return content 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 ────────────────────────────────────────────────────────────────── local COLOR_PROC_ROW = TM_THEME.colorPrimaryDark local COLOR_ROW_EVEN = TM_THEME.colorPrimary @@ -896,7 +895,7 @@ function gui:showTaskManager() -- ── load probe ──────────────────────────────────────────────────────────── -- 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) -- ── main-thread update ──────────────────────────────────────────────────── diff --git a/gui/core/color.lua b/gui/core/color.lua index 716ac07..d0b191c 100644 --- a/gui/core/color.lua +++ b/gui/core/color.lua @@ -1,22 +1,22 @@ local color={} local mt = { __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, __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, __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, __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, __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, __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, __unm = function (c1) 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") ..")" end, __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, __lt = function (c1,c2) - return (c1[1] #data then return nil end - bitBuffer = bitBuffer + bit.lshift(string.byte(data, pos), bits) - bits = bits + 8 - pos = pos + 1 - end - - local code = bit.band(bitBuffer, bit.lshift(1, codeSize) - 1) - bitBuffer = bit.rshift(bitBuffer, codeSize) - bits = bits - codeSize - return code - end - - local first = true - - while true do - local code = readCode() - if not code or code == endCode then break end - - if code == clearCode then - dict = {} - for i = 0, clearCode - 1 do - dict[i] = {string.byte(string.char(i))} - end - nextCode = endCode + 1 - codeSize = minCodeSize + 1 - prevCode = nil - first = true - else - local entry - if dict[code] then - entry = dict[code] - elseif code == nextCode and prevCode then - -- Special case: code not in dict yet - entry = {} - for i = 1, #dict[prevCode] do - entry[i] = dict[prevCode][i] - end - entry[#entry + 1] = dict[prevCode][1] - else - -- Invalid code, stop - break - end - - -- Output the entry - for i = 1, #entry do - table.insert(output, entry[i]) - end - - -- Add new entry to dictionary - if not first and prevCode and nextCode < 4096 then - local newEntry = {} - for i = 1, #dict[prevCode] do - newEntry[i] = dict[prevCode][i] - end - newEntry[#newEntry + 1] = entry[1] - dict[nextCode] = newEntry - nextCode = nextCode + 1 - - -- Increase code size when needed - if nextCode >= bit.lshift(1, codeSize) and codeSize < 12 then - codeSize = codeSize + 1 - end - end - - prevCode = code - first = false - end - end - - -- Convert output bytes to string - local result = {} - for i = 1, #output do - result[i] = string.char(output[i]) - end - return table.concat(result) -end - -function GifLoader.load(filepath) - local fileData = love.filesystem.read(filepath) - if not fileData then - error("Could not read GIF file: " .. filepath) - end - - local gif = { - frames = {}, - frameData = {}, -- Store ImageData for frame composition - delays = {}, - currentFrame = 1, - timer = 0, - width = 0, - height = 0, - playing = true, - loop = true, - getWidth = function(self) - return self.width - end, - getHeight = function(self) - return self.height - end, - } - - -- Parse GIF header - local header = fileData:sub(1, 6) - if header ~= "GIF87a" and header ~= "GIF89a" then - error("Not a valid GIF file") - end - - -- Read logical screen descriptor - local pos = 7 - gif.width = string.byte(fileData, pos) + string.byte(fileData, pos + 1) * 256 - gif.height = string.byte(fileData, pos + 2) + string.byte(fileData, pos + 3) * 256 - - local packed = string.byte(fileData, pos + 4) - local hasGlobalColorTable = bit.band(packed, 0x80) ~= 0 - local backgroundColorIndex = string.byte(fileData, pos + 5) - - pos = pos + 7 - - -- Read global color table - local globalColorTable = {} - if hasGlobalColorTable then - local size = 2 ^ (bit.band(packed, 0x07) + 1) - for i = 1, size do - local r = string.byte(fileData, pos) / 255 - local g = string.byte(fileData, pos + 1) / 255 - local b = string.byte(fileData, pos + 2) / 255 - table.insert(globalColorTable, {r, g, b, 1}) - pos = pos + 3 - end - end - - -- Parse blocks - local delay = 0.1 - local transparentIndex = nil - local disposalMethod = 0 - local delayForNextFrame = 0.1 -- Track delay for next frame - - while pos <= #fileData do - local separator = string.byte(fileData, pos) - - if separator == 0x21 then -- Extension - local label = string.byte(fileData, pos + 1) - pos = pos + 2 - - if label == 0xF9 then -- Graphic Control Extension - local blockSize = string.byte(fileData, pos) - pos = pos + 1 - - local flags = string.byte(fileData, pos) - disposalMethod = bit.rshift(bit.band(flags, 0x1C), 2) - local hasTransparency = bit.band(flags, 0x01) ~= 0 - - local delayTime = string.byte(fileData, pos + 1) + string.byte(fileData, pos + 2) * 256 - -- GIF delay is in hundredths of a second, convert to seconds - -- Many GIFs use 0 or very small delays, set a minimum - if delayTime == 0 then - delayForNextFrame = 0.1 -- Default 100ms - elseif delayTime <= 2 then - delayForNextFrame = 0.02 -- Minimum 20ms for very fast animations - else - delayForNextFrame = delayTime / 100 - end - - if hasTransparency then - transparentIndex = string.byte(fileData, pos + 3) - else - transparentIndex = nil - end - - pos = pos + blockSize + 1 - else - -- Skip other extensions - repeat - local blockSize = string.byte(fileData, pos) - pos = pos + 1 - if blockSize > 0 then - pos = pos + blockSize - end - until blockSize == 0 - end - - elseif separator == 0x2C then -- Image Descriptor - pos = pos + 1 - - local left = string.byte(fileData, pos) + string.byte(fileData, pos + 1) * 256 - local top = string.byte(fileData, pos + 2) + string.byte(fileData, pos + 3) * 256 - local width = string.byte(fileData, pos + 4) + string.byte(fileData, pos + 5) * 256 - local height = string.byte(fileData, pos + 6) + string.byte(fileData, pos + 7) * 256 - - local imgPacked = string.byte(fileData, pos + 8) - local hasLocalColorTable = bit.band(imgPacked, 0x80) ~= 0 - local interlaced = bit.band(imgPacked, 0x40) ~= 0 - - pos = pos + 9 - - local colorTable = globalColorTable - if hasLocalColorTable then - local size = 2 ^ (bit.band(imgPacked, 0x07) + 1) - colorTable = {} - for i = 1, size do - local r = string.byte(fileData, pos) / 255 - local g = string.byte(fileData, pos + 1) / 255 - local b = string.byte(fileData, pos + 2) / 255 - table.insert(colorTable, {r, g, b, 1}) - pos = pos + 3 - end - end - - -- Read LZW minimum code size - local minCodeSize = string.byte(fileData, pos) - pos = pos + 1 - - -- Read compressed image data blocks - local compressedData = {} - while true do - local blockSize = string.byte(fileData, pos) - pos = pos + 1 - if blockSize == 0 then break end - table.insert(compressedData, fileData:sub(pos, pos + blockSize - 1)) - pos = pos + blockSize - end - - -- Decompress image data - local indexStream = decompressLZW(table.concat(compressedData), minCodeSize) - - -- Create image data - local imageData = love.image.newImageData(gif.width, gif.height) - - -- Fill with background if first frame - if #gif.frames == 0 and backgroundColorIndex and globalColorTable[backgroundColorIndex + 1] then - local bg = globalColorTable[backgroundColorIndex + 1] - for y = 0, gif.height - 1 do - for x = 0, gif.width - 1 do - imageData:setPixel(x, y, bg[1], bg[2], bg[3], bg[4]) - end - end - elseif #gif.frames > 0 then - -- Copy previous frame if needed - local prevData = gif.frameData[#gif.frameData] - imageData:paste(prevData, 0, 0, 0, 0, gif.width, gif.height) - end - - -- Draw current frame - if indexStream and #indexStream > 0 then - local idx = 1 - - -- Use mapPixel for faster pixel operations - local function setPixels(x, y, r, g, b, a) - if x >= left and x < left + width and y >= top and y < top + height then - local pixelIdx = (y - top) * width + (x - left) + 1 - if pixelIdx <= #indexStream then - local colorIndex = string.byte(indexStream, pixelIdx) - - -- Skip transparent pixels - if transparentIndex == nil or colorIndex ~= transparentIndex then - if colorTable[colorIndex + 1] then - local color = colorTable[colorIndex + 1] - return color[1], color[2], color[3], color[4] - end - end - end - end - return r, g, b, a - end - - -- Only update the region where the frame is located - for y = top, top + height - 1 do - for x = left, left + width - 1 do - local pixelIdx = (y - top) * width + (x - left) + 1 - if pixelIdx <= #indexStream then - local colorIndex = string.byte(indexStream, pixelIdx) - - -- Skip transparent pixels - if transparentIndex == nil or colorIndex ~= transparentIndex then - if colorTable[colorIndex + 1] then - local color = colorTable[colorIndex + 1] - imageData:setPixel(x, y, color[1], color[2], color[3], color[4]) - end - end - end - end - end - end - - table.insert(gif.frameData, imageData) - table.insert(gif.frames, love.graphics.newImage(imageData)) - table.insert(gif.delays, delayForNextFrame) - - -- Reset delay for next frame - delayForNextFrame = 0.1 - - elseif separator == 0x3B then -- Trailer - break - else - pos = pos + 1 - end - end - - if #gif.frames == 0 then - error("No frames found in GIF") - end - - -- Ensure all frames have valid delays - for i = 1, #gif.delays do - if gif.delays[i] <= 0 or gif.delays[i] ~= gif.delays[i] then -- check for 0 or NaN - gif.delays[i] = 0.1 - end - end - - return gif -end - -function GifLoader.Updater(gif, proc) - local wait = function() - return gif.playing or gif.kill - end - proc:newThread("Gif Handler",function() - while true do - -- Only run if not paused - if gif.kill then -- When we want to clean up - thread.kill() - end - thread.hold(wait) - thread.sleep(gif.delays[gif.currentFrame] * 4) - gif.currentFrame = gif.currentFrame + 1 - - if gif.currentFrame > #gif.frames then - if gif.loop then - gif.currentFrame = 1 - else - gif.currentFrame = #gif.frames - gif.playing = false - gif.timer = 0 - end - end - end - end) -end - -function GifLoader.update(gif, dt) - if not gif.playing or #gif.frames <= 1 then return end - - gif.timer = gif.timer + dt - - -- Simple, accurate frame advancement - if gif.timer >= gif.delays[gif.currentFrame] then - -- Subtract the current frame's delay - gif.timer = gif.timer - gif.delays[gif.currentFrame] - - -- Move to next frame - gif.currentFrame = gif.currentFrame + 1 - - if gif.currentFrame > #gif.frames then - if gif.loop then - gif.currentFrame = 1 - else - gif.currentFrame = #gif.frames - gif.playing = false - gif.timer = 0 - end - end - - -- If we've accumulated too much time (lag spike), cap it - if gif.timer > 0.5 then - gif.timer = 0 - end - end -end - -function GifLoader.draw(gif, x, y, r, sx, sy, ox, oy) - if gif.frames[gif.currentFrame] then - love.graphics.draw(gif.frames[gif.currentFrame], x, y, r or 0, sx or 1, sy or 1, ox or 0, oy or 0) - end -end - -function GifLoader.play(gif) - gif.playing = true -end - -function GifLoader.pause(gif) - gif.playing = false -end - -function GifLoader.reset(gif) - gif.currentFrame = 1 - gif.timer = 0 -end - -function GifLoader.setFixedFramerate(gif, fps) - local delay = 1 / fps - for i = 1, #gif.delays do - gif.delays[i] = delay - end -end - -function GifLoader.getInfo(gif) - return { - width = gif.width, - height = gif.height, - frameCount = #gif.frames, - delays = gif.delays, -- Show actual delays for debugging - totalDuration = (function() - local total = 0 - for i = 1, #gif.delays do - total = total + gif.delays[i] - end - return total - end)() - } -end - -return GifLoader - --- USAGE: ---[[ -local GifLoader = require("gifloader") - -function love.load() - myGif = GifLoader.load("animation.gif") - myGif.loop = true -end - -function love.update(dt) - GifLoader.update(myGif, dt) -end - -function love.draw() - GifLoader.draw(myGif, 100, 100) - - -- Draw scaled - GifLoader.draw(myGif, 300, 100, 0, 2, 2) -end -]] \ No newline at end of file +-- GIF Loader for Love2D with LZW Decompression +-- Note: love.data.compress/decompress don't support LZW, so we implement it + +local GifLoader = {} + +-- Pure Lua LZW decompression for GIF +local function decompressLZW(data, minCodeSize) + local clearCode = 2 ^ minCodeSize + local endCode = clearCode + 1 + local nextCode = endCode + 1 + local codeSize = minCodeSize + 1 + + local dict = {} + for i = 0, clearCode - 1 do + dict[i] = {string.byte(string.char(i))} + end + + local output = {} + local bits = 0 + local bitBuffer = 0 + local pos = 1 + local prevCode = nil + + local function readCode() + while bits < codeSize do + if pos > #data then return nil end + bitBuffer = bitBuffer + bit.lshift(string.byte(data, pos), bits) + bits = bits + 8 + pos = pos + 1 + end + + local code = bit.band(bitBuffer, bit.lshift(1, codeSize) - 1) + bitBuffer = bit.rshift(bitBuffer, codeSize) + bits = bits - codeSize + return code + end + + local first = true + + while true do + local code = readCode() + if not code or code == endCode then break end + + if code == clearCode then + dict = {} + for i = 0, clearCode - 1 do + dict[i] = {string.byte(string.char(i))} + end + nextCode = endCode + 1 + codeSize = minCodeSize + 1 + prevCode = nil + first = true + else + local entry + if dict[code] then + entry = dict[code] + elseif code == nextCode and prevCode then + -- Special case: code not in dict yet + entry = {} + for i = 1, #dict[prevCode] do + entry[i] = dict[prevCode][i] + end + entry[#entry + 1] = dict[prevCode][1] + else + -- Invalid code, stop + break + end + + -- Output the entry + for i = 1, #entry do + table.insert(output, entry[i]) + end + + -- Add new entry to dictionary + if not first and prevCode and nextCode < 4096 then + local newEntry = {} + for i = 1, #dict[prevCode] do + newEntry[i] = dict[prevCode][i] + end + newEntry[#newEntry + 1] = entry[1] + dict[nextCode] = newEntry + nextCode = nextCode + 1 + + -- Increase code size when needed + if nextCode >= bit.lshift(1, codeSize) and codeSize < 12 then + codeSize = codeSize + 1 + end + end + + prevCode = code + first = false + end + end + + -- Convert output bytes to string + local result = {} + for i = 1, #output do + result[i] = string.char(output[i]) + end + return table.concat(result) +end + +function GifLoader.load(filepath) + local fileData = love.filesystem.read(filepath) + if not fileData then + error("Could not read GIF file: " .. filepath) + end + + local gif = { + frames = {}, + frameData = {}, -- Store ImageData for frame composition + delays = {}, + currentFrame = 1, + timer = 0, + width = 0, + height = 0, + playing = true, + loop = true, + getWidth = function(self) + return self.width + end, + getHeight = function(self) + return self.height + end, + } + + -- Parse GIF header + local header = fileData:sub(1, 6) + if header ~= "GIF87a" and header ~= "GIF89a" then + error("Not a valid GIF file") + end + + -- Read logical screen descriptor + local pos = 7 + gif.width = string.byte(fileData, pos) + string.byte(fileData, pos + 1) * 256 + gif.height = string.byte(fileData, pos + 2) + string.byte(fileData, pos + 3) * 256 + + local packed = string.byte(fileData, pos + 4) + local hasGlobalColorTable = bit.band(packed, 0x80) ~= 0 + local backgroundColorIndex = string.byte(fileData, pos + 5) + + pos = pos + 7 + + -- Read global color table + local globalColorTable = {} + if hasGlobalColorTable then + local size = 2 ^ (bit.band(packed, 0x07) + 1) + for i = 1, size do + local r = string.byte(fileData, pos) / 255 + local g = string.byte(fileData, pos + 1) / 255 + local b = string.byte(fileData, pos + 2) / 255 + table.insert(globalColorTable, {r, g, b, 1}) + pos = pos + 3 + end + end + + -- Parse blocks + local delay = 0.1 + local transparentIndex = nil + local disposalMethod = 0 + local delayForNextFrame = 0.1 -- Track delay for next frame + + while pos <= #fileData do + local separator = string.byte(fileData, pos) + + if separator == 0x21 then -- Extension + local label = string.byte(fileData, pos + 1) + pos = pos + 2 + + if label == 0xF9 then -- Graphic Control Extension + local blockSize = string.byte(fileData, pos) + pos = pos + 1 + + local flags = string.byte(fileData, pos) + disposalMethod = bit.rshift(bit.band(flags, 0x1C), 2) + local hasTransparency = bit.band(flags, 0x01) ~= 0 + + local delayTime = string.byte(fileData, pos + 1) + string.byte(fileData, pos + 2) * 256 + -- GIF delay is in hundredths of a second, convert to seconds + -- Many GIFs use 0 or very small delays, set a minimum + if delayTime == 0 then + delayForNextFrame = 0.1 -- Default 100ms + elseif delayTime <= 2 then + delayForNextFrame = 0.02 -- Minimum 20ms for very fast animations + else + delayForNextFrame = delayTime / 100 + end + + if hasTransparency then + transparentIndex = string.byte(fileData, pos + 3) + else + transparentIndex = nil + end + + pos = pos + blockSize + 1 + else + -- Skip other extensions + repeat + local blockSize = string.byte(fileData, pos) + pos = pos + 1 + if blockSize > 0 then + pos = pos + blockSize + end + until blockSize == 0 + end + + elseif separator == 0x2C then -- Image Descriptor + pos = pos + 1 + + local left = string.byte(fileData, pos) + string.byte(fileData, pos + 1) * 256 + local top = string.byte(fileData, pos + 2) + string.byte(fileData, pos + 3) * 256 + local width = string.byte(fileData, pos + 4) + string.byte(fileData, pos + 5) * 256 + local height = string.byte(fileData, pos + 6) + string.byte(fileData, pos + 7) * 256 + + local imgPacked = string.byte(fileData, pos + 8) + local hasLocalColorTable = bit.band(imgPacked, 0x80) ~= 0 + local interlaced = bit.band(imgPacked, 0x40) ~= 0 + + pos = pos + 9 + + local colorTable = globalColorTable + if hasLocalColorTable then + local size = 2 ^ (bit.band(imgPacked, 0x07) + 1) + colorTable = {} + for i = 1, size do + local r = string.byte(fileData, pos) / 255 + local g = string.byte(fileData, pos + 1) / 255 + local b = string.byte(fileData, pos + 2) / 255 + table.insert(colorTable, {r, g, b, 1}) + pos = pos + 3 + end + end + + -- Read LZW minimum code size + local minCodeSize = string.byte(fileData, pos) + pos = pos + 1 + + -- Read compressed image data blocks + local compressedData = {} + while true do + local blockSize = string.byte(fileData, pos) + pos = pos + 1 + if blockSize == 0 then break end + table.insert(compressedData, fileData:sub(pos, pos + blockSize - 1)) + pos = pos + blockSize + end + + -- Decompress image data + local indexStream = decompressLZW(table.concat(compressedData), minCodeSize) + + -- Create image data + local imageData = love.image.newImageData(gif.width, gif.height) + + -- Fill with background if first frame + if #gif.frames == 0 and backgroundColorIndex and globalColorTable[backgroundColorIndex + 1] then + local bg = globalColorTable[backgroundColorIndex + 1] + for y = 0, gif.height - 1 do + for x = 0, gif.width - 1 do + imageData:setPixel(x, y, bg[1], bg[2], bg[3], bg[4]) + end + end + elseif #gif.frames > 0 then + -- Copy previous frame if needed + local prevData = gif.frameData[#gif.frameData] + imageData:paste(prevData, 0, 0, 0, 0, gif.width, gif.height) + end + + -- Draw current frame + if indexStream and #indexStream > 0 then + local idx = 1 + + -- Use mapPixel for faster pixel operations + local function setPixels(x, y, r, g, b, a) + if x >= left and x < left + width and y >= top and y < top + height then + local pixelIdx = (y - top) * width + (x - left) + 1 + if pixelIdx <= #indexStream then + local colorIndex = string.byte(indexStream, pixelIdx) + + -- Skip transparent pixels + if transparentIndex == nil or colorIndex ~= transparentIndex then + if colorTable[colorIndex + 1] then + local color = colorTable[colorIndex + 1] + return color[1], color[2], color[3], color[4] + end + end + end + end + return r, g, b, a + end + + -- Only update the region where the frame is located + for y = top, top + height - 1 do + for x = left, left + width - 1 do + local pixelIdx = (y - top) * width + (x - left) + 1 + if pixelIdx <= #indexStream then + local colorIndex = string.byte(indexStream, pixelIdx) + + -- Skip transparent pixels + if transparentIndex == nil or colorIndex ~= transparentIndex then + if colorTable[colorIndex + 1] then + local color = colorTable[colorIndex + 1] + imageData:setPixel(x, y, color[1], color[2], color[3], color[4]) + end + end + end + end + end + end + + table.insert(gif.frameData, imageData) + table.insert(gif.frames, love.graphics.newImage(imageData)) + table.insert(gif.delays, delayForNextFrame) + + -- Reset delay for next frame + delayForNextFrame = 0.1 + + elseif separator == 0x3B then -- Trailer + break + else + pos = pos + 1 + end + end + + if #gif.frames == 0 then + error("No frames found in GIF") + end + + -- Ensure all frames have valid delays + for i = 1, #gif.delays do + if gif.delays[i] <= 0 or gif.delays[i] ~= gif.delays[i] then -- check for 0 or NaN + gif.delays[i] = 0.1 + end + end + + return gif +end + +function GifLoader.Updater(gif, proc) + local wait = function() + return gif.playing or gif.kill + end + proc:newThread("Gif Handler",function() + while true do + -- Only run if not paused + if gif.kill then -- When we want to clean up + thread.kill() + end + thread.hold(wait) + thread.sleep(gif.delays[gif.currentFrame] * 4) + gif.currentFrame = gif.currentFrame + 1 + + if gif.currentFrame > #gif.frames then + if gif.loop then + gif.currentFrame = 1 + else + gif.currentFrame = #gif.frames + gif.playing = false + gif.timer = 0 + end + end + end + end) +end + +function GifLoader.update(gif, dt) + if not gif.playing or #gif.frames <= 1 then return end + + gif.timer = gif.timer + dt + + -- Simple, accurate frame advancement + if gif.timer >= gif.delays[gif.currentFrame] then + -- Subtract the current frame's delay + gif.timer = gif.timer - gif.delays[gif.currentFrame] + + -- Move to next frame + gif.currentFrame = gif.currentFrame + 1 + + if gif.currentFrame > #gif.frames then + if gif.loop then + gif.currentFrame = 1 + else + gif.currentFrame = #gif.frames + gif.playing = false + gif.timer = 0 + end + end + + -- If we've accumulated too much time (lag spike), cap it + if gif.timer > 0.5 then + gif.timer = 0 + end + end +end + +function GifLoader.draw(gif, x, y, r, sx, sy, ox, oy) + if gif.frames[gif.currentFrame] then + love.graphics.draw(gif.frames[gif.currentFrame], x, y, r or 0, sx or 1, sy or 1, ox or 0, oy or 0) + end +end + +function GifLoader.play(gif) + gif.playing = true +end + +function GifLoader.pause(gif) + gif.playing = false +end + +function GifLoader.reset(gif) + gif.currentFrame = 1 + gif.timer = 0 +end + +function GifLoader.setFixedFramerate(gif, fps) + local delay = 1 / fps + for i = 1, #gif.delays do + gif.delays[i] = delay + end +end + +function GifLoader.getInfo(gif) + return { + width = gif.width, + height = gif.height, + frameCount = #gif.frames, + delays = gif.delays, -- Show actual delays for debugging + totalDuration = (function() + local total = 0 + for i = 1, #gif.delays do + total = total + gif.delays[i] + end + return total + end)() + } +end + +return GifLoader diff --git a/gui/addons/probe.lua b/gui/core/probe.lua similarity index 100% rename from gui/addons/probe.lua rename to gui/core/probe.lua diff --git a/gui/core/simulate.lua b/gui/core/simulate.lua index 89172d1..15484dd 100644 --- a/gui/core/simulate.lua +++ b/gui/core/simulate.lua @@ -1,6 +1,6 @@ local gui = require("gui") local multi, thread = require("multi"):init() -local transition = require("gui.elements.transitions") +local transition = require("gui.core.transitions") -- Triggers press then release local function getPosition(obj, x, y) diff --git a/gui/elements/transitions.lua b/gui/core/transitions.lua similarity index 95% rename from gui/elements/transitions.lua rename to gui/core/transitions.lua index 53de95f..768ef0c 100644 --- a/gui/elements/transitions.lua +++ b/gui/core/transitions.lua @@ -1,78 +1,78 @@ -local gui = require("gui") -local multi, thread = require("multi"):init() -local processor = gui:newProcessor("Transistion Processor") -local transition = {} - -local width, height, flags = love.window.getMode() -local fps = 60 - -if flags.refreshrate > 0 then - fps = flags.refreshrate -end - -transition.__index = transition -transition.__call = function(t, start, stop, time, ...) - local args = {...} - return function(st, sp, ti) -- allow these values to be overridden - if not (st or start) or not (sp or stop) then return multi.error("start and stop must be supplied") end - if start == stop then - local temp = { - OnStep = function() end, - OnStop = multi:newConnection() - } - proc:newTask(function() - temp.OnStop:Fire() - end) - return temp - end - local handle = t.func(t, start, stop, (ti or time) or 1, unpack(args)) - return { - OnStep = handle.OnStatus, - OnStop = handle.OnReturn + handle.OnError, - Kill = t.Kill - } - end -end - -function transition:newTransition(func) - local c = {} - setmetatable(c, self) - - c.fps = fps - c.func = processor:newFunction(func) - c.OnStop = multi:newConnection() - c.kill = false - - function c:SetFPS(fps) - self.fps = fps - end - - function c:GetFPS(fps) - return self.fps - end - - function c:Kill() - if c.running then - c.kill = true - end - end - - return c -end - -transition.glide = transition:newTransition(function(t, start, stop, time, ...) - local steps = t.fps*time - local piece = time/steps - local split = stop-start - t.running = true - for i = 0, steps do - if not(t.kill) then - thread.sleep(piece) - thread.pushStatus(start + i*(split/steps),piece*i) - end - end - t.running = false - t.kill = false -end) - +local gui = require("gui") +local multi, thread = require("multi"):init() +local processor = gui:newProcessor("Transistion Processor") +local transition = {} + +local width, height, flags = love.window.getMode() +local fps = 60 + +if flags.refreshrate > 0 then + fps = flags.refreshrate +end + +transition.__index = transition +transition.__call = function(t, start, stop, time, ...) + local args = {...} + return function(st, sp, ti) -- allow these values to be overridden + if not (st or start) or not (sp or stop) then return multi.error("start and stop must be supplied") end + if start == stop then + local temp = { + OnStep = function() end, + OnStop = multi:newConnection() + } + proc:newTask(function() + temp.OnStop:Fire() + end) + return temp + end + local handle = t.func(t, start, stop, (ti or time) or 1, unpack(args)) + return { + OnStep = handle.OnStatus, + OnStop = handle.OnReturn + handle.OnError, + Kill = t.Kill + } + end +end + +function transition:newTransition(func) + local c = {} + setmetatable(c, self) + + c.fps = fps + c.func = processor:newFunction(func) + c.OnStop = multi:newConnection() + c.kill = false + + function c:SetFPS(fps) + self.fps = fps + end + + function c:GetFPS(fps) + return self.fps + end + + function c:Kill() + if c.running then + c.kill = true + end + end + + return c +end + +transition.glide = transition:newTransition(function(t, start, stop, time, ...) + local steps = t.fps*time + local piece = time/steps + local split = stop-start + t.running = true + for i = 0, steps do + if not(t.kill) then + thread.sleep(piece) + thread.pushStatus(start + i*(split/steps),piece*i) + end + end + t.running = false + t.kill = false +end) + return transition \ No newline at end of file diff --git a/gui/docs/gui-library-docs.md b/gui/docs/gui-library-docs.md index e410a19..5da4675 100644 --- a/gui/docs/gui-library-docs.md +++ b/gui/docs/gui-library-docs.md @@ -1,62 +1,54 @@ # GUI Library Documentation -A component-based UI framework for LÖVE2D (Love2D) built on top of the `multi` concurrency library. The library provides a scene graph with dual-dimension layout, event-driven input handling, and a rich set of built-in element types. +A LÖVE2D-based GUI framework with a dual-dimension layout system, event-driven architecture, theming, transitions, and a rich set of built-in widgets. --- ## Table of Contents -1. [Setup & Initialization](#setup--initialization) -2. [Core Concepts](#core-concepts) - - [The Scene Graph](#the-scene-graph) - - [Dual-Dimension Layout (DualDim)](#dual-dimension-layout-dualdim) - - [Element Types (Bitmask)](#element-types-bitmask) - - [Form Factors](#form-factors) -3. [Creating Elements](#creating-elements) - - [Frames](#frames) - - [Text Labels](#text-labels) - - [Text Buttons](#text-buttons) - - [Text Boxes (Input)](#text-boxes-input) - - [Image Labels](#image-labels) - - [Image Buttons](#image-buttons) - - [Videos](#videos) -4. [Layout & Positioning](#layout--positioning) -5. [Events & Connections](#events--connections) - - [Global GUI Events](#global-gui-events) - - [Per-Element Events](#per-element-events) - - [Hot Keys](#hot-keys) -6. [Element Methods](#element-methods) - - [Positioning & Sizing](#positioning--sizing) - - [Visual Properties](#visual-properties) - - [Hierarchy & Parenting](#hierarchy--parenting) - - [Utilities](#utilities) -7. [Text Elements](#text-elements) - - [Font Management](#font-management) - - [Text Box Internals](#text-box-internals) -8. [Image Elements](#image-elements) -9. [Clipping & Scissor](#clipping--scissor) -10. [Roundness & Shape](#roundness--shape) -11. [Aspect Ratio & Resize Handling](#aspect-ratio--resize-handling) -12. [The `apply` Helper](#the-apply-helper) -13. [Tagging System](#tagging-system) -14. [Cloning Elements](#cloning-elements) -15. [Processors & Threading](#processors--threading) -16. [Drawing Internals](#drawing-internals) -17. [Virtual GUI](#virtual-gui) +1. [Setup & Integration](#1-setup--integration) +2. [The Dual-Dimension Layout System](#2-the-dual-dimension-layout-system) +3. [Core Widget Reference](#3-core-widget-reference) + - [Frame](#31-frame) + - [TextLabel](#32-textlabel) + - [TextButton](#33-textbutton) + - [TextBox (Input)](#34-textbox-input) + - [ImageLabel](#35-imagelabel) + - [ImageButton](#36-imagebutton) + - [Video](#37-video) +4. [Base Object API](#4-base-object-api) + - [Positioning & Sizing](#41-positioning--sizing) + - [Appearance](#42-appearance) + - [Visibility & Lifecycle](#43-visibility--lifecycle) + - [Interaction](#44-interaction) + - [Shape & Form Factor](#45-shape--form-factor) +5. [Events & Connections](#5-events--connections) + - [Per-Object Events](#51-per-object-events) + - [Global Events](#52-global-events) + - [Hotkeys](#53-hotkeys) +6. [Theming](#6-theming) +7. [Color Module](#7-color-module) +8. [Transitions & Animation](#8-transitions--animation) +9. [Add-on Widgets](#9-add-on-widgets) + - [Window](#91-window) + - [ScrollFrame](#92-scrollframe) + - [Slide-in Menu](#93-slide-in-menu) + - [Video Player](#94-video-player) +10. [Canvas](#10-canvas) +11. [Simulation (Testing)](#11-simulation-testing) +12. [Scheduler Probe (Load Monitoring)](#12-scheduler-probe-load-monitoring) +13. [Task Manager](#13-task-manager) +14. [Tips & Patterns](#14-tips--patterns) --- -## Setup & Initialization +## 1. Setup & Integration + +Require the library at the top of your project. The library hooks into LÖVE's callbacks automatically. ```lua -local gui = require("path.to.gui") -``` +local gui = require("gui") -The library self-initializes on `require`. It hooks into LÖVE's callback system automatically (quit, resize, mouse, keyboard, touch, gamepad, etc.) and starts its internal update and draw processors. - -In your `love.update` and `love.draw`: - -```lua function love.update(dt) gui.update(dt) end @@ -66,801 +58,1065 @@ function love.draw() end ``` -> **Note:** The library hooks LÖVE callbacks via a `Hook` function that wraps any pre-existing handler you define. Define your own `love.*` callbacks **before** `require`-ing the library, or they will be chained automatically. +That is all that is required. The library installs its own hooks for `love.keypressed`, `love.mousepressed`, `love.resize`, etc., so you do not need to forward those manually. + +### Creating Additional Processors + +The library runs on the `multi` scheduler. If you need background work to integrate with the GUI update cycle, use `gui:newProcessor`: + +```lua +local proc = gui:newProcessor("MyProcessor") +proc:newThread(function() ... end) +``` --- -## Core Concepts +## 2. The Dual-Dimension Layout System -### The Scene Graph - -The library maintains two root nodes: - -| Root | Description | -|---|---| -| `gui` | The main scene root. All elements created with `gui:newXxx()` are parented here by default. | -| `gui.virtual` | A secondary root for off-screen or hidden elements. Children here are not drawn but still have their absolute positions updated. | - -Elements form a tree. Every element has a `parent`, a `children` table, and inherits methods from `gui` via `__index`. - -### Dual-Dimension Layout (DualDim) - -Every element stores its position and size as a **dual dimension**: a combination of a scale component (relative to the parent) and an offset component (absolute pixels). +Every object's size and position is described by **eight numbers** that combine pixel offsets with fractional (0–1) scale values relative to the parent. This is the library's central concept. ``` -actualX = parent.w * scale.pos.x + offset.pos.x + parent.x -actualY = parent.h * scale.pos.y + offset.pos.y + parent.y -actualW = parent.w * scale.size.x + offset.size.x -actualH = parent.h * scale.size.y + offset.size.y +setDualDim(x, y, w, h, sx, sy, sw, sh) ``` -Constructor signature for `newDualDim` / all `newXxx` creation functions: +| Parameter | Meaning | +|-----------|---------| +| `x` | Pixel offset from parent's left edge | +| `y` | Pixel offset from parent's top edge | +| `w` | Pixel width | +| `h` | Pixel height | +| `sx` | Fractional X position (0 = left, 1 = right of parent) | +| `sy` | Fractional Y position (0 = top, 1 = bottom of parent) | +| `sw` | Fractional width (0 = 0px, 1 = full parent width) | +| `sh` | Fractional height (0 = 0px, 1 = full parent height) | + +The resolved absolute values are calculated as: ``` -x, y, w, h -- pixel offset for position and size -sx, sy, sw, sh -- scale (0–1) for position and size +absolute_x = parent.w * sx + x + parent.x +absolute_y = parent.h * sy + y + parent.y +absolute_w = parent.w * sw + w +absolute_h = parent.h * sh + h ``` -Examples: +### Examples ```lua --- 200×100 box at pixel position (50, 50): -gui:newFrame(50, 50, 200, 100) +-- Full-screen frame (fills parent completely) +frame:setDualDim(0, 0, 0, 0, 0, 0, 1, 1) --- Full-screen frame (uses scale only): -local f = gui:newFrame() -f:fullFrame() -- sets scale size to (1,1) and offset to (0,0,0,0) +-- Fixed 200×50 button in the top-left corner +btn:setDualDim(10, 10, 200, 50) --- Half-width, 40px tall, starting at 25% from left: -gui:newFrame(0, 100, 0, 40, 0.25, 0, 0.5, 0) +-- Centered horizontally, 300px wide, 5% from the top +panel:setDualDim(-150, 0, 300, 0, 0.5, 0.05, 0, 0.4) +-- sx=0.5 puts the left edge at the parent's midpoint; +-- x=-150 shifts it left by half the panel's own width. + +-- Right-aligned sidebar, 20% of parent width, full height +sidebar:setDualDim(0, 0, 0, 0, 0.8, 0, 0.2, 1) + +-- Anchored to bottom-right corner, fixed 100×30 +btn:setDualDim(-110, -40, 100, 30, 1, 1) ``` -Retrieve the computed screen-space rectangle at any time: +### Helper: `fullFrame()` + +Sets the object to fill its parent completely (equivalent to `setDualDim(0,0,0,0,0,0,1,1)`). ```lua -local x, y, w, h = element:getAbsolutes() +frame:fullFrame() ``` -### Element Types (Bitmask) - -Types are stored as a bitmask so an element can have multiple roles: - -| Constant | Value | Meaning | -|---|---|---| -| `gui.TYPE_FRAME` | 0 | Basic container | -| `gui.TYPE_IMAGE` | 1 | Renders an image | -| `gui.TYPE_TEXT` | 2 | Renders text | -| `gui.TYPE_BOX` | 4 | Text input cursor/selection overlay | -| `gui.TYPE_VIDEO` | 8 | Renders a video | -| `gui.TYPE_BUTTON` | 16 | Interactive button (sets hand cursor) | -| `gui.TYPE_ANIM` | 32 | Animation / spritesheet | - -Test membership: +### Reading Position ```lua -if element:hasType(gui.TYPE_TEXT) then ... end -if element:hasType(gui.TYPE_TEXT + gui.TYPE_BOX) then ... end -- is a text box +local x, y, w, h = obj:getAbsolutes() -- resolved pixel values + +local x, y, w, h, sx, sy, sw, sh = obj:getDualDim() -- raw dual-dim values ``` -### Form Factors +### Squaring -Controls the shape used for both fills and hit-testing: - -| Constant | Shape | -|---|---| -| `gui.FORM_RECTANGLE` | Rounded or plain rectangle (default) | -| `gui.FORM_CIRCLE` | Circle; `w` and `h` are set to `2*r` | -| `gui.FORM_ARC` | Arc segment | +Setting `obj.square = "w"` forces `h = w` (width-driven square). Setting `obj.square = "h"` forces `w = h` (height-driven square). Useful for icon buttons and circle elements. --- -## Creating Elements +## 3. Core Widget Reference -All creation functions are called on a **parent** element (or on `gui` itself for top-level elements). The new element is automatically inserted into the parent's `children` table. - -### Frames - -A plain container with a background fill and optional border. +All constructors follow the same signature pattern: ```lua -local frame = parent:newFrame(x, y, w, h, sx, sy, sw, sh) +parent:newXxx(x, y, w, h, sx, sy, sw, sh) ``` -A **virtual frame** is parented to `gui.virtual` regardless of the caller: +where `parent` is either `gui` (the root) or another object. Children are drawn on top of and clipped by (if `clipDescendants` is set) their parent. + +--- + +### 3.1 Frame + +A plain rectangular container. The base building block. ```lua -local vframe = parent:newVirtualFrame(x, y, w, h, sx, sy, sw, sh) +local panel = gui:newFrame(x, y, w, h, sx, sy, sw, sh) ``` -A **visual frame** is a regular frame tagged `"visual"`. Mouse events on it and its descendants are suppressed (useful for purely decorative overlays): - ```lua -local overlay = parent:newVisualFrame(x, y, w, h, sx, sy, sw, sh) +-- Example: a full-screen dark overlay +local overlay = gui:newFrame(0, 0, 0, 0, 0, 0, 1, 1) +overlay.color = {0, 0, 0} +overlay.visibility = 0.6 ``` -### Text Labels - -A non-interactive text element. +**Virtual Frame** — exists in memory and updates but is never drawn. Used to move objects off-screen without destroying them. ```lua -local label = parent:newTextLabel("Hello world", x, y, w, h, sx, sy, sw, sh) +local vframe = gui:newVirtualFrame(...) ``` -### Text Buttons - -A text element that fires pointer events and shows a hand cursor on hover. +**Visual Frame** — a frame that does not participate in mouse hit-testing. Children of a visual frame are also non-interactive. ```lua -local btn = parent:newTextButton("Click me", x, y, w, h, sx, sy, sw, sh) -btn.OnPressed(function(self, x, y) print("pressed!") end) +local display = gui:newVisualFrame(...) ``` -### Text Boxes (Input) +--- -A single-line text input field. +### 3.2 TextLabel + +A non-interactive frame that renders text. ```lua -local box = parent:newTextBox("default text", x, y, w, h, sx, sy, sw, sh) -box.OnReturn(function(self, text) print("Submitted:", text) end) +local label = parent:newTextLabel("Hello, World!", x, y, w, h, sx, sy, sw, sh) ``` -Keyboard navigation, backspace/delete, selection (click-drag or Ctrl+A), copy/paste/cut, and undo/redo are all handled automatically when the box has focus. +**Key properties:** -### Image Labels +| Property | Type | Description | +|----------|------|-------------| +| `text` | string | The displayed text | +| `textColor` | color | Text color (default: black) | +| `font` | Font | LÖVE font object | +| `align` | constant | `gui.ALIGN_LEFT`, `gui.ALIGN_CENTER`, or `gui.ALIGN_RIGHT` | +| `textOffsetX/Y` | number | Pixel nudge for text rendering | +| `textScaleX/Y` | number | Scale multiplier for text | +| `textVisibility` | number | 0–1 alpha for text only | -A non-interactive image element. +**Font methods:** + +```lua +label:setFont(16) -- set by size (default font) +label:setFont("fonts/myfont.ttf", 18) -- set by file + size +label:setFont(myLoveFont) -- set by existing font object + +label:fitFont(minSize, maxSize) -- auto-fit text to the element's bounds +label:centerFont() -- vertically center text within element +``` + +--- + +### 3.3 TextButton + +An interactive button with text. Automatically shows a hand cursor on hover. + +```lua +local btn = parent:newTextButton("Click Me", x, y, w, h, sx, sy, sw, sh) +``` + +```lua +btn.OnReleased(function(self, x, y, button, istouch) + print("Button clicked!") +end) +``` + +Buttons automatically integrate with the theming system inside a `newWindow` — they receive button colors, hover highlights, and the button font. + +--- + +### 3.4 TextBox (Input) + +An editable single-line text input field. Gains focus on click and shows a blinking cursor. + +```lua +local input = parent:newTextBox("placeholder", x, y, w, h, sx, sy, sw, sh) +``` + +**Key properties:** + +| Property | Type | Description | +|----------|------|-------------| +| `text` | string | Current text value | +| `cur_pos` | number | Cursor position (character index) | +| `blink` | boolean | Whether cursor blinks (default: true) | + +**Selection API:** + +```lua +input:HasSelection() -- boolean +input:GetSelection() -- start, stop (always start <= stop) +input:GetSelectedText() -- string +input:ClearSelection() +``` + +**Events:** + +```lua +input.OnReturn(function(self, text) + print("Submitted:", text) +end) +``` + +**Built-in hotkeys** (active when the textbox has focus): +- `Ctrl+A` — select all +- `Ctrl+C` — copy selection +- `Ctrl+V` — paste +- `Ctrl+X` — cut selection +- `Left/Right` — move cursor +- `Backspace/Delete` — delete character + +--- + +### 3.5 ImageLabel + +Displays an image. Supports PNG, JPG, and GIF. ```lua local img = parent:newImageLabel("path/to/image.png", x, y, w, h, sx, sy, sw, sh) ``` -GIF files are detected automatically by the `.gif` extension and animated. +The image is stretched to fill the element's bounds. GIFs animate automatically. -### Image Buttons - -An image element that fires pointer events and shows a hand cursor on hover. +**Changing the image:** ```lua -local ibtn = parent:newImageButton("icon.png", x, y, w, h, sx, sy, sw, sh) -ibtn.OnPressed(function(self, x, y) print("image clicked") end) +img:setImage("path/to/other.png") +img:setImage(loveImageObject) +-- Tile from a spritesheet: +img:setImage("spritesheet.png", srcX, srcY, srcW, srcH) ``` -### Videos - -Wraps a LÖVE `Video` object. +**Flipping:** ```lua -local vid = parent:newVideo("clip.ogv", x, y, w, h, sx, sy, sw, sh) -vid:play() -vid.OnVideoFinished(function(self) print("done") end) +img:flip() -- flip horizontally +img:flip(true) -- flip vertically ``` -Video methods: - -| Method | Description | -|---|---| -| `vid:setVideo(path_or_video)` | Load or swap the video source | -| `vid:play()` | Start playback | -| `vid:pause()` | Pause without rewinding | -| `vid:stop()` | Pause and rewind | -| `vid:rewind()` | Seek to start | -| `vid:seek(seconds)` | Jump to position | -| `vid:tell()` | Return current playback position (seconds) | -| `vid:getDuration()` | Return total duration (seconds) | -| `vid:setVolume(vol)` | Set audio volume (0–1) | -| `vid:getVideo()` | Return the underlying LÖVE Video object | - ---- - -## Layout & Positioning - -### Setting the Dual Dimension +**Gradient fill** (applies a gradient image to any frame or image element): ```lua --- Fires OnSizeChanged -element:setDualDim(x, y, w, h, sx, sy, sw, sh) - --- Silent version (no event) -element:rawSetDualDim(x, y, w, h, sx, sy, sw, sh) - --- Read back -local x, y, w, h, sx, sy, sw, sh = element:getDualDim() +panel:applyGradient("vertical", color1, color2, color3) +panel:applyGradient("horizontal", {1,0,0,1}, {0,0,1,1}) ``` -Pass `nil` for any argument to keep the current value. - -### Moving and Resizing +**Pre-loading images into cache:** ```lua --- Delta move (fires OnPositionChanged) -element:move(dx, dy) - --- Delta resize (fires OnSizeChanged) -element:size(dw, dh) - --- Move but clamp to parent bounds -element:moveInBounds(dx, dy) -``` - -### Centering - -```lua -element:centerX(true) -- horizontally center within parent -element:centerY(true) -- vertically center within parent -``` - -These attach internal loops that continuously recompute the offset whenever the element's size or position changes. - -### Convenience - -```lua -element:fullFrame() -- scale size (1,1), offset (0,0,0,0) — fills parent -``` - -### Dragging - -```lua -element:enableDragging(button) -- button = love mouse button number (1=left, 2=right, …) -element:enableDragging(nil) -- disable dragging -``` - -While dragging, `OnDragging`, `OnDragStart`, and `OnDragEnd` are fired. - -### Z-Order - -```lua -element:topStack() -- move to end of parent.children (drawn last = on top) -element:bottomStack() -- move to front of parent.children (drawn first = behind) +gui.cacheImage(gui, "assets/hero.png") +gui.cacheImage(gui, {"assets/a.png", "assets/b.png"}) ``` --- -## Events & Connections +### 3.6 ImageButton -Events use the `multi` connection system. Connect a handler by calling the connection as a function: +An image that responds to click events. Shows a hand cursor on hover. ```lua -element.OnPressed(function(self, x, y, button, istouch, presses) - -- ... +local btn = parent:newImageButton("icon.png", x, y, w, h, sx, sy, sw, sh) + +btn.OnReleased(function(self) + self:setImage("icon_pressed.png") end) ``` -Connections support composition: - -```lua --- OR: fires when either fires -(connA + connB)(handler) - --- AND: fires only when both conditions are met -(connA * connB)(handler) -``` - -### Global GUI Events - -These fire for the entire application window regardless of which element is focused. - -| Event | LÖVE callback | Arguments | -|---|---|---| -| `gui.Events.OnQuit` | `love.quit` | — | -| `gui.Events.OnDirectoryDropped` | `love.directorydropped` | `dir` | -| `gui.Events.OnDisplayRotated` | `love.displayrotated` | `index, orient` | -| `gui.Events.OnFilesDropped` | `love.filedropped` | `file` | -| `gui.Events.OnFocus` | `love.focus` | `focused` | -| `gui.Events.OnMouseFocus` | `love.mousefocus` | `focused` | -| `gui.Events.OnResized` | `love.resize` | `w, h` | -| `gui.Events.OnVisible` | `love.visible` | `visible` | -| `gui.Events.OnKeyPressed` | `love.keypressed` | `key, scancode, isrepeat` | -| `gui.Events.OnKeyReleased` | `love.keyreleased` | `key, scancode` | -| `gui.Events.OnTextEdited` | `love.textedited` | `text, start, length` | -| `gui.Events.OnTextInputed` | `love.textinput` | `text` | -| `gui.Events.OnMouseMoved` | `love.mousemoved` | `x, y, dx, dy, istouch` | -| `gui.Events.OnMousePressed` | `love.mousepressed` | `x, y, button, istouch, presses` | -| `gui.Events.OnMouseReleased` | `love.mousereleased` | `x, y, button, istouch, presses` | -| `gui.Events.OnWheelMoved` | `love.wheelmoved` | `x, y` | -| `gui.Events.OnTouchMoved` | `love.touchmoved` | `id, x, y, dx, dy, pressure` | -| `gui.Events.OnTouchPressed` | `love.touchpressed` | `id, x, y, dx, dy, pressure` | -| `gui.Events.OnTouchReleased` | `love.touchreleased` | `id, x, y, dx, dy, pressure` | -| `gui.Events.OnGamepadPressed` | `love.gamepadpressed` | `joystick, button` | -| `gui.Events.OnGamepadReleased` | `love.gamepadreleased` | `joystick, button` | -| `gui.Events.OnGamepadAxis` | `love.gamepadaxis` | `joystick, axis, value` | -| `gui.Events.OnJoystickAdded` | `love.joystickadded` | `joystick` | -| `gui.Events.OnJoystickRemoved` | `love.joystickremoved` | `joystick` | -| `gui.Events.OnJoystickHat` | `love.joystickhat` | `joystick, hat, dir` | -| `gui.Events.OnJoystickPressed` | `love.joystickpressed` | `joystick, button` | -| `gui.Events.OnJoystickReleased` | `love.joystickreleased` | `joystick, button` | -| `gui.Events.OnCreated` | internal | `element` — fires when any element is created | -| `gui.Events.OnObjectFocusChanged` | internal | `old, new` — fires when click focus changes | - -### Per-Element Events - -These are attached to each element instance. All mouse/pointer events are automatically pre-filtered: they only fire when the element is `active` and (for most events) when the pointer is within the element's bounds. - -| Event | Fires when… | -|---|---| -| `OnLoad` | (manual) element is "loaded" — user-defined | -| `OnPressed` | pointer pressed **inside** element | -| `OnPressedOuter` | pointer pressed **outside** element | -| `OnReleased` | pointer released **inside** element | -| `OnReleasedOuter` | pointer released **outside** (but was pressed inside) | -| `OnReleasedOther` | pointer released with no relevant press history | -| `OnDragStart` | drag begins (element must have `enableDragging` set) | -| `OnDragging` | pointer moves while dragging | -| `OnDragEnd` | drag ends | -| `OnEnter` | pointer enters the element bounds | -| `OnExit` | pointer leaves the element bounds | -| `OnMoved` | pointer moves while inside (or while dragging) | -| `OnWheelMoved` | scroll wheel moves while pointer is inside element | -| `OnSizeChanged` | `setDualDim` or `size` called | -| `OnPositionChanged` | `setDualDim` or `move` called | -| `OnDestroy` | element is about to be destroyed | -| `OnCreated` | element was created (forwarded from `gui.Events.OnCreated`) | -| `OnReturn` | (text boxes only) Enter/Return key pressed | -| `OnFontUpdated` | (text elements only) font changed via `setFont` | -| `OnVideoFinished` | (video elements only) video reaches its end | -| `OnLeftStickUp/Down/Left/Right` | gamepad left-stick events | -| `OnRightStickUp/Down/Left/Right` | gamepad right-stick events | - -#### Hierarchy Mode - -By default events fire if another element is not on top. Call: - -```lua -element:respectHierarchy(false) -- events will fire regardless -``` - -to make `OnPressed`, `OnReleased`, `OnEnter`, and `OnMoved` skip when the element is covered by a sibling. - --- -### Hot Keys +### 3.7 Video -Register a keyboard shortcut that fires a connection: +Plays a LÖVE-supported video file inside an element. ```lua -local conn = element:setHotKey({"lctrl", "s"}) -- returns a connection -conn(function(ref) print("Ctrl+S on", ref) end) +local vid = parent:newVideo("movie.ogv", x, y, w, h, sx, sy, sw, sh) ``` -You may pass an existing connection as the second argument to reuse it. - -#### Built-in Hot Keys - -| Hot Key | Trigger | -|---|---| -| `gui.HotKeys.OnSelectAll` | Ctrl+A | -| `gui.HotKeys.OnCopy` | Ctrl+C | -| `gui.HotKeys.OnPaste` | Ctrl+V | -| `gui.HotKeys.OnCut` | Ctrl+X | -| `gui.HotKeys.OnUndo` | Ctrl+Z | -| `gui.HotKeys.OnRedo` | Ctrl+Y / Ctrl+Shift+Z | - -These are already wired to the currently-focused text box for standard editing operations. - ---- - -## Element Methods - -### Positioning & Sizing - -| Method | Description | -|---|---| -| `el:getAbsolutes([transform])` | Returns `x, y, w, h` in screen space. Optional `transform` function is applied to each value. | -| `el:setDualDim(x,y,w,h,sx,sy,sw,sh)` | Set layout, fires `OnSizeChanged`. | -| `el:rawSetDualDim(...)` | Set layout, no event. | -| `el:getDualDim()` | Returns all 8 dual-dim components. | -| `el:move(dx, dy)` | Translate by delta, fires `OnPositionChanged`. | -| `el:size(dw, dh)` | Resize by delta, fires `OnSizeChanged`. | -| `el:moveInBounds(dx, dy)` | Translate while keeping element inside parent. | -| `el:fullFrame()` | Fill parent entirely. | -| `el:centerX(bool)` | Auto-center horizontally. | -| `el:centerY(bool)` | Auto-center vertically. | -| `el:getLocalCords(mx, my)` | Convert screen coordinates to element-local coordinates. | - -### Visual Properties - -| Property | Type | Default | Description | -|---|---|---|---| -| `color` | `{r,g,b}` | `{0.6, 0.6, 0.6}` | Background fill color | -| `borderColor` | `{r,g,b}` | black | Border color | -| `drawBorder` | boolean | `true` | Whether to draw the border | -| `visibility` | number | `1` | Background alpha (0–1) | -| `rotation` | number | `0` | Rotation in degrees | -| `active` | boolean | `true` | When `false`, element and all descendants ignore input | -| `visible` | boolean | `true` | Controls `getAllChildren` visibility filter | -| `ignore` | boolean | — | When `true`, element is skipped in coverage tests | - -Set color (also sets `visibility` if a 4th component is present): +**Playback control:** ```lua -element:setColor("color", {1, 0, 0, 0.8}) -element:setColor("borderColor", {0, 0, 0}) +vid:play() +vid:pause() +vid:stop() -- pause + rewind +vid:rewind() +vid:seek(t) -- seek to time in seconds +vid:tell() -- returns current time in seconds +vid:getDuration() +vid:setVolume(0.8) ``` -Apply a LÖVE shader: +**Events:** ```lua -element.shader = love.graphics.newShader(...) -``` - -Apply an effect wrapper (called around the draw call): - -```lua -element.effect = function(drawFunc) - love.graphics.push() - -- setup - drawFunc() - love.graphics.pop() -end -``` - -Apply a post-draw hook: - -```lua -element.post = function(self) - -- called after drawing, inside the same scissor/shader state -end -``` - -### Hierarchy & Parenting - -| Method | Description | -|---|---| -| `el:setParent(newParent)` | Re-parent element. Pass `nil` to detach. | -| `el:getChildren()` | Returns direct children table. | -| `el:getAllChildren([includeHidden])` | Returns all visible descendants recursively. | -| `el:isDescendantOf(obj)` | Returns `true` if `obj` is an ancestor of `el`. | -| `el:topStack()` | Draw on top of siblings. | -| `el:bottomStack()` | Draw behind siblings. | -| `el:destroy()` | Destroy element, its children, and all connections. | -| `el:removeChildren()` | Destroy all children but leave element itself. | -| `el:isActive()` | `true` if `active` and not parented under `gui.virtual`. | -| `el:isOffScreen()` | `true` if element rect is entirely outside screen bounds. | - -### Utilities - -| Method | Description | -|---|---| -| `el:hasType(t)` | Bitmask type test. | -| `el:canPress(mx, my)` | `true` if point is inside element (respects clip area). | -| `el:isBeingCovered(mx, my)` | `true` if a sibling is in front of this element at the given point. | -| `el:intersecpt(x, y, w, h)` | Returns intersection rect with a given AABB. | -| `el:newThread(func)` | Spawn a coroutine-style thread scoped to this element. | -| `el:getObjectFocus()` | Returns the currently focused element. | -| `el:getProcessor()` | Returns the internal updater processor. | - ---- - -## Text Elements - -All text elements (`newTextLabel`, `newTextButton`, `newTextBox`) inherit from `newTextBase`. - -### Properties - -| Property | Type | Default | Description | -|---|---|---|---| -| `text` | string | — | Displayed string | -| `textColor` | `{r,g,b}` | black | Text color | -| `font` | Font | 12px default | LÖVE Font object | -| `align` | constant | `ALIGN_LEFT` | `gui.ALIGN_LEFT`, `ALIGN_CENTER`, `ALIGN_RIGHT` | -| `textOffsetX/Y` | number | `0` | Additional pixel offset for text drawing | -| `textScaleX/Y` | number | `1` | Scale applied to text rendering | -| `textShearingFactorX/Y` | number | `0` | Shearing factor for text transform | -| `textVisibility` | number | `1` | Text alpha (0–1) | - -### Font Management - -```lua --- By size (default font) -element:setFont(14) - --- By path and size -element:setFont("fonts/myfont.ttf", 18) - --- By LÖVE font object -element:setFont(love.graphics.newFont("fonts/myfont.ttf", 18)) -``` - -Automatically resize font to fill element bounds: - -```lua --- Binary-search fit between min and max size -element:fitFont(minSize, maxSize, {scale = 1}) --- Returns bestFont, bestSize -``` - -Center text vertically inside the element: - -```lua -element:centerFont(y_offset) -``` - -Calculate where the top and bottom of rendered text actually are (pixel offsets within element): - -```lua -local top, bottom = element:calculateFontOffset(font, adjust) -``` - -### Text Box Internals - -| Property | Description | -|---|---| -| `cur_pos` | Integer cursor position (0 = before first character) | -| `selection` | `{start, stop}` character indices (may be reversed) | -| `bar_show` | `true` when the cursor bar should be visible (blinks via internal thread) | -| `doSelection` | `true` while a drag-selection is in progress | - -Methods: - -```lua -box:HasSelection() -- returns true/false -box:GetSelection() -- returns start, stop (always start ≤ stop) -box:GetSelectedText() -- returns selected substring -box:ClearSelection() -- clear selection state +vid.OnVideoFinished(function(self) + print("Video ended") +end) ``` --- -## Image Elements +## 4. Base Object API -All image elements (`newImageLabel`, `newImageButton`) inherit from `newImageBase`. +Every object in the hierarchy inherits the following API. -### `setImage` +--- + +### 4.1 Positioning & Sizing ```lua --- From a file path (PNG, JPG, etc.) -element:setImage("path/to/image.png") - --- GIF animation (auto-detected by extension) -element:setImage("path/to/anim.gif") - --- From a LÖVE Image object -element:setImage(loveImageObject) -``` - -### Properties - -| Property | Description | -|---|---| -| `imageColor` | Tint color applied when drawing | -| `imageVisibility` | Image alpha (0–1) | -| `scaleX / scaleY` | Flip/scale. Negative values flip the axis. | -| `quad` | LÖVE Quad used for rendering (sub-region) | - -### Flipping - -```lua -element:flip(false) -- flip horizontally -element:flip(true) -- flip vertically -``` - -### Gradient - -Apply a gradient as the image of any element: - -```lua -element:applyGradient("horizontal", {r,g,b,a}, {r,g,b,a}, ...) -element:applyGradient("vertical", {r,g,b,a}, {r,g,b,a}, ...) -``` - -### Image Caching - -```lua --- Pre-load a single image into the cache -gui.cacheImage(gui, "path/to/img.png") - --- Pre-load multiple images; reports progress via OnStatus -gui.cacheImage(gui, {"img1.png", "img2.png"}) - --- Tile helper: returns imagedata and quad -local imgdata, quad = gui:getTile("sheet.png", tileX, tileY, tileW, tileH) +obj:setDualDim(x, y, w, h, sx, sy, sw, sh) -- update position/size; nil preserves current value +obj:getAbsolutes() -- returns resolved x, y, w, h in pixels +obj:getDualDim() -- returns all 8 raw values +obj:move(dx, dy) -- increment pixel offset (fires OnPositionChanged) +obj:size(dx, dy) -- increment pixel size (fires OnSizeChanged) +obj:moveInBounds(dx, dy) -- move but keep within parent bounds +obj:fullFrame() -- shorthand for 100%×100% fill +obj:centerX(true) -- auto-center horizontally within parent +obj:centerY(true) -- auto-center vertically within parent ``` --- -## Clipping & Scissor - -Clipping is set on a **parent** and affects all descendants: +### 4.2 Appearance ```lua -parent.clipDescendants = true -``` +obj.color = {r, g, b} -- background color (0–1 range) +obj.borderColor = {r, g, b} -- border color +obj.drawBorder = true/false -- show/hide border +obj.visibility = 0.8 -- overall opacity 0–1 +obj.rotation = 45 -- rotation in degrees -During each draw pass, the parent propagates its screen-space rectangle to each child's `__variables.clip`. Children then apply LÖVE's scissor test to avoid drawing outside the parent. +obj:setRoundness(rx, ry, segments) -- round all corners +obj:setRoundness(rx, ry, seg, "top") -- round top corners only +obj:setRoundness(rx, ry, seg, "bottom") -- round bottom corners only + +obj.shader = myShader -- apply a LÖVE shader (image elements only) +obj.clipDescendants = true -- clip child rendering to this element's bounds +``` --- -## Roundness & Shape +### 4.3 Visibility & Lifecycle ```lua --- Rounded corners -element:setRoundness(rx, ry, segments, side) --- rx, ry: x/y radius (default 5) --- segments: arc segments (default 30) --- side: "top", "bottom", or true (all corners) +obj.visible = false -- hide (and stop receiving events) +obj.active = false -- deactivate without hiding --- Directional override -element:setRoundnessDirection(horizontal, vertical) +obj:isActive() -- true if active and not in the virtual tree +obj:isOffScreen() -- true if fully outside the window + +obj:topStack() -- move to top of parent's draw order (drawn last = on top) +obj:bottomStack() -- move to bottom of draw order + +obj:destroy() -- remove from tree, disconnect all events, free resources +obj:removeChildren() -- destroy all children but keep the object itself ``` -Circle and arc shapes are set at creation time: +--- + +### 4.4 Interaction + +```lua +-- Drag support +obj:enableDragging(gui.MOUSE_PRIMARY) -- enable drag with left mouse button +obj:enableDragging(gui.MOUSE_SECONDARY) -- enable drag with right button +obj:enableDragging(false) -- disable dragging + +-- Hierarchy hit-testing: only fires events when not occluded by a sibling +obj:respectHierarchy(true) + +-- Tag system (used for identifying objects and filtering events) +obj:tag("myTag") -- set the primary tag (accessible via :getTag()) +obj:setTag("category") -- set an arbitrary tag key +obj:hasTag("category") -- boolean +obj:parentHasTag("visual") -- checks the ancestor chain + +-- Tree queries +obj:getChildren() -- immediate children table +obj:getAllChildren() -- flat list of all visible descendants +obj:isDescendantOf(other) -- boolean +obj:canPress(mx, my) -- boolean: would a click at mx,my hit this object? + +-- Cloning +local copy = obj:clone() +local copy = obj:clone({copyTo = parent, connections = true}) +``` + +--- + +### 4.5 Shape & Form Factor + +By default elements are rectangles. You can change an element's form factor: ```lua -- Circle -element:makeCircle(x, y, radius, sx, sy, sr, segments) +obj:makeCircle(x, y, radius, sx, sy, sr, segments) -- Arc -element:makeArc(arcType, x, y, radius, sx, sy, sr, startAngle, endAngle, segments) --- arcType: "open", "closed", or "pie" (passed to love.graphics.arc) --- Angles in radians +obj:makeArc(arcType, x, y, radius, sx, sy, sr, angle1, angle2, segments) +-- arcType is a LÖVE arc type string: "open", "closed", or "pie" +``` + +The `formFactor` property can also be set directly: + +```lua +obj.formFactor = gui.FORM_RECTANGLE -- default +obj.formFactor = gui.FORM_CIRCLE +obj.formFactor = gui.FORM_ARC ``` --- -## Aspect Ratio & Resize Handling +## 5. Events & Connections -Lock the root GUI to a design resolution: +The library uses the `multi` connection system. Connections are called with the syntax: ```lua -gui:setAspectSize(1920, 1080) -- set design resolution -gui.aspect_ratio = true -- enable aspect-ratio mode +obj.OnSomeEvent(function(self, ...) + -- handler +end) ``` -When the window resizes, the library calculates letterbox/pillarbox offsets and adjusts `gui.x`, `gui.y`, `gui.w`, `gui.h` (and the same on `gui.virtual`) so all elements remain proportional. - -Disable it: +Multiple handlers can be attached to a single event. Connections can be combined: ```lua -gui:setAspectSize(nil, nil) -gui.aspect_ratio = false -``` +-- OR: fires the handler if either event fires +(obj.OnReleased + obj.OnReleasedOuter)(function() ... end) -Utility to compute the scaled size manually: - -```lua -local nw, nh, offsetX, offsetY = gui:GetSizeAdjustedToAspectRatio(windowW, windowH) +-- AND: fires the handler only when both have fired +(conn1 * conn2)(function() ... end) ``` --- -## The `apply` Helper +### 5.1 Per-Object Events -`gui.apply` is a batch property setter that inspects each field name for a prefix: +| Event | Arguments | Description | +|-------|-----------|-------------| +| `OnPressed` | `self, x, y, dx, dy, istouch` | Mouse pressed inside the element | +| `OnReleased` | `self, x, y, button, istouch, presses` | Mouse released inside the element | +| `OnReleasedOuter` | same | Released after a press, but outside the element | +| `OnReleasedOther` | same | Released with no previous press on this element | +| `OnPressedOuter` | `self, x, y, button, istouch, presses` | Pressed outside this element | +| `OnEnter` | `self, x, y` | Mouse moved onto the element | +| `OnExit` | `self, x, y` | Mouse moved off the element | +| `OnMoved` | `self, x, y, dx, dy, istouch` | Mouse moved while over the element | +| `OnDragStart` | `self, dx, dy, x, y, istouch` | Drag began | +| `OnDragging` | `self, dx, dy, x, y, istouch` | Drag in progress | +| `OnDragEnd` | `self, dx, dy, x, y, istouch, presses` | Drag ended | +| `OnWheelMoved` | `x, y` | Scroll wheel moved while cursor is over element | +| `OnSizeChanged` | `self, ...` | Element size changed | +| `OnPositionChanged` | `self, ...` | Element position changed | +| `OnDestroy` | `self` | Element is being destroyed | +| `OnCreated` | `element` | Fires for any descendant created under this element | +| `OnLoad` | — | Fires once when the element is first set up | +| `OnUpdate` | `self, dt` | Fires every update frame | -| Prefix | Meaning | -|---|---| -| `C_` | Connect to the named connection (value = handler function) | -| `I_` | Invoke the named method with args from a table | -| *(none)* | Direct assignment or smart detection (connection vs function vs value) | +**Gamepad / joystick events** are also available on every object: ```lua -gui.apply({ - color = {1, 0, 0}, - C_OnPressed = function(self) print("pressed") end, - I_setFont = {"fonts/bold.ttf", 16}, -}, buttonA, buttonB, buttonC) +obj.OnLeftStickUp / Down / Left / Right +obj.OnRightStickUp / Down / Left / Right +``` + +**TextLabel / TextButton / TextBox extras:** + +```lua +obj.OnFontUpdated(function(self) end) -- font changed +input.OnReturn(function(self, text) end) -- Enter key pressed in textbox +``` + +**Video extras:** + +```lua +vid.OnVideoFinished(function(self) end) ``` --- -## Tagging System +### 5.2 Global Events -Arbitrary string tags can be attached to any element: +All global events live under `gui.Events`: ```lua -element:setTag("draggable") -element:setTag("ui-panel") +gui.Events.OnKeyPressed(function(key, scancode, isrepeat) end) +gui.Events.OnKeyReleased(function(key, scancode) end) +gui.Events.OnTextInputed(function(text) end) +gui.Events.OnMouseMoved(function(x, y, dx, dy, istouch) end) +gui.Events.OnMousePressed(function(x, y, button, istouch, presses) end) +gui.Events.OnMouseReleased(function(x, y, button, istouch, presses) end) +gui.Events.OnWheelMoved(function(x, y) end) +gui.Events.OnResized(function(w, h) end) +gui.Events.OnQuit(function() end) +gui.Events.OnFilesDropped(function(x, y, files) end) +gui.Events.OnFocus(function(focus) end) +gui.Events.OnObjectFocusChanged(function(previous, current) end) -element:hasTag("draggable") -- true / false (direct tag) -element:parentHasTag("ui-panel") -- true if any ancestor has the tag -``` - -The built-in `"visual"` tag suppresses all mouse event connections: - -```lua -local deco = parent:newVisualFrame(...) -- automatically gets "visual" tag +-- Gamepad / joystick +gui.Events.OnGamepadPressed(function(joystick, button) end) +gui.Events.OnGamepadAxis(function(joystick, axis, value) end) ``` --- -## Cloning Elements +### 5.3 Hotkeys -Deep-copy an element and optionally its connection handlers: +Register a hotkey and get back a connection object: ```lua -local copy = element:clone({ - copyTo = targetParent, -- parent for the clone (default: gui.virtual) - connections = true, -- also copy connection handlers +local conn = obj:setHotKey({"lctrl", "s"}) +conn(function(ref) + print("Save triggered from", ref) +end) +``` + +Multiple key combinations for the same action: + +```lua +local onSave = gui:setHotKey({"lctrl", "s"}) + gui:setHotKey({"rctrl", "s"}) +onSave(function() save() end) +``` + +**Built-in hotkeys:** + +| Hotkey | Connection | +|--------|-----------| +| Ctrl+A | `gui.HotKeys.OnSelectAll` | +| Ctrl+C | `gui.HotKeys.OnCopy` | +| Ctrl+V | `gui.HotKeys.OnPaste` | +| Ctrl+X | `gui.HotKeys.OnCut` | +| Ctrl+Z | `gui.HotKeys.OnUndo` | +| Ctrl+Y / Ctrl+Shift+Z | `gui.HotKeys.OnRedo` | +| Ctrl+T | Toggle Task Manager | + +--- + +## 6. Theming + +The `theme` module generates consistent color palettes for use with `newWindow` and other themed widgets. + +```lua +local theme = require("gui.core.theme") +``` + +### Creating a Theme + +**From explicit colors:** + +```lua +local t = theme:new(primaryColor, primaryText, buttonText, buttonNormal, primaryFont, buttonFont) + +-- Using hex strings (most convenient): +local t = theme:new("#2d6a9f", "#f0f0f0", "#ffffff") +``` + +**From a table (preferred for full control):** + +```lua +local t = theme:new({ + primary = "#124559", + primaryDark = "#01161E", + primaryText = "#AEC3B0", + buttonNormal = "#1e6f8a", + buttonHighlight = "#2a9bbf", + buttonText = "#ffffff", + textFont = myFont, + buttonTextFont = myBoldFont, + -- Any extra keys are kept and accessible on the theme object }) ``` -`clone` recurses through all children. Connection handlers from the original are **bound** (not moved) to the clone's connections, so both elements remain independently connected. +**Random harmonious theme:** + +```lua +local t = theme:random() -- any brightness +local t = theme:random(nil, "dark") -- dark palette +local t = theme:random(nil, "light") -- light palette +local t = theme:random(12345) -- reproducible from a seed +print(t:getSeed()) -- retrieve the seed +print(t:dump()) -- export as hex string +``` + +### Theme Properties + +| Property | Description | +|----------|-------------| +| `colorPrimary` | Main background color | +| `colorPrimaryDark` | Darker variant (headers, accents) | +| `colorPrimaryText` | Text on primary backgrounds | +| `colorButtonNormal` | Button resting color | +| `colorButtonHighlight` | Button hover color | +| `colorButtonText` | Text on buttons | +| `fontPrimary` | Font for labels | +| `fontButton` | Font for buttons | + +### Applying a Theme to a Window + +Pass the theme as the last argument to `newWindow` — the window will automatically style all child buttons and labels that are created inside it: + +```lua +local win = gui:newWindow(x, y, w, h, "Title", draggable, myTheme) +``` --- -## Processors & Threading +## 7. Color Module -The library uses two internal processors from the `multi` library: - -| Processor | Purpose | -|---|---| -| `updater` | Input hooks, hot keys, text-box blink, video completion, image loading | -| `drawer` | Per-frame draw loop, virtual element position pass | - -Create a new processor that participates in `gui.update`: +The `color` module handles color creation, conversion, and manipulation. All colors in the library are `{r, g, b}` or `{r, g, b, a}` tables with values in the 0–1 range. ```lua -local proc = gui:newProcessor("MyProcessor") --- proc is a multi Processor; attach tasks/loops to it normally +local color = require("gui.core.color") ``` -Spawn a coroutine thread scoped to an element: +### Creating Colors ```lua -element:newThread(function(self, thread) - while true do - thread.sleep(1) - print("tick", self.text) +-- Hex string +color.new("#ff5500") +color.new("#ff550088") -- with alpha + +-- CSS-style strings +color.new("rgb(255,85,0)") +color.new("rgba(255,85,0,0.5)") +color.new("hsl(20,100,50)") +color.new("hsla(20,100,50,0.8)") + +-- Raw 0–1 floats +color.new(1, 0.33, 0) + +-- HSL (hue 0–360, sat 0–100, light 0–100) +color.new(color.hsl(200, 60, 40)) + +-- HSV (hue 0–360, sat 0–1, val 0–1) +color.new(color.hsv(200, 0.6, 0.8)) +``` + +### Manipulation + +```lua +color.lighten(c, amount) -- amount is 0–1 factor +color.darken(c, amount) +color.saturate(c, amount) +color.desaturate(c, amount) +color.invert(c) +color.lerp(c1, c2, t) -- blend; t is 0–1 +color.mix(c1, c2, t) -- alias for lerp +``` + +### Queries + +```lua +color.isLight(c) -- boolean +color.getAverageLightness(c) -- 0–1 float +color.rgbToHex(c) -- returns hex string without "#" +``` + +### Arithmetic + +Color objects support `+`, `-`, `*`, `/`, `%`, `^`, and unary `-` operators, applied component-wise. + +### Named Colors + +```lua +color.white +color.black +color.red +color.green +color.blue +color.highlighter_blue +-- (and more — check core_color.lua for the full list) +``` + +--- + +## 8. Transitions & Animation + +The `transitions` module provides smooth interpolated animations for numeric values. + +```lua +local transition = require("gui.elements.transitions") +``` + +### Built-in Transitions + +Currently `transition.glide` is provided — a linear glide from a start value to a stop value. + +### Using a Transition + +A transition factory is created by calling `transition.glide(start, stop, duration)`. This returns a **factory function** that, when called, starts the animation and returns a handle. + +```lua +-- Animate a panel sliding in from the left +local slideIn = transition.glide(-200, 0, 0.3) -- from -200 to 0 in 0.3 seconds + +local t = slideIn() -- start the animation +t.OnStep(function(position) + panel:setDualDim(position) +end) +t.OnStop(function() + print("Animation complete") +end) +``` + +### Overriding Values at Runtime + +The factory function accepts optional overrides: + +```lua +local t = slideIn(newStart, newStop, newDuration) +``` + +### Stopping Early + +```lua +t.Kill() +``` + +### Custom Transitions + +```lua +local myTransition = transition:newTransition(function(t, start, stop, time) + -- t.fps is the target FPS for this transition + local steps = t.fps * time + local piece = time / steps + t.running = true + for i = 0, steps do + if not t.kill then + thread.sleep(piece) + -- push the current interpolated value as a status update + thread.pushStatus(start + i * ((stop - start) / steps)) + end end + t.running = false + t.kill = false end) ``` -Attach a per-frame update callback (called every update loop): +### Changing FPS ```lua -gui:OnUpdate(function(self, dt) - -- called every frame -end) - -element:OnUpdate(function(self, dt) - -- called every frame with element as self -end) -``` - -Create a one-shot or reusable function that runs asynchronously: - -```lua -local fn = gui.newFunction(function(arg1, arg2) - -- runs in updater context -end) -fn(arg1, arg2) +transition.glide:SetFPS(30) -- lower for performance-sensitive animations ``` --- -## Drawing Internals +## 9. Add-on Widgets -The draw loop iterates `gui:getAllChildren()` each frame and calls `draw_handler` on each element in order (back-to-front). - -`draw_handler` does, in order: - -1. Compute and cache `child.x/y/w/h` via `getAbsolutes`. -2. Propagate clip rects to descendants if `clipDescendants` is set. -3. Activate shader if present. -4. Apply LÖVE scissor (clip or roundness-based). -5. Fill background with `child.color` and `child.visibility`. -6. Draw border with `child.borderColor`. -7. Handle special roundness sides ("top"/"bottom"). -8. Dispatch to type-specific draw functions (video → image → text → box cursor/selection). -9. Call `child:post()` if defined. -10. Remove scissor and shader. - -`gui.draw_handler` is exposed publicly so custom renderers can call it directly. +These widgets live in `gui/addons` and extend the core library. --- -## Virtual GUI +### 9.1 Window -`gui.virtual` is a root node whose children are never rendered on screen but still participate in the layout pass (absolute positions are computed). Use it to keep pre-built off-screen components ready to be re-parented: +A resizable, draggable floating window with a title bar and a close button. ```lua --- Create off-screen -local popup = gui.virtual:newFrame(0, 0, 400, 300) +require("gui.addons") -- loads addons --- Show it by re-parenting -popup:setParent(gui) - --- Hide it again -popup:setParent(gui.virtual) +local win = gui:newWindow(x, y, width, height, "Window Title", draggable, theme) ``` -`gui.virtual` shares the same screen dimensions as `gui`, so positions remain correct when an element moves between them. +The returned object is the **inner content frame** (inside the title bar). Add children directly to `win`. + +**API:** + +```lua +win:setTitle("New Title") +win:close() -- moves the window to the virtual tree (hides it) +win:open() -- brings the window back to the main tree +win:setTheme(theme) -- re-apply a different theme +win:getTheme() -- returns current theme + +win.OnClose(function(self) + -- fires when the X button is pressed +end) +``` + +**Minimum dimensions:** 200px wide, 100px tall (enforced by resize handles). + +**Children auto-styled:** Any `TYPE_BUTTON` or `TYPE_TEXT` created inside the window automatically receives the theme's colors and fonts via the `OnCreated` event. + +--- + +### 9.2 ScrollFrame + +A viewport with automatic vertical and horizontal scrollbars. Returns the **content frame** — add children to that. + +```lua +require("gui.addons") + +local content = gui:newScrollFrame(x, y, w, h, sx, sy, sw, sh) +-- Add children to `content`: +local row = content:newFrame(0, rowY, 0, 30, 0, 0, 1) +``` + +**Scrolling API (on the content frame):** + +```lua +content:scrollTo(scrollY, scrollX) -- jump to absolute scroll position +content:scrollBy(deltaY, deltaX) -- scroll by a relative amount +content:scrollToTop() +content:scrollToBottom() +content:setScrollSpeed(speed) -- default is 40 pixels per wheel tick +content:getScrollPos() -- returns scrollX, scrollY +content:getMaxScroll() -- returns maxScrollX, maxScrollY +content:setContentSize(width, height) -- explicitly set the content dimensions +``` + +The scrollbars appear automatically when the content overflows the viewport and hide when it does not. + +--- + +### 9.3 Slide-in Menu + +A panel that slides in from the left, right, or top with an animated transition. + +```lua +local transition = require("gui.elements.transitions") + +local menu = gui:newMenu(title, size, position, trans) +-- title : string label (required) +-- size : fractional width/height (e.g. 0.25 for 25% of screen) +-- position : gui.ALIGN_LEFT (default), gui.ALIGN_RIGHT, or gui.ALIGN_CENTER +-- trans : transition factory (default: transition.glide) +``` + +**API:** + +```lua +menu:Open(true) -- slide open +menu:Open(false) -- slide closed +menu:isOpen() -- boolean +``` + +**Example:** + +```lua +local sidebar = gui:newMenu("Navigation", 0.2, gui.ALIGN_LEFT) + +-- Add content to the menu frame +local navBtn = sidebar:newTextButton("Home", 10, 60, 180, 40) + +-- Wire toggle +toggleBtn.OnReleased(function() + sidebar:Open(not sidebar:isOpen()) +end) +``` + +--- + +### 9.4 Video Player + +A pre-built media player widget with play/pause toggle and a seek bar. + +```lua +require("gui.addons") + +gui:newVideoPlayer(source, x, y, w, h, sx, sy, sw, sh) +``` + +The player creates its own themed window containing the video, a play/pause button, and a progress bar. + +--- + +## 10. Canvas + +The `canvas` module creates full-screen root frames. + +```lua +local newCanvas = require("gui.core.canvas") + +local visual = newCanvas("visual") -- visual frame: non-interactive, for backgrounds/effects +local regular = newCanvas() -- regular interactive frame +``` + +**Swapping child trees between canvases:** + +```lua +visual:swap(frameA, frameB) +-- Exchanges the children of frameA and frameB, re-parenting correctly. +-- Useful for scene transitions. +``` + +--- + +## 11. Simulation (Testing) + +The `simulate` module lets you programmatically fire mouse events, useful for automated testing or scripted UI demos. + +```lua +local simulate = require("gui.core.simulate") +``` + +### Methods + +```lua +-- Immediate (synchronous) mouse press and release +simulate:Press(button, x, y, istouch) +simulate:Release(button, x, y, istouch) + +-- Async click (press then release after one scheduler tick) +simulate.Click(obj, button, x, y, istouch) + +-- Animated mouse movement +simulate.Move(obj, dx, dy, x, y, istouch) +``` + +When called on an object (`obj:Press()`), the position defaults to the object's center. When called on `simulate` directly, `x` and `y` default to the current mouse position. + +**Example:** + +```lua +-- Programmatically click a button +simulate.Click(myButton) + +-- Simulate a drag from one point to another +simulate.Move(nil, 100, 0, startX, startY) -- move 100px to the right +``` + +--- + +## 12. Scheduler Probe (Load Monitoring) + +Measures scheduler responsiveness using tick-slip detection. Gives a 0–100% load estimate without blocking. + +```lua +local probe = require("gui.core.probe") +probe:install(multi) +``` + +**Options:** + +```lua +probe:install(multi, { + interval = 0.05, -- probe fires every N seconds (default: 0.05) + alpha = 0.15, -- EMA smoothing factor 0–1 (default: 0.15, lower = smoother) + maxLag = 0.5, -- seconds of lag that equals 100% load (default: 0.5) +}) +``` + +**Reading load:** + +```lua +-- Both are non-blocking — safe to call every frame +local load, lagMs = multi:getLoad() +-- load : integer 0–100 +-- lagMs : smoothed scheduler lag in milliseconds + +local lagMs, lagRatio = multi:getSchedulerLag() +``` + +The probe is automatically installed when `gui:showTaskManager()` is called. + +--- + +## 13. Task Manager + +A built-in debug overlay showing all active scheduler tasks, their state, uptime, and priority. + +```lua +require("gui.addons") +gui:showTaskManager() +``` + +**Default hotkey:** `Ctrl+T` toggles it open/closed. + +The task manager window provides: +- A list of all processors, threads, and tasks with live state +- Pause/Resume buttons for individual tasks +- Kill buttons to terminate tasks +- Clickable priority column to cycle a task's scheduler priority +- An Error Log tab that captures all thread errors in real-time +- A load bar showing current scheduler utilization + +--- + +## 14. Tips & Patterns + +### Aspect Ratio Locking + +```lua +-- Lock the root to a 16:9 canvas; letterbox on resize +gui:setAspectSize(1920, 1080) +gui.aspect_ratio = true +``` + +### Clipping Children + +```lua +local container = gui:newFrame(x, y, w, h) +container.clipDescendants = true -- children are scissored to container bounds +``` + +### Tag-Based Queries + +```lua +-- Mark a group of elements and query by tag +for _, child in ipairs(parent:getAllChildren()) do + if child:hasTag("card") then + child.color = selectedColor + end +end +``` + +### Using `gui.apply` for Bulk Property Setting + +`gui.apply` sets properties on multiple objects at once. It understands connection names (`C_` prefix), invoke-style functions (`I_` prefix), and plain properties. + +```lua +gui.apply({ + color = {0.2, 0.2, 0.2}, + drawBorder = false, + I_enableDragging = {gui.MOUSE_PRIMARY}, + OnReleased = function(self) print("clicked", self:getTag()) end, +}, btn1, btn2, btn3) +``` + +### Stacking Order + +Objects are drawn in the order they appear in their parent's `children` table. The last child is drawn on top. + +```lua +obj:topStack() -- draw on top of siblings +obj:bottomStack() -- draw below all siblings +``` + +### Responsive Layouts + +Combine fractional scale with negative pixel offsets for padding: + +```lua +-- A frame inset 10px on all sides within its parent +inner:setDualDim(10, 10, -20, -20, 0, 0, 1, 1) +``` + +### Intersection Testing + +```lua +local ix, iy, iw, ih = obj:intersecpt(x, y, w, h) +-- Returns the overlapping rectangle, or 0,0,0,0 if no overlap +``` + +### Programmatic Focus + +```lua +local focused = gui:getObjectFocus() -- the currently focused object +``` + +### OnUpdate (per-frame callback) + +```lua +obj:OnUpdate(function(self, dt) + -- runs every frame while the object exists + self.rotation = self.rotation + 90 * dt +end) +``` diff --git a/gui/elements/init.lua b/gui/elements/init.lua deleted file mode 100644 index caf5822..0000000 --- a/gui/elements/init.lua +++ /dev/null @@ -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 diff --git a/gui/init.lua b/gui/init.lua index 44c834a..7339ecc 100644 --- a/gui/init.lua +++ b/gui/init.lua @@ -2,10 +2,9 @@ local utf8 = require("utf8") local multi, thread = require("multi"):init() local GLOBAL, THREAD = require("multi.integration.loveManager"):init() local color = require("gui.core.color") -local gif = require("gui.addons.gifloader") +local gif = require("gui.core.gifloader") local gui = {} local updater = multi:newProcessor("UpdateManager", true) - local drawer = multi:newProcessor("DrawManager", true) local bit = require("bit") @@ -139,6 +138,10 @@ end) -- Hotkeys +local function noOf(sx,sy,sw,sh) + return nil,nil,nil,nil,sx,sy,sw,sh +end + local has_hotkey = false local hot_keys = {} @@ -575,8 +578,7 @@ function gui:isActive() end function gui:isOnScreen() - - return + return not self:isOffScreen() end -- Base get uniques @@ -1123,6 +1125,302 @@ function gui:newTextBase(typ, txt, x, y, w, h, sx, sy, sw, sh) return c 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) local c = self:newTextBase(button, txt, x, y, w, h, sx, sy, sw, sh) c:respectHierarchy(true) diff --git a/main.lua b/main.lua index 17036dc..8f602ac 100644 --- a/main.lua +++ b/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 playerList = {} local playerStaticList = {} @@ -49,8 +48,7 @@ function init() board = require("board") yaml = require("yaml") loader = require("loader") - system = require("gui.addons.system") - elements = require("gui.elements") + require("gui.addons") scoreUpdater = gui:getProcessor():newProcessor("score-updater") scoreUpdater.Start() @@ -165,7 +163,44 @@ function ScoreBoard(frame, x, y, w, h, sx, sy, sw, sh) end 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:OnReleased(function() local player = GetActivePlayer() @@ -185,42 +220,16 @@ function ScoreBoard(frame, x, y, w, h, sx, sy, sw, sh) scoreboard:RenderPlayer(playerList) player.Ref.Frame:destroy() end) - add_player.color = C_BORDER_NRM - local textbox = add_player:newTextBox("Player name",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 = "" + + embedTextEdit(add_player, "Player Name", "Add", function(self) + scoreboard:AddPlayer(self.text, "0") end) - -- A bit glitchy - -- gui:setHotKey({"return"})(function() - -- local object_focus = gui:getObjectFocus() - -- if object_focus:hasType(gui.TYPE_BOX) then - -- scoreboard:AddPlayer(textbox.text, "0") - -- 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() + embedTextEdit(edit_player, "Modify Score", "Edit", function(self) + local player = GetActivePlayer() + if player then + player.Score = self.text + scoreboard:RenderPlayer(playerList) end end) @@ -305,22 +314,51 @@ function ScoreBoard(frame, x, y, w, h, sx, sy, sw, sh) end -require("gui.addons.players") + -- 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() - init() gui:cacheImage({"assets/checked.png","assets/unchecked.png"}) gui:setAspectSize(1920, 1080) gui.aspect_ratio = true + -- local ext = require("gui.addons.extensions") local bg = gui:newFrame() bg:fullFrame() 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) qframe.color = color.new("#060ee9") 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) -- local img = webp.load("test.webp") diff --git a/test.webp b/test.webp deleted file mode 100644 index 0aed682f83aa8beed06a92294395757b6c699abe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 256480 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
}oL25%2vq}_~MSoobK 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 z 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 diff --git a/utils.lua b/utils.lua index cce8378..c9a2fc4 100644 --- a/utils.lua +++ b/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 color = require("gui.core.color") -local transition = require("gui.elements.transitions") -local multi = require("multi"):init() +local multi, thread = require("multi"):init() local function startTimer(opt) local default = { @@ -23,19 +133,16 @@ local function startTimer(opt) opt.duration = d elseif type(opt) ~= "table" then opt = default - elseif type(opt) == "table" then - for i,v in pairs(opt) do + else + for i, v in pairs(opt) do default[i] = v end opt = default end local timeRemaining = opt.duration or 30 - local timer = transition.glide(3.5,1.5,5) - transition.glide:SetFPS(120) - local handle = timer(3.5,1.5,timeRemaining) - local tpie = gui:newFrame():makeArc("pie",-200, 0, 100,1 ,0 ,0 ,1.5*math.pi, 3.5*math.pi, 360) + local 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 @@ -44,62 +151,92 @@ local function startTimer(opt) 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 + local tm + local stopped = false + local onTimeConn = multi:newConnection() + local onStopConn = multi:newConnection() - 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 + local function cleanup() + if stopped then return end + stopped = true + if onTimeConn and not onTimeConn.destroyed then + onTimeConn:Destroy() + 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 - 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 - tlabel.text = num + 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) - if num == 0 then - thread:newThread("Pie Timer",function() - thread.yield() - if opt.autoText then - tlabel.text = "Time" - tlabel:fitFont(nil, nil, {scale=1/2}) - tlabel:centerFont() - end - tpie:makeArc("pie", -200, 0, 100, 1, 0, 0, 1.5 * math.pi, 3.5 * math.pi, 360) - tm.OnStop:Fire(tm) - if opt.autoCleanup then - tm:Cleanup() - end - end) - end + local onStop = onStopConn + cleanup() + onStop:Fire(tm) + end) - return tm, num, t - end - tm = { Duration = timeRemaining, - Cleanup = function() handle:Kill() tpie:destroy() tm.Cleanup = function() end end, + OnTime = onTimeConn, + OnStop = onStopConn, SetText = function(str) tlabel.text = str 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 end diff --git a/webp-old.lua b/webp-old.lua deleted file mode 100644 index 7f01cdf..0000000 --- a/webp-old.lua +++ /dev/null @@ -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 diff --git a/webp.lua b/webp.lua deleted file mode 100644 index 757619c..0000000 --- a/webp.lua +++ /dev/null @@ -1,1626 +0,0 @@ --- webp.lua — Pure Lua WebP decoder for Love2D (LuaJIT) --- Supports: VP8 (lossy), VP8L (lossless), VP8X (extended container) --- Usage: local WebP = require("webp"); local img = WebP.load("sprite.webp") - -local WebP = {} - -local bit = require("bit") -local band = bit.band -local bor = bit.bor -local lshift = bit.lshift -local rshift = bit.rshift -local tobit = bit.tobit - --- ─── Bitstream reader ──────────────────────────────────────────────────────── --- VP8L is LSB-first. We maintain a 32-bit window since LuaJIT bit ops are 32-bit. - --- ─── Bitstream reader (unchanged, still LSB‑first) ─────────────────────────── - -local function newBitReader(data) - local r = { - data = data, - pos = 1, - window = 0, - bits = 0, - } - - function r:fill() - while self.bits <= 24 and self.pos <= #self.data do - local byte = self.data:byte(self.pos) - self.window = bor(self.window, lshift(byte, self.bits)) - self.bits = self.bits + 8 - self.pos = self.pos + 1 - end - end - - function r:read(n) - if self.bits < n then self:fill() end - local v = band(self.window, lshift(1, n) - 1) - self.window = rshift(self.window, n) - self.bits = self.bits - n - return v - end - - function r:readBool() - return self:read(1) == 1 - end - - r:fill() - return r -end - --- ─── Helper: reverse bits for LSB‑first Huffman codes ─────────────────────── - -local function reverseBits(code, len) - local r = 0 - for _ = 1, len do - r = lshift(r, 1) + band(code, 1) - code = rshift(code, 1) - end - return r -end - --- ─── Huffman trees (fixed for LSB‑first) ──────────────────────────────────── - -local function buildHuffmanTable(codeLengths) - local n = #codeLengths - local counts = {} - for i = 0, 15 do counts[i] = 0 end - for _, cl in ipairs(codeLengths) do - if cl > 0 then counts[cl] = counts[cl] + 1 end - end - - local nextCode = {} - local code = 0 - counts[0] = 0 - for bits = 1, 15 do - code = lshift(code + counts[bits - 1], 1) - nextCode[bits] = code - end - - local htable = {} - for i = 1, n do - local len = codeLengths[i] - if len > 0 then - local c = nextCode[len] - -- IMPORTANT: reverse bits because we read LSB‑first - local rev = reverseBits(c, len) - htable[rev] = { sym = i - 1, len = len } - nextCode[len] = c + 1 - end - end - return htable -end - -local function decodeHuffman(br, htable) - local code = 0 - for len = 1, 15 do - -- we read one bit at a time, LSB‑first - local bit = br:read(1) - code = bor(code, lshift(bit, len - 1)) - local entry = htable[code] - if entry and entry.len == len then - return entry.sym - end - end - error("Invalid Huffman code") -end - --- ─── Code length decoding ──────────────────────────────────────────────────── - -local CODE_LENGTH_ORDER = {17,18,0,1,2,3,4,5,16,6,7,8,9,10,11,12,13,14,15} - -local function readCodeLengths(br, n) - local numCLCodes = br:read(4) + 4 - local clLengths = {} - for i = 1, 19 do clLengths[i] = 0 end - for i = 1, numCLCodes do - clLengths[CODE_LENGTH_ORDER[i] + 1] = br:read(3) - end - local clTable = buildHuffmanTable(clLengths) - - local lengths = {} - local prev = 8 - while #lengths < n do - local sym = decodeHuffman(br, clTable) - if sym <= 15 then - lengths[#lengths + 1] = sym - if sym ~= 0 then prev = sym end - elseif sym == 16 then - local rep = br:read(2) + 3 - for _ = 1, rep do lengths[#lengths + 1] = prev end - elseif sym == 17 then - local rep = br:read(3) + 3 - for _ = 1, rep do lengths[#lengths + 1] = 0 end - elseif sym == 18 then - local rep = br:read(7) + 11 - for _ = 1, rep do lengths[#lengths + 1] = 0 end - end - end - return lengths -end - --- ─── Prefix code reading ───────────────────────────────────────────────────── - -local function readPrefixCode(br, alphabetSize) - local simpleCode = br:read(1) - if simpleCode == 1 then - local numSyms = br:read(1) + 1 - local firstSym = br:read(1) - local sym1 = br:read(firstSym == 1 and 8 or 1) - local lengths = {} - for i = 1, alphabetSize do lengths[i] = 0 end - lengths[sym1 + 1] = 1 - if numSyms == 2 then - local sym2 = br:read(8) - lengths[sym2 + 1] = 1 - end - return buildHuffmanTable(lengths) - else - local lengths = readCodeLengths(br, alphabetSize) - return buildHuffmanTable(lengths) - end -end - --- ─── Color cache ───────────────────────────────────────────────────────────── - -local function newColorCache(bits) - local size = lshift(1, bits) - local cache = {} - for i = 0, size - 1 do cache[i] = 0 end - return { - size = size, - data = cache, - insert = function(self, color) - -- hash: (0x1e35a7bd * color) >> (32 - bits), masked to cache size - local hash = tobit(0x1e35a7bd * color) - local idx = band(rshift(hash, 32 - bits), size - 1) - self.data[idx] = color - end, - lookup = function(self, idx) - return self.data[idx] - end, - } -end - --- ─── Transform type constants ──────────────────────────────────────────────── - -local TRANSFORM_PREDICTOR = 0 -local TRANSFORM_COLOR = 1 -local TRANSFORM_SUBTRACT_GREEN = 2 -local TRANSFORM_COLOR_INDEXING = 3 - --- ─── Prefix length/distance tables ────────────────────────────────────────── - -local PREFIX_EXTRA_BITS = { - 0,0,0,0, 1,1,2,2, 3,3,4,4, 5,5,6,6, 7,7,8,8, 9,9,10,10, 11,11,12,12, 13,13 -} -local PREFIX_OFFSET = { - 0,1,2,3, 4,6,8,12, 16,24,32,48, 64,96,128,192, 256,384,512,768, - 1024,1536,2048,3072, 4096,6144,8192,12288, 16384,24576 -} - -local function prefixToValue(br, code) - if code < 4 then return code end - local extra = PREFIX_EXTRA_BITS[code + 1] or 0 - local offset = PREFIX_OFFSET[code + 1] or 0 - return offset + br:read(extra) -end - --- ─── VP8L distance offset table (120 entries) ──────────────────────────────── - -local DIST_MAP = { - {0,1},{1,0},{1,1},{-1,1},{0,2},{2,0},{1,2},{-1,2}, - {2,1},{-2,1},{2,2},{-2,2},{0,3},{3,0},{1,3},{-1,3}, - {3,1},{-3,1},{2,3},{-2,3},{3,2},{-3,2},{0,4},{4,0}, - {1,4},{-1,4},{4,1},{-4,1},{3,3},{-3,3},{2,4},{-2,4}, - {4,2},{-4,2},{0,5},{3,4},{-3,4},{4,3},{-4,3},{5,0}, - {1,5},{-1,5},{5,1},{-5,1},{2,5},{-2,5},{5,2},{-5,2}, - {4,4},{-4,4},{3,5},{-3,5},{5,3},{-5,3},{0,6},{6,0}, - {1,6},{-1,6},{6,1},{-6,1},{2,6},{-2,6},{6,2},{-6,2}, - {4,5},{-4,5},{5,4},{-5,4},{3,6},{-3,6},{6,3},{-6,3}, - {0,7},{7,0},{1,7},{-1,7},{5,5},{-5,5},{7,1},{-7,1}, - {4,6},{-4,6},{6,4},{-6,4},{2,7},{-2,7},{7,2},{-7,2}, - {3,7},{-3,7},{7,3},{-7,3},{5,6},{-5,6},{6,5},{-6,5}, - {8,0},{4,7},{-4,7},{7,4},{-7,4},{8,1},{8,2},{6,6}, - {-6,6},{8,3},{5,7},{-5,7},{7,5},{-7,5},{8,4},{6,7}, - {-6,7},{7,6},{-7,6},{8,5},{8,6},{7,7},{-7,7},{8,7}, -} - --- ─── Main VP8L decode (recursive for transform sub-images) ─────────────────── - -local function decodeVP8L(br, width, height) - - -- color cache - local hasCCache = br:readBool() - local ccache = nil - local ccacheBits = 0 - if hasCCache then - ccacheBits = br:read(4) - ccache = newColorCache(ccacheBits) - end - - -- collect transforms (applied in reverse after decode) - local transforms = {} - while br:readBool() do - local ttype = br:read(2) - local t = { ttype = ttype } - if ttype == TRANSFORM_PREDICTOR or ttype == TRANSFORM_COLOR then - t.sizeBits = br:read(3) + 2 - local tw = math.floor((width + lshift(1, t.sizeBits) - 1) / lshift(1, t.sizeBits)) - local th = math.floor((height + lshift(1, t.sizeBits) - 1) / lshift(1, t.sizeBits)) - t.data = decodeVP8L(br, tw, th) - elseif ttype == TRANSFORM_COLOR_INDEXING then - t.numColors = br:read(8) + 1 - t.colors = decodeVP8L(br, t.numColors, 1) - end - -- SUBTRACT_GREEN has no extra data - transforms[#transforms + 1] = t - end - - -- entropy/meta-Huffman image - local groupBits = 0 - local groupImage = nil - local numGroups = 1 - if br:readBool() then - groupBits = br:read(3) + 2 - local gw = math.floor((width + lshift(1, groupBits) - 1) / lshift(1, groupBits)) - local gh = math.floor((height + lshift(1, groupBits) - 1) / lshift(1, groupBits)) - groupImage = decodeVP8L(br, gw, gh) - local maxG = 0 - for _, v in ipairs(groupImage) do - local g = band(rshift(v, 8), 0xffff) - if g > maxG then maxG = g end - end - numGroups = maxG + 1 - end - - -- alphabet sizes - local ccSize = hasCCache and lshift(1, ccacheBits) or 0 - local alphabetG = 256 + 24 + ccSize - - -- read Huffman tables for each group (G, R, B, A + distance) - local huffGroups = {} - for g = 1, numGroups do - huffGroups[g] = { - G = readPrefixCode(br, alphabetG), - R = readPrefixCode(br, 256), - B = readPrefixCode(br, 256), - A = readPrefixCode(br, 256), - dist = readPrefixCode(br, 40), - } - end - - -- decode pixels - local pixels = {} - local numPixels = width * height - local px, py = 0, 0 - - local function getGroup() - if not groupImage then return huffGroups[1] end - local gx = rshift(px, groupBits) - local gy = rshift(py, groupBits) - local gw = math.floor((width + lshift(1, groupBits) - 1) / lshift(1, groupBits)) - local idx = gy * gw + gx + 1 - local g = band(rshift(groupImage[idx] or 0, 8), 0xffff) - return huffGroups[g + 1] or huffGroups[1] - end - - while #pixels < numPixels do - local hg = getGroup() - local code = decodeHuffman(br, hg.G) - - if code < 256 then - -- literal ARGB (green first, then R, B, A) - local g = code - local r = decodeHuffman(br, hg.R) - local b = decodeHuffman(br, hg.B) - local a = decodeHuffman(br, hg.A) - local color = bor(bor(bor(lshift(a, 24), lshift(r, 16)), lshift(g, 8)), b) - pixels[#pixels + 1] = color - if ccache then ccache:insert(color) end - - elseif code < 256 + 24 then - -- LZ77 back-reference - local lenCode = code - 256 - local length = prefixToValue(br, lenCode) + 1 - local distCode = decodeHuffman(br, hg.dist) - local dist = prefixToValue(br, distCode) + 1 - - -- remap small distances through VP8L spatial table - if dist <= 120 then - local d = DIST_MAP[dist] - local srcX = px - d[1] - local srcY = py - d[2] - dist = py * width + px - (srcY * width + srcX) - end - - local src = #pixels - dist + 1 - for i = 0, length - 1 do - local c = pixels[src + i] or 0 - pixels[#pixels + 1] = c - if ccache then ccache:insert(c) end - end - - else - -- color cache reference - local cacheIdx = code - 256 - 24 - pixels[#pixels + 1] = ccache:lookup(cacheIdx) - end - - px = px + 1 - if px >= width then px = 0; py = py + 1 end - end - - -- ─── Apply transforms in reverse order ─────────────────────────────────── - - for i = #transforms, 1, -1 do - local t = transforms[i] - - -- SUBTRACT_GREEN: R += G, B += G (mod 256) - if t.ttype == TRANSFORM_SUBTRACT_GREEN then - for idx = 1, #pixels do - local c = pixels[idx] - local a = band(rshift(c, 24), 0xff) - local r = band(rshift(c, 16), 0xff) - local g = band(rshift(c, 8), 0xff) - local b = band(c, 0xff) - r = band(r + g, 0xff) - b = band(b + g, 0xff) - pixels[idx] = bor(bor(bor(lshift(a, 24), lshift(r, 16)), lshift(g, 8)), b) - end - - -- COLOR_INDEXING: replace each pixel's green channel with palette entry - elseif t.ttype == TRANSFORM_COLOR_INDEXING then - local bpp = 8 - if t.numColors <= 2 then bpp = 1 - elseif t.numColors <= 4 then bpp = 2 - elseif t.numColors <= 16 then bpp = 4 end - - local newPixels = {} - if bpp == 8 then - for _, c in ipairs(pixels) do - local idx = band(rshift(c, 8), 0xff) - newPixels[#newPixels + 1] = t.colors[idx + 1] or 0 - end - else - local pxPerByte = 8 / bpp - local mask = lshift(1, bpp) - 1 - for _, c in ipairs(pixels) do - local packed = band(rshift(c, 8), 0xff) - for p = 0, pxPerByte - 1 do - if #newPixels < width * height then - local idx = band(rshift(packed, p * bpp), mask) - newPixels[#newPixels + 1] = t.colors[idx + 1] or 0 - end - end - end - end - pixels = newPixels - - -- PREDICTOR: undo per-block spatial prediction - elseif t.ttype == TRANSFORM_PREDICTOR then - local sb = t.sizeBits - local tw = math.floor((width + lshift(1, sb) - 1) / lshift(1, sb)) - - local function getpx(x, y) - if x < 0 then x = 0 end - if y < 0 then return tobit(0xff000000) end - return pixels[y * width + x + 1] or 0 - end - - local function addARGB(a, b) - local aa = band(rshift(a, 24), 0xff) - local ar = band(rshift(a, 16), 0xff) - local ag = band(rshift(a, 8), 0xff) - local ab = band(a, 0xff) - local ba = band(rshift(b, 24), 0xff) - local br_ = band(rshift(b, 16), 0xff) - local bg = band(rshift(b, 8), 0xff) - local bb = band(b, 0xff) - return bor(bor(bor( - lshift(band(aa + ba, 0xff), 24), - lshift(band(ar + br_, 0xff), 16)), - lshift(band(ag + bg, 0xff), 8)), - band(ab + bb, 0xff)) - end - - local function avgARGB(a, b) - local aa = band(rshift(a, 24), 0xff) - local ar = band(rshift(a, 16), 0xff) - local ag = band(rshift(a, 8), 0xff) - local ab = band(a, 0xff) - local ba = band(rshift(b, 24), 0xff) - local br_ = band(rshift(b, 16), 0xff) - local bg = band(rshift(b, 8), 0xff) - local bb = band(b, 0xff) - return bor(bor(bor( - lshift(rshift(aa + ba, 1), 24), - lshift(rshift(ar + br_, 1), 16)), - lshift(rshift(ag + bg, 1), 8)), - rshift(ab + bb, 1)) - end - - local function clampByte(v) - return math.max(0, math.min(255, v)) - end - - local function clampAddARGB(a, b) - local aa = band(rshift(a, 24), 0xff) - local ar = band(rshift(a, 16), 0xff) - local ag = band(rshift(a, 8), 0xff) - local ab = band(a, 0xff) - local ba = band(rshift(b, 24), 0xff) - local br_ = band(rshift(b, 16), 0xff) - local bg = band(rshift(b, 8), 0xff) - local bb = band(b, 0xff) - return bor(bor(bor( - lshift(clampByte(aa + ba), 24), - lshift(clampByte(ar + br_), 16)), - lshift(clampByte(ag + bg), 8)), - clampByte(ab + bb)) - end - - local function subARGB(a, b) - local aa = band(rshift(a, 24), 0xff) - local ar = band(rshift(a, 16), 0xff) - local ag = band(rshift(a, 8), 0xff) - local ab = band(a, 0xff) - local ba = band(rshift(b, 24), 0xff) - local br_ = band(rshift(b, 16), 0xff) - local bg = band(rshift(b, 8), 0xff) - local bb = band(b, 0xff) - return bor(bor(bor( - lshift(band(aa - ba, 0xff), 24), - lshift(band(ar - br_, 0xff), 16)), - lshift(band(ag - bg, 0xff), 8)), - band(ab - bb, 0xff)) - end - - local function halfSubARGB(a, b) - local aa = band(rshift(a, 24), 0xff) - local ar = band(rshift(a, 16), 0xff) - local ag = band(rshift(a, 8), 0xff) - local ab = band(a, 0xff) - local ba = band(rshift(b, 24), 0xff) - local br_ = band(rshift(b, 16), 0xff) - local bg = band(rshift(b, 8), 0xff) - local bb = band(b, 0xff) - return bor(bor(bor( - lshift(rshift(aa - ba, 1), 24), - lshift(rshift(ar - br_, 1), 16)), - lshift(rshift(ag - bg, 1), 8)), - rshift(ab - bb, 1)) - end - - local function selectARGB(l, tp, tl) - local function absdiff(x, y, s) - return math.abs(band(rshift(x, s), 0xff) - band(rshift(y, s), 0xff)) - end - local function dist(x, y) - return absdiff(x,y,24) + absdiff(x,y,16) + absdiff(x,y,8) + absdiff(x,y,0) - end - return dist(l, tl) <= dist(tp, tl) and l or tp - end - - for iy = 0, height - 1 do - for ix = 0, width - 1 do - if not (ix == 0 and iy == 0) then - local pidx = iy * width + ix + 1 - local L = getpx(ix - 1, iy) - local T = getpx(ix, iy - 1) - local TL = getpx(ix - 1, iy - 1) - local TR = getpx(ix + 1, iy - 1) - local tx = rshift(ix, sb) - local ty = rshift(iy, sb) - local mode = band(t.data[ty * tw + tx + 1] or 0, 0xff) - - local pred - if mode == 0 then pred = tobit(0xff000000) - elseif mode == 1 then pred = L - elseif mode == 2 then pred = T - elseif mode == 3 then pred = TR - elseif mode == 4 then pred = TL - elseif mode == 5 then pred = avgARGB(avgARGB(L, TR), T) - elseif mode == 6 then pred = avgARGB(L, TL) - elseif mode == 7 then pred = avgARGB(L, T) - elseif mode == 8 then pred = avgARGB(TL, T) - elseif mode == 9 then pred = avgARGB(T, TR) - elseif mode == 10 then pred = avgARGB(avgARGB(L, TL), avgARGB(T, TR)) - elseif mode == 11 then pred = selectARGB(L, T, TL) - elseif mode == 12 then pred = clampAddARGB(L, subARGB(T, TL)) - elseif mode == 13 then - local avg = avgARGB(L, T) - pred = clampAddARGB(avg, halfSubARGB(avg, TL)) - else pred = L end - - pixels[pidx] = addARGB(pixels[pidx], pred) - end - end - end - - -- COLOR: undo green/red channel correlations - elseif t.ttype == TRANSFORM_COLOR then - local sb = t.sizeBits - local tw = math.floor((width + lshift(1, sb) - 1) / lshift(1, sb)) - for iy = 0, height - 1 do - for ix = 0, width - 1 do - local pidx = iy * width + ix + 1 - local c = pixels[pidx] or 0 - local tx = rshift(ix, sb) - local ty = rshift(iy, sb) - local m = t.data[ty * tw + tx + 1] or 0 - - -- unpack signed bytes from transform pixel (stored as ARGB) - local g2r = band(rshift(m, 16), 0xff) - local r2b = band(rshift(m, 8), 0xff) - local g2b = band(m, 0xff) - if g2r >= 128 then g2r = g2r - 256 end - if r2b >= 128 then r2b = r2b - 256 end - if g2b >= 128 then g2b = g2b - 256 end - - local a = band(rshift(c, 24), 0xff) - local r = band(rshift(c, 16), 0xff) - local g = band(rshift(c, 8), 0xff) - local b = band(c, 0xff) - - r = band(r + math.floor(g2r * g / 256), 0xff) - b = band(b + math.floor(g2b * g / 256) + math.floor(r2b * r / 256), 0xff) - - pixels[pidx] = bor(bor(bor(lshift(a, 24), lshift(r, 16)), lshift(g, 8)), b) - end - end - end - end - - return pixels -end - --- ═══════════════════════════════════════════════════════════════════════════ --- VP8 (LOSSY) DECODER --- ═══════════════════════════════════════════════════════════════════════════ - --- ─── Boolean arithmetic decoder ────────────────────────────────────────────── --- VP8 uses a range coder (not Huffman). Each symbol is decoded against a --- probability in [0,255]. The range is maintained in [128,255]<= bigsplit then - retval = 1 - b.range = b.range - split - b.value = b.value - bigsplit - else - retval = 0 - b.range = split - end - -- renormalise - while b.range < 128 do - b.range = lshift(b.range, 1) - b.value = lshift(b.value, 1) - b.bits = b.bits + 1 - if b.bits == 8 then - local byte = b.data:byte(b.pos) or 0 - b.pos = b.pos + 1 - b.value = bor(b.value, byte) - b.bits = 0 - end - end - return retval -end - -local function boolReadLit(b, n) - local v = 0 - for _ = 1, n do - v = lshift(v, 1) + boolRead(b, 128) - end - return v -end - -local function boolReadSigned(b, n) - local v = boolReadLit(b, n) - if boolRead(b, 128) == 1 then v = -v end - return v -end - --- ─── VP8 probability tables ─────────────────────────────────────────────────── - --- Coefficient probability table (384 entries, from the VP8 spec Annex B) --- Indexed as [band][ctx][node] but flattened here for brevity. --- This is the default/initial probability table before any updates. -local VP8_COEF_PROBS = { - -- block type 0: Y after DC (4x4 intra) - { -- band 0 - {128,128,128,128,128,128,128,128,128,128,128}, - {128,128,128,128,128,128,128,128,128,128,128}, - {128,128,128,128,128,128,128,128,128,128,128}, - }, - { -- band 1 - {253,136,254,255,228,219,128,128,128,128,128}, - {189,129,242,255,227,213,255,219,128,128,128}, - {106,126,227,252,214,209,255,255,128,128,128}, - }, - { -- band 2 - { 1, 98,248,255,236,226,255,255,128,128,128}, - {181,133,238,254,221,234,255,154,128,128,128}, - { 78,134,202,247,198,180,255,219,128,128,128}, - }, - { -- band 3 - { 1,185,249,255,243,255,128,128,128,128,128}, - {184,150,247,255,236,224,128,128,128,128,128}, - { 77,110,216,255,236,230,128,128,128,128,128}, - }, - { -- band 4 - { 1,101,251,255,241,255,128,128,128,128,128}, - {170,139,241,252,236,209,255,255,128,128,128}, - { 37, 116,196,243,228,255,255,255,128,128,128}, - }, - { -- band 5 - { 1,204,254,255,245,255,128,128,128,128,128}, - {207,160,250,255,238,128,128,128,128,128,128}, - {102,103,231,255,211,171,128,128,128,128,128}, - }, - { -- band 6 - { 1,152,252,255,240,255,128,128,128,128,128}, - {177,135,243,255,234,225,128,128,128,128,128}, - { 80,129,211,255,194,224,128,128,128,128,128}, - }, - { -- band 7 - { 1, 1,255,128,128,128,128,128,128,128,128}, - {246, 1,255,128,128,128,128,128,128,128,128}, - {255,128,128,128,128,128,128,128,128,128,128}, - }, -} - --- Intra mode probabilities (from VP8 spec section 11) -local VP8_KEYFRAME_YMODE_PROB = {145, 156, 163, 128} -- DC, V, H, TM -local VP8_KEYFRAME_UVMODE_PROB = {142, 114, 183} - --- Default MV probabilities (from spec) -local VP8_MV_UPDATE_PROBS = { - {237,246,253,253,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254}, - {231,243,245,253,254,254,254,254,254,254,254,254,254,254,254,254,254,254,254}, -} -local VP8_MV_DEFAULT_PROBS = { - {162,128,225,146,172,147,214, 39,156,128,129,132, 75,145,178,206,239,254,254}, - {164,128,204,170,119,235,140,230,228,128,130,130, 74,148,180,203,236,254,254}, -} - --- ─── Zigzag scan order for 4x4 DCT block ───────────────────────────────────── -local ZIGZAG = {0,1,4,8,5,2,3,6,9,12,13,10,7,11,14,15} - --- ─── Quantizer table ────────────────────────────────────────────────────────── -local function dcQ(q) - if q == 0 then return 4 end - return q < 134 and (q * 2 + 3 > 132 and 132 or q * 2 + 3) - or (5 * q + 77 > 2040 and 2040 or 5 * q + 77) -end -local function acQ(q) - return q < 6 and 8 or - q < 10 and (6 * q - 12) or - q < 126 and (q * 2 + 3 > 255 and 255 or q * 2 + 3) or - (5 * q - 370 > 2040 and 2040 or 5 * q - 370) -end - --- From VP8 spec Table 14 (DC/AC quantizer index tables) -local VP8_DC_QLOOKUP = {} -local VP8_AC_QLOOKUP = {} -do - -- spec tables verbatim - local dc = {4,5,6,7,8,9,10,10,11,12,13,14,15,16,17,17,18,19,20,20,21,21,22,22,23,23,24,25,25,26,27,28,29,30,31,32,33,34,35,36,37,37,38,39,40,41,42,43,44,45,46,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,76,77,78,79,80,81,82,83,84,85,86,87,88,89,91,93,95,96,97,98,100,101,102,104,106,108,110,112,114,116,118,122,124,126,128,130,132,134,136,138,140,143,145,148,151,154,157} - local ac = {4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,60,62,64,66,68,70,72,74,76,79,81,84,87,90,93,96,99,102,105,108,111,114,117,120,124,128,132,136,140,144,148,152,156,160,164,168,172,176,180,184,188,192,196,200,205,210,215,220,225,230,235,240,245,249,254,254} - for i,v in ipairs(dc) do VP8_DC_QLOOKUP[i-1] = v end - for i,v in ipairs(ac) do VP8_AC_QLOOKUP[i-1] = v end -end - --- ─── Inverse DCT (4x4, in-place on a 16-element array, 1-based) ────────────── -local function idct4x4(c) - -- rows - for i = 0, 3 do - local a0 = c[i*4+1] + c[i*4+3] - local a1 = c[i*4+1] - c[i*4+3] - local a2 = rshift(c[i*4+2], 1) - c[i*4+4] - local a3 = c[i*4+2] + rshift(c[i*4+4], 1) - c[i*4+1] = a0 + a3 - c[i*4+2] = a1 + a2 - c[i*4+3] = a1 - a2 - c[i*4+4] = a0 - a3 - end - -- columns - for i = 0, 3 do - local a0 = c[i+1] + c[i+9] - local a1 = c[i+1] - c[i+9] - local a2 = rshift(c[i+5], 1) - c[i+13] - local a3 = c[i+5] + rshift(c[i+13], 1) - c[i+1] = rshift(a0 + a3 + 4, 3) - c[i+5] = rshift(a1 + a2 + 4, 3) - c[i+9] = rshift(a1 - a2 + 4, 3) - c[i+13] = rshift(a0 - a3 + 4, 3) - end -end - --- WHT (Walsh-Hadamard, 4x4 for DC coefficients) -local function iwht4x4(c) - for i = 0, 3 do - local a0 = c[i*4+1] + c[i*4+3] - local a1 = c[i*4+2] + c[i*4+4] - local a2 = c[i*4+2] - c[i*4+4] - local a3 = c[i*4+1] - c[i*4+3] - c[i*4+1] = a0 + a1 - c[i*4+2] = a3 + a2 - c[i*4+3] = a0 - a1 - c[i*4+4] = a3 - a2 - end - for i = 0, 3 do - local a0 = c[i+1] + c[i+9] - local a1 = c[i+5] + c[i+13] - local a2 = c[i+5] - c[i+13] - local a3 = c[i+1] - c[i+9] - c[i+1] = rshift(a0 + a1, 3) - c[i+5] = rshift(a3 + a2, 3) - c[i+9] = rshift(a0 - a1, 3) - c[i+13] = rshift(a3 - a2, 3) - end -end - --- ─── Coefficient decoding ───────────────────────────────────────────────────── --- Decode one 4x4 block of DCT coefficients from the bool decoder. --- plane: 0=Y 1=UV, blockCtx: context from neighbouring blocks -local function decodeCoefficients(bool, probs, firstCoef, lastCoef, blockCtx) - local coeffs = {} - for i = 0, 15 do coeffs[i] = 0 end - - local ctx = blockCtx - local i = firstCoef - - while i <= lastCoef do - local p = probs[i + 1] or probs[1] - -- EOB or nonzero? - if boolRead(bool, p[ctx + 1] and p[ctx+1][1] or 128) == 0 then - break -- end of block - end - -- skip zeros - while boolRead(bool, (p[ctx+1] and p[ctx+1][2]) or 128) == 0 do - i = i + 1 - if i > lastCoef then return coeffs, 0 end - ctx = 0 - p = probs[i + 1] or probs[1] - end - -- read coefficient value - local v - local pp = p[ctx+1] or {} - if boolRead(bool, pp[3] or 128) == 0 then - v = 1 - elseif boolRead(bool, pp[4] or 128) == 0 then - if boolRead(bool, pp[5] or 128) == 0 then - v = 2 - else - v = 3 + boolRead(bool, pp[6] or 128) - end - elseif boolRead(bool, pp[7] or 128) == 0 then - if boolRead(bool, pp[8] or 128) == 0 then - v = 5 + boolRead(bool, 159) - else - v = 7 + 2 * boolRead(bool, 165) + boolRead(bool, 145) - end - elseif boolRead(bool, pp[9] or 128) == 0 then - local cat3 = {pp[10] or 128, 165, 145} - v = 11 - for _, pr in ipairs(cat3) do v = v * 2 + boolRead(bool, pr) end - elseif boolRead(bool, pp[10] or 128) == 0 then - local cat4 = {pp[11] or 128, 165, 145, 128} - v = 19 - for _, pr in ipairs(cat4) do v = v * 2 + boolRead(bool, pr) end - else - -- cat5 or cat6 - if boolRead(bool, 128) == 0 then - v = 35 - for _, pr in ipairs({165,145,128,128,128}) do - v = v * 2 + boolRead(bool, pr) - end - else - v = 67 - for _, pr in ipairs({145,128,128,128,128,128}) do - v = v * 2 + boolRead(bool, pr) - end - end - end - -- sign bit - if boolRead(bool, 128) == 1 then v = -v end - coeffs[ZIGZAG[i + 1]] = v - ctx = (math.abs(v) == 1) and 1 or 2 - i = i + 1 - end - - local nzCount = 0 - for _, v in ipairs(coeffs) do if v ~= 0 then nzCount = nzCount + 1 end end - return coeffs, (nzCount > 0 and 1 or 0) -end - --- ─── Intra prediction modes ─────────────────────────────────────────────────── --- B_PRED sub-modes for 4x4 luma intra prediction -local B_DC_PRED, B_TM_PRED, B_VE_PRED, B_HE_PRED = 0,1,2,3 -local B_LD_PRED, B_RD_PRED, B_VR_PRED, B_VL_PRED = 4,5,6,7 -local B_HD_PRED, B_HU_PRED = 8,9 - --- Clamp to [0,255] -local function clamp8(v) return math.max(0, math.min(255, v)) end - --- Fill a 4x4 block in a flat array (stride = mbw*4) using intra prediction. --- out: flat Y/U/V plane array (1-based, row-major) --- x4,y4: top-left pixel coords in the plane --- stride: row stride --- mode: prediction mode --- above: 8 pixels above (indices 0..7, may be nil for top-of-image) --- left: 4 pixels to left (indices 0..3, may be nil for left-of-image) -local function predictBlock4x4(out, x4, y4, stride, mode, above, left, aboveLeft) - local function set(px, py, v) - out[(y4 + py) * stride + x4 + px + 1] = v - end - local A = above or {127,127,127,127,127,127,127,127} - local L = left or {129,129,129,129} - local TL = aboveLeft or 127 - - if mode == B_DC_PRED then - local sum, n = 0, 0 - if above then for i=0,3 do sum=sum+A[i+1]; n=n+1 end end - if left then for i=0,3 do sum=sum+L[i+1]; n=n+1 end end - local dc = n > 0 and rshift(sum + rshift(n, 1), n == 8 and 3 or 2) or 128 - for py=0,3 do for px=0,3 do set(px,py,dc) end end - - elseif mode == B_TM_PRED then - for py=0,3 do for px=0,3 do - set(px, py, clamp8(A[px+1] + L[py+1] - TL)) - end end - - elseif mode == B_VE_PRED then - for py=0,3 do for px=0,3 do set(px,py,A[px+1]) end end - - elseif mode == B_HE_PRED then - for py=0,3 do for px=0,3 do set(px,py,L[py+1]) end end - - elseif mode == B_LD_PRED then - local a = {A[1],A[2],A[3],A[4],A[5],A[6],A[7],A[8]} - local function ld(i) - local x0 = i < 7 and a[i+1] or a[8] - local x1 = i < 7 and a[i+2] or a[8] - local x2 = i < 6 and a[i+3] or a[8] - return rshift(x0 + 2*x1 + x2 + 2, 2) - end - for py=0,3 do for px=0,3 do set(px,py,ld(px+py)) end end - - elseif mode == B_RD_PRED then - local a = {TL,A[1],A[2],A[3],A[4],L[1],L[2],L[3],L[4]} - local function rd(i) -- i in 0..6 - local x0 = a[i+1] or a[1] - local x1 = a[i+2] or a[1] - local x2 = a[i+3] or a[1] - return rshift(x0 + 2*x1 + x2 + 2, 2) - end - for py=0,3 do for px=0,3 do - local d = px - py - set(px, py, rd(d + 4)) - end end - - elseif mode == B_VR_PRED then - local a = {TL,A[1],A[2],A[3],A[4],L[1],L[2],L[3]} - for py=0,3 do for px=0,3 do - local x = 2*px - py - local v - if x >= 0 then - local ai = x / 2 + 1 - if x % 2 == 0 then - v = rshift((a[ai] or 127) + (a[ai+1] or 127) + 1, 1) - else - v = rshift((a[ai] or 127) + 2*(a[ai+1] or 127) + (a[ai+2] or 127) + 2, 2) - end - else - v = rshift(L[-x] + 2*(L[-x+1] or L[4]) + (L[-x+2] or L[4]) + 2, 2) - end - set(px, py, clamp8(v)) - end end - - elseif mode == B_VL_PRED then - for py=0,3 do for px=0,3 do - local x = px + rshift(py, 1) - local v - if py % 2 == 0 then - v = rshift((A[x+1] or A[8]) + (A[x+2] or A[8]) + 1, 1) - else - v = rshift((A[x+1] or A[8]) + 2*(A[x+2] or A[8]) + (A[x+3] or A[8]) + 2, 2) - end - set(px, py, clamp8(v)) - end end - - elseif mode == B_HD_PRED then - local a = {L[4],L[3],L[2],L[1],TL,A[1],A[2],A[3],A[4]} - for py=0,3 do for px=0,3 do - local x = 2*py - px - local v - if x >= 0 then - local ai = rshift(x, 1) + 1 - if x % 2 == 0 then - v = rshift((a[ai] or 127) + (a[ai+1] or 127) + 1, 1) - else - v = rshift((a[ai] or 127) + 2*(a[ai+1] or 127) + (a[ai+2] or 127) + 2, 2) - end - else - v = rshift(A[-x] + 2*(A[-x+1] or A[8]) + (A[-x+2] or A[8]) + 2, 2) - end - set(px, py, clamp8(v)) - end end - - elseif mode == B_HU_PRED then - for py=0,3 do for px=0,3 do - local x = px + 2*py - local v - if x + 1 < 4 then - v = rshift(L[x+1] + L[x+2] + 1, 1) - elseif x == 6 then - v = rshift(L[4] + 3*L[4] + 2, 2) - else - v = L[4] - end - set(px, py, clamp8(v)) - end end - end -end - --- 16x16 luma intra prediction (DC, V, H, TM) -local function predictMB16(plane, mbx, mby, mbw, mode, aboveRow, leftCol) - local base_x = mbx * 16 - local base_y = mby * 16 - local stride = mbw * 16 - - if mode == 0 then -- DC_PRED - local sum, n = 0, 0 - if aboveRow then for i=0,15 do sum=sum+aboveRow[i+1]; n=n+1 end end - if leftCol then for i=0,15 do sum=sum+leftCol[i+1]; n=n+1 end end - local dc = n > 0 and rshift(sum + rshift(n, 1), n == 32 and 5 or 4) or 128 - for py=0,15 do for px=0,15 do - plane[(base_y+py)*stride + base_x+px + 1] = dc - end end - - elseif mode == 1 then -- V_PRED - local src = aboveRow or {} - for py=0,15 do for px=0,15 do - plane[(base_y+py)*stride + base_x+px + 1] = src[px+1] or 127 - end end - - elseif mode == 2 then -- H_PRED - local src = leftCol or {} - for py=0,15 do for px=0,15 do - plane[(base_y+py)*stride + base_x+px + 1] = src[py+1] or 129 - end end - - elseif mode == 3 then -- TM_PRED - local tl = (aboveRow and leftCol) and 127 or 128 - if aboveRow and leftCol then - -- tl is above-left of macroblock - tl = 127 -- approximation; proper value from frame buffer - end - local A = aboveRow or {} - local L = leftCol or {} - for py=0,15 do for px=0,15 do - plane[(base_y+py)*stride + base_x+px + 1] = - clamp8((A[px+1] or 127) + (L[py+1] or 129) - tl) - end end - end -end - --- 8x8 chroma intra prediction -local function predictMB8(plane, mbx, mby, mbw, mode, aboveRow, leftCol) - local base_x = mbx * 8 - local base_y = mby * 8 - local stride = mbw * 8 - - if mode == 0 then -- DC - local sum, n = 0, 0 - if aboveRow then for i=0,7 do sum=sum+aboveRow[i+1]; n=n+1 end end - if leftCol then for i=0,7 do sum=sum+leftCol[i+1]; n=n+1 end end - local dc = n > 0 and rshift(sum + rshift(n, 1), n == 16 and 4 or 3) or 128 - for py=0,7 do for px=0,7 do - plane[(base_y+py)*stride + base_x+px + 1] = dc - end end - elseif mode == 1 then -- V - local src = aboveRow or {} - for py=0,7 do for px=0,7 do - plane[(base_y+py)*stride + base_x+px + 1] = src[px+1] or 127 - end end - elseif mode == 2 then -- H - local src = leftCol or {} - for py=0,7 do for px=0,7 do - plane[(base_y+py)*stride + base_x+px + 1] = src[py+1] or 129 - end end - elseif mode == 3 then -- TM - local A = aboveRow or {} - local L = leftCol or {} - for py=0,7 do for px=0,7 do - plane[(base_y+py)*stride + base_x+px + 1] = clamp8((A[px+1] or 127) + (L[py+1] or 129) - 127) - end end - end -end - --- ─── Add residuals to prediction ───────────────────────────────────────────── -local function addResiduals(plane, base_x, base_y, stride, coeffs) - for py = 0, 3 do - for px = 0, 3 do - local idx = (base_y + py) * stride + base_x + px + 1 - plane[idx] = clamp8((plane[idx] or 0) + (coeffs[py * 4 + px + 1] or 0)) - end - end -end - --- ─── Simple loop filter ─────────────────────────────────────────────────────── -local function filterSimple(plane, stride, w, h) - -- horizontal edges - for y = 1, h - 1 do - for x = 0, w - 1 do - local p1 = plane[(y-1)*stride + x + 1] or 128 - local q1 = plane[ y *stride + x + 1] or 128 - local d = rshift(3*(q1 - p1) + 4, 3) - d = math.max(-4, math.min(4, d)) - plane[(y-1)*stride+x+1] = clamp8(p1 + d) - plane[ y *stride+x+1] = clamp8(q1 - d) - end - end - -- vertical edges - for y = 0, h - 1 do - for x = 1, w - 1 do - local p1 = plane[y*stride + x] or 128 - local q1 = plane[y*stride + x + 1] or 128 - local d = rshift(3*(q1 - p1) + 4, 3) - d = math.max(-4, math.min(4, d)) - plane[y*stride+x] = clamp8(p1 + d) - plane[y*stride+x+1] = clamp8(q1 - d) - end - end -end - --- ─── YCbCr → RGB conversion ─────────────────────────────────────────────────── --- VP8 uses BT.601 studio swing: --- R = Y + 1.402*(Cr-128) --- G = Y - 0.344*(Cb-128) - 0.714*(Cr-128) --- B = Y + 1.772*(Cb-128) -local function yuvToRgb(y, cb, cr) - local r = clamp8(math.floor(y + 1.402 * (cr - 128) + 0.5)) - local g = clamp8(math.floor(y - 0.34414 * (cb - 128) - 0.71414 * (cr - 128) + 0.5)) - local b = clamp8(math.floor(y + 1.772 * (cb - 128) + 0.5)) - return r, g, b -end - --- ─── Main VP8 decode ────────────────────────────────────────────────────────── -local function decodeVP8(data, offset, length) - offset = offset or 1 - -- Frame tag (3 bytes) - local b0 = data:byte(offset) - local b1 = data:byte(offset + 1) - local b2 = data:byte(offset + 2) - local keyFrame = band(b0, 1) == 0 - local version = band(rshift(b0, 1), 7) - local showFrame = band(rshift(b0, 4), 1) == 1 - local firstPartSize = bor(rshift(b0, 5), bor(lshift(b1, 3), lshift(b2, 11))) - - assert(keyFrame, "VP8 inter frames not supported (not a keyframe)") - - -- Start code (3 bytes: 0x9d 0x01 0x2a) - assert(data:byte(offset+3) == 0x9d and - data:byte(offset+4) == 0x01 and - data:byte(offset+5) == 0x2a, "Invalid VP8 start code") - - local w_raw = bor(data:byte(offset+6), lshift(data:byte(offset+7), 8)) - local h_raw = bor(data:byte(offset+8), lshift(data:byte(offset+9), 8)) - local width = band(w_raw, 0x3fff) - local height = band(h_raw, 0x3fff) - local hscale = rshift(w_raw, 14) - local vscale = rshift(h_raw, 14) - - -- macroblock dimensions - local mbw = rshift(width + 15, 4) - local mbh = rshift(height + 15, 4) - - -- First partition starts at offset+3 (after frame tag) - local bool = newBoolDecoder(data, offset + 3) - - -- ── Frame header ── - -- color space and clamping - local colorSpace = boolRead(bool, 128) - local clampType = boolRead(bool, 128) - - -- segmentation - local segmentEnabled = boolRead(bool, 128) - local segmentAbsDelta = false - local segQuant = {0,0,0,0} - local segFilter = {0,0,0,0} - if segmentEnabled == 1 then - local updateMap = boolRead(bool, 128) - local updateData = boolRead(bool, 128) - if updateData == 1 then - segmentAbsDelta = boolRead(bool, 128) == 1 - for i = 1, 4 do - if boolRead(bool, 128) == 1 then - segQuant[i] = boolReadSigned(bool, 7) - end - end - for i = 1, 4 do - if boolRead(bool, 128) == 1 then - segFilter[i] = boolReadSigned(bool, 6) - end - end - end - if updateMap == 1 then - -- read 3 probabilities (skip for keyframe - all MBs resend) - for _ = 1, 3 do - if boolRead(bool, 128) == 1 then boolReadLit(bool, 8) end - end - end - end - - -- loop filter - local filterType = boolRead(bool, 128) -- 0=normal, 1=simple - local filterLevel = boolReadLit(bool, 6) - local filterSharp = boolReadLit(bool, 3) - local lfAdjEnable = boolRead(bool, 128) - if lfAdjEnable == 1 then - if boolRead(bool, 128) == 1 then -- mode_ref_lf_delta_update - for _ = 1, 4 do -- ref_frame deltas - if boolRead(bool, 128) == 1 then boolReadSigned(bool, 6) end - end - for _ = 1, 4 do -- mb mode deltas - if boolRead(bool, 128) == 1 then boolReadSigned(bool, 6) end - end - end - end - - -- partition count - local log2Partitions = boolReadLit(bool, 2) - local numPartitions = lshift(1, log2Partitions) - - -- quantizer indices - local baseQ = boolReadLit(bool, 7) - local dqY1dc = boolRead(bool,128)==1 and boolReadSigned(bool,4) or 0 - local dqY2dc = boolRead(bool,128)==1 and boolReadSigned(bool,4) or 0 - local dqY2ac = boolRead(bool,128)==1 and boolReadSigned(bool,4) or 0 - local dqUVdc = boolRead(bool,128)==1 and boolReadSigned(bool,4) or 0 - local dqUVac = boolRead(bool,128)==1 and boolReadSigned(bool,4) or 0 - - -- build quantizer tables per segment - local function qi(base, delta, isAbs) - local q = isAbs and delta or base + delta - return math.max(0, math.min(127, q)) - end - - -- coefficient probability updates - -- (use default table; read updates from bitstream) - local coefProbs = {} - for t = 1, 4 do - coefProbs[t] = {} - for b = 1, 8 do - coefProbs[t][b] = {} - for c = 1, 3 do - coefProbs[t][b][c] = {} - for n = 1, 11 do - coefProbs[t][b][c][n] = VP8_COEF_PROBS[b] and - VP8_COEF_PROBS[b][c] and VP8_COEF_PROBS[b][c][n] or 128 - end - end - end - end - -- read coefficient probability updates - for t = 1, 4 do - for b = 1, 8 do - for c = 1, 3 do - for n = 1, 11 do - if boolRead(bool, 255) == 1 then - coefProbs[t][b][c][n] = boolReadLit(bool, 8) - end - end - end - end - end - - -- skip probability - local mbSkipEnabled = boolRead(bool, 128) - local skipProb = mbSkipEnabled == 1 and boolReadLit(bool, 8) or 0 - - -- intra mode probabilities for key frames are fixed (not transmitted) - -- (use VP8_KEYFRAME_YMODE_PROB / VP8_KEYFRAME_UVMODE_PROB) - - -- ── Locate second partition(s) ── - -- The first partition ends at offset+3+firstPartSize - -- Immediately after are (numPartitions-1) * 3 bytes of partition sizes, - -- then the partition data. - local firstPartEnd = offset + 3 + firstPartSize - local partSizeBase = firstPartEnd - local partDataBase = partSizeBase + (numPartitions - 1) * 3 - local partOffsets = {} - local acc = 0 - for p = 1, numPartitions - 1 do - local o = partSizeBase + (p-1)*3 - local sz = bor(data:byte(o) or 0, - bor(lshift(data:byte(o+1) or 0, 8), - lshift(data:byte(o+2) or 0, 16))) - partOffsets[p] = partDataBase + acc - acc = acc + sz - end - partOffsets[numPartitions] = partDataBase + acc - - -- create bool decoders for each residual partition - local partBools = {} - for p = 1, numPartitions do - partBools[p] = newBoolDecoder(data, partOffsets[p]) - end - - -- ── Allocate planes ── - local Yplane = {} - local Uplane = {} - local Vplane = {} - local Ystride = mbw * 16 - local Cstride = mbw * 8 - for i = 1, Ystride * mbh * 16 do Yplane[i] = 128 end - for i = 1, Cstride * mbh * 8 do Uplane[i] = 128; Vplane[i] = 128 end - - -- ── Per-macroblock decode ── - local partIdx = 1 - - for mby = 0, mbh - 1 do - for mbx = 0, mbw - 1 do - -- select residual partition (round-robin) - local pb = partBools[partIdx] - partIdx = (partIdx % numPartitions) + 1 - - -- skip flag - local mbSkip = false - if mbSkipEnabled == 1 then - mbSkip = boolRead(bool, skipProb) == 1 - end - - -- intra prediction mode for luma (16x16 or B_PRED / 4x4) - local lumaMode = 0 -- DC default - -- read y_mode - local yMode - if boolRead(bool, 145) == 0 then - -- B_PRED: 4x4 sub-modes per sub-block - yMode = 4 -- B_PRED sentinel - else - if boolRead(bool, 156) == 0 then - if boolRead(bool, 163) == 0 then - yMode = 1 -- V_PRED - else - yMode = 2 -- H_PRED - end - else - if boolRead(bool, 128) == 0 then - yMode = 0 -- DC_PRED - else - yMode = 3 -- TM_PRED - end - end - end - - -- UV mode - local uvMode - if boolRead(bool, 142) == 0 then - uvMode = 0 -- DC - elseif boolRead(bool, 114) == 0 then - uvMode = 1 -- V - elseif boolRead(bool, 183) == 0 then - uvMode = 2 -- H - else - uvMode = 3 -- TM - end - - -- read 4x4 sub-modes if B_PRED - local subModes = {} - if yMode == 4 then - local B_PROB = { - {231,120, 48, 89,115,113,120,152,112}, - {152,179, 64,126,170,118, 46, 70, 95}, - {175, 69,143, 80, 85, 82, 72,155, 64}, - {212,188,128, 97,151,195, 9, 41, 15}, - { 3, 9, 1, 7, 3, 3, 5, 1, 16}, - } - for _ = 0, 15 do - -- simplified: use DC_PRED as default for all sub-blocks - -- full implementation would use context-adaptive probs - local m = 0 - if boolRead(bool, 128) == 0 then m = 0 - elseif boolRead(bool, 156) == 0 then m = 1 - elseif boolRead(bool, 163) == 0 then m = 2 - elseif boolRead(bool, 128) == 0 then m = 3 - elseif boolRead(bool, 128) == 0 then m = 4 - elseif boolRead(bool, 128) == 0 then m = 5 - elseif boolRead(bool, 128) == 0 then m = 6 - elseif boolRead(bool, 128) == 0 then m = 7 - elseif boolRead(bool, 128) == 0 then m = 8 - else m = 9 end - subModes[#subModes + 1] = m - end - end - - -- ── Intra prediction (luma) ── - local base_x = mbx * 16 - local base_y = mby * 16 - - -- gather above/left context - local function getAbove16(plane, bx, by, stride) - if by == 0 then return nil end - local row = {} - for i = 0, 15 do - row[i+1] = plane[(by-1)*stride + bx + i + 1] or 127 - end - return row - end - local function getLeft16(plane, bx, by, stride) - if bx == 0 then return nil end - local col = {} - for i = 0, 15 do - col[i+1] = plane[by*stride + bx - 1 + (i * stride) - (stride-1)] or 129 - end - return col - end - - if yMode ~= 4 then - -- whole-MB prediction - local abv = getAbove16(Yplane, base_x, base_y, Ystride) - local lft = nil - if mbx > 0 then - lft = {} - for i = 0, 15 do - lft[i+1] = Yplane[(base_y+i)*Ystride + base_x] or 129 - end - end - predictMB16(Yplane, mbx, mby, mbw, yMode, abv, lft) - else - -- B_PRED: 4x4 sub-block prediction - for si = 0, 15 do - local sx = band(si, 3) * 4 - local sy = rshift(si, 2) * 4 - local px = base_x + sx - local py = base_y + sy - - local above4 = {} - local left4 = {} - local tl - - for i = 0, 7 do - above4[i+1] = py > 0 and (Yplane[(py-1)*Ystride + px + i + 1] or 127) or 127 - end - for i = 0, 3 do - left4[i+1] = px > 0 and (Yplane[(py+i)*Ystride + px] or 129) or 129 - end - tl = (py > 0 and px > 0) and (Yplane[(py-1)*Ystride + px] or 127) or 127 - - predictBlock4x4(Yplane, px, py, Ystride, subModes[si+1] or 0, - above4, left4, tl) - end - end - - -- UV prediction - do - local ubx = mbx * 8 - local uby = mby * 8 - local function above8(plane) - if mby == 0 then return nil end - local r = {} - for i=0,7 do r[i+1]=plane[(uby-1)*Cstride+ubx+i+1] or 127 end - return r - end - local function left8(plane) - if mbx == 0 then return nil end - local c = {} - for i=0,7 do c[i+1]=plane[(uby+i)*Cstride+ubx] or 129 end - return c - end - predictMB8(Uplane, mbx, mby, mbw, uvMode, above8(Uplane), left8(Uplane)) - predictMB8(Vplane, mbx, mby, mbw, uvMode, above8(Vplane), left8(Vplane)) - end - - -- ── Residuals ── - if not mbSkip then - local q = math.max(0, math.min(127, baseQ)) - local yDCq = VP8_DC_QLOOKUP[math.max(0,math.min(127, q + dqY1dc))] - local yACq = VP8_AC_QLOOKUP[q] - local y2DCq= VP8_DC_QLOOKUP[math.max(0,math.min(127, q + dqY2dc))] - local y2ACq= VP8_AC_QLOOKUP[math.max(0,math.min(127, q + dqY2ac))] - local uvDCq= VP8_DC_QLOOKUP[math.max(0,math.min(127, q + dqUVdc))] - local uvACq= VP8_AC_QLOOKUP[math.max(0,math.min(127, q + dqUVac))] - - -- Y2 (DC only for 16x16 modes) - local y2coeffs = nil - if yMode ~= 4 then - local c16 = {} - for i=1,16 do c16[i] = 0 end - -- decode 4x4 WHT block for Y2 - local rawC, nz = decodeCoefficients(pb, coefProbs[1], 0, 15, 0) - if nz > 0 then - for k=0,15 do rawC[k] = (rawC[k] or 0) * (k==0 and y2DCq or y2ACq) end - local wht = {} - for k=1,16 do wht[k] = rawC[k-1] or 0 end - iwht4x4(wht) - y2coeffs = wht - end - end - - -- 16 Y sub-blocks - for si = 0, 15 do - local sx = band(si, 3) * 4 - local sy = rshift(si, 2) * 4 - local px = base_x + sx - local py = base_y + sy - local firstCoef = yMode ~= 4 and 1 or 0 - local rawC, nz = decodeCoefficients(pb, coefProbs[yMode~=4 and 2 or 1], - firstCoef, 15, 0) - -- inject Y2 DC if present - if y2coeffs then - rawC[0] = y2coeffs[si + 1] or 0 - end - -- dequantize - for k = 0, 15 do - rawC[k] = (rawC[k] or 0) * (k == 0 and yDCq or yACq) - end - -- inverse DCT - local block = {} - for k = 1, 16 do block[k] = rawC[k-1] or 0 end - idct4x4(block) - addResiduals(Yplane, px, py, Ystride, block) - end - - -- 4 U + 4 V sub-blocks - for _, plane in ipairs({Uplane, Vplane}) do - for si = 0, 3 do - local sx = band(si, 1) * 4 - local sy = rshift(si, 1) * 4 - local px = mbx * 8 + sx - local py = mby * 8 + sy - local rawC, nz = decodeCoefficients(pb, coefProbs[3], 0, 15, 0) - for k = 0, 15 do - rawC[k] = (rawC[k] or 0) * (k == 0 and uvDCq or uvACq) - end - local block = {} - for k = 1, 16 do block[k] = rawC[k-1] or 0 end - idct4x4(block) - addResiduals(plane, px, py, Cstride, block) - end - end - end - - end -- mbx - end -- mby - - -- optional simple loop filter - if filterType == 1 and filterLevel > 0 then - filterSimple(Yplane, Ystride, mbw*16, mbh*16) - filterSimple(Uplane, Cstride, mbw*8, mbh*8) - filterSimple(Vplane, Cstride, mbw*8, mbh*8) - end - - -- ── Assemble RGBA image ── - local imageData = love.image.newImageData(width, height, "rgba8") - imageData:mapPixel(function(px, py) - local yi = py * Ystride + px + 1 - local cy = rshift(py, 1) - local cx = rshift(px, 1) - local ci = cy * Cstride + cx + 1 - local Y = Yplane[yi] or 128 - local Cb = Uplane[ci] or 128 - local Cr = Vplane[ci] or 128 - local r, g, b = yuvToRgb(Y, Cb, Cr) - return r/255, g/255, b/255, 1 - end) - - return love.graphics.newImage(imageData) -end - --- ═══════════════════════════════════════════════════════════════════════════ --- VP8L → Love2D image (unchanged, just wrapped for the dispatcher) --- ═══════════════════════════════════════════════════════════════════════════ - -local function decodeVP8L_image(data, offset) - offset = offset or 1 - assert(data:byte(offset) == 0x2f, "Invalid VP8L signature byte") - local br = newBitReader(data:sub(offset + 1)) - local width = br:read(14) + 1 - local height = br:read(14) + 1 - br:readBool() -- alpha hint - local version = br:read(3) - assert(version == 0, "Unsupported VP8L version: " .. version) - - local pixels = decodeVP8L(br, width, height) - - local imageData = love.image.newImageData(width, height, "rgba8") - imageData:mapPixel(function(x, y) - local c = pixels[y * width + x + 1] or 0 - local a = band(rshift(c, 24), 0xff) - local r = band(rshift(c, 16), 0xff) - local g = band(rshift(c, 8), 0xff) - local b = band(c, 0xff) - return r/255, g/255, b/255, a/255 - end) - - return love.graphics.newImage(imageData) -end - --- ═══════════════════════════════════════════════════════════════════════════ --- PUBLIC API --- ═══════════════════════════════════════════════════════════════════════════ - --- Read a little-endian uint32 from data at pos (1-based) -local function readU32LE(data, pos) - return bor(data:byte(pos), - bor(lshift(data:byte(pos+1), 8), - bor(lshift(data:byte(pos+2), 16), - lshift(data:byte(pos+3), 24)))) -end - -function WebP.decode(data) - assert(data:sub(1, 4) == "RIFF", "Not a RIFF file") - assert(data:sub(9, 12) == "WEBP", "Not a WEBP file") - - local fourCC = data:sub(13, 16) - - if fourCC == "VP8 " then - -- Simple lossy: VP8 bitstream starts after the 8-byte chunk header - -- bytes 13-16: "VP8 ", 17-20: chunk size (LE), 21+: VP8 bitstream - return decodeVP8(data, 21) - - elseif fourCC == "VP8L" then - -- Simple lossless: VP8L bitstream starts at byte 21 - return decodeVP8L_image(data, 21) - - elseif fourCC == "VP8X" then - -- Extended format: parse chunks after the VP8X header - -- VP8X chunk: 4cc(4) + size(4) + flags(4) + canvas_w-1(3) + canvas_h-1(3) = 18 bytes - local pos = 13 -- start of VP8X chunk - local chunkSize = readU32LE(data, pos + 4) - pos = pos + 8 + chunkSize -- skip past VP8X chunk data - - -- walk remaining chunks - while pos + 8 <= #data do - local cc = data:sub(pos, pos + 3) - local sz = readU32LE(data, pos + 4) - local dOff = pos + 8 -- chunk data offset (1-based) - - if cc == "VP8 " then - return decodeVP8(data, dOff) - elseif cc == "VP8L" then - return decodeVP8L_image(data, dOff) - elseif cc == "ANIM" or cc == "ANMF" then - error("Animated WebP is not supported") - end - - pos = pos + 8 + sz - if band(sz, 1) == 1 then pos = pos + 1 end -- padding byte - end - error("VP8X: no VP8 or VP8L chunk found") - else - error("Unsupported WebP chunk type: " .. fourCC) - end -end - -function WebP.load(path) - local data = love.filesystem.read(path) - assert(data, "Could not read file: " .. path) - return WebP.decode(data) -end - -return WebP

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