diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..8491fc9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+.git/
+.vscode/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8e20d63
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2025 unreal79
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/ReadMe.md b/ReadMe.md
index bb4cd32..80d2c8a 100644
--- a/ReadMe.md
+++ b/ReadMe.md
@@ -1,3 +1,9 @@
+
+This repo hosts `animX` version with memory leak patch.
+
+Proof-of-concept demonstrating that `animX` (a LÖVE animation library) leaks memory is there: https://github.com/unreal79/animX-memory-leak
+
+
# animX
*Animation in Love2d has never been so easy!!* Now I hate to look like a soap salesman... but animX has [all the features](#features-of-animx) you'd expect from an animation library plus some extra features of its own! I suggest you head over to [A quick walkthrough](#a-quick-walkthrough) if you are short on time!
diff --git a/actor.lua b/actor.lua
index b3a52e6..1c97edd 100644
--- a/actor.lua
+++ b/actor.lua
@@ -4,9 +4,9 @@
]]
local Actor={
- animations, --list of all the animations in the actor
- current, --the current animation (by it's name)
- p_onAnimSwitch, --handler called when an animation is switched [internal]
+ animations = {}, --list of all the animations in the actor
+ current = nil, --the current animation (by it's name)
+ p_onAnimSwitch = function(...) end, --handler called when an animation is switched [internal]
}
--an internal function which just sets the default values
@@ -42,7 +42,7 @@ end
--Starts the animation for the actor
function Actor:startAnimation()
- if self:getCurrentAnimation() then
+ if not self:isActive() and self:getCurrentAnimation() then
self:getCurrentAnimation():start()
end
return self
@@ -94,7 +94,7 @@ function Actor:getWidth() return self:getCurrentAnimation():getWidth() end
function Actor:getHeight() return self:getCurrentAnimation():getHeight() end
--Sets the style of the image
-function Actor:setStyle(val) self:getCurrentAnimation():setStyle(val) return self end
+function Actor:setStyle(val) self:getCurrentAnimation():setStyle(val) return self end
--Flips the current animation
function Actor:flip(...) self:getCurrentAnimation():flip(...) return self end
@@ -105,6 +105,49 @@ function Actor:render(...)
self:getCurrentAnimation():render(...)
end
+--Update the current animation
+function Actor:update(dt)
+ if self:getCurrentAnimation() then
+ self:getCurrentAnimation():update(dt)
+ end
+ return self
+end
+
+--Cleanup resources to prevent memory leaks
+function Actor:destroy()
+ -- Stop current animation
+ if self:isActive() then
+ self:stopAnimation()
+ end
+
+ -- Get reference to animx module (should be available as global)
+ local animx = _G.animx
+
+ -- Destroy all animations and remove them from global list
+ for name, anim in pairs(self.animations) do
+ if anim then
+ -- Remove from global animx.animObjs list
+ if animx and animx.removeAnimation then
+ animx.removeAnimation(anim)
+ end
+
+ -- Destroy the animation
+ if anim.destroy then
+ anim:destroy()
+ end
+ end
+ end
+
+ -- Clear animations table
+ self.animations = {}
+ self.current = nil
+
+ -- Clear callback
+ self.p_onAnimSwitch = nil
+
+ return self
+end
+
--==Aliases==--
Actor.getCurrentAnim=Actor.getCurrentAnimation
@@ -113,4 +156,4 @@ Actor.changeAnim=Actor.switch
Actor.set=Actor.switch
Actor.draw=Actor.render
-return Actor
\ No newline at end of file
+return Actor
diff --git a/animation.lua b/animation.lua
index d1d3441..9df7a6b 100644
--- a/animation.lua
+++ b/animation.lua
@@ -5,31 +5,31 @@
--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]
+ 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 = 1, --the sense of the animation
+ curFrame = 1, --the current frame that's being rendered
+ active = false, --whether the animation is being played or not
+ curTimes = 0, --keeps count of number of times animation executed
+ timer = 0, --an internal timer variable
+ p_flipX = false, --whether to flip along x-axis [internal]
+ p_flipY = false, --whether to flip along y-axis [internal]
+ p_onCycleOver = function(...) end, --handler called when an animation cycle is complete [internal]
+ p_onAnimOver = function(...) end, --handler called when the entire animation is over [internal]
+ p_onAnimStart = function(...) end, --handler called when the animation is started [internal]
+ p_onAnimRestart = function(...) end, --handler called whenever the animation is restarted [internal]
+ p_onChange = function(...) end, --handler called whenever current frame is changed [internal]
+ p_onUpdate = function(...) end, --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:start()
self.mode={'loop',1}
end
@@ -205,7 +205,7 @@ end
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)
+ self.p_onAnimOver(self)
end
--gets the duration of the given frame or current frame if provided nil
@@ -227,14 +227,14 @@ end
function Animation:setDelay(frame,delay)
if not delay then
--Set delay for all frames
- delay,frame=frame
+ delay,frame=frame, nil
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
@@ -296,7 +296,7 @@ function Animation:update(dt)
if self.timer>delay then
self.timer=self.timer%delay
self:change()
- if self.curFrame>self:getSize() then
+ if self.curFrame>self:getSize() then
self:cycle()
if self:getMode()=='bounce' then
--If we are bouncing and we are done then stop
@@ -326,7 +326,7 @@ function Animation:update(dt)
elseif self.curFrame<1 then
self.curFrame=1
-
+
if self:getMode()=='bounce' then
self:cycle()
end
@@ -373,6 +373,30 @@ end
--This function is not defined here but on the main library file!
Animation.exportToXML=nil
+--Cleanup resources to prevent memory leaks
+function Animation:destroy()
+ -- Clear all callbacks
+ self.p_onAnimOver = nil
+ self.p_onCycleOver = nil
+ self.p_onAnimStart = nil
+ self.p_onAnimRestart = nil
+ self.p_onChange = nil
+ self.p_onUpdate = nil
+
+ -- Clear frames (quads will be garbage collected)
+ self.frames = {}
+ self.duration = {}
+ self.cache = {}
+
+ -- Clear texture reference
+ self.texture = nil
+
+ -- Mark as inactive
+ self.active = false
+
+ return self
+end
+
--Just setting some aliases- kinda my speciality
Animation.getTexture=Animation.getAtlas
diff --git a/init.lua b/init.lua
index 68db3bb..f3079f5 100644
--- a/init.lua
+++ b/init.lua
@@ -24,11 +24,11 @@ end
--Borrowed from [euler](https://github.com/YoungNeer/euler)
function round(value,precision)
- local temp = 10^(precision or 0)
- if value >= 0 then
+ local temp = 10^(precision or 0)
+ if value >= 0 then
return math.floor(value * temp + 0.5) / temp
- else
- return math.ceil(value * temp - 0.5) / temp
+ else
+ return math.ceil(value * temp - 0.5) / temp
end
end
@@ -36,7 +36,7 @@ end
local function removePath(filename)
local pos=1
local i = string.find(filename,'[\\/]', pos)
- pos=i
+ pos=i or 1
while i do
i = string.find(filename,'[\\/]', pos)
if i then
@@ -127,7 +127,7 @@ function animx.newAnimation(params)
]]--
--if user has not given sprites per row then let qh simply be image height
- if not spr then
+ if not spr then
qh=qh or img:getHeight()
else
if not qh then
@@ -151,7 +151,7 @@ function animx.newAnimation(params)
--If user has given us some quads to work with - we set this to zero if nil
nq=nq or 0
end
-
+
spr = spr or nq
--If user has not given anything dissecting then make the image a quad!
@@ -196,7 +196,7 @@ function animx.newAnimation(params)
assert(qw and qh,"animX Error: Quad dimensions coudn't be calculated in `newAnimation`!")
--IMPORTANT: We want integers not highly precise floats or doubles
qw,qh=round(qw),round(qh)
- end
+ end
--Calculate offset from the startpoint
if startPoint then
@@ -231,7 +231,11 @@ function animx.newAnimation(params)
['texture']=img,
['frames']=quads
}
- table.insert(animx.animObjs,setmetatable(animation_obj,{__index=Animation}))
+ setmetatable(animation_obj,{__index=Animation})
+
+ -- Always add to global list for animx.update() to work
+ table.insert(animx.animObjs, animation_obj)
+
animation_obj:onAnimStart(onAnimStart):init(startingFrame,delay)
animation_obj
:onAnimOver(onAnimOver)
@@ -239,8 +243,8 @@ function animx.newAnimation(params)
:onChange(onChange)
:onCycleOver(onCycleOver)
:onUpdate(onUpdate)
-
- return animx.animObjs[#animx.animObjs]
+
+ return animation_obj
end
--[[:-
@@ -300,7 +304,7 @@ end
The actor itself
]]--
function Actor:addAnimation(name,anim)
- if anim.cache and anim.direction then
+ if anim.cache and anim.direction then
--User provided an already created animation
self.animations[name]=anim
else
@@ -325,18 +329,21 @@ function animx.newAnimationXML(image,filename)
local _, frameNo = string.match(line, "name=([\"'])(.-)%1")
frameNo=tonumber(frameNo)
--Frames must start from 1!
- if not frameNo or frameNo<=0 then goto continue end
+ if not frameNo or frameNo<=0 then
+ print("avoid to use goto in anumX")
+ -- goto continue
+ else
+ assert(not t[frameNo],
+ "animX Error!! Duplicate Frames found for ("..frameNo..") for "..filename
+ )
+ local _, x = string.match(line, "x=([\"'])(.-)%1")
+ local _, y = string.match(line, "y=([\"'])(.-)%1")
+ local _, width = string.match(line, "width=([\"'])(.-)%1")
+ local _, height = string.match(line, "height=([\"'])(.-)%1")
- assert(not t[frameNo],
- "animX Error!! Duplicate Frames found for ("..frameNo..") for "..filename
- )
- local _, x = string.match(line, "x=([\"'])(.-)%1")
- local _, y = string.match(line, "y=([\"'])(.-)%1")
- local _, width = string.match(line, "width=([\"'])(.-)%1")
- local _, height = string.match(line, "height=([\"'])(.-)%1")
-
- t[frameNo]=love.graphics.newQuad(x,y,width,height,sw,sh)
- ::continue::
+ t[frameNo]=love.graphics.newQuad(x,y,width,height,sw,sh)
+ end
+ -- ::continue::
end
i=i+1
end
@@ -359,19 +366,22 @@ function animx.newActorXML(image,filename)
local animName=frameNo:match('[%a ]+')
frameNo=tonumber(frameNo:match('%d+'))
--Frames must exist and must start from 1! Also animation name must be present
- if not animName or not frameNo or frameNo<=0 then goto continue end
+ if not animName or not frameNo or frameNo<=0 then
+ -- goto continue
+ print("avoid to use goto in anumX")
+ else
+ if not t[animName] then t[animName]={} end
+ assert(not t[animName][frameNo],
+ "animX Error!! Duplicate Frames found for ("..frameNo..") for "..filename
+ )
+ local _, x = string.match(line, "x=([\"'])(.-)%1")
+ local _, y = string.match(line, "y=([\"'])(.-)%1")
+ local _, width = string.match(line, "width=([\"'])(.-)%1")
+ local _, height = string.match(line, "height=([\"'])(.-)%1")
- if not t[animName] then t[animName]={} end
- assert(not t[animName][frameNo],
- "animX Error!! Duplicate Frames found for ("..frameNo..") for "..filename
- )
- local _, x = string.match(line, "x=([\"'])(.-)%1")
- local _, y = string.match(line, "y=([\"'])(.-)%1")
- local _, width = string.match(line, "width=([\"'])(.-)%1")
- local _, height = string.match(line, "height=([\"'])(.-)%1")
-
- t[animName][frameNo]=love.graphics.newQuad(x,y,width,height,sw,sh)
- ::continue::
+ t[animName][frameNo]=love.graphics.newQuad(x,y,width,height,sw,sh)
+ end
+ -- ::continue::
end
i=i+1
end
@@ -388,7 +398,7 @@ end
function Animation:exportToXML(filename)
filename=removePath(filename)
if fileExists(filename) then
- if not animx.hideWarnings then
+ if not animx.hideWarnings then
error(string.format("animX Warning! File '%s' Already Exists!",filename))
end
end
@@ -402,17 +412,21 @@ function Animation:exportToXML(filename)
end
local sname,x,y,width,height
- outfile:write(string.format('\n',removeExtension(filename)))
- for i=1,#self.frames do
- x,y,width,height=self.frames[i]:getViewport()
- outfile:write(
- string.format('\t\n',
- i,x,y,width,height
+ if outfile then
+ outfile:write(string.format('\n',removeExtension(filename)))
+ for i=1,#self.frames do
+ x,y,width,height=self.frames[i]:getViewport()
+ outfile:write(
+ string.format('\t\n',
+ i,x,y,width,height
+ )
)
- )
+ end
+ outfile:write("")
+ return outfile:close()
+ else
+ error("animx Error! Something's wrong with the io")
end
- outfile:write("")
- return outfile:close()
end
--[[
@@ -425,7 +439,7 @@ end
function Actor:exportToXML(filename)
filename=removePath(filename)
if fileExists(filename) then
- if not animx.hideWarnings then
+ if not animx.hideWarnings then
error(string.format("animX Warning! File '%s' Already Exists!",filename))
end
end
@@ -439,30 +453,60 @@ function Actor:exportToXML(filename)
end
local sname,x,y,width,height
- outfile:write(string.format('\n',removeExtension(filename)))
- for anim in pairs(self.animations) do
- for i=1,#self.animations[anim].frames do
- x,y,width,height=self.animations[anim].frames[i]:getViewport()
- outfile:write(
- string.format('\t\n',
- anim..i,x,y,width,height
+ if outfile then
+ outfile:write(string.format('\n',removeExtension(filename)))
+ for anim in pairs(self.animations) do
+ for i=1,#self.animations[anim].frames do
+ x,y,width,height=self.animations[anim].frames[i]:getViewport()
+ outfile:write(
+ string.format('\t\n',
+ anim..i,x,y,width,height
+ )
)
- )
+ end
end
+ outfile:write("")
+ return outfile:close()
+ else
+ error("animx Error! Something's wrong with the io")
end
- outfile:write("")
- return outfile:close()
end
--Updates all the animation objects at once so you won't see them in your code
function animx.update(dt)
for i=1,#animx.animObjs do
- animx.animObjs[i]:update(dt)
+ if animx.animObjs[i] then
+ animx.animObjs[i]:update(dt)
+ end
+ end
+end
+
+--Manually clear the image cache (useful for freeing memory)
+function animx.clearImageCache()
+ for k in pairs(imgCache) do
+ imgCache[k] = nil
+ end
+ collectgarbage("collect")
+end
+
+--Remove a specific animation from the global list
+function animx.removeAnimation(anim)
+ for i = #animx.animObjs, 1, -1 do
+ if animx.animObjs[i] == anim then
+ table.remove(animx.animObjs, i)
+ return true
+ end
end
+ return false
+end
+
+--Clear all animations (useful for scene changes)
+function animx.clearAllAnimations()
+ animx.animObjs = {}
end
-love.update=function(dt) animx.update(dt) end
-animx.newAnimatedSprite=animx.newActor
+-- love.update=function(dt) animx.update(dt) end
+-- animx.newAnimatedSprite=animx.newActor -- ?
return animx