Files
Basalt2/src/plugins/animation.lua
Robert Jelic 3009df028b Updated Eventsystem
Improved List with multiple Selections
2025-02-21 18:24:19 +01:00

428 lines
14 KiB
Lua

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
}