396 lines
12 KiB
Lua
396 lines
12 KiB
Lua
|
--[[
|
||
|
The Animation Class for animX
|
||
|
Author: Neer
|
||
|
]]
|
||
|
|
||
|
--Unlike iffy we use metatables
|
||
|
local Animation={
|
||
|
texture, --the spritesheet for the animation
|
||
|
frames, --the frames in the animation
|
||
|
duration, --duration for each sprite- a smart table (idea stolen from Walt)
|
||
|
cache, --an internal variable to account for same duration across multiple frames
|
||
|
mode, --the mode of the animation - {'loop'/'rewind'/'once'/'bounce',times}
|
||
|
direction, --the sense of the animation
|
||
|
curFrame, --the current frame that's being rendered
|
||
|
active, --whether the animation is being played or not
|
||
|
curTimes, --keeps count of number of times animation executed
|
||
|
timer, --an internal timer variable
|
||
|
p_flipX, --whether to flip along x-axis [internal]
|
||
|
p_flipY, --whether to flip along y-axis [internal]
|
||
|
p_onCycleOver, --handler called when an animation cycle is complete [internal]
|
||
|
p_onAnimOver, --handler called when the entire animation is over [internal]
|
||
|
p_onAnimStart, --handler called when the animation is started [internal]
|
||
|
p_onAnimRestart, --handler called whenever the animation is restarted [internal]
|
||
|
p_onChange, --handler called whenever current frame is changed [internal]
|
||
|
p_onUpdate, --called at every frame regardless of it's active property [internal]
|
||
|
}
|
||
|
|
||
|
--an internal function which just sets the default values
|
||
|
function Animation:init(startingFrame,delay)
|
||
|
self:setDelay(delay)
|
||
|
self.startingFrame=startingFrame
|
||
|
self:start()
|
||
|
self.mode={'loop',1}
|
||
|
end
|
||
|
|
||
|
--[[the addFrame concept was stolen from BartBes' animation library. Anyways -
|
||
|
what this func does is create a new quad and associate it with the given
|
||
|
animation. The frame would be appended to the last of the framelist!
|
||
|
]]
|
||
|
function Animation:addFrame(x,y,w,h,delay)
|
||
|
if y then
|
||
|
--User passed in position of the quad
|
||
|
table.insert(self.frames,
|
||
|
love.graphics.newQuad(x,y,w,h,self:getAtlas():getDimensions())
|
||
|
)
|
||
|
else
|
||
|
--User passed in a quad
|
||
|
table.insert(self.frames,x)
|
||
|
end
|
||
|
|
||
|
--Note we are not calling setDelay cause it's a new frame!
|
||
|
if delay and delay~=self.duration[#self.duration] then
|
||
|
self.duration[#self.duration+1]=delay
|
||
|
self.cache[#self.cache+1]=1
|
||
|
else
|
||
|
self.cache[#self.cache]=self.cache[#self.cache]+1
|
||
|
end
|
||
|
end
|
||
|
|
||
|
--Not to be confused with p_onAnimOver!
|
||
|
function Animation:onAnimOver(fn)
|
||
|
self.p_onAnimOver=fn or function() end
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
--Not to be confused with p_onCycleOver!
|
||
|
function Animation:onCycleOver(fn)
|
||
|
self.p_onCycleOver=fn or function() end
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
--Not to be confused with p_onAnimStart!
|
||
|
function Animation:onAnimStart(fn)
|
||
|
self.p_onAnimStart=fn or function() end
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
--Not to be confused with p_onAnimRestart!
|
||
|
function Animation:onAnimRestart(fn)
|
||
|
self.p_onAnimRestart=fn or function() end
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
--Not to be confused with p_onChange!
|
||
|
function Animation:onChange(fn)
|
||
|
self.p_onChange=fn or function() end
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
--Not to be confused with p_onUpdate!
|
||
|
function Animation:onUpdate(fn)
|
||
|
self.p_onUpdate=fn or function() end
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
--Rewind the animation any number of times, (nil or negative)=>infinite
|
||
|
function Animation:rewind(times)
|
||
|
self.mode={'rewind',times or -1}
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
--Similar to rewind except the definition of 'cycle' is different!
|
||
|
--also `bounce` by default reverses only once while `rewind` rewinds forever
|
||
|
function Animation:bounce(times)
|
||
|
self.mode={'bounce',times or 1}
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
--loops the animation in obverse or reverse direction (obverse by default)
|
||
|
function Animation:loop(times,dir)
|
||
|
self.mode={'loop',times or -1}
|
||
|
self.direction=dir or 1
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
--same as loop just direction is always reversed
|
||
|
function Animation:reverse(times)
|
||
|
return self:loop(times,-1)
|
||
|
end
|
||
|
|
||
|
--Executes an animation once in the obverse direction!
|
||
|
function Animation:once()
|
||
|
return self:loop(1,1)
|
||
|
end
|
||
|
|
||
|
--restarts the animation from where it was
|
||
|
function Animation:restart()
|
||
|
self.active=true
|
||
|
self.curFrame=self.startingFrame
|
||
|
self.curTimes=0
|
||
|
self.p_onAnimRestart(self)
|
||
|
end
|
||
|
|
||
|
--Returns the total number of frames in animation
|
||
|
function Animation:getSize() return #self.frames end
|
||
|
|
||
|
--Returns the total number of times the animation has been played
|
||
|
function Animation:getTimes() return self.curTimes end
|
||
|
|
||
|
--Returns the current Frame of the animation (a number)
|
||
|
function Animation:getCurrentFrame() return math.max(1,math.min(self.curFrame,self:getSize())) end
|
||
|
|
||
|
--Returns the current quad that's being rendered (a Quad)
|
||
|
function Animation:getCurrentQuad() return self.frames[self:getCurrentFrame()] end
|
||
|
|
||
|
--Returns whether or not is the animation active!
|
||
|
function Animation:isActive() return self.active end
|
||
|
|
||
|
--Returns the dimensions of the current frame
|
||
|
function Animation:getDimensions() local x,y,w,h=self:getCurrentQuad():getViewport(); return w,h end
|
||
|
function Animation:getWidth() return select(1,self:getDimensions()) end
|
||
|
function Animation:getHeight() return select(2,self:getDimensions()) end
|
||
|
|
||
|
function Animation:setStyle(style)
|
||
|
assert(style=='rough' or style=='smooth',"animX Error!! Expected 'smooth' or 'rough' in setStyle fn")
|
||
|
if style=='rough' then
|
||
|
self:getImage():setFilter('nearest','nearest')
|
||
|
else
|
||
|
self:getImage():setFilter('linear','linear')
|
||
|
end
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
--Returns the texture for the animation (an Image)
|
||
|
function Animation:getAtlas() return self.texture end
|
||
|
|
||
|
--Sometimes you may want to change the atlas (maybe for something like character clothes)
|
||
|
--The next fn changes the texture for the animation
|
||
|
function Animation:setAtlas(img)
|
||
|
if type(img)=='string' then img=love.graphics.newImage(img) end
|
||
|
self.texture = img
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
--Sets the current frame as n
|
||
|
function Animation:jumpToFrame(n)
|
||
|
assert(n>=1 and n<=self:getSize(), "animX Error: Frame is out of bounds!")
|
||
|
self.curFrame=n
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
--To be called only when an animation just starts (private fn)
|
||
|
function Animation:start()
|
||
|
self.p_onAnimStart(self)
|
||
|
self.curFrame=self.startingFrame
|
||
|
self.direction=1
|
||
|
self.active=true
|
||
|
self.timer=0
|
||
|
self.curTimes=0
|
||
|
end
|
||
|
|
||
|
--To be called only when an animation has completed a 'cycle' (private fn)
|
||
|
function Animation:cycle()
|
||
|
self.curTimes=self.curTimes+1
|
||
|
self.p_onCycleOver(self)
|
||
|
end
|
||
|
|
||
|
--To be called only when the current frame has to be changed (private fn)
|
||
|
function Animation:change()
|
||
|
self.curFrame=self.curFrame+self.direction
|
||
|
self.p_onChange(self)
|
||
|
end
|
||
|
|
||
|
--Stops the animation! Use anim.active=false if you don't want to trigger any callback!
|
||
|
function Animation:stop()
|
||
|
--It is important that active is first set to false and then handler is called
|
||
|
self.active=false
|
||
|
self.p_onAnimOver(self)
|
||
|
end
|
||
|
|
||
|
--gets the duration of the given frame or current frame if provided nil
|
||
|
function Animation:getDelay(frame)
|
||
|
frame=frame or self:getCurrentFrame()
|
||
|
--u for cache and v for duration
|
||
|
local u,v=1,1
|
||
|
assert(frame>=1 and frame<=self:getSize(),"animX Error: Frame is out of bounds!")
|
||
|
for i=1,self:getSize() do
|
||
|
if u>self.cache[v] then u=1 v=v+1 end
|
||
|
if i==frame then
|
||
|
return self.duration[v]
|
||
|
end
|
||
|
u=u+1
|
||
|
end
|
||
|
end
|
||
|
|
||
|
--sets the duration of the given frame or all frames if only one arg
|
||
|
function Animation:setDelay(frame,delay)
|
||
|
if not delay then
|
||
|
--Set delay for all frames
|
||
|
delay,frame=frame
|
||
|
self.duration={delay}
|
||
|
self.cache={self:getSize()}
|
||
|
return self
|
||
|
end
|
||
|
--Set delay for only one frame
|
||
|
assert(frame>=1 and frame<=self:getSize(),"animX Error: Frame is out of bounds!")
|
||
|
|
||
|
local u,v=1,1
|
||
|
|
||
|
for i=1,self:getSize() do
|
||
|
if u>self.cache[v] then u=1 v=v+1 end
|
||
|
if i==frame then
|
||
|
--If same delay as before then breakout!
|
||
|
if delay==self.duration[v] then break end
|
||
|
if self.cache[v]==1 then
|
||
|
--This is the most simple case (when no buffer)
|
||
|
self.duration[v]=delay
|
||
|
--so in other cases self.cache[v] is asserted to be >1
|
||
|
elseif u==1 then
|
||
|
--for beginning of the buffer
|
||
|
table.insert(self.duration,v+1,self.duration[v])
|
||
|
table.insert(self.cache,v+1,self.cache[v]-1)
|
||
|
self.duration[v],self.cache[v]=delay,1
|
||
|
elseif self.cache[v]==u then
|
||
|
--for ending of the buffer
|
||
|
table.insert(self.duration,v+1,delay)
|
||
|
table.insert(self.cache,v+1,1)
|
||
|
self.cache[v]=self.cache[v]-1
|
||
|
else
|
||
|
--at the middle of the buffer (darn it!)
|
||
|
table.insert(self.duration,v+1,self.duration[v])
|
||
|
table.insert(self.duration,v+1,delay)
|
||
|
table.insert(self.cache,v+1,self.cache[v]-u)
|
||
|
table.insert(self.cache,v+1,1)
|
||
|
self.cache[v]=self.cache[v]-1
|
||
|
end
|
||
|
end
|
||
|
u=u+1
|
||
|
end
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
--gets the current animation mode
|
||
|
function Animation:getMode()
|
||
|
return self.mode and self.mode[1]
|
||
|
end
|
||
|
|
||
|
--check if current mode is equal to one of the given modes
|
||
|
function Animation:isMode(...)
|
||
|
for i=1,select('#',...) do
|
||
|
if self.mode[1]==select(i,...) then
|
||
|
return true
|
||
|
end
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function Animation:update(dt)
|
||
|
self:p_onUpdate(dt)
|
||
|
if not self.active then return end
|
||
|
|
||
|
self.timer=self.timer+dt
|
||
|
|
||
|
--Get the delay for the current frame!
|
||
|
local delay=self:getDelay(self.curFrame)
|
||
|
|
||
|
if self.timer>delay then
|
||
|
self.timer=self.timer%delay
|
||
|
self:change()
|
||
|
if self.curFrame>self:getSize() then
|
||
|
self:cycle()
|
||
|
if self:getMode()=='bounce' then
|
||
|
--If we are bouncing and we are done then stop
|
||
|
if self.mode[2]>0 and self.curTimes>=self.mode[2] then return self:stop() end
|
||
|
end
|
||
|
if self:isMode('bounce','rewind') then
|
||
|
--If we are bouncing and we are NOT done or if we are rewinding then continue
|
||
|
self.curFrame=self:getSize()
|
||
|
self.direction=-1
|
||
|
elseif self:getMode()=='loop' then
|
||
|
--If we are looping in the obverse direction and we are done then stop
|
||
|
--Regardless of whether we are done reset the current frame back to 1
|
||
|
if self.direction==1 then
|
||
|
self.curFrame=1
|
||
|
if self.mode[2]>0 and self.curTimes>=self.mode[2] then return self:stop() end
|
||
|
end
|
||
|
else
|
||
|
--I think I was drunk when I wrote this section but I'm letting it be
|
||
|
--just in case I wasn't drunk!
|
||
|
if self.mode[2]>0 and self.curTimes>=self.mode[2] then
|
||
|
return self:stop()
|
||
|
else
|
||
|
self.curFrame=self:getSize()
|
||
|
self:cycle()
|
||
|
end
|
||
|
end
|
||
|
|
||
|
elseif self.curFrame<1 then
|
||
|
self.curFrame=1
|
||
|
|
||
|
if self:getMode()=='bounce' then
|
||
|
self:cycle()
|
||
|
end
|
||
|
if self:isMode('bounce','rewind') then
|
||
|
if self.mode[2]>0 and self.curTimes>=self.mode[2] then self:stop()
|
||
|
else
|
||
|
self.direction=1
|
||
|
end
|
||
|
elseif self:getMode()=='loop' then
|
||
|
if self.mode[2]>0 and self.curTimes>=self.mode[2] then self:stop()
|
||
|
else
|
||
|
self:cycle()
|
||
|
if self.mode[2]>0 and self.curTimes==self.mode[2] then return self:stop() end
|
||
|
self.curFrame=self:getSize()
|
||
|
end
|
||
|
else
|
||
|
self:stop()
|
||
|
end
|
||
|
end
|
||
|
self.curFrame=math.max(1,self.curFrame)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
--Whether to flip horizontally and/or vertically
|
||
|
function Animation:flip(flipX,flipY)
|
||
|
self.p_flipX,self.p_flipY=flipX,flipY
|
||
|
return self
|
||
|
end
|
||
|
|
||
|
--Specifically flip only along one axis!
|
||
|
function Animation:flipX(b) return self:flip(b) end
|
||
|
function Animation:flipY(b) return self:flip(nil,b) end
|
||
|
|
||
|
function Animation:render(x,y,r,sx,sy,ox,oy,...)
|
||
|
sx,sy=sx or 1, sy or 1
|
||
|
sx=self.p_flipX and -sx or sx
|
||
|
sy=self.p_flipY and -sy or sy
|
||
|
love.graphics.draw(
|
||
|
self:getTexture(),self:getCurrentQuad(),
|
||
|
x,y,r,sx,sy,ox,oy,...
|
||
|
)
|
||
|
end
|
||
|
|
||
|
--This function is not defined here but on the main library file!
|
||
|
Animation.exportToXML=nil
|
||
|
|
||
|
--Just setting some aliases- kinda my speciality
|
||
|
|
||
|
Animation.getTexture=Animation.getAtlas
|
||
|
Animation.getImage=Animation.getAtlas
|
||
|
Animation.getSource=Animation.getAtlas
|
||
|
Animation.setTexture=Animation.setAtlas
|
||
|
Animation.setSource=Animation.setAtlas
|
||
|
Animation.setImage=Animation.setAtlas
|
||
|
Animation.getTotalFrames=Animation.getTimes
|
||
|
Animation.draw=Animation.render
|
||
|
Animation.goToFrame=Animation.jumpToFrame
|
||
|
Animation.setFrame=Animation.jumpToFrame
|
||
|
Animation.getActive=Animation.isActive
|
||
|
Animation.onAnimationStart=Animation.onAnimStart
|
||
|
Animation.onAnimationOver=Animation.onAnimOver
|
||
|
Animation.onAnimationRestart=Animation.onAnimRestart
|
||
|
Animation.onFrameChange=Animation.onChange
|
||
|
Animation.onTick=Animation.onUpdate
|
||
|
|
||
|
return Animation
|