Spritesheet animator!

Hello to the JGo community!

I have written an application for creating sprites and animations and outputting XML which can be imported into a game engine.

I’m just looking for a bit of feedback on what sort of features you feel would help you in your day to day life as a game developer so I can incorporate the most useful features into the tool!

Cheers!

http://www.darkfunction.com

Coloring in grayscale images, this is very useful if you allow a way for the player to pick a hair color or recoloring clothes etc.

Impressive. I like it. I’m going to start using it. Thanks.

Did you make it using Swing, TWL, or some other GUI app?
Very impressive though. I’m also gonna start using it. I like apps that make my life easier :slight_smile:

It’s made in Swing.

If you find any bugs or issues please let me know and they will be fixed quickly. Thanks!

Hi,
This editor is very interresting.
Do you provide in the other side any lib implementation that can handle your output format ? (like a Slick one, Swing, …) that can be used inside our games ?

I will start work on library implementations soon, but I’m only one person and have a day job. I’m thinking of doing one for AndEngine first but haven’t made my mind up yet!

Hi!

What do you use to encode/decode XML? It is a free of charge software but will it become open source?

Anyway, creating such a tool is a good idea as there is a real lack of WYSIWYG editor for Java environment.

Hi gouessej

I use xmlenc for the XML.

The tool is free of charge but will not be made open source. It will remain free and continue to be supported with updates in line with user feedback. However, in time I hope to launch an advanced professional version with specialist features suited for large game studios, which will be a paid app. So I don’t want to release the source code.

All libraries that I write to support various engines will be open sourced and available on the website.

Version 1.2 available now :smiley:

http://darkfunction.com/editor/wp-content/uploads/2011/05/screenshot11.png

Nice tool :slight_smile: (although I havent jump into animated sprites)

Great tool, something I wanting to have (and I wanted to do) from a long time.

Some suggestions and/or questions:

  • is it possible to define the sprite center? to rotate the sprite over that center instead the default center, it is useful when defining an animation for a character and you want the center of the arm is on the shoulder. Not sure if the center should be defined when editing the animation or when editing the sprite sheet.
  • is it possible to rotate using the mouse directly instead having to put the angle in the input field?
  • which units is the cell delay using? doesn’t seem to be milliseconds nor seconds.
  • do you prefer I make suggestions/questions here or in your page?
  • is it open source?
  • some future suggestions: defining restrictions between sprites could be great to “simulate” a skeleton, or making a hierarchy even, like this weapon is always attached to the arm. I saw you have a hierarchy to the left for the sprites but didn’t see if you already have this kind of restrictions.

Thanks for sharing your tool.

Another suggestion:

  • export current animation (or all animation) in one or more .gif files, to share with someone for example (my case) or to use it to create animated gifs

Pretty cool. Allow me to contribute some source code:


import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Color;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;
import javax.imageio.ImageIO;
import javax.swing.JFileChooser;

public class AutoRipper
{
	private static BufferedImage spriteSheet;
	private static Color color;
	private static ArrayList<BufferedImage> sprites;
	private static int tolerance = 75;
	
	public static void main(String[] args)
	{
		sprites = new ArrayList<BufferedImage>();
		spriteSheet = open();
		scanAutomaticallyIteratively();
		saveSprites();
	}
	
	private static BufferedImage open()
	{
		JFileChooser fc = new JFileChooser();
		
		fc.setCurrentDirectory(new File("/Users/Will/Desktop"));
		fc.setAcceptAllFileFilterUsed(false);
		fc.setFileFilter(new ImageFileFilter());
		fc.setDialogTitle("Choose a Sprite Sheet"); 
		
		int returnVal = fc.showDialog(null, "Choose a Sprite Sheet");
		
		if (returnVal == JFileChooser.APPROVE_OPTION)
		{
        		File file = fc.getSelectedFile();
			try
			{
				//Read the sprite sheet, then translate it to ARGB so I know how it is formatted
				BufferedImage im = ImageIO.read(file);
				BufferedImage ret = new BufferedImage(im.getWidth(),im.getHeight(),BufferedImage.TYPE_INT_ARGB);
				Graphics2D g = ret.createGraphics();
				g.drawImage(im,0,0,null);
				g.dispose();
				int[] c = ret.getRaster().getPixel(0,0,new int[4]);
				color = new Color(c[0],c[1],c[2],c[3]);
				return ret;
			}
			catch(Exception e) {e.printStackTrace();}
        }
		return null;
	}
	
	private static String chooseSaveDirectory()
	{
		JFileChooser fc = new JFileChooser();
		
		fc.setCurrentDirectory(new File(System.getProperty("user.dir")));
		fc.setAcceptAllFileFilterUsed(false);
		fc.setFileFilter(new DirectoryFileFilter());
		fc.setDialogTitle("Choose a Folder"); 
		fc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
		
		int returnVal = fc.showDialog(null, "Save Results in Folder...");
		
		if (returnVal == JFileChooser.APPROVE_OPTION)
		{
        	return fc.getSelectedFile().getAbsolutePath();
        }
		return null;
	}
	
	private static class ImageFileFilter extends javax.swing.filechooser.FileFilter
	{
		public boolean accept(File f)
		{
			if (f.isDirectory())
		        return true;

			String e = "";
	        	String s = f.getName();
	        	int i = s.lastIndexOf('.');
	        	if (i > 0 &&  i < s.length() - 1)
	            e = s.substring(i+1).toLowerCase();
	        	
	        	String[] types = {"jpg","gif","tif","bmp","png","tiff","jpeg"};
	        	
	        	for (int j = 0; j < types.length; j++)
	        		if (e.equals(types[j]))
	        			return true;
		    return false;
		}
		
		public String getDescription()
		{
			return ".jpg .gif .tif .bmp .png";
		}
	}
	
	private static class DirectoryFileFilter extends javax.swing.filechooser.FileFilter
	{
		public boolean accept(File f)
		{
			if (f.isDirectory())
		        return true;
		    return false;
		}
		
		public String getDescription()
		{
			return "Directories";
		}
	}
	
	public static void scanAutomaticallyIteratively()
	{
		if (spriteSheet == null)
			return;
		
		int minSize = 50;
		
		//A 2D array of booleans that tells whether a pixel has already been factored or not
		boolean[][] scanned = new boolean[spriteSheet.getWidth()][spriteSheet.getHeight()];
		//An ArrayList of all pixels that are connected. Is refreshed automatically per scan
		ArrayList<Point> connectedPixels = new ArrayList<Point>();
		
		for (int y = 0; y < spriteSheet.getHeight(); y++)
		{
			for (int x = 0; x < spriteSheet.getWidth(); x++)
			{
				//If a pixel is found that is within bounds, hasn't been scanned and
				//is not transparent, we need to search its connected pixels
				if (inBounds(x,y) && !scanned[x][y] && !pixelIsTransparent(x,y))
				{
					//Add the point to the list of connected pixels
					connectedPixels.add(new Point(x,y));
					scanned[x][y] = true;
					
					//Keep searching every pixel within the list until all the
					//pixels within the list's adjacent pixels have been scanned,
					//are transparent, or are not in bounds.
					int search = 0;
					while (search < connectedPixels.size())
					{
						Point p = (Point) connectedPixels.get(search);
						//Search the surrounding pixels. If any of them are not scanned, not transparent,
						//and within bounds (inside the image), call this method recursively with new x and y.
						for (int i = -1; i <= 1; i++)
						{
							for (int j = -1; j <= 1; j++)
							{
								if (inBounds(i+p.x,j+p.y) && !scanned[p.x+i][p.y+j] && !pixelIsTransparent(i+p.x,j+p.y))
								{
									connectedPixels.add(new Point(i+p.x,j+p.y));
									scanned[i+p.x][j+p.y] = true;
								}
							}
						}
						search++;
					}
					
					//There should now be a completed sprite in the list
					addSpriteFromPixelList(connectedPixels,minSize);
					connectedPixels.clear();
				}
			}
		}
	}
	
	public static boolean inBounds(int x, int y)
	{
		return (x >= 0 && x < spriteSheet.getWidth() && y >= 0 && y < spriteSheet.getHeight());
	}
	
	public static boolean pixelIsTransparent(int x, int y)
	{
		int[] sprite = spriteSheet.getRaster().getPixel(x,y,new int[4]);
		int[] transColor = {color.getRed(),color.getBlue(),color.getGreen(),color.getAlpha()};
		int total = 0;
		
		for (int i = 0; i < sprite.length; i++)
		{
			total += Math.abs(sprite[i]-transColor[i]);
			if (total > tolerance)
				return false;
		}
		return true;
	}
	
	public static void addSpriteFromPixelList(ArrayList<Point> connectedPixels, int minSize)
	{
		//Don't add if there are not minmum size pixels in the sprite
		if (connectedPixels.size() < minSize)
			return;
		//Theoretically, a new sprite has now been created and all of its points
		//are within the connectedPixels list. Now we need translate a list of
		//pixels into a BufferedImage, first finding its min/max coordinates.
		int minX, minY, maxX, maxY;
		minX = maxX = ((Point)connectedPixels.get(0)).x;
		minY = maxY = ((Point)connectedPixels.get(0)).y;
		for (int i = 1; i < connectedPixels.size(); i++)
		{
			Point p = (Point) connectedPixels.get(i);
			if (p.x < minX)
				minX = p.x;
			if (p.x > maxX)
				maxX = p.x;
			if (p.y < minY)
				minY = p.y;
			if (p.y > maxY)
				maxY = p.y;
		}
		//Now that we have the bounds of the image, we can add the sprite and clear the pixels
		addSprite(minX,minY,maxX-minX+1,maxY-minY+1);
	}
	
	public static void addSprite(int x, int y, int width, int height)
	{
		if (width > 0 && height > 0)
		{
			BufferedImage sprite = new BufferedImage(width,height,BufferedImage.TYPE_INT_ARGB);
			Graphics2D g = sprite.createGraphics();
			g.drawImage(spriteSheet,-x,-y,null);
			g.dispose();
			
			//Turn the specified color transparent
			for (int i = 0; i < sprite.getWidth(); i++)
				for (int j = 0; j < sprite.getHeight(); j++)
					if(pixelIsTransparent(i+x,j+y))
						sprite.getRaster().setPixel(i,j,new int[]{255,255,255,255});
			
			if (sprite.getWidth() > 0 && sprite.getHeight() > 0)
				sprites.add(sprite);
		}
	}
	public static void saveSprites()
	{
		String saveFolder = chooseSaveDirectory();
		
		if (saveFolder != null)
		{
			for (int i = 0; i < sprites.size(); i++)
			{
				try
				{
					ImageIO.write((BufferedImage)sprites.get(i), "png", new java.io.File(saveFolder + "/" + i + ".png"));
				}
				catch (Exception ex) {ex.printStackTrace();}
			}
		}
	}
}


This code automatically takes a sprite sheet and splits it into individual images. You can modify it a bit to give you the rects for each sprite, then allow the player to correct them as desired. That would be a lot better than having to do it all manually, as this tool is 99% correct (except in sheets where stuff overlaps rects, which is bad anyway).

@Arielsan

  • is it possible to define the sprite center?

    • no but a great suggestion, thanks!
  • is it possible to rotate using the mouse directly instead having to put the angle in the input field?

    • no, something I wanted to do but wanted to get a release out quickly. It’s on the TODO list already as an enhancement
  • which units is the cell delay using? doesn’t seem to be milliseconds nor seconds.

    • frames. It’s intended so you can call animation.tick() in your game every frame or multiple of frames. I have had a lot of questions about this actually. I think it makes more sense to use frames so it has easier engine integration however an option to set the FPS for the preview should be made available.
  • do you prefer I make suggestions/questions here or in your page?

  • wherever you like! I will check back here regularly
  • is it open source?
  • no. See comments below
  • some future suggestions: defining restrictions between sprites could be great to “simulate” a skeleton, or making a hierarchy even, like this weapon is always attached to the arm. I saw you have a hierarchy to the left for the sprites but didn’t see if you already have this kind of restrictions.

    • cool idea! As you say, something for the future maybe
  • export current animation (or all animation) in one or more .gif files, to share with someone for example (my case) or to use it to create animated gifs

    • I really like this idea, in fact I’m quite excited to start implementing it straight away! haha

Questions regarding Open Source
I won’t be Open Sourcing unfortunately (for now) as I hope to release a ‘Pro’ version in the future with some advanced features which will require a small payment. I’m hoping to make something of a microISV from this project, but feel strongly that indy developers should be allowed free access to time saving tools like this. So I have decided to make the essential functionality (everything so far) free for everybody but charge for game studios or professionals that require more advanced features and are willing to pay for them. I hope you understand!

When I say advanced features, this probably means

  • Collision polygons
  • Multiple sheet support
  • Customisable output formats
  • etc

@Eli Delventhal
… with that said, I would graciously accept your code and include it in this free version. Let me know if you are not happy with this!

I guess Eli’s code would be good. You current trim function doesnt seem to do anything useful for my sprites.

Trim should just tighten the box around any non-transparent pixels :wink:

yeah no, at least not for me, it like quaters them and seems to just start at the left top, and the image is than cropped somewhere arbitrary

[quote=“darkFunction,post:15,topic:36844”]
That’s fine with me, it’s free code, and not rocket science or anything.

Skeletal animations?