--- 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) --- Coordinate multiple animations to play together, in sequence, or staggered for complex choreographed effects --- Use this to synchronize related UI changes like simultaneous fades or sequential reveals ---@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 --- Advance all animations in the group according to their coordination mode --- Call this each frame to progress parallel, sequential, or staggered animations ---@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 --- Freeze the entire animation sequence in unison --- Use this to pause complex multi-part animations during game pauses function AnimationGroup:pause() self._paused = true for _, anim in ipairs(self.animations) do if type(anim.pause) == "function" then anim:pause() end end end --- Continue all paused animations simultaneously from their paused states --- Use this to unpause coordinated animation sequences function AnimationGroup:resume() self._paused = false for _, anim in ipairs(self.animations) do if type(anim.resume) == "function" then anim:resume() end end end --- Determine if the entire group is currently paused --- Use this to sync other game logic with animation group state ---@return boolean paused function AnimationGroup:isPaused() return self._paused end --- Flip all animations to play backwards together --- Use this to reverse complex transitions like panel opens/closes function AnimationGroup:reverse() for _, anim in ipairs(self.animations) do if type(anim.reverse) == "function" then anim:reverse() end end end --- Control the tempo of all animations simultaneously --- Use this for slow-motion effects or debugging without adjusting individual animations ---@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 --- Abort all animations in the group immediately without completion --- Use this when UI is dismissed mid-animation or transitions are interrupted ---@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 --- Restart the entire group from the beginning for reuse --- Use this to replay animation sequences without recreating objects 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 --- Check the overall lifecycle state of the animation group --- Use this to conditionally trigger follow-up actions or cleanup ---@return string state "ready", "playing", "completed", "cancelled" function AnimationGroup:getState() return self._state end --- Calculate completion percentage across all animations in the group --- Use this for progress bars or to synchronize other effects with the group ---@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 --- Attach this group to an element for automatic updates and integration --- Use this for hands-off animation management within FlexLove's system ---@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