local registeredAnimations = {} local easings = { linear = function(progress) return progress end, easeInQuad = function(progress) return progress * progress end, easeOutQuad = function(progress) return 1 - (1 - progress) * (1 - progress) end, easeInOutQuad = function(progress) if progress < 0.5 then return 2 * progress * progress end return 1 - (-2 * progress + 2)^2 / 2 end } ---@splitClass --- This is the AnimationInstance class. It represents a single animation instance ---@class AnimationInstance ---@field element VisualElement The element being animated ---@field type string The type of animation ---@field args table The animation arguments ---@field duration number The duration in seconds ---@field startTime number The animation start time ---@field isPaused boolean Whether the animation is paused ---@field handlers table The animation handlers ---@field easing string The easing function name local AnimationInstance = {} AnimationInstance.__index = AnimationInstance --- Creates a new AnimationInstance --- @shortDescription Creates a new animation instance --- @param element VisualElement The element to animate --- @param animType string The type of animation --- @param args table The animation arguments --- @param duration number Duration in seconds --- @param easing string The easing function name --- @return AnimationInstance The new animation instance function AnimationInstance.new(element, animType, args, duration, easing) local self = setmetatable({}, AnimationInstance) self.element = element self.type = animType self.args = args self.duration = duration or 1 self.startTime = 0 self.isPaused = false self.handlers = registeredAnimations[animType] self.easing = easing return self end --- Starts the animation --- @shortDescription Starts the animation --- @return AnimationInstance self The animation instance function AnimationInstance:start() self.startTime = os.epoch("local") / 1000 if self.handlers.start then self.handlers.start(self) end return self end --- Updates the animation --- @shortDescription Updates the animation --- @param elapsed number The elapsed time in seconds --- @return boolean Whether the animation is finished function AnimationInstance:update(elapsed) local rawProgress = math.min(1, elapsed / self.duration) local progress = easings[self.easing](rawProgress) return self.handlers.update(self, progress) end --- Gets called when the animation is completed --- @shortDescription Called when the animation is completed function AnimationInstance:complete() if self.handlers.complete then self.handlers.complete(self) end end --- This is the animation plugin. It provides a animation system for visual elements --- with support for sequences, easing functions, and multiple animation types. ---@class Animation local Animation = {} Animation.__index = Animation --- Registers a new animation type --- @shortDescription Registers a custom animation type --- @param name string The name of the animation --- @param handlers table Table containing start, update and complete handlers --- @usage Animation.registerAnimation("fade", {start=function(anim) end, update=function(anim,progress) end}) function Animation.registerAnimation(name, handlers) registeredAnimations[name] = handlers Animation[name] = function(self, ...) local args = {...} local easing = "linear" if(type(args[#args]) == "string") then easing = table.remove(args, #args) end local duration = table.remove(args, #args) return self:addAnimation(name, args, duration, easing) end end --- Registers a new easing function --- @shortDescription Adds a custom easing function --- @param name string The name of the easing function --- @param func function The easing function (takes progress 0-1, returns modified progress) function Animation.registerEasing(name, func) easings[name] = func end --- Creates a new Animation --- @shortDescription Creates a new animation --- @param element VisualElement The element to animate --- @return Animation The new animation function Animation.new(element) local self = {} self.element = element self.sequences = {{}} self.sequenceCallbacks = {} self.currentSequence = 1 self.timer = nil setmetatable(self, Animation) return self end --- Creates a new sequence --- @shortDescription Creates a new sequence --- @return Animation self The animation instance function Animation:sequence() table.insert(self.sequences, {}) self.currentSequence = #self.sequences self.sequenceCallbacks[self.currentSequence] = { start = nil, update = nil, complete = nil } return self end --- Registers a callback for the start event --- @shortDescription Registers a callback for the start event --- @param callback function The callback function to register function Animation:onStart(callback) if not self.sequenceCallbacks[self.currentSequence] then self.sequenceCallbacks[self.currentSequence] = {} end self.sequenceCallbacks[self.currentSequence].start = callback return self end --- Registers a callback for the update event --- @shortDescription Registers a callback for the update event --- @param callback function The callback function to register --- @return Animation self The animation instance function Animation:onUpdate(callback) if not self.sequenceCallbacks[self.currentSequence] then self.sequenceCallbacks[self.currentSequence] = {} end self.sequenceCallbacks[self.currentSequence].update = callback return self end --- Registers a callback for the complete event --- @shortDescription Registers a callback for the complete event --- @param callback function The callback function to register --- @return Animation self The animation instance function Animation:onComplete(callback) if not self.sequenceCallbacks[self.currentSequence] then self.sequenceCallbacks[self.currentSequence] = {} end self.sequenceCallbacks[self.currentSequence].complete = callback return self end --- Adds a new animation to the sequence --- @shortDescription Adds a new animation to the sequence --- @param type string The type of animation --- @param args table The animation arguments --- @param duration number The duration in seconds --- @param easing string The easing function name function Animation:addAnimation(type, args, duration, easing) local anim = AnimationInstance.new(self.element, type, args, duration, easing) table.insert(self.sequences[self.currentSequence], anim) return self end --- Starts the animation --- @shortDescription Starts the animation --- @return Animation self The animation instance function Animation:start() self.currentSequence = 1 if(self.sequenceCallbacks[self.currentSequence])then if(self.sequenceCallbacks[self.currentSequence].start) then self.sequenceCallbacks[self.currentSequence].start(self.element) end end if #self.sequences[self.currentSequence] > 0 then self.timer = os.startTimer(0.05) for _, anim in ipairs(self.sequences[self.currentSequence]) do anim:start() end end return self end --- The event handler for the animation (listens to timer events) --- @shortDescription The event handler for the animation --- @param event string The event type --- @param timerId number The timer ID function Animation:event(event, timerId) if event == "timer" and timerId == self.timer then local currentTime = os.epoch("local") / 1000 local sequenceFinished = true local remaining = {} local callbacks = self.sequenceCallbacks[self.currentSequence] for _, anim in ipairs(self.sequences[self.currentSequence]) do local elapsed = currentTime - anim.startTime local progress = elapsed / anim.duration local finished = anim:update(elapsed) if callbacks and callbacks.update then callbacks.update(self.element, progress) end if not finished then table.insert(remaining, anim) sequenceFinished = false else anim:complete() end end if sequenceFinished then if callbacks and callbacks.complete then callbacks.complete(self.element) end if self.currentSequence < #self.sequences then self.currentSequence = self.currentSequence + 1 remaining = {} local nextCallbacks = self.sequenceCallbacks[self.currentSequence] if nextCallbacks and nextCallbacks.start then nextCallbacks.start(self.element) end for _, anim in ipairs(self.sequences[self.currentSequence]) do anim:start() table.insert(remaining, anim) end end end if #remaining > 0 then self.timer = os.startTimer(0.05) end end end Animation.registerAnimation("move", { start = function(anim) anim.startX = anim.element.get("x") anim.startY = anim.element.get("y") end, update = function(anim, progress) local x = anim.startX + (anim.args[1] - anim.startX) * progress local y = anim.startY + (anim.args[2] - anim.startY) * progress anim.element.set("x", math.floor(x)) anim.element.set("y", math.floor(y)) return progress >= 1 end, complete = function(anim) anim.element.set("x", anim.args[1]) anim.element.set("y", anim.args[2]) end }) Animation.registerAnimation("moveOffset", { start = function(anim) anim.startX = anim.element.get("offsetX") anim.startY = anim.element.get("offsetY") end, update = function(anim, progress) local x = anim.startX + (anim.args[1] - anim.startX) * progress local y = anim.startY + (anim.args[2] - anim.startY) * progress anim.element.set("offsetX", math.floor(x)) anim.element.set("offsetY", math.floor(y)) return progress >= 1 end, complete = function(anim) anim.element.set("offsetX", anim.args[1]) anim.element.set("offsetY", anim.args[2]) end }) Animation.registerAnimation("morphText", { start = function(anim) local startText = anim.element.get(anim.args[1]) local targetText = anim.args[2] local maxLength = math.max(#startText, #targetText) local startSpace = string.rep(" ", math.floor(maxLength - #startText)/2) anim.startText = startSpace .. startText .. startSpace anim.targetText = targetText .. string.rep(" ", maxLength - #targetText) anim.length = maxLength end, update = function(anim, progress) local currentText = "" for i = 1, anim.length do local startChar = anim.startText:sub(i,i) local targetChar = anim.targetText:sub(i,i) if progress < 0.5 then currentText = currentText .. (math.random() > progress*2 and startChar or " ") else currentText = currentText .. (math.random() > (progress-0.5)*2 and " " or targetChar) end end anim.element.set(anim.args[1], currentText) return progress >= 1 end, complete = function(anim) anim.element.set(anim.args[1], anim.targetText:gsub("%s+$", "")) -- Entferne trailing spaces end }) Animation.registerAnimation("typewrite", { start = function(anim) anim.targetText = anim.args[2] anim.element.set(anim.args[1], "") end, update = function(anim, progress) local length = math.floor(#anim.targetText * progress) anim.element.set(anim.args[1], anim.targetText:sub(1, length)) return progress >= 1 end }) Animation.registerAnimation("fadeText", { start = function(anim) anim.chars = {} for i=1, #anim.args[2] do anim.chars[i] = {char = anim.args[2]:sub(i,i), visible = false} end end, update = function(anim, progress) local text = "" for i, charData in ipairs(anim.chars) do if math.random() < progress then charData.visible = true end text = text .. (charData.visible and charData.char or " ") end anim.element.set(anim.args[1], text) return progress >= 1 end }) Animation.registerAnimation("scrollText", { start = function(anim) anim.width = anim.element.get("width") anim.targetText = anim.args[2] anim.element.set(anim.args[1], "") end, update = function(anim, progress) local offset = math.floor(anim.width * (1-progress)) local spaces = string.rep(" ", offset) anim.element.set(anim.args[1], spaces .. anim.targetText) return progress >= 1 end }) ---@splitClass --- Adds additional methods for VisualElement when adding animation plugin --- @class VisualElement local VisualElement = {hooks={}} ---@private function VisualElement.hooks.dispatchEvent(self, event, ...) if event == "timer" then local animation = self.get("animation") if animation then animation:event(event, ...) end return true end end ---@private function VisualElement.setup(element) element.defineProperty(element, "animation", {default = nil, type = "table"}) element.defineEvent(element, "timer") end --- Creates a new Animation Object --- @shortDescription Creates a new animation --- @return Animation animation The new animation function VisualElement:animate() local animation = Animation.new(self) self.set("animation", animation) return animation end return { VisualElement = VisualElement }