ExperimentalGear/scripts/api/animation.lua

203 lines
6.1 KiB
Lua

require "common.class"
require "api.graphics"
local Image = require "api.image"
---@class AnimationParams
---@field fps number?
---@field loop boolean?
---@field loopPoint integer?
---@field width number?
---@field height number?
---@field x number?
---@field y number?
---@field scaleX number?
---@field scaleY number?
---@field centered boolean?
---@field blendOp integer?
---@field color number[]?
---@field alpha number?
---@field stroke StrokeParams?
---@class Animation
---@field frames Image[]
---@field frameCount integer
---@field frameTime number
---@field loop boolean
---@field loopPoint integer
---@field width number?
---@field height number?
---@field x number?
---@field y number?
---@field scaleX number?
---@field scaleY number?
---@field centered boolean?
---@field blendOp integer?
---@field color number[]?
---@field alpha number?
---@field stroke StrokeParams?
local Animation = { };
---@class AnimationState
---@field animation Animation # The animation data this state is playing through
---@field frameIndex integer # Current frame in the animation
---@field timer number # Timer used to determine when to change to the next frame
---@field running boolean # Is the animation currently running and accepting updates?
---@field callback function? # Called when the animation completes
local AnimationState = { };
local function loadSequentialAnimationFrames(animPath)
local frames = { };
local count = 0;
local detectedFormat = nil;
while (true) do
local frame = nil;
if (detectedFormat) then
frame = Image.new(detectedFormat:format(animPath, count + 1), true);
else
for i = 1, 4 do
local format = '%s/%0' .. i .. 'd.png';
frame = Image.new(format:format(animPath, count + 1), true);
if (frame) then
detectedFormat = format;
break;
end
end
end
if (not frame) then
break;
end
count = count + 1;
frames[count] = frame;
end
return frames, count;
end
---Animation constructor
---@param animPath string
---@param params AnimationParams
---@return Animation
function Animation.new(animPath, params)
local frames, frameCount = loadSequentialAnimationFrames(animPath);
local instance = {
frames = frames,
frameCount = frameCount,
frameTime = 1 / (params.fps or 30),
loop = params.loop or false,
loopPoint = params.loopPoint or 1,
};
if (params.width ~= nil) then instance.width = params.width; end
if (params.height ~= nil) then instance.height = params.height; end
if (params.x ~= nil) then instance.x = params.x; end
if (params.y ~= nil) then instance.y = params.y; end
if (params.scaleX ~= nil) then instance.scaleX = params.scaleX; end
if (params.scaleY ~= nil) then instance.scaleY = params.scaleY; end
if (params.centered ~= nil) then instance.centered = params.centered; end
if (params.blendOp ~= nil) then instance.blendOp = params.blendOp; end
if (params.color ~= nil) then instance.color = params.color; end
if (params.alpha ~= nil) then instance.alpha = params.alpha; end
if (params.stroke ~= nil) then instance.stroke = params.stroke; end
return CreateInstance(Animation, instance);
end
---Create an AnimationState to play this animation.
---The AnimationState is not started.
---@param callback function?
---@return AnimationState
function Animation:createState(callback)
---@type AnimationState
local state = { animation = self, callback = callback, frameIndex = 1, timer = 0, running = false };
return CreateInstance(AnimationState, state);
end
---Create an AnimationState to play this animation and start it.
---@param callback function?
---@return AnimationState
function Animation:start(callback)
local state = self:createState(callback);
state:start();
return state;
end
---Start this AnimationState.
---Does nothing if it's already running.
function AnimationState:start()
self.running = true;
end
---Restart this AnimationState.
---The frame index is reset to 1.
function AnimationState:restart()
self.running = true;
self.frameIndex = 1;
self.timer = 0;
end
---Stop this AnimationState.
function AnimationState:stop()
self.running = false;
end
---Updates this AnimationState and then rendersit, passing on the given ImageParams to each frame.
---@param deltaTime number
---@param params? ImageParams
function AnimationState:render(deltaTime, params)
if (not self.running) then return; end;
self.timer = self.timer + deltaTime;
while (self.timer > self.animation.frameTime) do
self.timer = self.timer - self.animation.frameTime;
self.frameIndex = self.frameIndex + 1;
if (self.frameIndex > self.animation.frameCount) then
if (self.animation.loop) then
self.frameIndex = self.animation.loopPoint;
else
self.running = false;
if (self.callback) then
self.callback();
end
return;
end
end
end
if (params) then
if (params.width == nil) then params.width = self.animation.width; end
if (params.height == nil) then params.height = self.animation.height; end
if (params.x == nil) then params.x = self.animation.x; end
if (params.y == nil) then params.y = self.animation.y; end
if (params.scaleX == nil) then params.scaleX = self.animation.scaleX; end
if (params.scaleY == nil) then params.scaleY = self.animation.scaleY; end
if (params.centered == nil) then params.centered = self.animation.centered; end
if (params.blendOp == nil) then params.blendOp = self.animation.blendOp; end
if (params.alpha == nil) then params.alpha = self.animation.alpha; end
if (params.stroke == nil) then params.stroke = self.animation.stroke; end
end
local frame = self.animation.frames[self.frameIndex];
if (not frame) then
-- TODO(local): what do
else
frame:render(params);
end
end
return Animation;