continued refactor
This commit is contained in:
File diff suppressed because it is too large
Load Diff
353
modules/Blur.lua
353
modules/Blur.lua
@@ -1,99 +1,34 @@
|
|||||||
local Blur = {}
|
local Cache = {
|
||||||
|
canvases = {},
|
||||||
-- Canvas cache to avoid recreating canvases every frame
|
quads = {},
|
||||||
local canvasCache = {}
|
MAX_CANVAS_SIZE = 20,
|
||||||
local MAX_CACHE_SIZE = 20
|
MAX_QUAD_SIZE = 20,
|
||||||
|
}
|
||||||
-- Quad cache to avoid recreating quads every frame
|
|
||||||
local quadCache = {}
|
|
||||||
local MAX_QUAD_CACHE_SIZE = 20
|
|
||||||
|
|
||||||
-- Quad cache to avoid recreating quads every frame
|
|
||||||
local quadCache = {}
|
|
||||||
local MAX_QUAD_CACHE_SIZE = 20
|
|
||||||
|
|
||||||
--- Build Gaussian blur shader with given parameters
|
|
||||||
---@param taps number -- Number of samples (must be odd, >= 3)
|
|
||||||
---@param offset number
|
|
||||||
---@param offset_type string -- "weighted" or "center"
|
|
||||||
---@param sigma number
|
|
||||||
---@return love.Shader
|
|
||||||
local function buildShader(taps, offset, offset_type, sigma)
|
|
||||||
taps = math.floor(taps)
|
|
||||||
sigma = sigma >= 1 and sigma or (taps - 1) * offset / 6
|
|
||||||
sigma = math.max(sigma, 1)
|
|
||||||
|
|
||||||
local steps = (taps + 1) / 2
|
|
||||||
|
|
||||||
local g_offsets = {}
|
|
||||||
local g_weights = {}
|
|
||||||
for i = 1, steps, 1 do
|
|
||||||
g_offsets[i] = offset * (i - 1)
|
|
||||||
g_weights[i] = math.exp(-0.5 * (g_offsets[i] - 0) ^ 2 * 1 / sigma ^ 2)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Calculate offsets and weights for sub-pixel samples
|
|
||||||
local offsets = {}
|
|
||||||
local weights = {}
|
|
||||||
for i = #g_weights, 2, -2 do
|
|
||||||
local oA, oB = g_offsets[i], g_offsets[i - 1]
|
|
||||||
local wA, wB = g_weights[i], g_weights[i - 1]
|
|
||||||
wB = oB == 0 and wB / 2 or wB
|
|
||||||
local weight = wA + wB
|
|
||||||
offsets[#offsets + 1] = offset_type == "center" and (oA + oB) / 2 or (oA * wA + oB * wB) / weight
|
|
||||||
weights[#weights + 1] = weight
|
|
||||||
end
|
|
||||||
|
|
||||||
local code = { [[
|
|
||||||
extern vec2 direction;
|
|
||||||
vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) {]] }
|
|
||||||
|
|
||||||
local norm = 0
|
|
||||||
if #g_weights % 2 == 0 then
|
|
||||||
code[#code + 1] = "vec4 c = vec4( 0.0 );"
|
|
||||||
else
|
|
||||||
local weight = g_weights[1]
|
|
||||||
norm = norm + weight
|
|
||||||
code[#code + 1] = ("vec4 c = %f * texture2D(tex, tc);"):format(weight)
|
|
||||||
end
|
|
||||||
|
|
||||||
local tmpl = "c += %f * ( texture2D(tex, tc + %f * direction)+ texture2D(tex, tc - %f * direction));\n"
|
|
||||||
for i = 1, #offsets, 1 do
|
|
||||||
local offset = offsets[i]
|
|
||||||
local weight = weights[i]
|
|
||||||
norm = norm + weight * 2
|
|
||||||
code[#code + 1] = tmpl:format(weight, offset, offset)
|
|
||||||
end
|
|
||||||
code[#code + 1] = ("return c * vec4(%f) * color; }"):format(1 / norm)
|
|
||||||
|
|
||||||
local shader = table.concat(code)
|
|
||||||
return love.graphics.newShader(shader)
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Get or create a canvas from cache
|
--- Get or create a canvas from cache
|
||||||
---@param width number
|
---@param width number Canvas width
|
||||||
---@param height number
|
---@param height number Canvas height
|
||||||
---@return love.Canvas
|
---@return love.Canvas canvas The cached or new canvas
|
||||||
local function getCanvas(width, height)
|
function Cache.getCanvas(width, height)
|
||||||
local key = string.format("%dx%d", width, height)
|
local key = string.format("%dx%d", width, height)
|
||||||
|
|
||||||
if not canvasCache[key] then
|
if not Cache.canvases[key] then
|
||||||
canvasCache[key] = {}
|
Cache.canvases[key] = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
local cache = canvasCache[key]
|
local cache = Cache.canvases[key]
|
||||||
|
|
||||||
for i, canvas in ipairs(cache) do
|
for i, entry in ipairs(cache) do
|
||||||
if not canvas.inUse then
|
if not entry.inUse then
|
||||||
canvas.inUse = true
|
entry.inUse = true
|
||||||
return canvas.canvas
|
return entry.canvas
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local canvas = love.graphics.newCanvas(width, height)
|
local canvas = love.graphics.newCanvas(width, height)
|
||||||
table.insert(cache, { canvas = canvas, inUse = true })
|
table.insert(cache, { canvas = canvas, inUse = true })
|
||||||
|
|
||||||
if #cache > MAX_CACHE_SIZE then
|
if #cache > Cache.MAX_CANVAS_SIZE then
|
||||||
table.remove(cache, 1)
|
table.remove(cache, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -101,9 +36,9 @@ local function getCanvas(width, height)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Release a canvas back to the cache
|
--- Release a canvas back to the cache
|
||||||
---@param canvas love.Canvas
|
---@param canvas love.Canvas Canvas to release
|
||||||
local function releaseCanvas(canvas)
|
function Cache.releaseCanvas(canvas)
|
||||||
for _, sizeCache in pairs(canvasCache) do
|
for _, sizeCache in pairs(Cache.canvases) do
|
||||||
for _, entry in ipairs(sizeCache) do
|
for _, entry in ipairs(sizeCache) do
|
||||||
if entry.canvas == canvas then
|
if entry.canvas == canvas then
|
||||||
entry.inUse = false
|
entry.inUse = false
|
||||||
@@ -114,21 +49,21 @@ local function releaseCanvas(canvas)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Get or create a quad from cache
|
--- Get or create a quad from cache
|
||||||
---@param x number
|
---@param x number X position
|
||||||
---@param y number
|
---@param y number Y position
|
||||||
---@param width number
|
---@param width number Quad width
|
||||||
---@param height number
|
---@param height number Quad height
|
||||||
---@param sw number -- Source width
|
---@param sw number Source width
|
||||||
---@param sh number -- Source height
|
---@param sh number Source height
|
||||||
---@return love.Quad
|
---@return love.Quad quad The cached or new quad
|
||||||
local function getQuad(x, y, width, height, sw, sh)
|
function Cache.getQuad(x, y, width, height, sw, sh)
|
||||||
local key = string.format("%d,%d,%d,%d,%d,%d", x, y, width, height, sw, sh)
|
local key = string.format("%d,%d,%d,%d,%d,%d", x, y, width, height, sw, sh)
|
||||||
|
|
||||||
if not quadCache[key] then
|
if not Cache.quads[key] then
|
||||||
quadCache[key] = {}
|
Cache.quads[key] = {}
|
||||||
end
|
end
|
||||||
|
|
||||||
local cache = quadCache[key]
|
local cache = Cache.quads[key]
|
||||||
|
|
||||||
for i, entry in ipairs(cache) do
|
for i, entry in ipairs(cache) do
|
||||||
if not entry.inUse then
|
if not entry.inUse then
|
||||||
@@ -140,7 +75,7 @@ local function getQuad(x, y, width, height, sw, sh)
|
|||||||
local quad = love.graphics.newQuad(x, y, width, height, sw, sh)
|
local quad = love.graphics.newQuad(x, y, width, height, sw, sh)
|
||||||
table.insert(cache, { quad = quad, inUse = true })
|
table.insert(cache, { quad = quad, inUse = true })
|
||||||
|
|
||||||
if #cache > MAX_QUAD_CACHE_SIZE then
|
if #cache > Cache.MAX_QUAD_SIZE then
|
||||||
table.remove(cache, 1)
|
table.remove(cache, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -148,9 +83,9 @@ local function getQuad(x, y, width, height, sw, sh)
|
|||||||
end
|
end
|
||||||
|
|
||||||
--- Release a quad back to the cache
|
--- Release a quad back to the cache
|
||||||
---@param quad love.Quad
|
---@param quad love.Quad Quad to release
|
||||||
local function releaseQuad(quad)
|
function Cache.releaseQuad(quad)
|
||||||
for _, keyCache in pairs(quadCache) do
|
for _, keyCache in pairs(Cache.quads) do
|
||||||
for _, entry in ipairs(keyCache) do
|
for _, entry in ipairs(keyCache) do
|
||||||
if entry.quad == quad then
|
if entry.quad == quad then
|
||||||
entry.inUse = false
|
entry.inUse = false
|
||||||
@@ -160,11 +95,96 @@ local function releaseQuad(quad)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Create a blur effect instance
|
--- Clear all caches
|
||||||
---@param quality number -- Quality level (1-10, higher = better quality but slower)
|
function Cache.clear()
|
||||||
---@return table -- Blur effect instance
|
Cache.canvases = {}
|
||||||
function Blur.new(quality)
|
Cache.quads = {}
|
||||||
quality = math.max(1, math.min(10, quality or 5))
|
end
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- SHADER BUILDER
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
local ShaderBuilder = {}
|
||||||
|
|
||||||
|
--- Build Gaussian blur shader with given parameters
|
||||||
|
---@param taps number Number of samples (must be odd, >= 3)
|
||||||
|
---@param offset number Offset value
|
||||||
|
---@param offsetType string "weighted" or "center"
|
||||||
|
---@param sigma number Sigma value for Gaussian distribution
|
||||||
|
---@return love.Shader shader The compiled blur shader
|
||||||
|
function ShaderBuilder.build(taps, offset, offsetType, sigma)
|
||||||
|
taps = math.floor(taps)
|
||||||
|
sigma = sigma >= 1 and sigma or (taps - 1) * offset / 6
|
||||||
|
sigma = math.max(sigma, 1)
|
||||||
|
|
||||||
|
local steps = (taps + 1) / 2
|
||||||
|
|
||||||
|
local gOffsets = {}
|
||||||
|
local gWeights = {}
|
||||||
|
for i = 1, steps do
|
||||||
|
gOffsets[i] = offset * (i - 1)
|
||||||
|
gWeights[i] = math.exp(-0.5 * (gOffsets[i] - 0) ^ 2 * 1 / sigma ^ 2)
|
||||||
|
end
|
||||||
|
|
||||||
|
local offsets = {}
|
||||||
|
local weights = {}
|
||||||
|
for i = #gWeights, 2, -2 do
|
||||||
|
local oA, oB = gOffsets[i], gOffsets[i - 1]
|
||||||
|
local wA, wB = gWeights[i], gWeights[i - 1]
|
||||||
|
wB = oB == 0 and wB / 2 or wB
|
||||||
|
local weight = wA + wB
|
||||||
|
offsets[#offsets + 1] = offsetType == "center" and (oA + oB) / 2 or (oA * wA + oB * wB) / weight
|
||||||
|
weights[#weights + 1] = weight
|
||||||
|
end
|
||||||
|
|
||||||
|
local code = {
|
||||||
|
[[
|
||||||
|
extern vec2 direction;
|
||||||
|
vec4 effect(vec4 color, Image tex, vec2 tc, vec2 sc) {]],
|
||||||
|
}
|
||||||
|
|
||||||
|
local norm = 0
|
||||||
|
if #gWeights % 2 == 0 then
|
||||||
|
code[#code + 1] = "vec4 c = vec4( 0.0 );"
|
||||||
|
else
|
||||||
|
local weight = gWeights[1]
|
||||||
|
norm = norm + weight
|
||||||
|
code[#code + 1] = string.format("vec4 c = %f * texture2D(tex, tc);", weight)
|
||||||
|
end
|
||||||
|
|
||||||
|
local template = "c += %f * ( texture2D(tex, tc + %f * direction)+ texture2D(tex, tc - %f * direction));\n"
|
||||||
|
for i = 1, #offsets do
|
||||||
|
local offset = offsets[i]
|
||||||
|
local weight = weights[i]
|
||||||
|
norm = norm + weight * 2
|
||||||
|
code[#code + 1] = string.format(template, weight, offset, offset)
|
||||||
|
end
|
||||||
|
code[#code + 1] = string.format("return c * vec4(%f) * color; }", 1 / norm)
|
||||||
|
|
||||||
|
local shaderCode = table.concat(code)
|
||||||
|
return love.graphics.newShader(shaderCode)
|
||||||
|
end
|
||||||
|
|
||||||
|
---@class BlurProps
|
||||||
|
---@field quality number? Quality level (1-10, default: 5)
|
||||||
|
|
||||||
|
---@class Blur
|
||||||
|
---@field shader love.Shader The blur shader
|
||||||
|
---@field quality number Quality level (1-10)
|
||||||
|
---@field taps number Number of shader taps
|
||||||
|
---@field _ErrorHandler table? Reference to ErrorHandler module
|
||||||
|
local Blur = {}
|
||||||
|
Blur.__index = Blur
|
||||||
|
|
||||||
|
--- Create a new blur effect instance
|
||||||
|
---@param props BlurProps? Blur configuration
|
||||||
|
---@return Blur blur The new blur instance
|
||||||
|
function Blur.new(props)
|
||||||
|
props = props or {}
|
||||||
|
|
||||||
|
local quality = props.quality or 5
|
||||||
|
quality = math.max(1, math.min(10, quality))
|
||||||
|
|
||||||
-- Map quality to shader parameters
|
-- Map quality to shader parameters
|
||||||
-- Quality 1: 3 taps (fastest, lowest quality)
|
-- Quality 1: 3 taps (fastest, lowest quality)
|
||||||
@@ -173,33 +193,38 @@ function Blur.new(quality)
|
|||||||
local taps = 3 + (quality - 1) * 1.5
|
local taps = 3 + (quality - 1) * 1.5
|
||||||
taps = math.floor(taps)
|
taps = math.floor(taps)
|
||||||
if taps % 2 == 0 then
|
if taps % 2 == 0 then
|
||||||
taps = taps + 1 -- Ensure odd number
|
taps = taps + 1
|
||||||
end
|
end
|
||||||
|
|
||||||
local offset = 1.0
|
local offset = 1.0
|
||||||
local offset_type = "weighted"
|
local offsetType = "weighted"
|
||||||
local sigma = -1
|
local sigma = -1
|
||||||
|
|
||||||
local shader = buildShader(taps, offset, offset_type, sigma)
|
local shader = ShaderBuilder.build(taps, offset, offsetType, sigma)
|
||||||
|
|
||||||
local instance = {
|
local self = setmetatable({}, Blur)
|
||||||
shader = shader,
|
self.shader = shader
|
||||||
quality = quality,
|
self.quality = quality
|
||||||
taps = taps,
|
self.taps = taps
|
||||||
}
|
|
||||||
|
|
||||||
return instance
|
return self
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Apply blur to a region of the screen
|
--- Apply blur to a region of the screen
|
||||||
---@param blurInstance table -- Blur effect instance from Blur.new()
|
---@param intensity number Blur intensity (0-100)
|
||||||
---@param intensity number -- Blur intensity (0-100)
|
---@param x number X position
|
||||||
---@param x number -- X position
|
---@param y number Y position
|
||||||
---@param y number -- Y position
|
---@param width number Width of region
|
||||||
---@param width number -- Width
|
---@param height number Height of region
|
||||||
---@param height number -- Height
|
---@param drawFunc function Function to draw content to be blurred
|
||||||
---@param drawFunc function -- Function to draw content to be blurred
|
function Blur:applyToRegion(intensity, x, y, width, height, drawFunc)
|
||||||
function Blur.applyToRegion(blurInstance, intensity, x, y, width, height, drawFunc)
|
if type(drawFunc) ~= "function" then
|
||||||
|
if Blur._ErrorHandler then
|
||||||
|
Blur._ErrorHandler:warn("Blur", "applyToRegion requires a draw function.")
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if intensity <= 0 or width <= 0 or height <= 0 then
|
if intensity <= 0 or width <= 0 or height <= 0 then
|
||||||
drawFunc()
|
drawFunc()
|
||||||
return
|
return
|
||||||
@@ -211,8 +236,8 @@ function Blur.applyToRegion(blurInstance, intensity, x, y, width, height, drawFu
|
|||||||
local passes = math.ceil(intensity / 20)
|
local passes = math.ceil(intensity / 20)
|
||||||
passes = math.max(1, math.min(5, passes))
|
passes = math.max(1, math.min(5, passes))
|
||||||
|
|
||||||
local canvas1 = getCanvas(width, height)
|
local canvas1 = Cache.getCanvas(width, height)
|
||||||
local canvas2 = getCanvas(width, height)
|
local canvas2 = Cache.getCanvas(width, height)
|
||||||
|
|
||||||
local prevCanvas = love.graphics.getCanvas()
|
local prevCanvas = love.graphics.getCanvas()
|
||||||
local prevShader = love.graphics.getShader()
|
local prevShader = love.graphics.getShader()
|
||||||
@@ -227,19 +252,19 @@ function Blur.applyToRegion(blurInstance, intensity, x, y, width, height, drawFu
|
|||||||
drawFunc()
|
drawFunc()
|
||||||
love.graphics.pop()
|
love.graphics.pop()
|
||||||
|
|
||||||
love.graphics.setShader(blurInstance.shader)
|
love.graphics.setShader(self.shader)
|
||||||
love.graphics.setColor(1, 1, 1, 1)
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
love.graphics.setBlendMode("alpha", "premultiplied")
|
love.graphics.setBlendMode("alpha", "premultiplied")
|
||||||
|
|
||||||
for i = 1, passes do
|
for i = 1, passes do
|
||||||
love.graphics.setCanvas(canvas2)
|
love.graphics.setCanvas(canvas2)
|
||||||
love.graphics.clear()
|
love.graphics.clear()
|
||||||
blurInstance.shader:send("direction", { 1 / width, 0 })
|
self.shader:send("direction", { 1 / width, 0 })
|
||||||
love.graphics.draw(canvas1, 0, 0)
|
love.graphics.draw(canvas1, 0, 0)
|
||||||
|
|
||||||
love.graphics.setCanvas(canvas1)
|
love.graphics.setCanvas(canvas1)
|
||||||
love.graphics.clear()
|
love.graphics.clear()
|
||||||
blurInstance.shader:send("direction", { 0, 1 / height })
|
self.shader:send("direction", { 0, 1 / height })
|
||||||
love.graphics.draw(canvas2, 0, 0)
|
love.graphics.draw(canvas2, 0, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -251,19 +276,25 @@ function Blur.applyToRegion(blurInstance, intensity, x, y, width, height, drawFu
|
|||||||
love.graphics.setShader(prevShader)
|
love.graphics.setShader(prevShader)
|
||||||
love.graphics.setColor(unpack(prevColor))
|
love.graphics.setColor(unpack(prevColor))
|
||||||
|
|
||||||
releaseCanvas(canvas1)
|
Cache.releaseCanvas(canvas1)
|
||||||
releaseCanvas(canvas2)
|
Cache.releaseCanvas(canvas2)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Apply backdrop blur effect (blur content behind a region)
|
--- Apply backdrop blur effect (blur content behind a region)
|
||||||
---@param blurInstance table -- Blur effect instance from Blur.new()
|
---@param intensity number Blur intensity (0-100)
|
||||||
---@param intensity number -- Blur intensity (0-100)
|
---@param x number X position
|
||||||
---@param x number -- X position
|
---@param y number Y position
|
||||||
---@param y number -- Y position
|
---@param width number Width of region
|
||||||
---@param width number -- Width
|
---@param height number Height of region
|
||||||
---@param height number -- Height
|
---@param backdropCanvas love.Canvas Canvas containing the backdrop content
|
||||||
---@param backdropCanvas love.Canvas -- Canvas containing the backdrop content
|
function Blur:applyBackdrop(intensity, x, y, width, height, backdropCanvas)
|
||||||
function Blur.applyBackdrop(blurInstance, intensity, x, y, width, height, backdropCanvas)
|
if not backdropCanvas then
|
||||||
|
if Blur._ErrorHandler then
|
||||||
|
Blur._ErrorHandler:warn("Blur", "applyBackdrop requires a backdrop canvas.")
|
||||||
|
end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
if intensity <= 0 or width <= 0 or height <= 0 then
|
if intensity <= 0 or width <= 0 or height <= 0 then
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -273,8 +304,8 @@ function Blur.applyBackdrop(blurInstance, intensity, x, y, width, height, backdr
|
|||||||
local passes = math.ceil(intensity / 20)
|
local passes = math.ceil(intensity / 20)
|
||||||
passes = math.max(1, math.min(5, passes))
|
passes = math.max(1, math.min(5, passes))
|
||||||
|
|
||||||
local canvas1 = getCanvas(width, height)
|
local canvas1 = Cache.getCanvas(width, height)
|
||||||
local canvas2 = getCanvas(width, height)
|
local canvas2 = Cache.getCanvas(width, height)
|
||||||
|
|
||||||
local prevCanvas = love.graphics.getCanvas()
|
local prevCanvas = love.graphics.getCanvas()
|
||||||
local prevShader = love.graphics.getShader()
|
local prevShader = love.graphics.getShader()
|
||||||
@@ -287,20 +318,20 @@ function Blur.applyBackdrop(blurInstance, intensity, x, y, width, height, backdr
|
|||||||
love.graphics.setBlendMode("alpha", "premultiplied")
|
love.graphics.setBlendMode("alpha", "premultiplied")
|
||||||
|
|
||||||
local backdropWidth, backdropHeight = backdropCanvas:getDimensions()
|
local backdropWidth, backdropHeight = backdropCanvas:getDimensions()
|
||||||
local quad = getQuad(x, y, width, height, backdropWidth, backdropHeight)
|
local quad = Cache.getQuad(x, y, width, height, backdropWidth, backdropHeight)
|
||||||
love.graphics.draw(backdropCanvas, quad, 0, 0)
|
love.graphics.draw(backdropCanvas, quad, 0, 0)
|
||||||
|
|
||||||
love.graphics.setShader(blurInstance.shader)
|
love.graphics.setShader(self.shader)
|
||||||
|
|
||||||
for i = 1, passes do
|
for i = 1, passes do
|
||||||
love.graphics.setCanvas(canvas2)
|
love.graphics.setCanvas(canvas2)
|
||||||
love.graphics.clear()
|
love.graphics.clear()
|
||||||
blurInstance.shader:send("direction", { 1 / width, 0 })
|
self.shader:send("direction", { 1 / width, 0 })
|
||||||
love.graphics.draw(canvas1, 0, 0)
|
love.graphics.draw(canvas1, 0, 0)
|
||||||
|
|
||||||
love.graphics.setCanvas(canvas1)
|
love.graphics.setCanvas(canvas1)
|
||||||
love.graphics.clear()
|
love.graphics.clear()
|
||||||
blurInstance.shader:send("direction", { 0, 1 / height })
|
self.shader:send("direction", { 0, 1 / height })
|
||||||
love.graphics.draw(canvas2, 0, 0)
|
love.graphics.draw(canvas2, 0, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -312,15 +343,35 @@ function Blur.applyBackdrop(blurInstance, intensity, x, y, width, height, backdr
|
|||||||
love.graphics.setShader(prevShader)
|
love.graphics.setShader(prevShader)
|
||||||
love.graphics.setColor(unpack(prevColor))
|
love.graphics.setColor(unpack(prevColor))
|
||||||
|
|
||||||
releaseCanvas(canvas1)
|
Cache.releaseCanvas(canvas1)
|
||||||
releaseCanvas(canvas2)
|
Cache.releaseCanvas(canvas2)
|
||||||
releaseQuad(quad)
|
Cache.releaseQuad(quad)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Clear canvas cache (call on window resize)
|
--- Get the current quality level
|
||||||
function Blur.clearCache()
|
---@return number quality Quality level (1-10)
|
||||||
canvasCache = {}
|
function Blur:getQuality()
|
||||||
quadCache = {}
|
return self.quality
|
||||||
end
|
end
|
||||||
|
|
||||||
|
--- Get the number of shader taps
|
||||||
|
---@return number taps Number of shader taps
|
||||||
|
function Blur:getTaps()
|
||||||
|
return self.taps
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Clear all caches (call on window resize or memory cleanup)
|
||||||
|
function Blur.clearCache()
|
||||||
|
Cache.clear()
|
||||||
|
end
|
||||||
|
|
||||||
|
--- Initialize Blur module with dependencies
|
||||||
|
---@param deps table Dependencies: { ErrorHandler = ErrorHandler? }
|
||||||
|
function Blur.init(deps)
|
||||||
|
Blur._ErrorHandler = deps.ErrorHandler
|
||||||
|
end
|
||||||
|
|
||||||
|
Blur.Cache = Cache
|
||||||
|
Blur.ShaderBuilder = ShaderBuilder
|
||||||
|
|
||||||
return Blur
|
return Blur
|
||||||
|
|||||||
@@ -1,36 +1,3 @@
|
|||||||
local ErrorHandler = nil
|
|
||||||
|
|
||||||
-- Named colors (CSS3 color names)
|
|
||||||
local NAMED_COLORS = {
|
|
||||||
-- Basic colors
|
|
||||||
black = { 0, 0, 0, 1 },
|
|
||||||
white = { 1, 1, 1, 1 },
|
|
||||||
red = { 1, 0, 0, 1 },
|
|
||||||
green = { 0, 0.502, 0, 1 },
|
|
||||||
blue = { 0, 0, 1, 1 },
|
|
||||||
yellow = { 1, 1, 0, 1 },
|
|
||||||
cyan = { 0, 1, 1, 1 },
|
|
||||||
magenta = { 1, 0, 1, 1 },
|
|
||||||
|
|
||||||
-- Extended colors
|
|
||||||
gray = { 0.502, 0.502, 0.502, 1 },
|
|
||||||
grey = { 0.502, 0.502, 0.502, 1 },
|
|
||||||
silver = { 0.753, 0.753, 0.753, 1 },
|
|
||||||
maroon = { 0.502, 0, 0, 1 },
|
|
||||||
olive = { 0.502, 0.502, 0, 1 },
|
|
||||||
lime = { 0, 1, 0, 1 },
|
|
||||||
aqua = { 0, 1, 1, 1 },
|
|
||||||
teal = { 0, 0.502, 0.502, 1 },
|
|
||||||
navy = { 0, 0, 0.502, 1 },
|
|
||||||
fuchsia = { 1, 0, 1, 1 },
|
|
||||||
purple = { 0.502, 0, 0.502, 1 },
|
|
||||||
orange = { 1, 0.647, 0, 1 },
|
|
||||||
pink = { 1, 0.753, 0.796, 1 },
|
|
||||||
brown = { 0.647, 0.165, 0.165, 1 },
|
|
||||||
transparent = { 0, 0, 0, 0 },
|
|
||||||
}
|
|
||||||
|
|
||||||
--- Utility class for color handling
|
|
||||||
---@class Color
|
---@class Color
|
||||||
---@field r number -- Red component (0-1)
|
---@field r number -- Red component (0-1)
|
||||||
---@field g number -- Green component (0-1)
|
---@field g number -- Green component (0-1)
|
||||||
@@ -79,13 +46,11 @@ end
|
|||||||
function Color.fromHex(hexWithTag)
|
function Color.fromHex(hexWithTag)
|
||||||
-- Validate input type
|
-- Validate input type
|
||||||
if type(hexWithTag) ~= "string" then
|
if type(hexWithTag) ~= "string" then
|
||||||
if ErrorHandler then
|
Color._ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
|
||||||
ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
|
input = tostring(hexWithTag),
|
||||||
input = tostring(hexWithTag),
|
issue = "not a string",
|
||||||
issue = "not a string",
|
fallback = "white (#FFFFFF)",
|
||||||
fallback = "white (#FFFFFF)",
|
})
|
||||||
})
|
|
||||||
end
|
|
||||||
return Color.new(1, 1, 1, 1)
|
return Color.new(1, 1, 1, 1)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -95,13 +60,11 @@ function Color.fromHex(hexWithTag)
|
|||||||
local g = tonumber("0x" .. hex:sub(3, 4))
|
local g = tonumber("0x" .. hex:sub(3, 4))
|
||||||
local b = tonumber("0x" .. hex:sub(5, 6))
|
local b = tonumber("0x" .. hex:sub(5, 6))
|
||||||
if not r or not g or not b then
|
if not r or not g or not b then
|
||||||
if ErrorHandler then
|
Color._ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
|
||||||
ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
|
input = hexWithTag,
|
||||||
input = hexWithTag,
|
issue = "invalid hex digits",
|
||||||
issue = "invalid hex digits",
|
fallback = "white (#FFFFFF)",
|
||||||
fallback = "white (#FFFFFF)",
|
})
|
||||||
})
|
|
||||||
end
|
|
||||||
return Color.new(1, 1, 1, 1) -- Return white as fallback
|
return Color.new(1, 1, 1, 1) -- Return white as fallback
|
||||||
end
|
end
|
||||||
return Color.new(r / 255, g / 255, b / 255, 1)
|
return Color.new(r / 255, g / 255, b / 255, 1)
|
||||||
@@ -111,25 +74,21 @@ function Color.fromHex(hexWithTag)
|
|||||||
local b = tonumber("0x" .. hex:sub(5, 6))
|
local b = tonumber("0x" .. hex:sub(5, 6))
|
||||||
local a = tonumber("0x" .. hex:sub(7, 8))
|
local a = tonumber("0x" .. hex:sub(7, 8))
|
||||||
if not r or not g or not b or not a then
|
if not r or not g or not b or not a then
|
||||||
if ErrorHandler then
|
Color._ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
|
||||||
ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
|
input = hexWithTag,
|
||||||
input = hexWithTag,
|
issue = "invalid hex digits",
|
||||||
issue = "invalid hex digits",
|
fallback = "white (#FFFFFFFF)",
|
||||||
fallback = "white (#FFFFFFFF)",
|
})
|
||||||
})
|
|
||||||
end
|
|
||||||
return Color.new(1, 1, 1, 1) -- Return white as fallback
|
return Color.new(1, 1, 1, 1) -- Return white as fallback
|
||||||
end
|
end
|
||||||
return Color.new(r / 255, g / 255, b / 255, a / 255)
|
return Color.new(r / 255, g / 255, b / 255, a / 255)
|
||||||
else
|
else
|
||||||
if ErrorHandler then
|
Color._ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
|
||||||
ErrorHandler.warn("Color", "VAL_004", "Invalid color format", {
|
input = hexWithTag,
|
||||||
input = hexWithTag,
|
expected = "#RRGGBB or #RRGGBBAA",
|
||||||
expected = "#RRGGBB or #RRGGBBAA",
|
hexLength = #hex,
|
||||||
hexLength = #hex,
|
fallback = "white (#FFFFFF)",
|
||||||
fallback = "white (#FFFFFF)",
|
})
|
||||||
})
|
|
||||||
end
|
|
||||||
return Color.new(1, 1, 1, 1) -- Return white as fallback
|
return Color.new(1, 1, 1, 1) -- Return white as fallback
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -227,23 +186,6 @@ function Color.validateRGBColor(r, g, b, a, max)
|
|||||||
return true, nil
|
return true, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Validate named color
|
|
||||||
---@param name string Color name (e.g. "red", "blue", "transparent")
|
|
||||||
---@return boolean valid True if valid
|
|
||||||
---@return string? error Error message if invalid, nil if valid
|
|
||||||
function Color.validateNamedColor(name)
|
|
||||||
if type(name) ~= "string" then
|
|
||||||
return false, "Color name must be a string"
|
|
||||||
end
|
|
||||||
|
|
||||||
local lowerName = name:lower()
|
|
||||||
if not NAMED_COLORS[lowerName] then
|
|
||||||
return false, string.format("Unknown color name: '%s'", name)
|
|
||||||
end
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Check if a value is a valid color format
|
--- Check if a value is a valid color format
|
||||||
---@param value any Value to check
|
---@param value any Value to check
|
||||||
---@return string? format Format type ("hex", "named", "table"), nil if invalid
|
---@return string? format Format type ("hex", "named", "table"), nil if invalid
|
||||||
@@ -259,11 +201,6 @@ function Color.isValidColorFormat(value)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Check for named color
|
|
||||||
if NAMED_COLORS[value:lower()] then
|
|
||||||
return "named"
|
|
||||||
end
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -296,42 +233,6 @@ function Color.isValidColorFormat(value)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Check if a color value is usable before processing to provide clear error messages
|
|
||||||
--- Use this for config validation and debugging malformed color data
|
|
||||||
---@param value any Color value to validate
|
|
||||||
---@param options table? Validation options {allowNamed: boolean, requireAlpha: boolean}
|
|
||||||
---@return boolean valid True if valid
|
|
||||||
---@return string? error Error message if invalid, nil if valid
|
|
||||||
function Color.validateColor(value, options)
|
|
||||||
options = options or {}
|
|
||||||
local allowNamed = options.allowNamed ~= false
|
|
||||||
local requireAlpha = options.requireAlpha or false
|
|
||||||
|
|
||||||
if value == nil then
|
|
||||||
return false, "Color value is nil"
|
|
||||||
end
|
|
||||||
|
|
||||||
local format = Color.isValidColorFormat(value)
|
|
||||||
|
|
||||||
if not format then
|
|
||||||
return false, string.format("Invalid color format: %s", tostring(value))
|
|
||||||
end
|
|
||||||
|
|
||||||
if format == "named" and not allowNamed then
|
|
||||||
return false, "Named colors not allowed"
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Additional validation for alpha requirement
|
|
||||||
if requireAlpha and format == "hex" then
|
|
||||||
local cleanHex = value:gsub("^#", "")
|
|
||||||
if #cleanHex ~= 8 then
|
|
||||||
return false, "Alpha channel required (use 8-digit hex)"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
end
|
|
||||||
|
|
||||||
--- Convert any color format to a valid Color object with graceful fallbacks
|
--- Convert any color format to a valid Color object with graceful fallbacks
|
||||||
--- Use this to robustly handle colors from any source without crashes
|
--- Use this to robustly handle colors from any source without crashes
|
||||||
---@param value any Color value to sanitize (hex, named, table, or Color instance)
|
---@param value any Color value to sanitize (hex, named, table, or Color instance)
|
||||||
@@ -364,17 +265,6 @@ function Color.sanitizeColor(value, default)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Handle named format
|
|
||||||
if format == "named" then
|
|
||||||
local lowerName = value:lower()
|
|
||||||
local rgba = NAMED_COLORS[lowerName]
|
|
||||||
if rgba then
|
|
||||||
return Color.new(rgba[1], rgba[2], rgba[3], rgba[4])
|
|
||||||
end
|
|
||||||
return default
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Handle table format
|
|
||||||
if format == "table" then
|
if format == "table" then
|
||||||
-- Color instance
|
-- Color instance
|
||||||
if getmetatable(value) == Color then
|
if getmetatable(value) == Color then
|
||||||
@@ -450,9 +340,7 @@ end
|
|||||||
--- Initialize dependencies
|
--- Initialize dependencies
|
||||||
---@param deps table Dependencies: { ErrorHandler = ErrorHandler }
|
---@param deps table Dependencies: { ErrorHandler = ErrorHandler }
|
||||||
function Color.init(deps)
|
function Color.init(deps)
|
||||||
if type(deps) == "table" then
|
Color._ErrorHandler = deps.ErrorHandler
|
||||||
ErrorHandler = deps.ErrorHandler
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return Color
|
return Color
|
||||||
|
|||||||
@@ -1,26 +1,14 @@
|
|||||||
---@class Context
|
---@class Context
|
||||||
local Context = {
|
local Context = {
|
||||||
-- Top-level elements
|
|
||||||
topElements = {},
|
topElements = {},
|
||||||
|
|
||||||
-- Base scale configuration
|
-- Base scale configuration
|
||||||
baseScale = nil, -- {width: number, height: number}
|
baseScale = nil, -- {width: number, height: number}
|
||||||
|
|
||||||
-- Current scale factors
|
-- Current scale factors
|
||||||
scaleFactors = { x = 1.0, y = 1.0 },
|
scaleFactors = { x = 1.0, y = 1.0 },
|
||||||
|
|
||||||
-- Default theme name
|
|
||||||
defaultTheme = nil,
|
defaultTheme = nil,
|
||||||
|
|
||||||
-- Currently focused element (for keyboard input)
|
|
||||||
_focusedElement = nil,
|
_focusedElement = nil,
|
||||||
|
|
||||||
-- Active event element (for current frame)
|
|
||||||
_activeEventElement = nil,
|
_activeEventElement = nil,
|
||||||
|
|
||||||
-- Cached viewport dimensions
|
|
||||||
_cachedViewport = { width = 0, height = 0 },
|
_cachedViewport = { width = 0, height = 0 },
|
||||||
|
|
||||||
-- Immediate mode state
|
-- Immediate mode state
|
||||||
_immediateMode = false,
|
_immediateMode = false,
|
||||||
_frameNumber = 0,
|
_frameNumber = 0,
|
||||||
@@ -28,12 +16,10 @@ local Context = {
|
|||||||
_immediateModeState = nil, -- Will be initialized if immediate mode is enabled
|
_immediateModeState = nil, -- Will be initialized if immediate mode is enabled
|
||||||
_frameStarted = false,
|
_frameStarted = false,
|
||||||
_autoBeganFrame = false,
|
_autoBeganFrame = false,
|
||||||
|
|
||||||
-- Z-index ordered element tracking for immediate mode
|
-- Z-index ordered element tracking for immediate mode
|
||||||
_zIndexOrderedElements = {}, -- Array of elements sorted by z-index (lowest to highest)
|
_zIndexOrderedElements = {}, -- Array of elements sorted by z-index (lowest to highest)
|
||||||
}
|
}
|
||||||
|
|
||||||
--- Get current scale factors
|
|
||||||
---@return number, number -- scaleX, scaleY
|
---@return number, number -- scaleX, scaleY
|
||||||
function Context.getScaleFactors()
|
function Context.getScaleFactors()
|
||||||
return Context.scaleFactors.x, Context.scaleFactors.y
|
return Context.scaleFactors.x, Context.scaleFactors.y
|
||||||
@@ -49,7 +35,6 @@ function Context.registerElement(element)
|
|||||||
table.insert(Context._zIndexOrderedElements, element)
|
table.insert(Context._zIndexOrderedElements, element)
|
||||||
end
|
end
|
||||||
|
|
||||||
--- Clear frame elements (called at start of each immediate mode frame)
|
|
||||||
function Context.clearFrameElements()
|
function Context.clearFrameElements()
|
||||||
Context._zIndexOrderedElements = {}
|
Context._zIndexOrderedElements = {}
|
||||||
end
|
end
|
||||||
@@ -88,7 +73,7 @@ local function isPointInElement(element, x, y)
|
|||||||
-- Calculate scroll offset from parent chain
|
-- Calculate scroll offset from parent chain
|
||||||
local scrollOffsetX = 0
|
local scrollOffsetX = 0
|
||||||
local scrollOffsetY = 0
|
local scrollOffsetY = 0
|
||||||
|
|
||||||
-- Walk up parent chain to check clipping and accumulate scroll offsets
|
-- Walk up parent chain to check clipping and accumulate scroll offsets
|
||||||
local current = element.parent
|
local current = element.parent
|
||||||
while current do
|
while current do
|
||||||
@@ -105,7 +90,7 @@ local function isPointInElement(element, x, y)
|
|||||||
if x < parentX or x > parentX + parentW or y < parentY or y > parentY + parentH then
|
if x < parentX or x > parentX + parentW or y < parentY or y > parentY + parentH then
|
||||||
return false -- Point is clipped by parent
|
return false -- Point is clipped by parent
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Accumulate scroll offset
|
-- Accumulate scroll offset
|
||||||
scrollOffsetX = scrollOffsetX + (current._scrollX or 0)
|
scrollOffsetX = scrollOffsetX + (current._scrollX or 0)
|
||||||
scrollOffsetY = scrollOffsetY + (current._scrollY or 0)
|
scrollOffsetY = scrollOffsetY + (current._scrollY or 0)
|
||||||
@@ -143,7 +128,6 @@ function Context.getTopElementAt(x, y)
|
|||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Traverse from highest to lowest z-index (reverse order)
|
|
||||||
for i = #Context._zIndexOrderedElements, 1, -1 do
|
for i = #Context._zIndexOrderedElements, 1, -1 do
|
||||||
local element = Context._zIndexOrderedElements[i]
|
local element = Context._zIndexOrderedElements[i]
|
||||||
|
|
||||||
@@ -152,7 +136,6 @@ function Context.getTopElementAt(x, y)
|
|||||||
if interactive then
|
if interactive then
|
||||||
return interactive
|
return interactive
|
||||||
end
|
end
|
||||||
-- This preserves backward compatibility for non-interactive overlays
|
|
||||||
return element
|
return element
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ function EventHandler:processMouseEvents(mx, my, isHovering, isActiveElement)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Stop performance timing
|
-- Stop performance timing
|
||||||
if Performance and Performance.isEnabled() then
|
if Performance and Performance.isEnabled() then
|
||||||
Performance.stopTimer("event_mouse")
|
Performance.stopTimer("event_mouse")
|
||||||
@@ -452,10 +452,10 @@ function EventHandler:processTouchEvents()
|
|||||||
local touchId = tostring(id)
|
local touchId = tostring(id)
|
||||||
local tx, ty = love.touch.getPosition(id)
|
local tx, ty = love.touch.getPosition(id)
|
||||||
local pressure = 1.0 -- LÖVE doesn't provide pressure by default
|
local pressure = 1.0 -- LÖVE doesn't provide pressure by default
|
||||||
|
|
||||||
-- Check if touch is within element bounds
|
-- Check if touch is within element bounds
|
||||||
local isInside = tx >= bx and tx <= bx + bw and ty >= by and ty <= by + bh
|
local isInside = tx >= bx and tx <= bx + bw and ty >= by and ty <= by + bh
|
||||||
|
|
||||||
if isInside then
|
if isInside then
|
||||||
if not self._touches[touchId] then
|
if not self._touches[touchId] then
|
||||||
-- New touch began
|
-- New touch began
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -254,35 +254,6 @@ function TestEasing:testElasticFactory()
|
|||||||
luaunit.assertAlmostEquals(customElastic(1), 1, 0.01)
|
luaunit.assertAlmostEquals(customElastic(1), 1, 0.01)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Test Easing.list() method
|
|
||||||
function TestEasing:testList()
|
|
||||||
local list = Easing.list()
|
|
||||||
luaunit.assertEquals(type(list), "table")
|
|
||||||
luaunit.assertEquals(#list, 31, "Should have exactly 31 easing functions")
|
|
||||||
|
|
||||||
-- Check that linear is in the list
|
|
||||||
local hasLinear = false
|
|
||||||
for _, name in ipairs(list) do
|
|
||||||
if name == "linear" then
|
|
||||||
hasLinear = true
|
|
||||||
break
|
|
||||||
end
|
|
||||||
end
|
|
||||||
luaunit.assertTrue(hasLinear, "List should contain 'linear'")
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Test Easing.get() method
|
|
||||||
function TestEasing:testGet()
|
|
||||||
local linear = Easing.get("linear")
|
|
||||||
luaunit.assertNotNil(linear)
|
|
||||||
luaunit.assertEquals(type(linear), "function")
|
|
||||||
luaunit.assertEquals(linear(0.5), 0.5)
|
|
||||||
|
|
||||||
-- Test non-existent easing
|
|
||||||
local nonExistent = Easing.get("nonExistentEasing")
|
|
||||||
luaunit.assertNil(nonExistent)
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Test that all InOut easings are symmetric around 0.5
|
-- Test that all InOut easings are symmetric around 0.5
|
||||||
function TestEasing:testInOutSymmetry()
|
function TestEasing:testInOutSymmetry()
|
||||||
local inOutEasings = {
|
local inOutEasings = {
|
||||||
|
|||||||
Reference in New Issue
Block a user