---@configDefault false local registeredAnimations = {} local easings = {} 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, easeInCubic = function(progress) return progress * progress * progress end, easeOutCubic = function(progress) return 1 - (1 - progress)^3 end, easeInOutCubic = function(progress) if progress < 0.5 then return 4 * progress * progress * progress end return 1 - (-2 * progress + 2)^3 / 2 end, easeInQuart = function(progress) return progress * progress * progress * progress end, easeOutQuart = function(progress) return 1 - (1 - progress)^4 end, easeInOutQuart = function(progress) if progress < 0.5 then return 8 * progress * progress * progress * progress end return 1 - (-2 * progress + 2)^4 / 2 end, easeInQuint = function(progress) return progress * progress * progress * progress * progress end, easeOutQuint = function(progress) return 1 - (1 - progress)^5 end, easeInOutQuint = function(progress) if progress < 0.5 then return 16 * progress * progress * progress * progress * progress end return 1 - (-2 * progress + 2)^5 / 2 end, easeInSine = function(progress) return 1 - math.cos(progress * math.pi / 2) end, easeOutSine = function(progress) return math.sin(progress * math.pi / 2) end, easeInOutSine = function(progress) return -(math.cos(math.pi * progress) - 1) / 2 end, easeInExpo = function(progress) if progress == 0 then return 0 end return 2^(10 * progress - 10) end, easeOutExpo = function(progress) if progress == 1 then return 1 end return 1 - 2^(-10 * progress) end, easeInOutExpo = function(progress) if progress == 0 then return 0 end if progress == 1 then return 1 end if progress < 0.5 then return 2^(20 * progress - 10) / 2 end return (2 - 2^(-20 * progress + 10)) / 2 end, easeInCirc = function(progress) return 1 - math.sqrt(1 - progress * progress) end, easeOutCirc = function(progress) return math.sqrt(1 - (progress - 1) * (progress - 1)) end, easeInOutCirc = function(progress) if progress < 0.5 then return (1 - math.sqrt(1 - (2 * progress)^2)) / 2 end return (math.sqrt(1 - (-2 * progress + 2)^2) + 1) / 2 end, easeInBack = function(progress) local c1 = 1.70158 local c3 = c1 + 1 return c3 * progress * progress * progress - c1 * progress * progress end, easeOutBack = function(progress) local c1 = 1.70158 local c3 = c1 + 1 return 1 + c3 * (progress - 1)^3 + c1 * (progress - 1)^2 end, easeInOutBack = function(progress) local c1 = 1.70158 local c2 = c1 * 1.525 if progress < 0.5 then return ((2 * progress)^2 * ((c2 + 1) * 2 * progress - c2)) / 2 end return ((2 * progress - 2)^2 * ((c2 + 1) * (progress * 2 - 2) + c2) + 2) / 2 end, easeInElastic = function(progress) local c4 = (2 * math.pi) / 3 if progress == 0 then return 0 end if progress == 1 then return 1 end return -(2^(10 * progress - 10)) * math.sin((progress * 10 - 10.75) * c4) end, easeOutElastic = function(progress) local c4 = (2 * math.pi) / 3 if progress == 0 then return 0 end if progress == 1 then return 1 end return 2^(-10 * progress) * math.sin((progress * 10 - 0.75) * c4) + 1 end, easeInOutElastic = function(progress) local c5 = (2 * math.pi) / 4.5 if progress == 0 then return 0 end if progress == 1 then return 1 end if progress < 0.5 then return -(2^(20 * progress - 10) * math.sin((20 * progress - 11.125) * c5)) / 2 end return (2^(-20 * progress + 10) * math.sin((20 * progress - 11.125) * c5)) / 2 + 1 end, easeInBounce = function(progress) return 1 - easings.easeOutBounce(1 - progress) end, easeOutBounce = function(progress) local n1 = 7.5625 local d1 = 2.75 if progress < 1 / d1 then return n1 * progress * progress elseif progress < 2 / d1 then progress = progress - 1.5 / d1 return n1 * progress * progress + 0.75 elseif progress < 2.5 / d1 then progress = progress - 2.25 / d1 return n1 * progress * progress + 0.9375 else progress = progress - 2.625 / d1 return n1 * progress * progress + 0.984375 end end, easeInOutBounce = function(progress) if progress < 0.5 then return (1 - easings.easeOutBounce(1 - 2 * progress)) / 2 end return (1 + easings.easeOutBounce(2 * progress - 1)) / 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 self.timer = nil 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 return true end end --- Stops the animation immediately: cancels timers, completes running anim instances and clears the element property --- @shortDescription Stops the animation function Animation:stop() if self.timer then pcall(os.cancelTimer, self.timer) self.timer = nil end for _, seq in ipairs(self.sequences) do for _, anim in ipairs(seq) do pcall(function() if anim and anim.complete then anim:complete() end end) end end if self.element and type(self.element.set) == "function" then pcall(function() self.element.set("animation", nil) 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("resize", { start = function(anim) anim.startW = anim.element.get("width") anim.startH = anim.element.get("height") end, update = function(anim, progress) local w = anim.startW + (anim.args[1] - anim.startW) * progress local h = anim.startH + (anim.args[2] - anim.startH) * progress anim.element.set("width", math.floor(w)) anim.element.set("height", math.floor(h)) return progress >= 1 end, complete = function(anim) anim.element.set("width", anim.args[1]) anim.element.set("height", 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("number", { start = function(anim) anim.startValue = anim.element.get(anim.args[1]) anim.targetValue = anim.args[2] end, update = function(anim, progress) local value = anim.startValue + (anim.targetValue - anim.startValue) * progress anim.element.set(anim.args[1], math.floor(value)) return progress >= 1 end, complete = function(anim) anim.element.set(anim.args[1], anim.targetValue) end }) Animation.registerAnimation("entries", { start = function(anim) anim.startColor = anim.element.get(anim.args[1]) anim.colorList = anim.args[2] end, update = function(anim, progress) local list = anim.colorList local index = math.floor(#list * progress) + 1 if index > #list then index = #list end anim.element.set(anim.args[1], list[index]) end, complete = function(anim) anim.element.set(anim.args[1], anim.colorList[#anim.colorList]) 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.startText = anim.element.get(anim.args[1]) or "" anim.targetText = anim.args[2] or "" anim.startText = tostring(anim.startText) anim.targetText = tostring(anim.targetText) end, update = function(anim, progress) local w = anim.width if progress < 0.5 then local p = progress / 0.5 local offset = math.floor(w * p) local visible = (anim.startText:sub(offset + 1) .. string.rep(" ", w)):sub(1, w) anim.element.set(anim.args[1], visible) else local p = (progress - 0.5) / 0.5 local leftSpaces = math.floor(w * (1 - p)) local incoming = string.rep(" ", leftSpaces) .. anim.targetText local visible = incoming:sub(1, w) anim.element.set(anim.args[1], visible) end return progress >= 1 end, complete = function(anim) local final = (anim.targetText .. string.rep(" ", anim.width)) anim.element.set(anim.args[1], final) end }) Animation.registerAnimation("marquee", { start = function(anim) anim.width = anim.element.get("width") anim.text = tostring(anim.args[2] or "") anim.speed = tonumber(anim.args[3]) or 0.15 anim.offset = 0 anim.lastShift = -1 anim.padded = anim.text .. string.rep(" ", anim.width) end, update = function(anim, progress) local elapsed = os.epoch("local") / 1000 - anim.startTime local step = math.max(0.01, anim.speed) local shifts = math.floor(elapsed / step) if shifts ~= anim.lastShift then anim.lastShift = shifts local totalLen = #anim.padded local idx = (shifts % totalLen) + 1 local doubled = anim.padded .. anim.padded local visible = doubled:sub(idx, idx + anim.width - 1) anim.element.set(anim.args[1], visible) end return false end, complete = function(anim) end }) Animation.registerAnimation("custom", { start = function(anim) anim.callback = anim.args[1] if type(anim.callback) ~= "function" then error("custom animation requires a function as first argument") end end, update = function(anim, progress) local elapsed = os.epoch("local") / 1000 - anim.startTime anim.callback(anim.element, progress, elapsed) return progress >= 1 end, complete = function(anim) if anim.callback then anim.callback(anim.element, 1, anim.duration) end end }) --- Adds additional methods for VisualElement when adding animation plugin --- @class VisualElement local VisualElement = {hooks={}} ---@private function VisualElement.hooks.handleEvent(self, event, ...) if event == "timer" then local animation = self.get("animation") if animation then animation:event(event, ...) end end end ---@private function VisualElement.setup(element) element.defineProperty(element, "animation", {default = nil, type = "table"}) element.defineEvent(element, "timer") end -- Convenience to stop animations from the element function VisualElement.stopAnimation(self) local anim = self.get("animation") if anim and type(anim.stop) == "function" then anim:stop() else -- fallback: clear property self.set("animation", nil) end return self 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 }