From ba550a60d04f961c360710c0c2db8fd95f67aa4a Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Wed, 15 Oct 2025 20:11:27 -0400 Subject: [PATCH] need to remove guiding bars --- FlexLove.lua | 306 ++++++++++++++++-- examples/NineSliceCornerScalingDemo.lua | 243 ++++++++++++++ examples/ThemeColorAccessSimple.lua | 181 ----------- testing/__tests__/12_units_system_tests.lua | 17 +- .../__tests__/14_text_scaling_basic_tests.lua | 10 + testing/__tests__/20_padding_resize_tests.lua | 2 + .../23_image_scaler_bilinear_tests.lua | 288 +++++++++++++++++ testing/loveStub.lua | 48 +++ themes/metal.lua | 69 ++++ themes/space.lua | 14 +- 10 files changed, 942 insertions(+), 236 deletions(-) create mode 100644 examples/NineSliceCornerScalingDemo.lua delete mode 100644 examples/ThemeColorAccessSimple.lua create mode 100644 testing/__tests__/23_image_scaler_bilinear_tests.lua diff --git a/FlexLove.lua b/FlexLove.lua index 77e12d8..8b0c119 100644 --- a/FlexLove.lua +++ b/FlexLove.lua @@ -352,37 +352,121 @@ function ImageScaler.scaleNearest(sourceImageData, srcX, srcY, srcW, srcH, destW if not sourceImageData then error(formatError("ImageScaler", "Source ImageData cannot be nil")) end - + if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then error(formatError("ImageScaler", "Dimensions must be positive")) end - + -- Create destination ImageData local destImageData = love.image.newImageData(destW, destH) - + -- Calculate scale ratios (cached outside loops for performance) local scaleX = srcW / destW local scaleY = srcH / destH - + -- Nearest-neighbor sampling for destY = 0, destH - 1 do for destX = 0, destW - 1 do -- Calculate source pixel coordinates using floor (nearest-neighbor) local srcPixelX = math.floor(destX * scaleX) + srcX local srcPixelY = math.floor(destY * scaleY) + srcY - + -- Clamp to source bounds (safety check) srcPixelX = math.min(srcPixelX, srcX + srcW - 1) srcPixelY = math.min(srcPixelY, srcY + srcH - 1) - + -- Sample source pixel local r, g, b, a = sourceImageData:getPixel(srcPixelX, srcPixelY) - + -- Write to destination destImageData:setPixel(destX, destY, r, g, b, a) end end - + + return destImageData +end + +--- Linear interpolation helper +--- Blends between two values based on interpolation factor +---@param a number -- Start value +---@param b number -- End value +---@param t number -- Interpolation factor [0, 1] +---@return number -- Interpolated value +local function lerp(a, b, t) + return a + (b - a) * t +end + +--- Scale an ImageData region using bilinear interpolation +--- Produces smooth, filtered scaling - ideal for high-quality upscaling +---@param sourceImageData love.ImageData -- Source image data +---@param srcX number -- Source region X (0-based) +---@param srcY number -- Source region Y (0-based) +---@param srcW number -- Source region width +---@param srcH number -- Source region height +---@param destW number -- Destination width +---@param destH number -- Destination height +---@return love.ImageData -- Scaled image data +function ImageScaler.scaleBilinear(sourceImageData, srcX, srcY, srcW, srcH, destW, destH) + if not sourceImageData then + error(formatError("ImageScaler", "Source ImageData cannot be nil")) + end + + if srcW <= 0 or srcH <= 0 or destW <= 0 or destH <= 0 then + error(formatError("ImageScaler", "Dimensions must be positive")) + end + + -- Create destination ImageData + local destImageData = love.image.newImageData(destW, destH) + + -- Calculate scale ratios + local scaleX = srcW / destW + local scaleY = srcH / destH + + -- Bilinear interpolation + for destY = 0, destH - 1 do + for destX = 0, destW - 1 do + -- Calculate fractional source position + local srcXf = destX * scaleX + local srcYf = destY * scaleY + + -- Get integer coordinates for 2x2 sampling grid + local x0 = math.floor(srcXf) + local y0 = math.floor(srcYf) + local x1 = math.min(x0 + 1, srcW - 1) + local y1 = math.min(y0 + 1, srcH - 1) + + -- Get fractional parts for interpolation + local fx = srcXf - x0 + local fy = srcYf - y0 + + -- Sample 4 neighboring pixels (with source offset) + local r00, g00, b00, a00 = sourceImageData:getPixel(srcX + x0, srcY + y0) + local r10, g10, b10, a10 = sourceImageData:getPixel(srcX + x1, srcY + y0) + local r01, g01, b01, a01 = sourceImageData:getPixel(srcX + x0, srcY + y1) + local r11, g11, b11, a11 = sourceImageData:getPixel(srcX + x1, srcY + y1) + + -- Interpolate horizontally (top and bottom rows) + local rTop = lerp(r00, r10, fx) + local gTop = lerp(g00, g10, fx) + local bTop = lerp(b00, b10, fx) + local aTop = lerp(a00, a10, fx) + + local rBottom = lerp(r01, r11, fx) + local gBottom = lerp(g01, g11, fx) + local bBottom = lerp(b01, b11, fx) + local aBottom = lerp(a01, a11, fx) + + -- Interpolate vertically (final result) + local r = lerp(rTop, rBottom, fy) + local g = lerp(gTop, gBottom, fy) + local b = lerp(bTop, bBottom, fy) + local a = lerp(aTop, aBottom, fy) + + -- Write to destination + destImageData:setPixel(destX, destY, r, g, b, a) + end + end + return destImageData end @@ -996,27 +1080,119 @@ function NineSlice.draw(component, atlas, x, y, width, height, opacity) return love.graphics.newQuad(region.x, region.y, region.w, region.h, atlasWidth, atlasHeight) end - -- CORNERS (no scaling - 1:1 pixel perfect) - love.graphics.draw(atlas, makeQuad(regions.topLeft), x, y) - love.graphics.draw(atlas, makeQuad(regions.topRight), x + left + contentWidth, y) - love.graphics.draw(atlas, makeQuad(regions.bottomLeft), x, y + top + contentHeight) - love.graphics.draw(atlas, makeQuad(regions.bottomRight), x + left + contentWidth, y + top + contentHeight) + -- Check if corner scaling is enabled + local scaleCorners = component.scaleCorners or false + local scalingAlgorithm = component.scalingAlgorithm or "bilinear" - -- TOP/BOTTOM EDGES (stretch horizontally only) - if contentWidth > 0 then - love.graphics.draw(atlas, makeQuad(regions.topCenter), x + left, y, 0, scaleX, 1) - love.graphics.draw(atlas, makeQuad(regions.bottomCenter), x + left, y + top + contentHeight, 0, scaleX, 1) - end + if scaleCorners and Gui and Gui.scaleFactors then + -- Initialize cache if needed + if not component._scaledRegionCache then + component._scaledRegionCache = {} + end - -- LEFT/RIGHT EDGES (stretch vertically only) - if contentHeight > 0 then - love.graphics.draw(atlas, makeQuad(regions.middleLeft), x, y + top, 0, 1, scaleY) - love.graphics.draw(atlas, makeQuad(regions.middleRight), x + left + contentWidth, y + top, 0, 1, scaleY) - end + -- Get current scale factors + local scaleFactorX = Gui.scaleFactors.x or 1 + local scaleFactorY = Gui.scaleFactors.y or 1 + local scaleFactor = math.max(scaleFactorX, scaleFactorY) - -- CENTER (stretch both dimensions) - if contentWidth > 0 and contentHeight > 0 then - love.graphics.draw(atlas, makeQuad(regions.middleCenter), x + left, y + top, 0, scaleX, scaleY) + -- Helper to get or create scaled region + local function getScaledRegion(regionName, region, targetWidth, targetHeight) + local cacheKey = string.format("%s_%.2f_%s", regionName, scaleFactor, scalingAlgorithm) + + if component._scaledRegionCache[cacheKey] then + return component._scaledRegionCache[cacheKey] + end + + -- Extract region from atlas + local atlasData = atlas:getData() + local scaledData + + if scalingAlgorithm == "nearest" then + scaledData = ImageScaler.scaleNearest(atlasData, region.x, region.y, region.w, region.h, targetWidth, targetHeight) + else + scaledData = ImageScaler.scaleBilinear(atlasData, region.x, region.y, region.w, region.h, targetWidth, targetHeight) + end + + -- Convert to image and cache + local scaledImage = love.graphics.newImage(scaledData) + component._scaledRegionCache[cacheKey] = scaledImage + + return scaledImage + end + + -- Calculate scaled dimensions for corners + local scaledLeft = math.floor(left * scaleFactor + 0.5) + local scaledRight = math.floor(right * scaleFactor + 0.5) + local scaledTop = math.floor(top * scaleFactor + 0.5) + local scaledBottom = math.floor(bottom * scaleFactor + 0.5) + + -- CORNERS (scaled using algorithm) + local topLeftScaled = getScaledRegion("topLeft", regions.topLeft, scaledLeft, scaledTop) + local topRightScaled = getScaledRegion("topRight", regions.topRight, scaledRight, scaledTop) + local bottomLeftScaled = getScaledRegion("bottomLeft", regions.bottomLeft, scaledLeft, scaledBottom) + local bottomRightScaled = getScaledRegion("bottomRight", regions.bottomRight, scaledRight, scaledBottom) + + love.graphics.draw(topLeftScaled, x, y) + love.graphics.draw(topRightScaled, x + scaledLeft + contentWidth, y) + love.graphics.draw(bottomLeftScaled, x, y + scaledTop + contentHeight) + love.graphics.draw(bottomRightScaled, x + scaledLeft + contentWidth, y + scaledTop + contentHeight) + + -- Update content dimensions to account for scaled borders + local adjustedContentWidth = width - scaledLeft - scaledRight + local adjustedContentHeight = height - scaledTop - scaledBottom + adjustedContentWidth = math.max(0, adjustedContentWidth) + adjustedContentHeight = math.max(0, adjustedContentHeight) + + -- Recalculate stretch scales + local adjustedScaleX = adjustedContentWidth / centerW + local adjustedScaleY = adjustedContentHeight / centerH + + -- TOP/BOTTOM EDGES (stretch horizontally, scale vertically) + if adjustedContentWidth > 0 then + local topCenterScaled = getScaledRegion("topCenter", regions.topCenter, regions.topCenter.w, scaledTop) + local bottomCenterScaled = getScaledRegion("bottomCenter", regions.bottomCenter, regions.bottomCenter.w, scaledBottom) + + love.graphics.draw(topCenterScaled, x + scaledLeft, y, 0, adjustedScaleX, 1) + love.graphics.draw(bottomCenterScaled, x + scaledLeft, y + scaledTop + adjustedContentHeight, 0, adjustedScaleX, 1) + end + + -- LEFT/RIGHT EDGES (stretch vertically, scale horizontally) + if adjustedContentHeight > 0 then + local middleLeftScaled = getScaledRegion("middleLeft", regions.middleLeft, scaledLeft, regions.middleLeft.h) + local middleRightScaled = getScaledRegion("middleRight", regions.middleRight, scaledRight, regions.middleRight.h) + + love.graphics.draw(middleLeftScaled, x, y + scaledTop, 0, 1, adjustedScaleY) + love.graphics.draw(middleRightScaled, x + scaledLeft + adjustedContentWidth, y + scaledTop, 0, 1, adjustedScaleY) + end + + -- CENTER (stretch both dimensions, no scaling) + if adjustedContentWidth > 0 and adjustedContentHeight > 0 then + love.graphics.draw(atlas, makeQuad(regions.middleCenter), x + scaledLeft, y + scaledTop, 0, adjustedScaleX, adjustedScaleY) + end + else + -- Original rendering logic (no scaling) + -- CORNERS (no scaling - 1:1 pixel perfect) + love.graphics.draw(atlas, makeQuad(regions.topLeft), x, y) + love.graphics.draw(atlas, makeQuad(regions.topRight), x + left + contentWidth, y) + love.graphics.draw(atlas, makeQuad(regions.bottomLeft), x, y + top + contentHeight) + love.graphics.draw(atlas, makeQuad(regions.bottomRight), x + left + contentWidth, y + top + contentHeight) + + -- TOP/BOTTOM EDGES (stretch horizontally only) + if contentWidth > 0 then + love.graphics.draw(atlas, makeQuad(regions.topCenter), x + left, y, 0, scaleX, 1) + love.graphics.draw(atlas, makeQuad(regions.bottomCenter), x + left, y + top + contentHeight, 0, scaleX, 1) + end + + -- LEFT/RIGHT EDGES (stretch vertically only) + if contentHeight > 0 then + love.graphics.draw(atlas, makeQuad(regions.middleLeft), x, y + top, 0, 1, scaleY) + love.graphics.draw(atlas, makeQuad(regions.middleRight), x + left + contentWidth, y + top, 0, 1, scaleY) + end + + -- CENTER (stretch both dimensions) + if contentWidth > 0 and contentHeight > 0 then + love.graphics.draw(atlas, makeQuad(regions.middleCenter), x + left, y + top, 0, scaleX, scaleY) + end end -- Reset color @@ -1500,6 +1676,17 @@ function Gui.resize() Gui.scaleFactors.y = newHeight / Gui.baseScale.height end + -- Clear scaled region caches for all themes + for _, theme in pairs(themes) do + if theme.components then + for _, component in pairs(theme.components) do + if component._scaledRegionCache then + component._scaledRegionCache = {} + end + end + end + end + for _, win in ipairs(Gui.topElements) do win:resize(newWidth, newHeight) end @@ -1584,6 +1771,8 @@ function Gui.destroy() -- Reset base scale and scale factors Gui.baseScale = nil Gui.scaleFactors = { x = 1.0, y = 1.0 } + -- Reset cached viewport + Gui._cachedViewport = { width = 0, height = 0 } end -- Simple GUI library for LOVE2D @@ -4030,25 +4219,28 @@ function Element:recalculateUnits(newViewportWidth, newViewportHeight) -- BORDER-BOX MODEL: Calculate content dimensions from border-box dimensions -- For explicitly-sized elements (non-auto), _borderBoxWidth/_borderBoxHeight were set earlier -- Now we calculate content width/height by subtracting padding - if self.units.width.unit ~= "auto" then - -- _borderBoxWidth was already set during width recalculation + -- Only recalculate if using viewport/percentage units (where _borderBoxWidth actually changed) + if self.units.width.unit ~= "auto" and self.units.width.unit ~= "px" then + -- _borderBoxWidth was recalculated for viewport/percentage units -- Calculate content width by subtracting padding self.width = math.max(0, self._borderBoxWidth - self.padding.left - self.padding.right) - else + elseif self.units.width.unit == "auto" then -- For auto-sized elements, width is content width (calculated in resize method) -- Update border-box to include padding self._borderBoxWidth = self.width + self.padding.left + self.padding.right end + -- For pixel units, width stays as-is (may have been manually modified) - if self.units.height.unit ~= "auto" then - -- _borderBoxHeight was already set during height recalculation + if self.units.height.unit ~= "auto" and self.units.height.unit ~= "px" then + -- _borderBoxHeight was recalculated for viewport/percentage units -- Calculate content height by subtracting padding self.height = math.max(0, self._borderBoxHeight - self.padding.top - self.padding.bottom) - else + elseif self.units.height.unit == "auto" then -- For auto-sized elements, height is content height (calculated in resize method) -- Update border-box to include padding self._borderBoxHeight = self.height + self.padding.top + self.padding.bottom end + -- For pixel units, height stays as-is (may have been manually modified) end --- Resize element and its children based on game window size change @@ -4057,6 +4249,14 @@ end function Element:resize(newGameWidth, newGameHeight) self:recalculateUnits(newGameWidth, newGameHeight) + -- For non-auto-sized elements with viewport/percentage units, update content dimensions from border-box + if not self.autosizing.width and self._borderBoxWidth and self.units.width.unit ~= "px" then + self.width = math.max(0, self._borderBoxWidth - self.padding.left - self.padding.right) + end + if not self.autosizing.height and self._borderBoxHeight and self.units.height.unit ~= "px" then + self.height = math.max(0, self._borderBoxHeight - self.padding.top - self.padding.bottom) + end + -- Update children for _, child in ipairs(self.children) do child:resize(newGameWidth, newGameHeight) @@ -4076,6 +4276,48 @@ function Element:resize(newGameWidth, newGameHeight) self.height = contentHeight end + -- Re-resolve ew/eh textSize units after all dimensions are finalized + -- This ensures textSize updates based on current width/height (whether calculated or manually set) + if self.units.textSize.value then + local unit = self.units.textSize.unit + local value = self.units.textSize.value + local scaleX, scaleY = Gui.getScaleFactors() + + if unit == "ew" then + -- Element width relative (use current width) + self.textSize = (value / 100) * self.width + + -- Apply min/max constraints + local minSize = self.minTextSize and (Gui.baseScale and (self.minTextSize * scaleY) or self.minTextSize) + local maxSize = self.maxTextSize and (Gui.baseScale and (self.maxTextSize * scaleY) or self.maxTextSize) + if minSize and self.textSize < minSize then + self.textSize = minSize + end + if maxSize and self.textSize > maxSize then + self.textSize = maxSize + end + if self.textSize < 1 then + self.textSize = 1 + end + elseif unit == "eh" then + -- Element height relative (use current height) + self.textSize = (value / 100) * self.height + + -- Apply min/max constraints + local minSize = self.minTextSize and (Gui.baseScale and (self.minTextSize * scaleY) or self.minTextSize) + local maxSize = self.maxTextSize and (Gui.baseScale and (self.maxTextSize * scaleY) or self.maxTextSize) + if minSize and self.textSize < minSize then + self.textSize = minSize + end + if maxSize and self.textSize > maxSize then + self.textSize = maxSize + end + if self.textSize < 1 then + self.textSize = 1 + end + end + end + self:layoutChildren() self.prevGameSize.width = newGameWidth self.prevGameSize.height = newGameHeight diff --git a/examples/NineSliceCornerScalingDemo.lua b/examples/NineSliceCornerScalingDemo.lua new file mode 100644 index 0000000..9b78685 --- /dev/null +++ b/examples/NineSliceCornerScalingDemo.lua @@ -0,0 +1,243 @@ +local FlexLove = require("FlexLove") +local Gui = FlexLove.GUI +local Theme = FlexLove.Theme +local Color = FlexLove.Color + +---@class CornerScalingDemo +---@field window Element +---@field currentMode string +---@field modeButtons table +local CornerScalingDemo = {} +CornerScalingDemo.__index = CornerScalingDemo + +function CornerScalingDemo.init() + local self = setmetatable({}, CornerScalingDemo) + + self.currentMode = "none" + self.modeButtons = {} + + -- Try to load theme + local themeLoaded = pcall(function() + Theme.load("space") + Theme.setActive("space") + end) + + -- Create main window + self.window = Gui.new({ + x = 50, + y = 50, + width = 900, + height = 650, + backgroundColor = Color.new(0.1, 0.1, 0.15, 0.95), + border = { top = true, bottom = true, left = true, right = true }, + borderColor = Color.new(0.6, 0.6, 0.7, 1), + positioning = "flex", + flexDirection = "vertical", + gap = 20, + padding = { top = 20, right = 20, bottom = 20, left = 20 }, + }) + + -- Title + Gui.new({ + parent = self.window, + height = 40, + text = "NineSlice Corner Scaling Demo", + textSize = 24, + textAlign = "center", + textColor = Color.new(1, 1, 1, 1), + backgroundColor = Color.new(0.15, 0.15, 0.25, 1), + }) + + -- Status + Gui.new({ + parent = self.window, + height = 30, + text = themeLoaded and "✓ Theme loaded - Scaling demonstration active" + or "⚠ Theme not loaded - Please ensure theme assets exist", + textSize = 14, + textAlign = "center", + textColor = themeLoaded and Color.new(0.3, 0.9, 0.3, 1) or Color.new(0.9, 0.6, 0.3, 1), + backgroundColor = Color.new(0.08, 0.08, 0.12, 0.8), + }) + + -- Mode selector section + local modeSection = Gui.new({ + parent = self.window, + height = 80, + backgroundColor = Color.new(0.12, 0.12, 0.18, 1), + padding = { top = 15, right = 15, bottom = 15, left = 15 }, + positioning = "flex", + flexDirection = "vertical", + gap = 10, + }) + + Gui.new({ + parent = modeSection, + height = 20, + text = "Select Scaling Mode:", + textSize = 14, + textColor = Color.new(0.8, 0.9, 1, 1), + backgroundColor = Color.new(0, 0, 0, 0), + }) + + -- Button container + local buttonContainer = Gui.new({ + parent = modeSection, + height = 40, + positioning = "flex", + flexDirection = "horizontal", + gap = 15, + backgroundColor = Color.new(0, 0, 0, 0), + }) + + -- Helper to create mode button + local function createModeButton(mode, label) + local button = Gui.new({ + parent = buttonContainer, + width = 180, + height = 40, + text = label, + textAlign = "center", + textSize = 14, + textColor = Color.new(1, 1, 1, 1), + backgroundColor = self.currentMode == mode and Color.new(0.3, 0.6, 0.9, 1) or Color.new(0.25, 0.25, 0.35, 1), + callback = function(element, event) + if event.type == "click" then + self:setMode(mode) + end + end, + }) + self.modeButtons[mode] = button + return button + end + + createModeButton("none", "No Scaling (Default)") + createModeButton("nearest", "Nearest Neighbor") + createModeButton("bilinear", "Bilinear Interpolation") + + -- Comparison section + local comparisonSection = Gui.new({ + parent = self.window, + height = 420, + backgroundColor = Color.new(0.08, 0.08, 0.12, 1), + padding = { top = 20, right = 20, bottom = 20, left = 20 }, + positioning = "flex", + flexDirection = "vertical", + gap = 15, + }) + + -- Description + Gui.new({ + parent = comparisonSection, + height = 60, + text = "The panels below demonstrate different scaling modes.\n" .. + "• No Scaling: Corners remain at original size (may appear small at high DPI)\n" .. + "• Nearest Neighbor: Sharp, pixelated scaling (ideal for pixel art)\n" .. + "• Bilinear: Smooth, filtered scaling (ideal for high-quality graphics)", + textSize = 12, + textColor = Color.new(0.7, 0.8, 0.9, 1), + backgroundColor = Color.new(0, 0, 0, 0), + }) + + -- Demo panels container + local panelsContainer = Gui.new({ + parent = comparisonSection, + positioning = "flex", + flexDirection = "horizontal", + gap = 20, + backgroundColor = Color.new(0, 0, 0, 0), + }) + + -- Helper to create demo panel + local function createDemoPanel(size, label) + local container = Gui.new({ + parent = panelsContainer, + width = (900 - 80 - 40) / 3, -- Divide available space + positioning = "flex", + flexDirection = "vertical", + gap = 10, + backgroundColor = Color.new(0, 0, 0, 0), + }) + + Gui.new({ + parent = container, + height = 20, + text = label, + textSize = 12, + textAlign = "center", + textColor = Color.new(0.8, 0.9, 1, 1), + backgroundColor = Color.new(0, 0, 0, 0), + }) + + local panel = Gui.new({ + parent = container, + width = size, + height = size, + backgroundColor = Color.new(0.2, 0.3, 0.4, 0.5), + theme = themeLoaded and "panel" or nil, + padding = { top = 15, right = 15, bottom = 15, left = 15 }, + }) + + Gui.new({ + parent = panel, + text = "Themed\nPanel", + textSize = 14, + textAlign = "center", + textColor = Color.new(1, 1, 1, 1), + backgroundColor = Color.new(0, 0, 0, 0), + }) + + return panel + end + + createDemoPanel(120, "Small (120x120)") + createDemoPanel(160, "Medium (160x160)") + createDemoPanel(200, "Large (200x200)") + + -- Info footer + Gui.new({ + parent = self.window, + height = 30, + text = "Resize the window to see how scaling adapts to different DPI settings", + textSize = 11, + textAlign = "center", + textColor = Color.new(0.5, 0.6, 0.7, 1), + backgroundColor = Color.new(0.08, 0.08, 0.12, 1), + }) + + return self +end + +function CornerScalingDemo:setMode(mode) + self.currentMode = mode + + -- Update button colors + for modeName, button in pairs(self.modeButtons) do + button.backgroundColor = modeName == mode and Color.new(0.3, 0.6, 0.9, 1) or Color.new(0.25, 0.25, 0.35, 1) + end + + -- Update theme components based on mode + local activeTheme = Theme.getActive() + if activeTheme and activeTheme.components then + for componentName, component in pairs(activeTheme.components) do + if mode == "none" then + component.scaleCorners = false + elseif mode == "nearest" then + component.scaleCorners = true + component.scalingAlgorithm = "nearest" + elseif mode == "bilinear" then + component.scaleCorners = true + component.scalingAlgorithm = "bilinear" + end + + -- Clear cache to force re-rendering + if component._scaledRegionCache then + component._scaledRegionCache = {} + end + end + end + + print("Scaling mode changed to: " .. mode) +end + +return CornerScalingDemo.init() diff --git a/examples/ThemeColorAccessSimple.lua b/examples/ThemeColorAccessSimple.lua deleted file mode 100644 index 6519cdd..0000000 --- a/examples/ThemeColorAccessSimple.lua +++ /dev/null @@ -1,181 +0,0 @@ --- Simple Theme Color Access Demo --- Shows how to access theme colors without creating GUI elements - -package.path = package.path .. ";./?.lua;../?.lua" - -local FlexLove = require("FlexLove") -local Theme = FlexLove.Theme -local Color = FlexLove.Color - --- Initialize minimal love stubs -love = { - graphics = { - newFont = function(size) return { getHeight = function() return size end } end, - newImage = function() return {} end, - newQuad = function() return {} end, - }, -} - -print("=== Theme Color Access - Simple Demo ===\n") - --- Load and activate the space theme -Theme.load("space") -Theme.setActive("space") - -print("✓ Theme 'space' loaded and activated\n") - --- ============================================ --- METHOD 1: Basic Color Access (Recommended) --- ============================================ -print("METHOD 1: Theme.getColor(colorName)") -print("------------------------------------") - -local primaryColor = Theme.getColor("primary") -local secondaryColor = Theme.getColor("secondary") -local textColor = Theme.getColor("text") -local textDarkColor = Theme.getColor("textDark") - -print(string.format("primary = Color(r=%.2f, g=%.2f, b=%.2f, a=%.2f)", - primaryColor.r, primaryColor.g, primaryColor.b, primaryColor.a)) -print(string.format("secondary = Color(r=%.2f, g=%.2f, b=%.2f, a=%.2f)", - secondaryColor.r, secondaryColor.g, secondaryColor.b, secondaryColor.a)) -print(string.format("text = Color(r=%.2f, g=%.2f, b=%.2f, a=%.2f)", - textColor.r, textColor.g, textColor.b, textColor.a)) -print(string.format("textDark = Color(r=%.2f, g=%.2f, b=%.2f, a=%.2f)", - textDarkColor.r, textDarkColor.g, textDarkColor.b, textDarkColor.a)) - --- ============================================ --- METHOD 2: Get All Color Names --- ============================================ -print("\nMETHOD 2: Theme.getColorNames()") -print("--------------------------------") - -local colorNames = Theme.getColorNames() -print("Available colors:") -for i, name in ipairs(colorNames) do - print(string.format(" %d. %s", i, name)) -end - --- ============================================ --- METHOD 3: Get All Colors at Once --- ============================================ -print("\nMETHOD 3: Theme.getAllColors()") -print("-------------------------------") - -local allColors = Theme.getAllColors() -print("All colors with values:") -for name, color in pairs(allColors) do - print(string.format(" %-10s = (%.2f, %.2f, %.2f, %.2f)", - name, color.r, color.g, color.b, color.a)) -end - --- ============================================ --- METHOD 4: Safe Access with Fallback --- ============================================ -print("\nMETHOD 4: Theme.getColorOrDefault(colorName, fallback)") -print("-------------------------------------------------------") - --- Try to get a color that exists -local existingColor = Theme.getColorOrDefault("primary", Color.new(1, 0, 0, 1)) -print(string.format("Existing color 'primary': (%.2f, %.2f, %.2f) ✓", - existingColor.r, existingColor.g, existingColor.b)) - --- Try to get a color that doesn't exist (will use fallback) -local missingColor = Theme.getColorOrDefault("accent", Color.new(1, 0, 0, 1)) -print(string.format("Missing color 'accent' (fallback): (%.2f, %.2f, %.2f) ✓", - missingColor.r, missingColor.g, missingColor.b)) - --- ============================================ --- PRACTICAL EXAMPLES --- ============================================ -print("\n=== Practical Usage Examples ===\n") - -print("Example 1: Using colors in element creation") -print("--------------------------------------------") -print([[ -local button = Gui.new({ - width = 200, - height = 50, - backgroundColor = Theme.getColor("primary"), - textColor = Theme.getColor("text"), - text = "Click Me!" -}) -]]) - -print("\nExample 2: Creating color variations") -print("-------------------------------------") -print([[ -local primary = Theme.getColor("primary") - --- Darker version (70% brightness) -local primaryDark = Color.new( - primary.r * 0.7, - primary.g * 0.7, - primary.b * 0.7, - primary.a -) - --- Lighter version (130% brightness) -local primaryLight = Color.new( - math.min(1, primary.r * 1.3), - math.min(1, primary.g * 1.3), - math.min(1, primary.b * 1.3), - primary.a -) - --- Semi-transparent version -local primaryTransparent = Color.new( - primary.r, - primary.g, - primary.b, - 0.5 -- 50% opacity -) -]]) - -print("\nExample 3: Safe color access") -print("-----------------------------") -print([[ --- With fallback to white if color doesn't exist -local bgColor = Theme.getColorOrDefault("background", Color.new(1, 1, 1, 1)) - --- With fallback to theme's secondary color -local borderColor = Theme.getColorOrDefault( - "border", - Theme.getColor("secondary") -) -]]) - -print("\nExample 4: Dynamic color selection") -print("-----------------------------------") -print([[ --- Get all available colors -local colors = Theme.getAllColors() - --- Pick a random color -local colorNames = {} -for name in pairs(colors) do - table.insert(colorNames, name) -end -local randomColorName = colorNames[math.random(#colorNames)] -local randomColor = colors[randomColorName] -]]) - -print("\n=== Quick Reference ===\n") -print("Theme.getColor(name) -- Get a specific color") -print("Theme.getColorOrDefault(n, fb) -- Get color with fallback") -print("Theme.getAllColors() -- Get all colors as table") -print("Theme.getColorNames() -- Get array of color names") -print("Theme.hasActive() -- Check if theme is active") -print("Theme.getActive() -- Get active theme object") - -print("\n=== Available Colors in 'space' Theme ===\n") -for i, name in ipairs(colorNames) do - local color = allColors[name] - print(string.format("%-10s RGB(%.0f, %.0f, %.0f)", - name, - color.r * 255, - color.g * 255, - color.b * 255)) -end - -print("\n=== Demo Complete ===") diff --git a/testing/__tests__/12_units_system_tests.lua b/testing/__tests__/12_units_system_tests.lua index 8ce1927..0ed8533 100644 --- a/testing/__tests__/12_units_system_tests.lua +++ b/testing/__tests__/12_units_system_tests.lua @@ -15,17 +15,13 @@ function TestUnitsSystem:setUp() -- Clear any existing GUI elements and reset viewport Gui.destroy() -- Set a consistent viewport size for testing - love.graphics.getDimensions = function() - return 1200, 800 - end + love.window.setMode(1200, 800) end function TestUnitsSystem:tearDown() Gui.destroy() -- Restore original viewport size - love.graphics.getDimensions = function() - return 800, 600 - end + love.window.setMode(800, 600) end -- ============================================ @@ -150,14 +146,15 @@ function TestUnitsSystem:testResizeViewportUnits() luaunit.assertEquals(container.width, 600) -- 50% of 1200 luaunit.assertEquals(container.height, 200) -- 25% of 800 - -- Simulate viewport resize - love.graphics.getDimensions = function() - return 1600, 1000 - end + -- Simulate viewport resize using setMode + love.window.setMode(1600, 1000) container:resize(1600, 1000) luaunit.assertEquals(container.width, 800) -- 50% of 1600 luaunit.assertEquals(container.height, 250) -- 25% of 1000 + + -- Restore viewport + love.window.setMode(1200, 800) end function TestUnitsSystem:testResizePercentageUnits() diff --git a/testing/__tests__/14_text_scaling_basic_tests.lua b/testing/__tests__/14_text_scaling_basic_tests.lua index c7d52f1..83d0f15 100644 --- a/testing/__tests__/14_text_scaling_basic_tests.lua +++ b/testing/__tests__/14_text_scaling_basic_tests.lua @@ -11,6 +11,16 @@ local Gui = FlexLove.GUI -- Test suite for comprehensive text scaling TestTextScaling = {} +function TestTextScaling:setUp() + -- Reset viewport to default before each test + love.window.setMode(800, 600) + Gui.destroy() +end + +function TestTextScaling:tearDown() + Gui.destroy() +end + -- Basic functionality tests function TestTextScaling.testFixedTextSize() -- Create an element with fixed textSize in pixels (auto-scaling disabled) diff --git a/testing/__tests__/20_padding_resize_tests.lua b/testing/__tests__/20_padding_resize_tests.lua index d11240c..5bcc4e8 100644 --- a/testing/__tests__/20_padding_resize_tests.lua +++ b/testing/__tests__/20_padding_resize_tests.lua @@ -1,6 +1,8 @@ -- Test padding resize behavior with percentage units package.path = package.path .. ";?.lua" local luaunit = require("testing.luaunit") +local loveStub = require("testing.loveStub") +_G.love = loveStub local FlexLove = require("FlexLove") TestPaddingResize = {} diff --git a/testing/__tests__/23_image_scaler_bilinear_tests.lua b/testing/__tests__/23_image_scaler_bilinear_tests.lua new file mode 100644 index 0000000..f19e393 --- /dev/null +++ b/testing/__tests__/23_image_scaler_bilinear_tests.lua @@ -0,0 +1,288 @@ +package.path = package.path .. ";?.lua" +local luaunit = require("testing.luaunit") +local loveStub = require("testing.loveStub") +_G.love = loveStub + +local FlexLove = require("FlexLove") + +TestImageScalerBilinear = {} + +function TestImageScalerBilinear:setUp() + -- Create a simple test image (2x2 with distinct colors) + self.testImage2x2 = love.image.newImageData(2, 2) + -- Top-left: red + self.testImage2x2:setPixel(0, 0, 1, 0, 0, 1) + -- Top-right: green + self.testImage2x2:setPixel(1, 0, 0, 1, 0, 1) + -- Bottom-left: blue + self.testImage2x2:setPixel(0, 1, 0, 0, 1, 1) + -- Bottom-right: white + self.testImage2x2:setPixel(1, 1, 1, 1, 1, 1) +end + +function TestImageScalerBilinear:test2xScaling() + -- Scale 2x2 to 4x4 (2x factor) + local scaled = FlexLove.ImageScaler.scaleBilinear(self.testImage2x2, 0, 0, 2, 2, 4, 4) + + luaunit.assertEquals(scaled:getWidth(), 4) + luaunit.assertEquals(scaled:getHeight(), 4) + + -- Corner pixels should match original (no interpolation at exact positions) + local r, g, b, a = scaled:getPixel(0, 0) + luaunit.assertAlmostEquals(r, 1, 0.01) -- Red + luaunit.assertAlmostEquals(g, 0, 0.01) + luaunit.assertAlmostEquals(b, 0, 0.01) + + -- Center pixel at (1,1) should be blend of all 4 corners + -- At (0.5, 0.5) in source space -> blend of all 4 colors + r, g, b, a = scaled:getPixel(1, 1) + -- Should be approximately (0.5, 0.5, 0.5) - average of red, green, blue, white + luaunit.assertTrue(r > 0.3 and r < 0.7, "Center pixel should be blended") + luaunit.assertTrue(g > 0.3 and g < 0.7, "Center pixel should be blended") + luaunit.assertTrue(b > 0.3 and b < 0.7, "Center pixel should be blended") +end + +function TestImageScalerBilinear:testGradientSmoothing() + -- Create a simple gradient: black to white horizontally + local gradient = love.image.newImageData(2, 1) + gradient:setPixel(0, 0, 0, 0, 0, 1) -- Black + gradient:setPixel(1, 0, 1, 1, 1, 1) -- White + + -- Scale to 4 pixels wide + local scaled = FlexLove.ImageScaler.scaleBilinear(gradient, 0, 0, 2, 1, 4, 1) + + luaunit.assertEquals(scaled:getWidth(), 4) + luaunit.assertEquals(scaled:getHeight(), 1) + + -- Check smooth gradient progression + local r0 = scaled:getPixel(0, 0) + local r1 = scaled:getPixel(1, 0) + local r2 = scaled:getPixel(2, 0) + local r3 = scaled:getPixel(3, 0) + + -- Should be monotonically increasing (or equal at end due to clamping) + luaunit.assertTrue(r0 < r1, "Gradient should increase") + luaunit.assertTrue(r1 < r2, "Gradient should increase") + luaunit.assertTrue(r2 <= r3, "Gradient should increase or stay same") + + -- First should be close to black, last close to white + luaunit.assertAlmostEquals(r0, 0, 0.15) + luaunit.assertAlmostEquals(r3, 1, 0.15) +end + +function TestImageScalerBilinear:testSameSizeScaling() + -- Scale 2x2 to 2x2 (should be identical) + local scaled = FlexLove.ImageScaler.scaleBilinear(self.testImage2x2, 0, 0, 2, 2, 2, 2) + + luaunit.assertEquals(scaled:getWidth(), 2) + luaunit.assertEquals(scaled:getHeight(), 2) + + -- Verify all pixels match original + for y = 0, 1 do + for x = 0, 1 do + local r1, g1, b1, a1 = self.testImage2x2:getPixel(x, y) + local r2, g2, b2, a2 = scaled:getPixel(x, y) + luaunit.assertAlmostEquals(r1, r2, 0.01) + luaunit.assertAlmostEquals(g1, g2, 0.01) + luaunit.assertAlmostEquals(b1, b2, 0.01) + luaunit.assertAlmostEquals(a1, a2, 0.01) + end + end +end + +function TestImageScalerBilinear:test1x1Scaling() + -- Create 1x1 image + local img1x1 = love.image.newImageData(1, 1) + img1x1:setPixel(0, 0, 0.5, 0.5, 0.5, 1) + + -- Scale to 4x4 + local scaled = FlexLove.ImageScaler.scaleBilinear(img1x1, 0, 0, 1, 1, 4, 4) + + luaunit.assertEquals(scaled:getWidth(), 4) + luaunit.assertEquals(scaled:getHeight(), 4) + + -- All pixels should be the same color (no neighbors to interpolate with) + for y = 0, 3 do + for x = 0, 3 do + local r, g, b = scaled:getPixel(x, y) + luaunit.assertAlmostEquals(r, 0.5, 0.01) + luaunit.assertAlmostEquals(g, 0.5, 0.01) + luaunit.assertAlmostEquals(b, 0.5, 0.01) + end + end +end + +function TestImageScalerBilinear:testPureColorMaintenance() + -- Create pure white image + local whiteImg = love.image.newImageData(2, 2) + for y = 0, 1 do + for x = 0, 1 do + whiteImg:setPixel(x, y, 1, 1, 1, 1) + end + end + + local scaled = FlexLove.ImageScaler.scaleBilinear(whiteImg, 0, 0, 2, 2, 4, 4) + + -- All pixels should remain pure white + for y = 0, 3 do + for x = 0, 3 do + local r, g, b = scaled:getPixel(x, y) + luaunit.assertAlmostEquals(r, 1, 0.01) + luaunit.assertAlmostEquals(g, 1, 0.01) + luaunit.assertAlmostEquals(b, 1, 0.01) + end + end + + -- Test pure black + local blackImg = love.image.newImageData(2, 2) + for y = 0, 1 do + for x = 0, 1 do + blackImg:setPixel(x, y, 0, 0, 0, 1) + end + end + + scaled = FlexLove.ImageScaler.scaleBilinear(blackImg, 0, 0, 2, 2, 4, 4) + + for y = 0, 3 do + for x = 0, 3 do + local r, g, b = scaled:getPixel(x, y) + luaunit.assertAlmostEquals(r, 0, 0.01) + luaunit.assertAlmostEquals(g, 0, 0.01) + luaunit.assertAlmostEquals(b, 0, 0.01) + end + end +end + +function TestImageScalerBilinear:testAlphaInterpolation() + -- Create image with varying alpha + local img = love.image.newImageData(2, 2) + img:setPixel(0, 0, 1, 0, 0, 1.0) -- Opaque red + img:setPixel(1, 0, 1, 0, 0, 0.0) -- Transparent red + img:setPixel(0, 1, 1, 0, 0, 1.0) -- Opaque red + img:setPixel(1, 1, 1, 0, 0, 0.0) -- Transparent red + + local scaled = FlexLove.ImageScaler.scaleBilinear(img, 0, 0, 2, 2, 4, 2) + + -- Check that alpha is interpolated smoothly + local r, g, b, a0 = scaled:getPixel(0, 0) + luaunit.assertAlmostEquals(a0, 1.0, 0.01) + + local r, g, b, a1 = scaled:getPixel(1, 0) + -- Should be between 1.0 and 0.0 + luaunit.assertTrue(a1 > 0.3 and a1 < 0.7, "Alpha should be interpolated") + + local r, g, b, a3 = scaled:getPixel(3, 0) + luaunit.assertAlmostEquals(a3, 0.0, 0.15) +end + +function TestImageScalerBilinear:testSubregionScaling() + -- Create 4x4 image with different quadrants + local img4x4 = love.image.newImageData(4, 4) + + -- Fill with pattern: top-left red, rest black + for y = 0, 3 do + for x = 0, 3 do + if x < 2 and y < 2 then + img4x4:setPixel(x, y, 1, 0, 0, 1) -- red + else + img4x4:setPixel(x, y, 0, 0, 0, 1) -- black + end + end + end + + -- Scale only the top-left 2x2 red quadrant to 4x4 + local scaled = FlexLove.ImageScaler.scaleBilinear(img4x4, 0, 0, 2, 2, 4, 4) + + luaunit.assertEquals(scaled:getWidth(), 4) + luaunit.assertEquals(scaled:getHeight(), 4) + + -- All pixels should be red (from source quadrant) + for y = 0, 3 do + for x = 0, 3 do + local r, g, b = scaled:getPixel(x, y) + luaunit.assertAlmostEquals(r, 1, 0.01) + luaunit.assertAlmostEquals(g, 0, 0.01) + luaunit.assertAlmostEquals(b, 0, 0.01) + end + end +end + +function TestImageScalerBilinear:testEdgePixelHandling() + -- Create 3x3 checkerboard + local checkerboard = love.image.newImageData(3, 3) + for y = 0, 2 do + for x = 0, 2 do + if (x + y) % 2 == 0 then + checkerboard:setPixel(x, y, 1, 1, 1, 1) -- white + else + checkerboard:setPixel(x, y, 0, 0, 0, 1) -- black + end + end + end + + -- Scale to 9x9 + local scaled = FlexLove.ImageScaler.scaleBilinear(checkerboard, 0, 0, 3, 3, 9, 9) + + luaunit.assertEquals(scaled:getWidth(), 9) + luaunit.assertEquals(scaled:getHeight(), 9) + + -- Verify corners are correct (no out-of-bounds access) + local r, g, b = scaled:getPixel(0, 0) + luaunit.assertAlmostEquals(r, 1, 0.01) -- Top-left should be white + + r, g, b = scaled:getPixel(8, 8) + luaunit.assertAlmostEquals(r, 1, 0.01) -- Bottom-right should be white +end + +function TestImageScalerBilinear:testNonUniformScaling() + -- Scale 2x2 to 6x4 (3x horizontal, 2x vertical) + local scaled = FlexLove.ImageScaler.scaleBilinear(self.testImage2x2, 0, 0, 2, 2, 6, 4) + + luaunit.assertEquals(scaled:getWidth(), 6) + luaunit.assertEquals(scaled:getHeight(), 4) + + -- Top-left corner should be red + local r, g, b = scaled:getPixel(0, 0) + luaunit.assertAlmostEquals(r, 1, 0.01) + luaunit.assertAlmostEquals(g, 0, 0.01) + + -- Should have smooth interpolation in between + r, g, b = scaled:getPixel(2, 1) + -- Middle area should have blended colors + luaunit.assertTrue(r > 0.1, "Should have some red component") + luaunit.assertTrue(g > 0.1, "Should have some green component") + luaunit.assertTrue(b > 0.1, "Should have some blue component") +end + +function TestImageScalerBilinear:testComparison_SmootherThanNearest() + -- Create gradient + local gradient = love.image.newImageData(2, 1) + gradient:setPixel(0, 0, 0, 0, 0, 1) + gradient:setPixel(1, 0, 1, 1, 1, 1) + + local bilinear = FlexLove.ImageScaler.scaleBilinear(gradient, 0, 0, 2, 1, 8, 1) + local nearest = FlexLove.ImageScaler.scaleNearest(gradient, 0, 0, 2, 1, 8, 1) + + -- Count unique values (nearest should have fewer due to blocky nature) + local bilinearValues = {} + local nearestValues = {} + + for x = 0, 7 do + local rb = bilinear:getPixel(x, 0) + local rn = nearest:getPixel(x, 0) + bilinearValues[string.format("%.2f", rb)] = true + nearestValues[string.format("%.2f", rn)] = true + end + + local bilinearCount = 0 + for _ in pairs(bilinearValues) do bilinearCount = bilinearCount + 1 end + + local nearestCount = 0 + for _ in pairs(nearestValues) do nearestCount = nearestCount + 1 end + + -- Bilinear should have more unique values (smoother gradient) + luaunit.assertTrue(bilinearCount >= nearestCount, + "Bilinear should produce smoother gradient with more unique values") +end + +luaunit.LuaUnit.run() diff --git a/testing/loveStub.lua b/testing/loveStub.lua index 37d0971..4072771 100644 --- a/testing/loveStub.lua +++ b/testing/loveStub.lua @@ -153,5 +153,53 @@ function love_helper.touch.getPosition(id) return 0, 0 -- Default touch position end +-- Mock image functions +love_helper.image = {} + +-- Mock ImageData object +local ImageData = {} +ImageData.__index = ImageData + +function ImageData.new(width, height) + local self = setmetatable({}, ImageData) + self.width = width + self.height = height + -- Store pixel data as a 2D array [y][x] = {r, g, b, a} + self.pixels = {} + for y = 0, height - 1 do + self.pixels[y] = {} + for x = 0, width - 1 do + self.pixels[y][x] = {0, 0, 0, 0} -- Default to transparent black + end + end + return self +end + +function ImageData:getWidth() + return self.width +end + +function ImageData:getHeight() + return self.height +end + +function ImageData:setPixel(x, y, r, g, b, a) + if x >= 0 and x < self.width and y >= 0 and y < self.height then + self.pixels[y][x] = {r, g, b, a or 1} + end +end + +function ImageData:getPixel(x, y) + if x >= 0 and x < self.width and y >= 0 and y < self.height then + local pixel = self.pixels[y][x] + return pixel[1], pixel[2], pixel[3], pixel[4] + end + return 0, 0, 0, 0 +end + +function love_helper.image.newImageData(width, height) + return ImageData.new(width, height) +end + _G.love = love_helper return love_helper diff --git a/themes/metal.lua b/themes/metal.lua index e69de29..a8b1875 100644 --- a/themes/metal.lua +++ b/themes/metal.lua @@ -0,0 +1,69 @@ +local Color = require("libs.FlexLove").Color + +return { + name = "Metal Theme", + contentAutoSizingMultiplier = { width = 1.05, height = 1.1 }, + components = { + framev1 = { + atlas = "themes/metal/Frame/Frame01a.9.png", + }, + framev2 = { + atlas = "themes/metal/Frame/Frame01b.9.png", + }, + framev3 = { + atlas = "themes/metal/Frame/Frame02a.9.png", + }, + framev4 = { + atlas = "themes/metal/Frame/Frame02b.9.png", + }, + framev5 = { + atlas = "themes/metal/Frame/Frame03a.9.png", + }, + framev6 = { + atlas = "themes/metal/Frame/Frame03b.9.png", + }, + buttonv1 = { + atlas = "themes/metal/Button/Button01a_1.9.png", + states = { + hover = { + atlas = "themes/metal/Button/Button01a_4.9.png", + }, + pressed = { + atlas = "themes/metal/Button/Button01a_2.9.png", + }, + disabled = { + atlas = "themes/metal/Button/Button01a_4.9.png", + }, + }, + }, + buttonv2 = { + atlas = "themes/metal/Button/Button02a_1.9.png", + states = { + hover = { + atlas = "themes/metal/Button/Button02a_4.9.png", + }, + pressed = { + atlas = "themes/metal/Button/Button02a_2.9.png", + }, + disabled = { + atlas = "themes/metal/Button/Button02a_4.9.png", + }, + }, + }, + }, + + -- Optional: Theme colors + colors = { + primary = Color.new(), + secondary = Color.new(), + text = Color.new(), + textDark = Color.new(), + }, + + -- Optional: Theme fonts + -- Define font families that can be referenced by name + -- Paths are relative to FlexLove location or absolute + fonts = { + default = "themes/space/VT323-Regular.ttf", + }, +} diff --git a/themes/space.lua b/themes/space.lua index c5fda88..fa8b49d 100644 --- a/themes/space.lua +++ b/themes/space.lua @@ -1,16 +1,4 @@ --- Space Theme - -local Color = {} -Color.__index = Color - -function Color.new(r, g, b, a) - local self = setmetatable({}, Color) - self.r = r or 0 - self.g = g or 0 - self.b = b or 0 - self.a = a or 1 - return self -end +local Color = require("libs.FlexLove").Color return { name = "Space Theme",