Coder Social home page Coder Social logo

anim8's Introduction

anim8

Animation library for LÖVE.

Build Status Coverage Status

In order to build animations more easily, anim8 divides the process in two steps: first you create a grid, which is capable of creating frames (Quads) easily and quickly. Then you use the grid to create one or more animations.

LÖVE compatibility

Since anim8 uses LÖVE's graphical functions, and they change from version to version, you must choose the version of anim8 which is compatible with your LÖVE.

  • The current version of anim8 is v2.1. It is compatible with LÖVE 0.9.x and 0.10.x
  • The last version of anim8 compatible with LÖVE 0.8.x was anim8 v2.0.

Example

local anim8 = require 'anim8'

local image, animation

function love.load()
  image = love.graphics.newImage('path/to/image.png')
  local g = anim8.newGrid(32, 32, image:getWidth(), image:getHeight())
  animation = anim8.newAnimation(g('1-8',1), 0.1)
end

function love.update(dt)
  animation:update(dt)
end

function love.draw()
  animation:draw(image, 100, 200)
end

You can see a more elaborated example in the demo branch.

That demo transforms this spritesheet:

1945

Into several animated objects:

1945

Explanation

Grids

Grids have only one purpose: To build groups of quads of the same size as easily as possible. In order to do this, they need to know only 2 things: the size of each quad and the size of the image they will be applied to. Each size is a width and a height, and those are the first 4 parameters of @anim8.newGrid@.

Grids are just a convenient way of getting frames from a sprite. Frames are assumed to be distributed in rows and columns. Frame 1,1 is the one in the first row, first column.

This is how you create a grid:

anim8.newGrid(frameWidth, frameHeight, imageWidth, imageHeight, left, top, border):
  • frameWidth and frameHeight are the dimensions of the animation frames - each of the individual "sub-images" that compose the animation. They are usually the same size as your character (so if the character is 32x32 pixels, frameWidth is 32 and so is frameHeight)
  • imageWidth and imageHeight are the dimensions of the image where all the frames are. In LÖVE, they can be obtained by doing image:getWidth() and image:getHeight().
  • left and top are optional, and both default to 0. They are "the left and top coordinates of the point in the image where you want to put the origin of coordinates of the grid". If all the frames in your grid are the same size, and the first one's top-left corner is 0,0, you probably won't need to use left or top.
  • border is also an optional value, and it also defaults to zero. What border does is allowing you to define "gaps" between your frames in the image. For example, imagine that you have frames of 32x32, but they have a 1-px border around each frame. So the first frame is not at 0,0, but at 1,1 (because of the border), the second one is at 1,33 (for the extra border) etc. You can take this into account and "skip" these borders.

To see this a bit more graphically, here are what those values mean for the grid which contains the "submarine" frames in the demo:

explanation

Grids only have one important method: Grid:getFrames(...).

Grid:getFrames accepts an arbitrary number of parameters. They can be either numbers or strings.

  • Each two numbers are interpreted as quad coordinates in the format (column, row). This way, grid:getFrames(3,4) will return the frame in column 3, row 4 of the grid. There can be more than just two: grid:getFrames(1,1, 1,2, 1,3) will return the frames in {1,1}, {1,2} and {1,3} respectively.
  • Using numbers for long rows or columns is tedious - so grids also accept strings indicating range plus a row/column index. Diferentiating rows and columns is based on the order in which the range and index are provided. A row can be fetch by calling grid:getFrames('range', rowNumber) and a column by calling grid:getFrames(columnNumber, 'range'). The previous column of 3 elements, for example, can be also expressed like this: grid:getFrames(1,'1-3'). Again, there can be more than one string-index pair (grid:getFrames(1,'1-3', '2-4',3))
  • It's also possible to combine both formats. For example: grid:getFrames(1,4, 1,'1-3') will get the frame in {1,4} plus the frames 1 to 3 in column 1

But you will probably never use getFrames directly. You can use a grid as if it was a function, and getFrames will be called. In other words, given a grid called g, this:

g:getFrames('2-8',1, 1,2)

Is equivalent to this:

g('2-8',1, 1,2)

This is very convenient to use in animations.

Let's consider the submarine in the previous example. It has 7 frames, arranged horizontally.

If you make its grid start on its first frame (using left and top), you can get its frames like this:

                           -- frame, image,    offsets, border
    local gs = anim8.newGrid(32,98, 1024,768,  366,102,   1)

    local frames = gs('1-7',1)

However that way you will get a submarine which "emerges", then "suddenly disappears", and emerges again. To make it look more natural, you must add some animation frames "backwards", to give the illusion of "submersion". Here's the complete list:

local frames = gs('1-7',1, '6-2',1)

Animations

Animations are groups of frames that are interchanged every now and then.

local animation = anim8.newAnimation(frames, durations, onLoop)
  • frames is an array of frames (Quads in LÖVE argot). You could provide your own quad array if you wanted to, but using a grid to get them is very convenient.
  • durations is a number or a table. When it's a number, it represents the duration of all frames in the animation. When it's a table, it can represent different durations for different frames. You can specify durations for all frames individually, like this: {0.1, 0.5, 0.1} or you can specify durations for ranges of frames: {['3-5']=0.2}.
  • onLoop is an optional parameter which can be a function or a string representing one of the animation methods. It does nothing by default. If specified, it will be called every time an animation "loops". It will have two parameters: the animation instance, and how many loops have been elapsed. The most usual value (apart from none) is the string 'pauseAtEnd'. It will make the animation loop once and then pause and stop on the last frame.

Animations have the following methods:

animation:update(dt)

Use this inside love.update(dt) so that your animation changes frames according to the time that has passed.

animation:draw(image, x,y, angle, sx, sy, ox, oy, kx, ky)

Draws the current frame in the specified coordinates with the right angle, scale, offset & shearing. These parameters work exactly the same way as in love.graphics.draw. The only difference is that they are properly recalculated when the animation is flipped horizontally, vertically or both. See getFrameInfo below for more details.

animation:gotoFrame(frame)

Moves the animation to a given frame (frames start counting in 1).

animation:pause()

Stops the animation from updating (@animation:update(dt)@ will have no effect)

animation:resume()

Unpauses an animation

animation:clone()

Creates a new animation identical to the current one. The only difference is that its internal counter is reset to 0 (it's on the first frame).

animation:flipH()

Flips an animation horizontally (left goes to right and viceversa). This means that the frames are simply drawn differently, nothing more.

Note that this method does not create a new animation. If you want to create a new one, use the clone method.

This method returns the animation, so you can do things like local a = anim8.newAnimation(g(1,'1-10'), 0.1):flipH() or local b = a:clone():flipV().

animation:flipV()

Flips an animation vertically. The same rules that apply to flipH also apply here.

animation:pauseAtEnd()

Moves the animation to its last frame and then pauses it.

animation:pauseAtStart()

Moves the animation to its first frame and then pauses it.

animation:getDimensions()

Returns the width and height of the current frame of the animation. This method assumes the frames passed to the animation are all quads (like the ones created by a grid).

animation:getFrameInfo(x,y, r, sx, sy, ox, oy, kx, ky)

This functions returns the parameters that would be passed to love.graphics.draw when drawing this animation:

frame, x, y, r, sx, sy, ox, oy, kx, ky
  • frame is the currently active frame for the animation (usually a quad produced by a grid)
  • x,y are the same coordinates passed as parameter to getFrame (there are no changes)
  • r is the same angle passed to getFrame, with no changes unless it is nil, in which case it becomes 0.
  • sx,sy are the scale values, with their sign changed if the animation is flipped vertically or horizontally
  • ox,oy are the offset values, with the width or height properly substracted if the animation is flipped. 0 is used as a initial value for these calculations if nil was passed.
  • kx,ky are the shearing factors, changed depending on the flip status.

The getFrame method can be used when working with spriteBatches. Here's how it can be used for adding & setting the corresponding quad in a spritebatch:

local id = spriteBatch:add(animation:getFrameInfo(x,y,r,sx,sy,ox,oy,kx,ky))

...

spriteBatch:set(id, animation:getFrameInfo(x,y,r,sx,sy,ox,oy,kx,ky))

You can see an example of this in the spritebatch-demo branch.

Installation

Just copy the anim8.lua file wherever you want it. Then require it wherever you need it:

local anim8 = require 'anim8'

Please make sure that you read the license, too (for your convenience it's included at the beginning of the anim8.lua file).

Specs

This project uses busted for its specs. If you want to run the specs, you will have to install it first. Then just execute the following from the root folder:

busted

anim8's People

Contributors

eevee avatar g3rley avatar idbrii avatar jaythomas avatar kikito avatar magroski avatar orthographic-pedant avatar tangentfoxy avatar tducasse avatar tesselode avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

anim8's Issues

Improving documentation

I think there's room for improvement on the Grid:getFrames(...) explanation.

Only after reading the demo code I understood how the parameters order affect the frames it fetches.

g('range', value) will fetch frames contained in the given range at row 'value'. frames are counted on the x-axis
g(value, 'range') will fetch frames contained in the given range at column 'value'. frames are counted on the y-axis

However, it's still not clear what calls such as g('2-8',1, 1,2) will work or in what ways I can use the function

Add support for SpriteBatches

SpriteBatches is an object in love to help speed up the rendering of an image if used tons of times. The reason Anim8 could use this is because if I wanted to render 50 animated enenmies with anim8 it will be 50 separate draw calls. If Anim8 supported spritebatches this could be one draw call.

Wrong frame duration for first and last frames

This is a problem that will only happen if increments to timer are discrete units, so it does not happen in the demo, where dt is used for increment.

Create an animation with frame duration 4, and then update it each frame with animation:update(1). Each frame in the animation should be displayed for 4 game frames. With the current anim8 code, while every inner frame shows with the correct duration, the first frame always shows for one frame more and the last frame for one frame less than expected. Let's say I have this animation with 4 frames, with each frame with duration 4. Instead of seeing the expected 1111 2222 3333 4444 1111 ... loop, I see 11111 2222 3333 444 11111 ... .

I corrected this for use in my personal project, and I think my modification could be also applied in the official anim8 code . The change is as follows:

This line in Animation:update:
local loops = math.floor(self.timer / self.totalDuration)

Becomes:
local loops = math.ceil(self.timer / self.totalDuration) - 1

(I also changed the next line if loops ~= 0 then to if loops > 0 then to make it clearer, but it's not necessary)

floor(x) is the same as ceil(x) - 1, except for integer numbers. (floor(1.2) == ceil(1.2) - 1, but floor(1.0) != ceil(1.0) - 1)
And that's exactly what causes this problem, because the frame should change only when loops is greater than 1 and not when it's exactly equal to 1. And that's also what makes it not appear when dt is not given as discrete units, since chances are you'll never get an exact number if you keep adding dt.

Ability to play animations backwards in time

Right now the condition of dt being posititive is pretty much "hard-coded" into anim8. The culprint is the update method:

function Animation:update(dt)
  if self.status ~= "playing" then return end

  self.timer = self.timer + dt

  while self.timer > self.delays[self.position] do
    self.timer = self.timer - self.delays[self.position]
    self.position = self.position + self.direction
    if self.position < 1 or self.position > #self.frames then
      self:endSequence()
    end
  end
end

It would be cool if animation:update admitted negative dts. This way we could use it in games that involve time manipulation (like in Braid).

Flickering with onLoop gotoFrame

When an animation is done with a full play, I want to rewind back to a mid-point in the animation and loop that indefinitely. My intent behind this is to avoid creating separate transition sprite animations. In order to do this, I am setting the gotoFrame in the onLoop function. Given I have 4 frames and want to loop 3-4 after the initial loop, my code looks like this:

  local onLoop = function(animation)
    animation:gotoFrame(3)
end

The problem is the animation very briefly (for one draw iteration) is set back to frame 2. You can visually see this issue in my example here. The set duration doesn't affect the results.

If I'm not using this appropriately then apologies.

"flip" method(s)

I tried this once and failed. Let me see if I can come up with an reasonable API for this.

There is no frame for x=value, y=value

When I tried to build my game with anim8, LÖVE gave me this error:
http://image.prntscr.com/image/2e7e915823f54e30aca049413725c6fb.png

I don't know why this is happening. Here's my code:

local anim8 = require "anim8-master/anim8"

function love.load()
    map_01 = love.graphics.newImage("map_01.png")
    spr_reimu = love.graphics.newImage("spr_reimu.png")
    local g = anim8.newGrid(48, 62, spr_reimu:getWidth(), spr_reimu:getHeight())
    local animation = anim8.newAnimation(g('1-3',1), 0.1)
end

function love.update(dt)
  animation:update(dt)
end

function love.draw()
    love.graphics.draw(map_01)
    animation:draw(spr_reimu, 100, 200)
end

"once" animation endings can't be tested

Currently there is no way to test if a "once" animation has reached its end since it simply resets the frame. This can be a problem if something needs to activate at the end of an animation, which is common.

I suggest having the animation status changed to "paused" after the last frame ends. It makes sense and it's already built in.

grid('a-b,q-r') unexpected behavior

Getting frames from a grid shows (at least for me) unexpected behavior when using ranges for both the x- and y-coordinates.

Expected: Get frames in order (a,q), (a+1,q), ... (b,q), (b,q+1) ... (b,r).
Result: Frames in order (a,q), (a,q+1), ... (a,r), (a+1,q) ... (b,r).
Possible Fix: Swap anim8.lua:78 with anim8.lua:79.

Example:
Consider this animation from opengameart.org: http://i.imgur.com/xFHuX.jpg
Frames are laid out as following:

01 02 03 04
05 06 07 08
...
25 26 27 28
29 30 31 32

grid('1-4,1-8') returns the frames 01,05,...,29,02,06,...30,03, etc. In order to get the correct frame ordering you have to specify the frames as grid('1-4,1', '1-4,2', '1-4,3', ..., '1-4,8').

It would be super cool if we could specify the preference of x and y, but I have no idea how the API would look like. ;)

I get an Error when starting my Program. No Idea how to fix it.

When I start my Program there is an error in the anim8.lua file. Here is the error message:

Error

/liberaries/anim8/anim8.lua:70: attempt to index local 'str' (a nil value)

Traceback

[love "callbacks.lua"]:228: in function 'handler'
/liberaries/anim8/anim8.lua:70: in function 'parseInterval'
/liberaries/anim8/anim8.lua:84: in function 'grid'
main.lua:10: in function 'load'
[love "callbacks.lua"]:136: in function <[love "callbacks.lua"]:135>
[C]: in function 'xpcall'
[C]: in function 'xpcall'

(suggestion)fps2ms

What about add fps to ms converting into library?
So if i want to export animation with specific framerate, after creating grid, i can define it as:

animation = grid1("1-8", 1), anim8.fps2ms(24))

instead of cryptic:

animation = grid1("1-8", 1), 0.041666667)

or

animation = grid1("1-8", 1), 1 / 24)

So it can be more readable?
While i can manually define it as:

anim8.fps2ms = function(fps)
return 1 / fps
end

I rather prefer have it available out-of-box
What to you think?

animation:update(dt) not working

When I pass the dt from love.update the animation does not work, but when I pass a higher number, it works.

Is this the expected behavior?

Multiple instances of anim8 causes distorted sprite

I'm using class.lua and anim8 so I can create sprite objects (say 100 coins placed across the screen). I pass the spritesheet image along with other parameters to the init function of the class, which in turn creates a grid and animation. The class also has update(dt) and render() to draw.

Strange thing is, when I create these multiple instances, one of the sprites becomes distorted. When I have only one instance, everything works as its supposed to...

Any ideas?

Wrong typo

The method was called as drawq, you need to change this to draw.

Callbacks

It would be pretty swanky for anim8 to allow callbacks at the end of an animation loop. A potential use case for this would be:

Assume a sprite has many animations, including "idleLeft" and "attackLeft". Once a player uses an attack, we want to switch back to the idle animation.

facing = "attackLeft"

sprites = {
    attackLeft = anim8.newAnimation(g(1-5,1), 0.1, "once", switchToIdle()), -- add more animation modes such as once/justonce
    idleLeft = anim8.newAnimation(g(1-5,2), 0.1)
}

function switchToIdle()
    attackLeft:gotoFrame(1) -- pausing at the end of an animation is super annoying. just setting the animation back to the beginning and leaving its state alone would be ideal.
    facing = "idleLeft"
end

function update(dt)
    sprites[facing]:update(dt)
end

Possible to crop frame of animation?

You can have a grid, and an animation. The grid seems to be defined as just an image, and spritesheet metadata such as width, height, frame width height...

Let's say we have a single image, single frame. If we're drawing with the simple '1-1' animation, is there a way to edit the grid / animation to reduce the height or width of an image, to basically take a 100% image, reduce it's height by 1% every dt, such that it shrinks to disappearing?

This is useful for say a healthbar image, where it needs to constantly increase, decrease.

The most obvious solution is just reducing the frame height by 1, and increase y by 1 so the "health bar" (as an example) stays in place, but reduces the actual image being printed.

I'm wondering if this is possible with anim8 and if how, I've looked into the docs quite a bit, but can't seem to find an obvious solution.

Thanks @kikito !

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.