more work on Animation
This commit is contained in:
@@ -1,6 +1,9 @@
|
||||
--- Easing function type
|
||||
---@alias EasingFunction fun(t: number): number
|
||||
|
||||
-- ErrorHandler dependency (injected via initializeErrorHandler)
|
||||
local ErrorHandler = nil
|
||||
|
||||
--- Easing functions for animations
|
||||
---@type table<string, EasingFunction>
|
||||
local Easing = {
|
||||
@@ -76,19 +79,23 @@ Animation.__index = Animation
|
||||
function Animation.new(props)
|
||||
-- Validate input
|
||||
if type(props) ~= "table" then
|
||||
error("[FlexLove.Animation] Animation.new() requires a table argument")
|
||||
ErrorHandler.warn("Animation", "Animation.new() requires a table argument. Using default values.")
|
||||
props = {duration = 1, start = {}, final = {}}
|
||||
end
|
||||
|
||||
if type(props.duration) ~= "number" or props.duration <= 0 then
|
||||
error("[FlexLove.Animation] Animation duration must be a positive number")
|
||||
ErrorHandler.warn("Animation", "Animation duration must be a positive number. Using 1 second.")
|
||||
props.duration = 1
|
||||
end
|
||||
|
||||
if type(props.start) ~= "table" then
|
||||
error("[FlexLove.Animation] Animation start must be a table")
|
||||
ErrorHandler.warn("Animation", "Animation start must be a table. Using empty table.")
|
||||
props.start = {}
|
||||
end
|
||||
|
||||
if type(props.final) ~= "table" then
|
||||
error("[FlexLove.Animation] Animation final must be a table")
|
||||
ErrorHandler.warn("Animation", "Animation final must be a table. Using empty table.")
|
||||
props.final = {}
|
||||
end
|
||||
|
||||
local self = setmetatable({}, Animation)
|
||||
@@ -144,6 +151,14 @@ function Animation:update(dt, element)
|
||||
return false
|
||||
end
|
||||
|
||||
-- Handle delay
|
||||
if self._delay and self._delayElapsed then
|
||||
if self._delayElapsed < self._delay then
|
||||
self._delayElapsed = self._delayElapsed + dt
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- Call onStart on first update
|
||||
if not self._hasStarted then
|
||||
self._hasStarted = true
|
||||
@@ -180,8 +195,32 @@ function Animation:update(dt, element)
|
||||
self.elapsed = self.elapsed + dt
|
||||
if self.elapsed >= self.duration then
|
||||
self.elapsed = self.duration
|
||||
self._state = "completed"
|
||||
self._resultDirty = true
|
||||
|
||||
-- Handle repeat and yoyo
|
||||
if self._repeatCount then
|
||||
self._repeatCurrent = (self._repeatCurrent or 0) + 1
|
||||
|
||||
if self._repeatCount == 0 or self._repeatCurrent < self._repeatCount then
|
||||
-- Continue repeating
|
||||
if self._yoyo then
|
||||
-- Reverse direction for yoyo
|
||||
self._reversed = not self._reversed
|
||||
if self._reversed then
|
||||
self.elapsed = self.duration
|
||||
else
|
||||
self.elapsed = 0
|
||||
end
|
||||
else
|
||||
-- Reset to beginning
|
||||
self.elapsed = 0
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
-- Animation truly completed
|
||||
self._state = "completed"
|
||||
-- Call onComplete callback
|
||||
if self.onComplete and type(self.onComplete) == "function" then
|
||||
local success, err = pcall(self.onComplete, self, element)
|
||||
@@ -355,8 +394,13 @@ end
|
||||
---Apply this animation to an element
|
||||
---@param element Element The element to apply animation to
|
||||
function Animation:apply(element)
|
||||
if not ErrorHandler then
|
||||
ErrorHandler = require("modules.ErrorHandler")
|
||||
end
|
||||
|
||||
if not element or type(element) ~= "table" then
|
||||
error("[FlexLove.Animation] Cannot apply animation to nil or non-table element")
|
||||
ErrorHandler.warn("Animation", "Cannot apply animation to nil or non-table element. Animation not applied.")
|
||||
return
|
||||
end
|
||||
element.animation = self
|
||||
end
|
||||
@@ -464,6 +508,71 @@ function Animation:getProgress()
|
||||
return math.min(self.elapsed / self.duration, 1)
|
||||
end
|
||||
|
||||
---Chain another animation after this one completes
|
||||
---@param nextAnimation Animation|function Animation instance or factory function that returns an animation
|
||||
---@return Animation nextAnimation The chained animation (for further chaining)
|
||||
function Animation:chain(nextAnimation)
|
||||
if not ErrorHandler then
|
||||
ErrorHandler = require("modules.ErrorHandler")
|
||||
end
|
||||
|
||||
if type(nextAnimation) == "function" then
|
||||
self._nextFactory = nextAnimation
|
||||
return self
|
||||
elseif type(nextAnimation) == "table" then
|
||||
self._next = nextAnimation
|
||||
return nextAnimation
|
||||
else
|
||||
ErrorHandler.warn("Animation", "chain() requires an Animation or function. Chaining not applied.")
|
||||
return self
|
||||
end
|
||||
end
|
||||
|
||||
---Add delay before animation starts
|
||||
---@param seconds number Delay duration in seconds
|
||||
---@return Animation self For chaining
|
||||
function Animation:delay(seconds)
|
||||
if not ErrorHandler then
|
||||
ErrorHandler = require("modules.ErrorHandler")
|
||||
end
|
||||
|
||||
if type(seconds) ~= "number" or seconds < 0 then
|
||||
ErrorHandler.warn("Animation", "delay() requires a non-negative number. Using 0.")
|
||||
seconds = 0
|
||||
end
|
||||
self._delay = seconds
|
||||
self._delayElapsed = 0
|
||||
return self
|
||||
end
|
||||
|
||||
---Repeat animation multiple times
|
||||
---@param count number Number of times to repeat (0 = infinite loop)
|
||||
---@return Animation self For chaining
|
||||
function Animation:repeatCount(count)
|
||||
if not ErrorHandler then
|
||||
ErrorHandler = require("modules.ErrorHandler")
|
||||
end
|
||||
|
||||
if type(count) ~= "number" or count < 0 then
|
||||
ErrorHandler.warn("Animation", "repeatCount() requires a non-negative number. Using 0.")
|
||||
count = 0
|
||||
end
|
||||
self._repeatCount = count
|
||||
self._repeatCurrent = 0
|
||||
return self
|
||||
end
|
||||
|
||||
---Enable yoyo mode (animation reverses direction on each repeat)
|
||||
---@param enabled boolean? Enable yoyo mode (default: true)
|
||||
---@return Animation self For chaining
|
||||
function Animation:yoyo(enabled)
|
||||
if enabled == nil then
|
||||
enabled = true
|
||||
end
|
||||
self._yoyo = enabled
|
||||
return self
|
||||
end
|
||||
|
||||
--- Create a simple fade animation
|
||||
---@param duration number Duration in seconds
|
||||
---@param fromOpacity number Starting opacity (0-1)
|
||||
@@ -520,4 +629,13 @@ function Animation.scale(duration, fromScale, toScale, easing)
|
||||
})
|
||||
end
|
||||
|
||||
--- Initialize ErrorHandler dependency
|
||||
---@param errorHandler table The ErrorHandler module
|
||||
local function initializeErrorHandler(errorHandler)
|
||||
ErrorHandler = errorHandler
|
||||
end
|
||||
|
||||
-- Export ErrorHandler initializer
|
||||
Animation.initializeErrorHandler = initializeErrorHandler
|
||||
|
||||
return Animation
|
||||
|
||||
327
modules/AnimationGroup.lua
Normal file
327
modules/AnimationGroup.lua
Normal file
@@ -0,0 +1,327 @@
|
||||
--- AnimationGroup module for running multiple animations together
|
||||
---@class AnimationGroup
|
||||
local AnimationGroup = {}
|
||||
AnimationGroup.__index = AnimationGroup
|
||||
|
||||
-- ErrorHandler dependency (injected via initializeErrorHandler)
|
||||
local ErrorHandler = nil
|
||||
|
||||
---@class AnimationGroupProps
|
||||
---@field animations table Array of Animation instances
|
||||
---@field mode string? "parallel", "sequence", or "stagger" (default: "parallel")
|
||||
---@field stagger number? Stagger delay in seconds (for stagger mode, default: 0.1)
|
||||
---@field onComplete function? Called when all animations complete: (group)
|
||||
---@field onStart function? Called when group starts: (group)
|
||||
|
||||
--- Create a new animation group
|
||||
---@param props AnimationGroupProps
|
||||
---@return AnimationGroup group
|
||||
function AnimationGroup.new(props)
|
||||
if type(props) ~= "table" then
|
||||
ErrorHandler.warn("AnimationGroup", "AnimationGroup.new() requires a table argument. Using default values.")
|
||||
props = {animations = {}}
|
||||
end
|
||||
|
||||
if type(props.animations) ~= "table" or #props.animations == 0 then
|
||||
ErrorHandler.warn("AnimationGroup", "AnimationGroup requires at least one animation. Creating empty group.")
|
||||
props.animations = {}
|
||||
end
|
||||
|
||||
local self = setmetatable({}, AnimationGroup)
|
||||
|
||||
self.animations = props.animations
|
||||
self.mode = props.mode or "parallel"
|
||||
self.stagger = props.stagger or 0.1
|
||||
self.onComplete = props.onComplete
|
||||
self.onStart = props.onStart
|
||||
|
||||
-- Validate mode
|
||||
if self.mode ~= "parallel" and self.mode ~= "sequence" and self.mode ~= "stagger" then
|
||||
ErrorHandler.warn("AnimationGroup", string.format("Invalid mode: %s. Using 'parallel'.", tostring(self.mode)))
|
||||
self.mode = "parallel"
|
||||
end
|
||||
|
||||
-- Internal state
|
||||
self._currentIndex = 1
|
||||
self._staggerElapsed = 0
|
||||
self._startedAnimations = {}
|
||||
self._hasStarted = false
|
||||
self._paused = false
|
||||
self._state = "ready" -- "ready", "playing", "completed", "cancelled"
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
--- Update all animations in parallel
|
||||
---@param dt number Delta time
|
||||
---@param element table? Optional element reference for callbacks
|
||||
---@return boolean finished True if all animations complete
|
||||
function AnimationGroup:_updateParallel(dt, element)
|
||||
local allFinished = true
|
||||
|
||||
for i, anim in ipairs(self.animations) do
|
||||
-- Check if animation has isCompleted method or check state
|
||||
local isCompleted = false
|
||||
if type(anim.getState) == "function" then
|
||||
isCompleted = anim:getState() == "completed"
|
||||
elseif anim._state then
|
||||
isCompleted = anim._state == "completed"
|
||||
end
|
||||
|
||||
if not isCompleted then
|
||||
local finished = anim:update(dt, element)
|
||||
if not finished then
|
||||
allFinished = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return allFinished
|
||||
end
|
||||
|
||||
--- Update animations in sequence (one after another)
|
||||
---@param dt number Delta time
|
||||
---@param element table? Optional element reference for callbacks
|
||||
---@return boolean finished True if all animations complete
|
||||
function AnimationGroup:_updateSequence(dt, element)
|
||||
if self._currentIndex > #self.animations then
|
||||
return true
|
||||
end
|
||||
|
||||
local currentAnim = self.animations[self._currentIndex]
|
||||
local finished = currentAnim:update(dt, element)
|
||||
|
||||
if finished then
|
||||
self._currentIndex = self._currentIndex + 1
|
||||
if self._currentIndex > #self.animations then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
--- Update animations with stagger delay
|
||||
---@param dt number Delta time
|
||||
---@param element table? Optional element reference for callbacks
|
||||
---@return boolean finished True if all animations complete
|
||||
function AnimationGroup:_updateStagger(dt, element)
|
||||
self._staggerElapsed = self._staggerElapsed + dt
|
||||
|
||||
-- Start animations based on stagger timing
|
||||
for i, anim in ipairs(self.animations) do
|
||||
local startTime = (i - 1) * self.stagger
|
||||
|
||||
if self._staggerElapsed >= startTime and not self._startedAnimations[i] then
|
||||
self._startedAnimations[i] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Update started animations
|
||||
local allFinished = true
|
||||
for i, anim in ipairs(self.animations) do
|
||||
if self._startedAnimations[i] then
|
||||
local isCompleted = false
|
||||
if type(anim.getState) == "function" then
|
||||
isCompleted = anim:getState() == "completed"
|
||||
elseif anim._state then
|
||||
isCompleted = anim._state == "completed"
|
||||
end
|
||||
|
||||
if not isCompleted then
|
||||
local finished = anim:update(dt, element)
|
||||
if not finished then
|
||||
allFinished = false
|
||||
end
|
||||
end
|
||||
else
|
||||
allFinished = false
|
||||
end
|
||||
end
|
||||
|
||||
return allFinished
|
||||
end
|
||||
|
||||
--- Update the animation group
|
||||
---@param dt number Delta time
|
||||
---@param element table? Optional element reference for callbacks
|
||||
---@return boolean finished True if group is complete
|
||||
function AnimationGroup:update(dt, element)
|
||||
-- Sanitize dt
|
||||
if type(dt) ~= "number" or dt < 0 or dt ~= dt or dt == math.huge then
|
||||
dt = 0
|
||||
end
|
||||
|
||||
if self._paused or self._state == "completed" or self._state == "cancelled" then
|
||||
return self._state == "completed"
|
||||
end
|
||||
|
||||
-- Call onStart on first update
|
||||
if not self._hasStarted then
|
||||
self._hasStarted = true
|
||||
self._state = "playing"
|
||||
if self.onStart and type(self.onStart) == "function" then
|
||||
local success, err = pcall(self.onStart, self)
|
||||
if not success then
|
||||
print(string.format("[AnimationGroup] onStart error: %s", tostring(err)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local finished = false
|
||||
|
||||
if self.mode == "parallel" then
|
||||
finished = self:_updateParallel(dt, element)
|
||||
elseif self.mode == "sequence" then
|
||||
finished = self:_updateSequence(dt, element)
|
||||
elseif self.mode == "stagger" then
|
||||
finished = self:_updateStagger(dt, element)
|
||||
end
|
||||
|
||||
if finished then
|
||||
self._state = "completed"
|
||||
if self.onComplete and type(self.onComplete) == "function" then
|
||||
local success, err = pcall(self.onComplete, self)
|
||||
if not success then
|
||||
print(string.format("[AnimationGroup] onComplete error: %s", tostring(err)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return finished
|
||||
end
|
||||
|
||||
--- Pause all animations in the group
|
||||
function AnimationGroup:pause()
|
||||
self._paused = true
|
||||
for _, anim in ipairs(self.animations) do
|
||||
if type(anim.pause) == "function" then
|
||||
anim:pause()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Resume all animations in the group
|
||||
function AnimationGroup:resume()
|
||||
self._paused = false
|
||||
for _, anim in ipairs(self.animations) do
|
||||
if type(anim.resume) == "function" then
|
||||
anim:resume()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Check if group is paused
|
||||
---@return boolean paused
|
||||
function AnimationGroup:isPaused()
|
||||
return self._paused
|
||||
end
|
||||
|
||||
--- Reverse all animations in the group
|
||||
function AnimationGroup:reverse()
|
||||
for _, anim in ipairs(self.animations) do
|
||||
if type(anim.reverse) == "function" then
|
||||
anim:reverse()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Set speed for all animations in the group
|
||||
---@param speed number Speed multiplier
|
||||
function AnimationGroup:setSpeed(speed)
|
||||
for _, anim in ipairs(self.animations) do
|
||||
if type(anim.setSpeed) == "function" then
|
||||
anim:setSpeed(speed)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Cancel all animations in the group
|
||||
---@param element table? Optional element reference for callbacks
|
||||
function AnimationGroup:cancel(element)
|
||||
if self._state ~= "cancelled" and self._state ~= "completed" then
|
||||
self._state = "cancelled"
|
||||
for _, anim in ipairs(self.animations) do
|
||||
if type(anim.cancel) == "function" then
|
||||
anim:cancel(element)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Reset the animation group to initial state
|
||||
function AnimationGroup:reset()
|
||||
self._currentIndex = 1
|
||||
self._staggerElapsed = 0
|
||||
self._startedAnimations = {}
|
||||
self._hasStarted = false
|
||||
self._paused = false
|
||||
self._state = "ready"
|
||||
|
||||
for _, anim in ipairs(self.animations) do
|
||||
if type(anim.reset) == "function" then
|
||||
anim:reset()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Get the current state of the group
|
||||
---@return string state "ready", "playing", "completed", "cancelled"
|
||||
function AnimationGroup:getState()
|
||||
return self._state
|
||||
end
|
||||
|
||||
--- Get the overall progress of the group (0-1)
|
||||
---@return number progress
|
||||
function AnimationGroup:getProgress()
|
||||
if #self.animations == 0 then
|
||||
return 1
|
||||
end
|
||||
|
||||
if self.mode == "sequence" then
|
||||
-- For sequence, progress is based on current animation index + current animation progress
|
||||
local completedAnims = self._currentIndex - 1
|
||||
local currentProgress = 0
|
||||
|
||||
if self._currentIndex <= #self.animations then
|
||||
local currentAnim = self.animations[self._currentIndex]
|
||||
if type(currentAnim.getProgress) == "function" then
|
||||
currentProgress = currentAnim:getProgress()
|
||||
end
|
||||
end
|
||||
|
||||
return (completedAnims + currentProgress) / #self.animations
|
||||
else
|
||||
-- For parallel and stagger, average progress of all animations
|
||||
local totalProgress = 0
|
||||
for _, anim in ipairs(self.animations) do
|
||||
if type(anim.getProgress) == "function" then
|
||||
totalProgress = totalProgress + anim:getProgress()
|
||||
else
|
||||
totalProgress = totalProgress + 1
|
||||
end
|
||||
end
|
||||
return totalProgress / #self.animations
|
||||
end
|
||||
end
|
||||
|
||||
--- Apply this animation group to an element
|
||||
---@param element Element The element to apply animations to
|
||||
function AnimationGroup:apply(element)
|
||||
if not element or type(element) ~= "table" then
|
||||
ErrorHandler.warn("AnimationGroup", "Cannot apply animation group to nil or non-table element. Group not applied.")
|
||||
return
|
||||
end
|
||||
element.animationGroup = self
|
||||
end
|
||||
|
||||
--- Initialize ErrorHandler dependency
|
||||
---@param errorHandler table The ErrorHandler module
|
||||
local function initializeErrorHandler(errorHandler)
|
||||
ErrorHandler = errorHandler
|
||||
end
|
||||
|
||||
-- Export ErrorHandler initializer
|
||||
AnimationGroup.initializeErrorHandler = initializeErrorHandler
|
||||
|
||||
return AnimationGroup
|
||||
@@ -2127,7 +2127,19 @@ function Element:update(dt)
|
||||
local finished = self.animation:update(dt, self)
|
||||
if finished then
|
||||
-- Animation:update() already called onComplete callback
|
||||
self.animation = nil -- remove finished animation
|
||||
-- Check for chained animation
|
||||
if self.animation._next then
|
||||
self.animation = self.animation._next
|
||||
elseif self.animation._nextFactory and type(self.animation._nextFactory) == "function" then
|
||||
local success, nextAnim = pcall(self.animation._nextFactory, self)
|
||||
if success and nextAnim then
|
||||
self.animation = nextAnim
|
||||
else
|
||||
self.animation = nil
|
||||
end
|
||||
else
|
||||
self.animation = nil
|
||||
end
|
||||
else
|
||||
-- Apply animation interpolation during update
|
||||
local anim = self.animation:interpolate()
|
||||
@@ -2968,4 +2980,121 @@ function Element:setTransformOrigin(originX, originY)
|
||||
self.transform.originY = originY
|
||||
end
|
||||
|
||||
--- Set transition configuration for a property
|
||||
---@param property string Property name or "all" for all properties
|
||||
---@param config table Transition config {duration, easing, delay, onComplete}
|
||||
function Element:setTransition(property, config)
|
||||
if not self.transitions then
|
||||
self.transitions = {}
|
||||
end
|
||||
|
||||
if type(config) ~= "table" then
|
||||
self._deps.ErrorHandler.warn("Element", "setTransition() requires a config table. Using default config.")
|
||||
config = {}
|
||||
end
|
||||
|
||||
-- Validate config
|
||||
if config.duration and (type(config.duration) ~= "number" or config.duration < 0) then
|
||||
self._deps.ErrorHandler.warn("Element", "transition duration must be a non-negative number. Using 0.3 seconds.")
|
||||
config.duration = 0.3
|
||||
end
|
||||
|
||||
self.transitions[property] = {
|
||||
duration = config.duration or 0.3,
|
||||
easing = config.easing or "easeOutQuad",
|
||||
delay = config.delay or 0,
|
||||
onComplete = config.onComplete
|
||||
}
|
||||
end
|
||||
|
||||
--- Set transition configuration for multiple properties
|
||||
---@param groupName string Name for this transition group
|
||||
---@param config table Transition config {duration, easing, delay, onComplete}
|
||||
---@param properties table Array of property names
|
||||
function Element:setTransitionGroup(groupName, config, properties)
|
||||
if type(properties) ~= "table" then
|
||||
self._deps.ErrorHandler.warn("Element", "setTransitionGroup() requires a properties array. No transitions set.")
|
||||
return
|
||||
end
|
||||
|
||||
for _, prop in ipairs(properties) do
|
||||
self:setTransition(prop, config)
|
||||
end
|
||||
end
|
||||
|
||||
--- Remove transition configuration for a property
|
||||
---@param property string Property name or "all" to remove all
|
||||
function Element:removeTransition(property)
|
||||
if not self.transitions then
|
||||
return
|
||||
end
|
||||
|
||||
if property == "all" then
|
||||
self.transitions = {}
|
||||
else
|
||||
self.transitions[property] = nil
|
||||
end
|
||||
end
|
||||
|
||||
--- Set property with automatic transition
|
||||
---@param property string Property name
|
||||
---@param value any New value
|
||||
function Element:setProperty(property, value)
|
||||
-- Check if transitions are enabled for this property
|
||||
local shouldTransition = false
|
||||
local transitionConfig = nil
|
||||
|
||||
if self.transitions then
|
||||
transitionConfig = self.transitions[property] or self.transitions["all"]
|
||||
shouldTransition = transitionConfig ~= nil
|
||||
end
|
||||
|
||||
-- Don't transition if value is the same
|
||||
if self[property] == value then
|
||||
return
|
||||
end
|
||||
|
||||
if shouldTransition and transitionConfig then
|
||||
-- Get current value
|
||||
local currentValue = self[property]
|
||||
|
||||
-- Only transition if we have a valid current value
|
||||
if currentValue ~= nil then
|
||||
-- Create animation for the property change
|
||||
local Animation = require("modules.Animation")
|
||||
local anim = Animation.new({
|
||||
duration = transitionConfig.duration,
|
||||
start = { [property] = currentValue },
|
||||
final = { [property] = value },
|
||||
easing = transitionConfig.easing,
|
||||
onComplete = transitionConfig.onComplete
|
||||
})
|
||||
|
||||
-- Set Color module reference if needed
|
||||
if self._deps and self._deps.Color then
|
||||
anim:setColorModule(self._deps.Color)
|
||||
end
|
||||
|
||||
-- Set Transform module reference if needed
|
||||
if self._deps and self._deps.Transform then
|
||||
anim:setTransformModule(self._deps.Transform)
|
||||
end
|
||||
|
||||
-- Apply delay if configured
|
||||
if transitionConfig.delay and transitionConfig.delay > 0 then
|
||||
anim:delay(transitionConfig.delay)
|
||||
end
|
||||
|
||||
-- Apply animation
|
||||
anim:apply(self)
|
||||
else
|
||||
-- No current value, set directly
|
||||
self[property] = value
|
||||
end
|
||||
else
|
||||
-- No transition, set directly
|
||||
self[property] = value
|
||||
end
|
||||
end
|
||||
|
||||
return Element
|
||||
|
||||
@@ -190,10 +190,45 @@ function Renderer:_drawImage(x, y, paddingLeft, paddingTop, contentWidth, conten
|
||||
|
||||
if hasCornerRadius then
|
||||
-- Use stencil to clip image to rounded corners
|
||||
love.graphics.stencil(function()
|
||||
self._RoundedRect.draw("fill", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius)
|
||||
end, "replace", 1)
|
||||
love.graphics.setStencilTest("greater", 0)
|
||||
local success, err = pcall(function()
|
||||
love.graphics.stencil(function()
|
||||
self._RoundedRect.draw("fill", x, y, borderBoxWidth, borderBoxHeight, self.cornerRadius)
|
||||
end, "replace", 1)
|
||||
love.graphics.setStencilTest("greater", 0)
|
||||
end)
|
||||
|
||||
if not success then
|
||||
-- Lazy-load ErrorHandler if needed
|
||||
if not ErrorHandler then
|
||||
ErrorHandler = require("modules.ErrorHandler")
|
||||
end
|
||||
|
||||
-- Check if it's a stencil buffer error
|
||||
if err and err:match("stencil") then
|
||||
ErrorHandler.warn(
|
||||
"Renderer",
|
||||
"IMG_001",
|
||||
"Cannot apply corner radius to image: stencil buffer not available",
|
||||
{
|
||||
imagePath = self.imagePath or "unknown",
|
||||
cornerRadius = string.format(
|
||||
"TL:%d TR:%d BL:%d BR:%d",
|
||||
self.cornerRadius.topLeft,
|
||||
self.cornerRadius.topRight,
|
||||
self.cornerRadius.bottomLeft,
|
||||
self.cornerRadius.bottomRight
|
||||
),
|
||||
error = tostring(err),
|
||||
},
|
||||
"Ensure the active canvas has stencil=true enabled, or remove cornerRadius from images"
|
||||
)
|
||||
-- Continue without corner radius
|
||||
hasCornerRadius = false
|
||||
else
|
||||
-- Re-throw if it's a different error
|
||||
error(err, 2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Draw the image based on repeat mode
|
||||
|
||||
Reference in New Issue
Block a user