package font;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;
import com.threed.jpct.FrameBuffer;
import com.threed.jpct.Texture;
import com.threed.jpct.TextureManager;
/**
* packs several arbitrary sized images into a jPCT texture.
* the created texture is 2^n x 2^m as required by jPCT. useful for
* blitting purposes.
*
* <p>see <a href='http://www.blackpawn.com/texts/lightmaps/default.html'
* target='_blank'>this page</a> for an explanation of packing algorithm</p>
*
* @author hakan eryargi (r a f t)
*/
public class TexturePack {
/** use alpha when creating texture */
public static boolean ALPHA_USE = true;
/** do not use alpha when creating texture */
public static boolean ALPHA_DONT_USE = false;
private static final boolean DEBUG = false;
private static int lastPackId = 0;
private static synchronized int nextPackId() {
return lastPackId++;
}
private final int packId = nextPackId();
private final String textureName = "imagepack/" + packId;
private final int imageType;
private boolean packed = false;
private final Dimension blittedSize = new Dimension();
private final List<Entry> entries = new ArrayList<Entry>();
private Texture texture = null;
/**
* creates an TexturePack using BufferedImage.TYPE_INT_ARGB as image type
*
* @see BufferedImage#TYPE_INT_ARGB
*/
public TexturePack() {
this(BufferedImage.TYPE_INT_ARGB);
}
/**
* creates an TexturePack
*
* @param imageType must be one of the image types defined in
* {@link BufferedImage} class
* @see BufferedImage
*/
public TexturePack(int imageType) {
this.imageType = imageType;
}
/**
* add image to be included in pack.
*
* @return image id which can later be used for blit(..) methods
*/
public int addImage(Image image) {
checkPacked();
if (image == null)
throw new NullPointerException();
if (image.getWidth(null) <= 0 || image.getHeight(null) <= 0)
throw new IllegalArgumentException("width and height must be positive");
this.entries.add(new Entry(image));
return entries.size() - 1;
}
/**
* packs images into an 2^n x 2^m size d image. after packing clears all
* references to given images hence this method can only be called once
*
* @return created image
*/
private BufferedImage packImage() {
checkPacked();
if (entries.isEmpty())
throw new IllegalStateException("nothing to pack");
int maxWidth = 0;
int maxHeight = 0;
int totalArea = 0;
for (Entry entry : entries) {
int width = entry.image.getWidth(null);
int height = entry.image.getHeight(null);
if (width > maxWidth)
maxWidth = width;
if (height > maxHeight)
maxHeight = height;
totalArea += width * height;
}
Dimension size = new Dimension(closestTwoPower(maxWidth), closestTwoPower(maxHeight));
boolean fitAll = false;
if (DEBUG)
System.out.println("initial size " + size);
loop: while (!fitAll) {
int area = size.width * size.height;
if (area < totalArea) {
nextSize(size);
if (DEBUG)
System.out.println("enlarging to " + size);
continue;
}
Node root = new Node(size.width, size.height);
for (Entry entry : entries) {
Node inserted = root.insert(entry);
if (inserted == null) {
nextSize(size);
if (DEBUG)
System.out.println("couldnt fit, enlarging to " + size);
continue loop;
}
}
fitAll = true;
if (DEBUG)
printTree(root, "", 0);
}
BufferedImage image = new BufferedImage(size.width, size.height, imageType);
Graphics2D g2d = image.createGraphics();
for (Entry entry : entries) {
g2d.drawImage(entry.image, entry.bounds.x, entry.bounds.y, null);
entry.image = null;
}
g2d.dispose();
packed = true;
// if (DEBUG) raft.karga.gui.Swing.showImage("texture pack", image);
return image;
}
/**
* packs image, creates a Texture out of it and adds texture to
* TextureManager.
*
* @param useAlpha will created texture use alpha ?
* @return created texture
*
* @see #ALPHA_USE
* @see #ALPHA_DONT_USE
*/
public Texture pack(boolean useAlpha) {
if (texture != null)
return texture;
BufferedImage image = packImage();
texture = new Texture(image, useAlpha);
TextureManager.getInstance().addTexture(textureName, texture);
return texture;
}
/**
* blits one of packed images completely without scaling and with
* transparency or additional color.
* see {@link FrameBuffer#blit(Texture, int, int, int, int, int, int, int, int, int, boolean, Color)
* FrameBuffer.blit(..)} for description of other parameters
*
* @param buffer the FrameBuffer
* @param imageId id of packed image as returned by addImage(..)
*
* @return size of blitted image. note this method always returns the same
* array instance
* @see FrameBuffer#blit(Texture, int, int, int, int, int, int, int, int, int, boolean, Color)
* FrameBuffer.blit(..)
*/
public Dimension blit(FrameBuffer buffer, int imageId, int destX, int destY,
int transparency, boolean additive, Color color) {
if (texture == null)
throw new IllegalStateException("not packed yet");
Entry entry = entries.get(imageId);
return blit(buffer, entry, destX, destY, entry.bounds.width,
entry.bounds.height, transparency, additive, color);
}
/**
* blits one of packed images completely with scaling or transparency or additional color
* see {@link FrameBuffer#blit(Texture, int, int, int, int, int, int, int, int, int, boolean, Color)
* FrameBuffer.blit(..)} for description of other parameters
*
* @param buffer the FrameBuffer
* @param imageId id of packed image as returned by addImage(..)
*
* @return size of blitted image. note this method always returns the same
* array instance
* @see FrameBuffer#blit(Texture, int, int, int, int, int, int, int, int, int, boolean, Color)
* FrameBuffer.blit(..)
*/
public Dimension blit(FrameBuffer buffer, int imageId, int destX, int destY,
int destWidth, int destHeight, int transparency, boolean additive, Color color) {
if (texture == null)
throw new IllegalStateException("not packed yet");
Entry entry = entries.get(imageId);
return blit(buffer, entry, destX, destY, destWidth, destHeight,
transparency, additive, color);
}
/**
* blits one of packed images completely without scaling
*
* @param imageId
* id of packed image as returned by addImage(..)
* @return size of blitted image. note this method always returns the same
* array instance
* @see FrameBuffer#blit(Texture, int, int, int, int, int, int, boolean)
* FrameBuffer.blit(..)
*/
public Dimension blit(FrameBuffer buffer, int imageId, int destX, int destY, boolean transparent) {
if (texture == null)
throw new IllegalStateException("not packed yet");
Entry entry = entries.get(imageId);
buffer.blit(texture, entry.bounds.x, entry.bounds.y, destX, destY,
entry.bounds.width, entry.bounds.height, transparent);
blittedSize.setSize(entry.bounds.width, entry.bounds.height);
return blittedSize;
}
private Dimension blit(FrameBuffer buffer, Entry entry, int destX, int destY,
int destWidth, int destHeight, int transparency, boolean additive, Color color) {
buffer.blit(texture, entry.bounds.x, entry.bounds.y, destX, destY, entry.bounds.width, entry.bounds.height,
destWidth, destHeight, transparency, additive, color);
blittedSize.setSize(entry.bounds.width, entry.bounds.height);
return blittedSize;
}
private void checkPacked() {
if (packed)
throw new IllegalStateException("already packed");
}
private void nextSize(Dimension size) {
if (size.width > size.height)
size.height <<= 1;
else
size.width <<= 1;
}
/** recursively prints placement tree starting from given node */
private void printTree(Node node, String prefix, int depth) {
if (node == null)
return;
System.out.println(depth + ":\t" + prefix + "--" + node);
printTree(node.child[0], " |" + prefix, depth + 1);
printTree(node.child[1], " |" + prefix, depth + 1);
}
/**
* returns the closest power of two that is equal or greater than given
* number
*/
private int closestTwoPower(int i) {
int power = 1;
while (power < i) {
power <<= 1;
}
return power;
}
/** contents of a node if any */
private static class Entry {
private final Rectangle bounds = new Rectangle();
private Image image;
private Entry(Image image) {
this.image = image;
}
@Override
public String toString() {
return "Entry: " + bounds;
}
}
/** a node in our placement tree */
private static class Node {
private final Node[] child = new Node[2];
private final Rectangle bounds = new Rectangle();
private Entry entry;
private Node() {
}
private Node(int width, int height) {
bounds.setBounds(0, 0, width, height);
}
private boolean isLeaf() {
return (child[0] == null) && (child[1] == null);
}
private Node insert(Entry entry) {
if (isLeaf()) {
// if there's already a image here, return
if (this.entry != null)
return null;
int width = entry.image.getWidth(null);
int height = entry.image.getHeight(null);
// (if we're too small, return)
if ((width > bounds.width) || (height > bounds.height))
return null;
// (if we're just right, accept)
if ((width == bounds.width) && (height == bounds.height)) {
this.entry = entry;
this.entry.bounds.setBounds(this.bounds);
return this;
}
// otherwise, split this node
child[0] = new Node();
child[1] = new Node();
// (decide which way to split)
int dw = bounds.width - width;
int dh = bounds.height - height;
if (dw > dh) { // split horizontally
child[0].bounds.setBounds(bounds.x, bounds.y, width, bounds.height);
child[1].bounds.setBounds(bounds.x + width, bounds.y, bounds.width - width, bounds.height);
} else { // split vertically
child[0].bounds.setBounds(bounds.x, bounds.y, bounds.width, height);
child[1].bounds.setBounds(bounds.x, bounds.y + height, bounds.width, bounds.height - height);
}
// insert into first child we created
return child[0].insert(entry);
} else {
// try inserting into first child
Node newNode = child[0].insert(entry);
if (newNode != null)
return newNode;
// no room, insert into second
return child[1].insert(entry);
}
}
@Override
public String toString() {
return bounds + ((entry == null) ? " <no entry>" : " " + entry.toString());
}
}
}