Behold, Nenya, Ring of Water and repository for our media and animation related

goodies, both Java 2D and LWJGL/JME 3D.


git-svn-id: svn+ssh://src.earth.threerings.net/nenya/trunk@1 ed5b42cb-e716-0410-a449-f6a68f950b19
This commit is contained in:
Michael Bayne
2006-06-23 18:07:28 +00:00
commit c2117ee86d
570 changed files with 61913 additions and 0 deletions
@@ -0,0 +1,71 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image;
import java.awt.Component;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
/**
* If the user of the image manager services intends to create and display
* images using the AWT, they can use this image creator which will use the
* AWT to determine the optimal image format.
*/
public class AWTImageCreator
implements ImageManager.OptimalImageCreator
{
/**
* Create an image creator that will rely on the AWT to determine the
* optimal image format.
*
* @param context if non-null, the graphics configuration will be obtained
* therefrom; otherwise {@link GraphicsDevice#getDefaultConfiguration}
* will be used.
*/
public AWTImageCreator (Component context)
{
// obtain our graphics configuration
if (context != null) {
_gc = context.getGraphicsConfiguration();
} else {
_gc = ImageUtil.getDefGC();
}
}
// documentation inherited from interface ImageManager.OptimalImageCreator
public BufferedImage createImage (int width, int height, int trans)
{
// DEBUG: override transparency for the moment on all images
trans = Transparency.TRANSLUCENT;
if (_gc != null) {
return _gc.createCompatibleImage(width, height, trans);
} else {
// if we're running in headless mode, do everything in 24-bit
return new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
}
}
/** The graphics configuration for the default screen device. */
protected GraphicsConfiguration _gc;
}
@@ -0,0 +1,82 @@
//
// $Id: BackedVolatileMirage.java 4191 2006-06-13 22:42:20Z ray $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import com.threerings.media.Log;
/**
* Provides a volatile mirage that is backed by a buffered image that is
* not obtained from the image manager but is instead provided at
* construct time and completely circumvents the image manager's cache. As
* such, this should not be used unless you know what you're doing.
*/
public class BackedVolatileMirage extends VolatileMirage
{
/**
* Creates a mirage with the supplied regeneration informoation and
* prepared image.
*/
public BackedVolatileMirage (ImageManager imgr, BufferedImage source)
{
super(imgr, new Rectangle(0, 0, source.getWidth(), source.getHeight()));
_source = source;
// create our volatile image for the first time
createVolatileImage();
}
// documentation inherited
protected int getTransparency ()
{
return _source.getColorModel().getTransparency();
}
// documentation inherited
protected void refreshVolatileImage ()
{
Graphics gfx = null;
try {
gfx = _image.getGraphics();
gfx.drawImage(_source, -_bounds.x, -_bounds.y, null);
} catch (Exception e) {
Log.warning("Failure refreshing mirage " + this + ".");
Log.logStackTrace(e);
} finally {
gfx.dispose();
}
}
// documentation inherited
protected void toString (StringBuilder buf)
{
super.toString(buf);
buf.append(", src=").append(_source);
}
protected BufferedImage _source;
}
@@ -0,0 +1,77 @@
//
// $Id: BlankMirage.java 4191 2006-06-13 22:42:20Z ray $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
/**
* A mirage implementation that contains no image data. Generally only
* useful for testing.
*/
public class BlankMirage implements Mirage
{
public BlankMirage (int width, int height)
{
_width = width;
_height = height;
}
// documentation inherited from interface
public void paint (Graphics2D gfx, int x, int y)
{
// nothing doing
}
// documentation inherited from interface
public int getWidth ()
{
return _width;
}
// documentation inherited from interface
public int getHeight ()
{
return _height;
}
// documentation inherited from interface
public boolean hitTest (int x, int y)
{
return false;
}
// documentation inherited from interface
public long getEstimatedMemoryUsage ()
{
return 0;
}
// documentation inherited from interface
public BufferedImage getSnapshot ()
{
return null;
}
protected int _width;
protected int _height;
}
@@ -0,0 +1,74 @@
//
// $Id: BufferedMirage.java 4191 2006-06-13 22:42:20Z ray $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
/**
* A simple mirage implementation that uses a buffered image.
*/
public class BufferedMirage implements Mirage
{
public BufferedMirage (BufferedImage image)
{
_image = image;
}
// documentation inherited from interface
public void paint (Graphics2D gfx, int x, int y)
{
gfx.drawImage(_image, x, y, null);
}
// documentation inherited from interface
public int getWidth ()
{
return _image.getWidth();
}
// documentation inherited from interface
public int getHeight ()
{
return _image.getHeight();
}
// documentation inherited from interface
public boolean hitTest (int x, int y)
{
return ImageUtil.hitTest(_image, x, y);
}
// documentation inherited from interface
public long getEstimatedMemoryUsage ()
{
return ImageUtil.getEstimatedMemoryUsage(_image.getRaster());
}
// documentation inherited from interface
public BufferedImage getSnapshot ()
{
return _image;
}
protected BufferedImage _image;
}
@@ -0,0 +1,104 @@
//
// $Id: CachedVolatileMirage.java 4191 2006-06-13 22:42:20Z ray $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import com.threerings.media.Log;
/**
* A mirage implementation which allows the image to be maintained in
* video memory and refetched from the image manager in the event that our
* target screen resolution changes or we are flushed from video memory
* for some other reason.
*
* <p> These objects are never created directly, but always obtained from
* the {@link ImageManager}.
*/
public class CachedVolatileMirage extends VolatileMirage
{
/**
* Creates a mirage with the supplied regeneration informoation and
* prepared image.
*/
protected CachedVolatileMirage (
ImageManager imgr, ImageManager.ImageKey source,
Rectangle bounds, Colorization[] zations)
{
super(imgr, bounds);
_source = source;
_zations = zations;
// create our volatile image for the first time
createVolatileImage();
}
// documentation inherited
protected int getTransparency ()
{
BufferedImage source = _imgr.getImage(_source, _zations);
return (source == null) ? Transparency.OPAQUE :
source.getColorModel().getTransparency();
}
// documentation inherited
protected void refreshVolatileImage ()
{
Graphics gfx = null;
try {
BufferedImage source = _imgr.getImage(_source, _zations);
if (source != null) {
gfx = _image.getGraphics();
gfx.drawImage(source, -_bounds.x, -_bounds.y, null);
}
} catch (Exception e) {
Log.warning("Failure refreshing mirage " + this + ".");
Log.logStackTrace(e);
} finally {
if (gfx != null) {
gfx.dispose();
}
}
}
// documentation inherited
protected void toString (StringBuilder buf)
{
super.toString(buf);
buf.append(", key=").append(_source);
buf.append(", zations=").append(_zations);
}
/** The key that identifies the image data used to create our volatile
* image. */
protected ImageManager.ImageKey _source;
/** Optional colorizations that are applied to our source image when
* creating our mirage. */
protected Colorization[] _zations;
}
@@ -0,0 +1,461 @@
//
// $Id: ColorPository.java 4188 2006-06-13 18:03:48Z mdb $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image;
import java.awt.Color;
import java.util.ArrayList;
import java.util.Iterator;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.text.ParseException;
import com.samskivert.util.HashIntMap;
import com.samskivert.util.RandomUtil;
import com.samskivert.util.StringUtil;
import com.threerings.media.Log;
import com.threerings.resource.ResourceManager;
import com.threerings.util.CompiledConfig;
/**
* A repository of image recoloration information. It was called the
* recolor repository but the re-s cancelled one another out.
*/
public class ColorPository implements Serializable
{
/**
* Used to store information on a class of colors. These are public to
* simplify the XML parsing process, so pay them no mind.
*/
public static class ClassRecord implements Serializable
{
/** An integer identifier for this class. */
public int classId;
/** The name of the color class. */
public String name;
/** The source color to use when recoloring colors in this class. */
public Color source;
/** Data identifying the range of colors around the source color
* that will be recolored when recoloring using this class. */
public float[] range;
/** The default starting legality value for this color class. See
* {@link ColorRecord#starter}. */
public boolean starter;
/** The default colorId to use for recoloration in this class, or
* 0 if there is no default defined. */
public int defaultId;
/** A table of target colors included in this class. */
public HashIntMap colors = new HashIntMap();
/** Used when parsing the color definitions. */
public void addColor (ColorRecord record)
{
// validate the color id
if (record.colorId > 255) {
Log.warning("Refusing to add color record; colorId > 255 " +
"[class=" + this + ", record=" + record + "].");
} else {
record.cclass = this;
colors.put(record.colorId, record);
}
}
/**
* Translants a color identified in string form into the id that
* should be used to look up its information. Throws an
* exception if no color could be found that associates with
* that name.
*
* FIXME: This lookup could be sped up a lot with some cached
* data tables if it looked like this function would get called
* in time critical situations.
*/
public int getColorId (String name)
throws ParseException
{
// Check if the string is itself a number
try {
int id = Integer.parseInt(name);
if (colors.containsKey(id)) {
return id;
}
} catch (NumberFormatException e) {
// Guess it must be something else
}
// Look for name matches among all colors
Iterator iter = colors.values().iterator();
while (iter.hasNext()) {
ColorRecord color = (ColorRecord)iter.next();
if (color.name.equalsIgnoreCase(name)) {
return color.colorId;
}
}
// That input wasn't a color
throw new ParseException("No color named '" + name + "'", 0);
}
/** Returns a random starting id from the entries in this
* class. */
public ColorRecord randomStartingColor ()
{
// figure out our starter ids if we haven't already
if (_starters == null) {
ArrayList list = new ArrayList();
Iterator iter = colors.values().iterator();
while (iter.hasNext()) {
ColorRecord color = (ColorRecord)iter.next();
if (color.starter) {
list.add(color);
}
}
_starters = (ColorRecord[])
list.toArray(new ColorRecord[list.size()]);
}
// sanity check
if (_starters.length < 1) {
Log.warning("Requested random starting color from " +
"colorless component class " + this + "].");
return null;
}
// return a random entry from the array
return _starters[RandomUtil.getInt(_starters.length)];
}
/**
* Get the default ColorRecord defined for this color class, or
* null if none.
*/
public ColorRecord getDefault ()
{
return (ColorRecord) colors.get(defaultId);
}
/**
* Returns a string representation of this instance.
*/
public String toString ()
{
return "[id=" + classId + ", name=" + name + ", source=#" +
Integer.toString(source.getRGB() & 0xFFFFFF, 16) +
", range=" + StringUtil.toString(range) +
", starter=" + starter + ", colors=" +
StringUtil.toString(colors.values().iterator()) + "]";
}
protected transient ColorRecord[] _starters;
/** Increase this value when object's serialized state is impacted
* by a class change (modification of fields, inheritance). */
private static final long serialVersionUID = 2;
}
/**
* Used to store information on a particular color. These are public
* to simplify the XML parsing process, so pay them no mind.
*/
public static class ColorRecord implements Serializable
{
/** The colorization class to which we belong. */
public ClassRecord cclass;
/** A unique colorization identifier (used in fingerprints). */
public int colorId;
/** The name of the target color. */
public String name;
/** Data indicating the offset (in HSV color space) from the
* source color to recolor to this color. */
public float[] offsets;
/** Tags this color as a legal starting color or not. This is a
* shameful copout, placing application-specific functionality
* into a general purpose library class. */
public boolean starter;
/**
* Returns a value that is the composite of our class id and color
* id which can be used to identify a colorization record. This
* value will always be a positive integer that fits into 16 bits.
*/
public int getColorPrint ()
{
return ((cclass.classId << 8) | colorId);
}
/**
* Returns the data in this record configured as a colorization
* instance.
*/
public Colorization getColorization ()
{
// if (_zation == null) {
// _zation = new Colorization(getColorPrint(), cclass.source,
// cclass.range, offsets);
// }
// return _zation;
return new Colorization(getColorPrint(), cclass.source,
cclass.range, offsets);
}
/**
* Returns a string representation of this instance.
*/
public String toString ()
{
return "[id=" + colorId + ", name=" + name +
", offsets=" + StringUtil.toString(offsets) +
", starter=" + starter + "]";
}
/** Our data represented as a colorization. */
protected transient Colorization _zation;
/** Increase this value when object's serialized state is impacted
* by a class change (modification of fields, inheritance). */
private static final long serialVersionUID = 2;
}
/**
* Returns an iterator over all color classes in this pository.
*/
public Iterator enumerateClasses ()
{
return _classes.values().iterator();
}
/**
* Returns an array containing the records for the colors in the
* specified class.
*/
public ColorRecord[] enumerateColors (String className)
{
// make sure the class exists
ClassRecord record = getClassRecord(className);
if (record == null) {
return null;
}
// create the array
ColorRecord[] crecs = new ColorRecord[record.colors.size()];
Iterator iter = record.colors.values().iterator();
for (int i = 0; iter.hasNext(); i++) {
crecs[i] = ((ColorRecord)iter.next());
}
return crecs;
}
/**
* Returns an array containing the ids of the colors in the specified
* class.
*/
public int[] enumerateColorIds (String className)
{
// make sure the class exists
ClassRecord record = getClassRecord(className);
if (record == null) {
return null;
}
int[] cids = new int[record.colors.size()];
Iterator crecs = record.colors.values().iterator();
for (int i = 0; crecs.hasNext(); i++) {
cids[i] = ((ColorRecord)crecs.next()).colorId;
}
return cids;
}
/**
* Returns true if the specified color is legal for use at character
* creation time. false is always returned for non-existent colors or
* classes.
*/
public boolean isLegalStartColor (int classId, int colorId)
{
ColorRecord color = getColorRecord(classId, colorId);
return (color == null) ? false : color.starter;
}
/**
* Returns a random starting color from the specified color class.
*/
public ColorRecord getRandomStartingColor (String className)
{
// make sure the class exists
ClassRecord record = getClassRecord(className);
return (record == null) ? null : record.randomStartingColor();
}
/**
* Looks up a colorization by id.
*/
public Colorization getColorization (int classId, int colorId)
{
ColorRecord color = getColorRecord(classId, colorId);
return (color == null) ? null : color.getColorization();
}
/**
* Looks up a colorization by color print.
*/
public Colorization getColorization (int colorPrint)
{
return getColorization(colorPrint >> 8, colorPrint & 0xFF);
}
/**
* Looks up a colorization by name.
*/
public Colorization getColorization (String className, int colorId)
{
ClassRecord crec = getClassRecord(className);
if (crec != null) {
ColorRecord color = (ColorRecord)crec.colors.get(colorId);
if (color != null) {
return color.getColorization();
}
}
return null;
}
/**
* Loads up a colorization class by name and logs a warning if it
* doesn't exist.
*/
public ClassRecord getClassRecord (String className)
{
Iterator iter = _classes.values().iterator();
while (iter.hasNext()) {
ClassRecord crec = (ClassRecord)iter.next();
if (crec.name.equals(className)) {
return crec;
}
}
Log.warning("No such color class [class=" + className + "].");
Thread.dumpStack();
return null;
}
/**
* Looks up the requested color record.
*/
public ColorRecord getColorRecord (int classId, int colorId)
{
ClassRecord record = (ClassRecord)_classes.get(classId);
if (record == null) {
// if they request color class zero, we assume they're just
// decoding a blank colorprint, otherwise we complain
if (classId != 0) {
Log.warning("Requested unknown color class " +
"[classId=" + classId +
", colorId=" + colorId + "].");
Thread.dumpStack();
}
return null;
}
return (ColorRecord)record.colors.get(colorId);
}
/**
* Adds a fully configured color class record to the pository. This is
* only called by the XML parsing code, so pay it no mind.
*/
public void addClass (ClassRecord record)
{
// validate the class id
if (record.classId > 255) {
Log.warning("Refusing to add class; classId > 255 " + record + ".");
} else {
_classes.put(record.classId, record);
}
}
/**
* Loads up a serialized color pository from the supplied resource
* manager.
*/
public static ColorPository loadColorPository (ResourceManager rmgr)
{
try {
return loadColorPository(rmgr.getResource(CONFIG_PATH));
} catch (IOException ioe) {
Log.warning("Failure loading color pository [path=" + CONFIG_PATH +
", error=" + ioe + "].");
return new ColorPository();
}
}
/**
* Loads up a serialized color pository from the supplied resource
* manager.
*/
public static ColorPository loadColorPository (InputStream source)
{
try {
return (ColorPository)CompiledConfig.loadConfig(source);
} catch (IOException ioe) {
Log.warning("Failure loading color pository: " + ioe + ".");
return new ColorPository();
}
}
/**
* Serializes and saves color pository to the supplied file.
*/
public static void saveColorPository (ColorPository posit, File root)
{
File path = new File(root, CONFIG_PATH);
try {
CompiledConfig.saveConfig(path, posit);
} catch (IOException ioe) {
Log.warning("Failure saving color pository " +
"[path=" + path + ", error=" + ioe + "].");
}
}
/** Our mapping from class names to class records. */
protected HashIntMap _classes = new HashIntMap();
/** Increase this value when object's serialized state is impacted by
* a class change (modification of fields, inheritance). */
private static final long serialVersionUID = 1;
/**
* The path (relative to the resource directory) at which the
* serialized recolorization repository should be loaded and stored.
*/
protected static final String CONFIG_PATH = "config/media/colordefs.dat";
}
@@ -0,0 +1,57 @@
//
// $Id: ColorUtil.java 4191 2006-06-13 22:42:20Z ray $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image;
import java.awt.Color;
/**
* Utilities to manipulate colors.
*/
public class ColorUtil
{
/**
* Blends the two supplied colors.
*
* @return a color halfway between the two colors.
*/
public static final Color blend (Color c1, Color c2)
{
return new Color((c1.getRed() + c2.getRed()) >> 1,
(c1.getGreen() + c2.getGreen()) >> 1,
(c1.getBlue() + c2.getBlue()) >> 1);
}
/**
* Blends the two supplied colors, using the supplied percentage
* as the amount of the first color to use.
*
* @param firstperc The percentage of the first color to use, from 0.0f
* to 1.0f inclusive.
*/
public static final Color blend (Color c1, Color c2, float firstperc)
{
float p2 = 1.0f - firstperc;
return new Color((int) (c1.getRed() * firstperc + c2.getRed() * p2),
(int) (c1.getGreen() * firstperc + c2.getGreen() * p2),
(int) (c1.getBlue() * firstperc + c2.getBlue() * p2));
}
}
@@ -0,0 +1,194 @@
//
// $Id: Colorization.java 4191 2006-06-13 22:42:20Z ray $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image;
import java.awt.Color;
import com.samskivert.util.StringUtil;
/**
* Used to support recoloring images.
*/
public class Colorization
{
/** Every colorization must have a unique id that can be used to
* compare a particular colorization record with another. */
public int colorizationId;
/** The root color for the colorization. */
public Color rootColor;
/** The range around the root color that will be colorized (in
* delta-hue, delta-saturation, delta-value. */
public float[] range;
/** The adjustments to make to hue, saturation and value. */
public float[] offsets;
/**
* Constructs a colorization record with the specified identifier.
*/
public Colorization (int colorizationId, Color rootColor,
float[] range, float[] offsets)
{
this.colorizationId = colorizationId;
this.rootColor = rootColor;
this.range = range;
this.offsets = offsets;
// compute our HSV and fixed HSV
_hsv = Color.RGBtoHSB(rootColor.getRed(), rootColor.getGreen(),
rootColor.getBlue(), null);
_fhsv = toFixedHSV(_hsv, null);
}
/**
* Returns the root color adjusted by the colorization.
*/
public Color getColorizedRoot ()
{
return new Color(recolorColor(_hsv));
}
/**
* Adjusts the supplied color by the offests in this colorization,
* taking the appropriate measures for hue (wrapping it around) and
* saturation and value (clipping).
*
* @return the RGB value of the recolored color.
*/
public int recolorColor (float[] hsv)
{
// for hue, we wrap around
float hue = hsv[0] + offsets[0];
if (hue > 1.0) {
hue -= 1.0;
}
// otherwise we clip
float sat = Math.min(Math.max(hsv[1] + offsets[1], 0), 1);
float val = Math.min(Math.max(hsv[2] + offsets[2], 0), 1);
// convert back to RGB space
return Color.HSBtoRGB(hue, sat, val);
}
/**
* Returns true if this colorization matches the supplied color, false
* otherwise.
*
* @param hsv the HSV values for the color in question.
* @param fhsv the HSV values converted to fixed point via {@link
* #toFixedHSV} for the color in question.
*/
public boolean matches (float[] hsv, int[] fhsv)
{
// check to see that this color is sufficiently "close" to the
// root color based on the supplied distance parameters
if (distance(fhsv[0], _fhsv[0], Short.MAX_VALUE) >=
range[0] * Short.MAX_VALUE) {
return false;
}
// saturation and value don't wrap around like hue
if (Math.abs(_hsv[1] - hsv[1]) >= range[1] ||
Math.abs(_hsv[2] - hsv[2]) >= range[2]) {
return false;
}
return true;
}
// documentation inherited
public int hashCode ()
{
return colorizationId ^ rootColor.hashCode();
}
/**
* Compares this colorization to another based on id.
*/
public boolean equals (Object other)
{
if (other instanceof Colorization) {
return ((Colorization)other).colorizationId == colorizationId;
} else {
return false;
}
}
/**
* Returns a string representation of this colorization.
*/
public String toString ()
{
return String.valueOf(colorizationId);
}
/**
* Returns a long string representation of this colorization.
*/
public String toVerboseString ()
{
return StringUtil.fieldsToString(this);
}
/**
* Converts floating point HSV values to a fixed point integer
* representation.
*
* @param hsv the HSV values to be converted.
* @param fhsv the destination array into which the fixed values will
* be stored. If this is null, a new array will be created of the
* appropriate length.
*
* @return the <code>fhsv</code> parameter if it was non-null or the
* newly created target array.
*/
public static int[] toFixedHSV (float[] hsv, int[] fhsv)
{
if (fhsv == null) {
fhsv = new int[hsv.length];
}
for (int i = 0; i < hsv.length; i++) {
// fhsv[i] = (int)(hsv[i]*Integer.MAX_VALUE);
fhsv[i] = (int)(hsv[i]*Short.MAX_VALUE);
}
return fhsv;
}
/**
* Returns the distance between the supplied to numbers modulo N.
*/
public static int distance (int a, int b, int N)
{
return (a > b) ? Math.min(a-b, b+N-a) : Math.min(b-a, a+N-b);
}
/** Fixed HSV values for our root color; used when calculating
* recolorizations using this colorization. */
protected int[] _fhsv;
/** HSV values for our root color; used when calculating
* recolorizations using this colorization. */
protected float[] _hsv;
}
@@ -0,0 +1,96 @@
package com.threerings.media.image;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
public class CompositeMirage implements Mirage
{
public CompositeMirage (Mirage[] mirages)
{
_mirages = mirages;
}
public CompositeMirage (Mirage mirage1, Mirage mirage2)
{
_mirages = new Mirage[]{mirage1, mirage2};
}
// documentation inherited from interface Mirage
public long getEstimatedMemoryUsage ()
{
// Return the total memory of our component mirages.
long mem = 0;
for (Mirage m : _mirages) {
mem += m.getEstimatedMemoryUsage();
}
return mem;
}
// documentation inherited from interface Mirage
public int getHeight ()
{
// Return the maximal height of our component mirages.
int height = 0;
for (Mirage m : _mirages) {
height = Math.max(height, m.getHeight());
}
return height;
}
// documentation inherited from interface Mirage
public int getWidth ()
{
// Return the maximal width of our component mirages.
int width = 0;
for (Mirage m : _mirages) {
width = Math.max(width, m.getWidth());
}
return width;
}
// documentation inheritd from interface Mirage
public BufferedImage getSnapshot ()
{
BufferedImage img = new BufferedImage(getWidth(), getHeight(),
BufferedImage.TYPE_INT_ARGB);
Graphics2D gfx = img.createGraphics();
try {
for (Mirage m : _mirages) {
m.paint(gfx, 0, 0);
}
} finally {
gfx.dispose();
}
return img;
}
// documentation inheritd from interface Mirage
public boolean hitTest (int x, int y)
{
// If it hits any of our mirages, it hits us.
for (Mirage m : _mirages) {
if (m.hitTest(x, y)) {
return true;
}
}
return false;
}
// documentation inheritd from interface Mirage
public void paint (Graphics2D gfx, int x, int y)
{
// Paint everyone.
for (Mirage m : _mirages) {
m.paint(gfx, x, y);
}
}
/** All the component mirages we're made up of. */
protected Mirage[] _mirages;
}
@@ -0,0 +1,174 @@
//
// $Id: FastImageIO.java 4191 2006-06-13 22:42:20Z ray $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image;
import java.awt.Point;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.IndexColorModel;
import java.awt.image.PixelInterleavedSampleModel;
import java.awt.image.WritableRaster;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.IntBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
/**
* Provides routines for writing and reading uncompressed 8-bit color
* mapped images in a manner that is extremely fast and generates a
* minimal amount of garbage during the loading process.
*/
public class FastImageIO
{
/** A suffix for use when storing raw images in bundles or on the file
* system. */
public static final String FILE_SUFFIX = ".raw";
/**
* Returns true if the supplied image is of a format that is supported
* by the fast image I/O services, false if not.
*/
public static boolean canWrite (BufferedImage image)
{
return (image.getColorModel() instanceof IndexColorModel) &&
(image.getRaster().getDataBuffer() instanceof DataBufferByte);
}
/**
* Writes the supplied image to the supplied output stream.
*
* @exception IOException thrown if an error occurs writing to the
* output stream.
*/
public static void write (BufferedImage image, OutputStream out)
throws IOException
{
DataOutputStream dout = new DataOutputStream(out);
// write the image dimensions
int width = image.getWidth(), height = image.getHeight();
dout.writeInt(width);
dout.writeInt(height);
// write the color model information
IndexColorModel cmodel = (IndexColorModel)image.getColorModel();
int tpixel = cmodel.getTransparentPixel();
dout.writeInt(tpixel);
int msize = cmodel.getMapSize();
int[] map = new int[msize];
cmodel.getRGBs(map);
dout.writeInt(msize);
for (int ii = 0; ii < map.length; ii++) {
dout.writeInt(map[ii]);
}
// write the raster data
DataBufferByte dbuf = (DataBufferByte)image.getRaster().getDataBuffer();
byte[] data = dbuf.getData();
if (data.length != width * height) {
String errmsg = "Raster data not same size as image! [" +
width + "x" + height + " != " + data.length + "]";
throw new IllegalStateException(errmsg);
}
dout.write(data);
dout.flush();
}
/**
* Reads an image from the supplied file (which must contain an image
* previously written via a call to {@link #write}).
*
* @exception IOException thrown if an error occurs reading from the
* file.
*/
public static BufferedImage read (File file)
throws IOException
{
RandomAccessFile raf = new RandomAccessFile(file, "r");
FileChannel fchan = raf.getChannel();
try {
MappedByteBuffer mbuf = fchan.map(
FileChannel.MapMode.READ_ONLY, 0, file.length());
// read in our integer fields
IntBuffer ibuf = mbuf.asIntBuffer();
int width = ibuf.get();
int height = ibuf.get();
int tpixel = ibuf.get();
int msize = ibuf.get();
if (width > Short.MAX_VALUE || width < 0 ||
height > Short.MAX_VALUE || height < 0) {
throw new IOException("Bogus image size " +
width + "x" + height);
}
IndexColorModel cmodel;
synchronized (_origin) { // any old object will do
// make sure our colormap array is big enough
if (_cmap == null || _cmap.length < msize) {
_cmap = new int[msize];
}
// read in the data and create our colormap
ibuf.get(_cmap, 0, msize);
cmodel = new IndexColorModel(
8, msize, _cmap, 0, DataBuffer.TYPE_BYTE, null);
}
// advance the byte buffer accordingly
mbuf.position(ibuf.position() * 4);
// read in the image data itself
byte[] data = new byte[width*height];
mbuf.get(data);
// create the image from our component parts
DataBuffer dbuf = new DataBufferByte(data, data.length, 0);
int[] offsets = new int[] { 0 };
PixelInterleavedSampleModel smodel =
new PixelInterleavedSampleModel(
DataBuffer.TYPE_BYTE, width, height, 1, width, offsets);
WritableRaster raster = WritableRaster.createWritableRaster(
smodel, dbuf, _origin);
return new BufferedImage(cmodel, raster, false, null);
} finally {
fchan.close();
raf.close();
}
}
/** Used when loading our color map. */
protected static int[] _cmap;
/** Used when creating our writable raster. */
protected static Point _origin = new Point(0, 0);
}
@@ -0,0 +1,46 @@
//
// $Id: ImageDataProvider.java 4191 2006-06-13 22:42:20Z ray $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image;
import java.io.IOException;
import java.awt.image.BufferedImage;
/**
* Provides access to image data for the image with the specified
* path. Images loaded from different data providers (which are
* differentiated by reference equality) will be considered distinct
* images with respect to caching.
*/
public interface ImageDataProvider
{
/**
* Returns a string identifier for this image data provider which wil
* be used to differentiate it from other providers and thus should be
* unique.
*/
public String getIdent ();
/**
* Returns the image at the specified path.
*/
public BufferedImage loadImage (String path) throws IOException;
}
@@ -0,0 +1,615 @@
//
// $Id: ImageManager.java 4007 2006-04-10 08:59:30Z mdb $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image;
import java.awt.Component;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import javax.imageio.ImageIO;
import javax.imageio.stream.ImageInputStream;
import com.samskivert.swing.RuntimeAdjust;
import com.samskivert.util.LRUHashMap;
import com.samskivert.util.StringUtil;
import com.samskivert.util.Throttle;
import com.samskivert.util.Tuple;
import com.threerings.media.Log;
import com.threerings.media.MediaPrefs;
import com.threerings.resource.ResourceManager;
/**
* Provides a single point of access for image retrieval and caching.
*/
public class ImageManager
implements ImageUtil.ImageCreator
{
/**
* Used to identify an image for caching and reconstruction.
*/
public static class ImageKey
{
/** The data provider from which this image's data is loaded. */
public ImageDataProvider daprov;
/** The path used to identify the image to the data provider. */
public String path;
protected ImageKey (ImageDataProvider daprov, String path)
{
this.daprov = daprov;
this.path = path;
}
public int hashCode ()
{
return path.hashCode() ^ daprov.getIdent().hashCode();
}
public boolean equals (Object other)
{
if (other == null || !(other instanceof ImageKey)) {
return false;
}
ImageKey okey = (ImageKey)other;
return ((okey.daprov.getIdent().equals(daprov.getIdent())) &&
(okey.path.equals(path)));
}
public String toString ()
{
return daprov.getIdent() + ":" + path;
}
}
/**
* This interface allows the image manager to create images that are in a
* format optimal for rendering to the screen.
*/
public interface OptimalImageCreator
{
/**
* Requests that a blank image be created that is in a format and of a
* depth that are optimal for rendering to the screen.
*/
public BufferedImage createImage (int width, int height, int trans);
}
/**
* Construct an image manager with the specified {@link ResourceManager}
* from which it will obtain its data.
*/
public ImageManager (ResourceManager rmgr, OptimalImageCreator icreator)
{
_rmgr = rmgr;
_icreator = icreator;
// create our image cache
int icsize = _cacheSize.getValue();
Log.debug("Creating image cache [size=" + icsize + "k].");
_ccache = new LRUHashMap(icsize * 1024, new LRUHashMap.ItemSizer() {
public int computeSize (Object value) {
return (int)((CacheRecord)value).getEstimatedMemoryUsage();
}
});
_ccache.setTracking(true);
}
/**
* A convenience constructor that creates an {@link AWTImageCreator} for
* use by the image manager.
*/
public ImageManager (ResourceManager rmgr, Component context)
{
this(rmgr, new AWTImageCreator(context));
}
/**
* Creates a buffered image, optimized for display on our graphics
* device.
*/
public BufferedImage createImage (int width, int height, int transparency)
{
return _icreator.createImage(width, height, transparency);
}
/**
* Loads (and caches) the specified image from the resource manager
* using the supplied path to identify the image.
*/
public BufferedImage getImage (String path)
{
return getImage(null, path, null);
}
/**
* Like {@link #getImage(String)} but the specified colorizations are
* applied to the image before it is returned.
*/
public BufferedImage getImage (String path, Colorization[] zations)
{
return getImage(null, path, zations);
}
/**
* Like {@link #getImage(String)} but the image is loaded from the
* specified resource set rathern than the default resource set.
*/
public BufferedImage getImage (String rset, String path)
{
return getImage(rset, path, null);
}
/**
* Like {@link #getImage(String,String)} but the specified
* colorizations are applied to the image before it is returned.
*/
public BufferedImage getImage (String rset, String path,
Colorization[] zations)
{
if (StringUtil.isBlank(path)) {
String errmsg = "Invalid image path [rset=" + rset +
", path=" + path + "]";
throw new IllegalArgumentException(errmsg);
}
return getImage(getImageKey(rset, path), zations);
}
/**
* Loads (and caches) the specified image from the resource manager
* using the supplied path to identify the image.
*
* <p> Additionally the image is optimized for display in the current
* graphics configuration. Consider using {@link #getMirage(ImageKey)}
* instead of prepared images as they (some day) will automatically
* use volatile images to increase performance.
*/
public BufferedImage getPreparedImage (String path)
{
return getPreparedImage(null, path, null);
}
/**
* Loads (and caches) the specified image from the resource manager,
* obtaining the image from the supplied resource set.
*
* <p> Additionally the image is optimized for display in the current
* graphics configuration. Consider using {@link #getMirage(ImageKey)}
* instead of prepared images as they (some day) will automatically
* use volatile images to increase performance.
*/
public BufferedImage getPreparedImage (String rset, String path)
{
return getPreparedImage(rset, path, null);
}
/**
* Loads (and caches) the specified image from the resource manager,
* obtaining the image from the supplied resource set and applying the
* using the supplied path to identify the image.
*
* <p> Additionally the image is optimized for display in the current
* graphics configuration. Consider using {@link
* #getMirage(ImageKey,Colorization[])} instead of prepared images as
* they (some day) will automatically use volatile images to increase
* performance.
*/
public BufferedImage getPreparedImage (
String rset, String path, Colorization[] zations)
{
BufferedImage image = getImage(rset, path, zations);
BufferedImage prepped = null;
if (image != null) {
prepped = createImage(image.getWidth(), image.getHeight(),
image.getColorModel().getTransparency());
Graphics2D pg = prepped.createGraphics();
pg.drawImage(image, 0, 0, null);
pg.dispose();
}
return prepped;
}
/**
* Returns an image key that can be used to fetch the image identified
* by the specified resource set and image path.
*/
public ImageKey getImageKey (String rset, String path)
{
return getImageKey(getDataProvider(rset), path);
}
/**
* Returns an image key that can be used to fetch the image identified
* by the specified data provider and image path.
*/
public ImageKey getImageKey (ImageDataProvider daprov, String path)
{
return new ImageKey(daprov, path);
}
/**
* Obtains the image identified by the specified key, caching if
* possible. The image will be recolored using the supplied
* colorizations if requested.
*/
public BufferedImage getImage (ImageKey key, Colorization[] zations)
{
CacheRecord crec = null;
synchronized (_ccache) {
crec = (CacheRecord)_ccache.get(key);
}
if (crec != null) {
// Log.info("Cache hit [key=" + key + ", crec=" + crec + "].");
return crec.getImage(zations);
}
// Log.info("Cache miss [key=" + key + ", crec=" + crec + "].");
// load up the raw image
BufferedImage image = loadImage(key);
if (image == null) {
Log.warning("Failed to load image " + key + ".");
// create a blank image instead
image = new BufferedImage(10, 10, BufferedImage.TYPE_BYTE_INDEXED);
}
// Log.info("Loaded " + key.path + ", image=" + image +
// ", size=" + ImageUtil.getEstimatedMemoryUsage(image));
// create a cache record
crec = new CacheRecord(key, image);
synchronized (_ccache) {
_ccache.put(key, crec);
}
_keySet.add(key);
// periodically report our image cache performance
reportCachePerformance();
return crec.getImage(zations);
}
/**
* Returns the data provider configured to obtain image data from the
* specified resource set.
*/
protected ImageDataProvider getDataProvider (final String rset)
{
if (rset == null) {
return _defaultProvider;
}
ImageDataProvider dprov = (ImageDataProvider)_providers.get(rset);
if (dprov == null) {
dprov = new ImageDataProvider() {
public BufferedImage loadImage (String path)
throws IOException {
// first attempt to load the image from the specified
// resource set
try {
return read(_rmgr.getImageResource(rset, path));
} catch (FileNotFoundException fnfe) {
// fall back to trying the classpath
return read(_rmgr.getImageResource(path));
}
}
public String getIdent () {
return "rmgr:" + rset;
}
};
_providers.put(rset, dprov);
}
return dprov;
}
/**
* Loads and returns the image with the specified key from the
* supplied data provider.
*/
protected BufferedImage loadImage (ImageKey key)
{
// if (EventQueue.isDispatchThread()) {
// Log.info("Loading image on AWT thread " + key + ".");
// }
BufferedImage image = null;
try {
Log.debug("Loading image " + key + ".");
image = key.daprov.loadImage(key.path);
if (image == null) {
Log.warning("ImageIO.read(" + key + ") returned null.");
}
} catch (Exception e) {
Log.warning("Unable to load image '" + key + "'.");
Log.logStackTrace(e);
// create a blank image in its stead
image = createImage(1, 1, Transparency.OPAQUE);
}
return image;
}
/**
* Helper function for loading images.
*/
protected static BufferedImage read (ImageInputStream iis)
throws IOException
{
BufferedImage image = ImageIO.read(iis);
closeIIS(iis);
return image;
}
/**
* Helper function for closing image input streams.
*/
protected static void closeIIS (ImageInputStream iis)
{
if (iis == null) {
return;
}
try {
iis.close();
} catch (IOException ioe) {
// jesus fucking hoppalong cassidy christ on a polyester pogo
// stick! ImageInputStreamImpl.close() throws a fucking
// IOException if it's already closed; there's no way to find
// out if it's already closed or not, so we have to check for
// their bullshit exception; as if we should just "trust"
// ImageIO.read() to close the fucking input stream when it's
// done, especially after the goddamned fiasco with
// PNGImageReader not closing it's fucking inflaters; for the
// love of humanity
if (!"closed".equals(ioe.getMessage())) {
Log.warning("Failure closing image input '" + iis + "'.");
Log.logStackTrace(ioe);
}
}
}
/**
* Creates a mirage which is an image optimized for display on our
* current display device and which will be stored into video memory
* if possible.
*/
public Mirage getMirage (ImageKey key)
{
return getMirage(key, null, null);
}
/**
* Like {@link #getMirage(ImageKey)} but that only the specified
* subimage of the source image is used to build the mirage.
*/
public Mirage getMirage (ImageKey key, Rectangle bounds)
{
return getMirage(key, bounds, null);
}
/**
* Like {@link #getMirage(ImageKey)} but the supplied colorizations
* are applied to the source image before creating the mirage.
*/
public Mirage getMirage (ImageKey key, Colorization[] zations)
{
return getMirage(key, null, zations);
}
/**
* Like {@link #getMirage(ImageKey,Colorization[])} except that the
* mirage is created using only the specified subset of the original
* image.
*/
public Mirage getMirage (ImageKey key, Rectangle bounds,
Colorization[] zations)
{
BufferedImage src = null;
if (bounds == null) {
// if they specified no bounds, we need to load up the raw
// image and determine its bounds so that we can pass those
// along to the created mirage
src = getImage(key, zations);
bounds = new Rectangle(0, 0, src.getWidth(), src.getHeight());
} else if (!_prepareImages.getValue()) {
src = getImage(key, zations);
src = src.getSubimage(bounds.x, bounds.y,
bounds.width, bounds.height);
}
if (_runBlank.getValue()) {
return new BlankMirage(bounds.width, bounds.height);
} else if (_prepareImages.getValue()) {
return new CachedVolatileMirage(this, key, bounds, zations);
} else {
return new BufferedMirage(src);
}
}
/**
* Returns the image creator that can be used to create buffered images
* optimized for rendering to the screen.
*/
public OptimalImageCreator getImageCreator ()
{
return _icreator;
}
/**
* Reports statistics detailing the image manager cache performance
* and the current size of the cached images.
*/
protected void reportCachePerformance ()
{
if (/* Log.getLevel() != Log.log.DEBUG || */
_cacheStatThrottle.throttleOp()) {
return;
}
// compute our estimated memory usage
long size = 0;
int[] eff = null;
synchronized (_ccache) {
Iterator iter = _ccache.values().iterator();
while (iter.hasNext()) {
size += ((CacheRecord)iter.next()).getEstimatedMemoryUsage();
}
eff = _ccache.getTrackedEffectiveness();
}
Log.info("ImageManager LRU [mem=" + (size / 1024) + "k" +
", size=" + _ccache.size() + ", hits=" + eff[0] +
", misses=" + eff[1] + ", totalKeys=" + _keySet.size() + "].");
}
/** Maintains a source image and a set of colorized versions in the
* image cache. */
protected static class CacheRecord
{
public CacheRecord (ImageKey key, BufferedImage source)
{
_key = key;
_source = source;
}
public BufferedImage getImage (Colorization[] zations)
{
if (zations == null) {
return _source;
}
if (_colorized == null) {
_colorized = new ArrayList();
}
// we search linearly through our list of colorized copies
// because it is not likely to be very long
int csize = _colorized.size();
for (int ii = 0; ii < csize; ii++) {
Tuple tup = (Tuple)_colorized.get(ii);
Colorization[] tzations = (Colorization[])tup.left;
if (Arrays.equals(zations, tzations)) {
return (BufferedImage)tup.right;
}
}
try {
BufferedImage cimage = ImageUtil.recolorImage(_source, zations);
_colorized.add(new Tuple(zations, cimage));
return cimage;
} catch (Exception re) {
Log.warning("Failure recoloring image [source" + _key +
", zations=" + StringUtil.toString(zations) +
", error=" + re + "].");
// return the uncolorized version
return _source;
}
}
public long getEstimatedMemoryUsage ()
{
return ImageUtil.getEstimatedMemoryUsage(_source);
}
public String toString ()
{
return "[key=" + _key + ", wid=" + _source.getWidth() +
", hei=" + _source.getHeight() +
", ccount=" + ((_colorized == null) ? 0 : _colorized.size()) +
"]";
}
protected ImageKey _key;
protected BufferedImage _source;
protected ArrayList _colorized;
}
/** A reference to the resource manager via which we load image data
* by default. */
protected ResourceManager _rmgr;
/** We use this to create images optimized for rendering. */
protected OptimalImageCreator _icreator;
/** A cache of loaded images. */
protected LRUHashMap _ccache;
/** The set of all keys we've ever seen. */
protected HashSet _keySet = new HashSet();
/** Throttle our cache status logging to once every 300 seconds. */
protected Throttle _cacheStatThrottle = new Throttle(1, 300000L);
/** Our default data provider. */
protected ImageDataProvider _defaultProvider = new ImageDataProvider() {
public BufferedImage loadImage (String path) throws IOException {
return read(_rmgr.getImageResource(path));
}
public String getIdent () {
return "rmgr:default";
}
};
/** Data providers for different resource sets. */
protected HashMap _providers = new HashMap();
/** Register our image cache size with the runtime adjustments
* framework. */
protected static RuntimeAdjust.IntAdjust _cacheSize =
new RuntimeAdjust.IntAdjust(
"Size (in kb of memory used) of the image manager LRU cache " +
"[requires restart]", "narya.media.image.cache_size",
MediaPrefs.config, 2048);
/** Controls whether or not we prepare images or use raw versions. */
protected static RuntimeAdjust.BooleanAdjust _prepareImages =
new RuntimeAdjust.BooleanAdjust(
"Cause image manager to optimize all images for display.",
"narya.media.image.prep_images", MediaPrefs.config, true);
/** A debug toggle for running entirely without rendering images. */
protected static RuntimeAdjust.BooleanAdjust _runBlank =
new RuntimeAdjust.BooleanAdjust(
"Cause image manager to return blank images.",
"narya.media.image.run_blank", MediaPrefs.config, false);
}
@@ -0,0 +1,647 @@
//
// $Id: ImageUtil.java 3965 2006-03-21 02:32:27Z mthomas $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.HeadlessException;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.IndexColorModel;
import java.awt.image.Raster;
import java.awt.image.WritableRaster;
import java.util.Arrays;
import java.util.Iterator;
import com.samskivert.swing.Label;
/**
* Image related utility functions.
*/
public class ImageUtil
{
public static interface ImageCreator
{
/** Used by routines that need to create new images to allow the
* caller to dictate the format (which may mean using
* createCompatibleImage). */
public BufferedImage createImage (
int width, int height, int transparency);
}
/**
* Creates a new buffered image with the same sample model and color
* model as the source image but with the new width and height.
*/
public static BufferedImage createCompatibleImage (
BufferedImage source, int width, int height)
{
WritableRaster raster =
source.getRaster().createCompatibleWritableRaster(width, height);
return new BufferedImage(source.getColorModel(), raster, false, null);
}
/**
* Creates an image with the word "Error" written in it.
*/
public static BufferedImage createErrorImage (int width, int height)
{
BufferedImage img = new BufferedImage(
width, height, BufferedImage.TYPE_BYTE_INDEXED);
Graphics2D g = (Graphics2D)img.getGraphics();
g.setColor(Color.red);
Label l = new Label("Error");
l.layout(g);
Dimension d = l.getSize();
// fill that sucker with errors
for (int yy = 0; yy < height; yy += d.height) {
for (int xx = 0; xx < width; xx += (d.width+5)) {
l.render(g, xx, yy);
}
}
g.dispose();
return img;
}
/**
* Used to recolor images by shifting bands of color (in HSV color
* space) to a new hue. The source images must be 8-bit color mapped
* images, as the recoloring process works by analysing the color map
* and modifying it.
*/
public static BufferedImage recolorImage (
BufferedImage image, Color rootColor, float[] dists, float[] offsets)
{
return recolorImage(image, new Colorization[] {
new Colorization(-1, rootColor, dists, offsets) });
}
/**
* Recolors the supplied image as in {@link
* #recolorImage(BufferedImage,Color,float[],float[])} obtaining the
* recoloring parameters from the supplied {@link Colorization}
* instance.
*/
public static BufferedImage recolorImage (
BufferedImage image, Colorization cz)
{
return recolorImage(image, new Colorization[] { cz });
}
/**
* Recolors the supplied image using the supplied colorizations.
*/
public static BufferedImage recolorImage (
BufferedImage image, Colorization[] zations)
{
ColorModel cm = image.getColorModel();
if (!(cm instanceof IndexColorModel)) {
String errmsg = "Unable to recolor images with non-index color " +
"model [cm=" + cm.getClass() + "]";
throw new RuntimeException(errmsg);
}
// now process the image
IndexColorModel icm = (IndexColorModel)cm;
int size = icm.getMapSize();
int zcount = zations.length;
int[] rgbs = new int[size];
// fetch the color data
icm.getRGBs(rgbs);
// convert the colors to HSV
float[] hsv = new float[3];
int[] fhsv = new int[3];
int tpixel = -1;
for (int i = 0; i < size; i++) {
int value = rgbs[i];
// don't fiddle with alpha pixels
if ((value & 0xFF000000) == 0) {
tpixel = i;
continue;
}
// convert the color to HSV
int red = (value >> 16) & 0xFF;
int green = (value >> 8) & 0xFF;
int blue = (value >> 0) & 0xFF;
Color.RGBtoHSB(red, green, blue, hsv);
Colorization.toFixedHSV(hsv, fhsv);
// see if this color matches and of our colorizations and
// recolor it if it does
for (int z = 0; z < zcount; z++) {
Colorization cz = zations[z];
if (cz != null && cz.matches(hsv, fhsv)) {
// massage the HSV bands and update the RGBs array
rgbs[i] = cz.recolorColor(hsv);
break;
}
}
}
// create a new image with the adjusted color palette
IndexColorModel nicm = new IndexColorModel(
icm.getPixelSize(), size, rgbs, 0, icm.hasAlpha(),
icm.getTransparentPixel(), icm.getTransferType());
return new BufferedImage(nicm, image.getRaster(), false, null);
}
/**
* Paints multiple copies of the supplied image using the supplied
* graphics context such that the requested area is filled with the
* image.
*/
public static void tileImage (
Graphics2D gfx, Mirage image, int x, int y, int width, int height)
{
int iwidth = image.getWidth(), iheight = image.getHeight();
int xnum = width / iwidth, xplus = width % iwidth;
int ynum = height / iheight, yplus = height % iheight;
Shape oclip = gfx.getClip();
for (int ii=0; ii < ynum; ii++) {
// draw the full copies of the image across
int xx = x;
for (int jj=0; jj < xnum; jj++) {
image.paint(gfx, xx, y);
xx += iwidth;
}
if (xplus > 0) {
gfx.clipRect(xx, y, xplus, iheight);
image.paint(gfx, xx, y);
gfx.setClip(oclip);
}
y += iheight;
}
if (yplus > 0) {
int xx = x;
for (int jj=0; jj < xnum; jj++) {
gfx.clipRect(xx, y, iwidth, yplus);
image.paint(gfx, xx, y);
gfx.setClip(oclip);
xx += iwidth;
}
if (xplus > 0) {
gfx.clipRect(xx, y, xplus, yplus);
image.paint(gfx, xx, y);
gfx.setClip(oclip);
}
}
}
/**
* Paints multiple copies of the supplied image using the supplied
* graphics context such that the requested width is filled with the
* image.
*/
public static void tileImageAcross (Graphics2D gfx, Mirage image,
int x, int y, int width)
{
tileImage(gfx, image, x, y, width, image.getHeight());
}
/**
* Paints multiple copies of the supplied image using the supplied
* graphics context such that the requested height is filled with the
* image.
*/
public static void tileImageDown (Graphics2D gfx, Mirage image,
int x, int y, int height)
{
tileImage(gfx, image, x, y, image.getWidth(), height);
}
// Not fully added because we're not using it anywhere, plus
// it's probably a little sketchy to create Area objects with all
// this pixely data.
// Also, the Area was getting zeroed out when it was translated. Something
// to look into someday if anyone wants to use this method.
// /**
// * Creates a mask that is opaque in the non-transparent areas of the
// * source image.
// */
// public static Area createImageMask (BufferedImage src)
// {
// Raster srcdata = src.getData();
// int wid = src.getWidth(), hei = src.getHeight();
// Log.info("creating area of (" + wid + ", " + hei + ")");
// Area a = new Area(new Rectangle(wid, hei));
// Rectangle r = new Rectangle(1, 1);
//
// for (int yy=0; yy < hei; yy++) {
// for (int xx=0; xx < wid; xx++) {
// if (srcdata.getSample(xx, yy, 0) == 0) {
// r.setLocation(xx, yy);
// a.subtract(new Area(r));
// }
// }
// }
//
// return a;
// }
/**
* Creates and returns a new image consisting of the supplied image
* traced with the given color and thickness.
*/
public static BufferedImage createTracedImage (
ImageCreator isrc, BufferedImage src, Color tcolor, int thickness)
{
return createTracedImage(isrc, src, tcolor, thickness, 1.0f, 1.0f);
}
/**
* Creates and returns a new image consisting of the supplied image
* traced with the given color, thickness and alpha transparency.
*/
public static BufferedImage createTracedImage (
ImageCreator isrc, BufferedImage src, Color tcolor, int thickness,
float startAlpha, float endAlpha)
{
// create the destination image
int wid = src.getWidth(), hei = src.getHeight();
BufferedImage dest = isrc.createImage(
wid, hei, Transparency.TRANSLUCENT);
return createTracedImage(
src, dest, tcolor, thickness, startAlpha, endAlpha);
}
/**
* Creates and returns a new image consisting of the supplied image
* traced with the given color, thickness and alpha transparency.
*/
public static BufferedImage createTracedImage (
BufferedImage src, BufferedImage dest, Color tcolor, int thickness,
float startAlpha, float endAlpha)
{
// prepare various bits of working data
int wid = src.getWidth(), hei = src.getHeight();
int spixel = (tcolor.getRGB() & RGB_MASK);
int salpha = (int)(startAlpha * 255);
int tpixel = (spixel | (salpha << 24));
boolean[] traced = new boolean[wid * hei];
int stepAlpha = (thickness <= 1) ? 0 :
(int)(((startAlpha - endAlpha) * 255) / (thickness - 1));
// TODO: this could be made more efficient, e.g., if we made four
// passes through the image in a vertical scan, horizontal scan,
// and opposing diagonal scans, making sure each non-transparent
// pixel found during each scan is traced on both sides of the
// respective scan direction. for now, we just naively check all
// eight pixels surrounding each pixel in the image and fill the
// center pixel with the tracing color if it's transparent but has
// a non-transparent pixel around it.
for (int tt = 0; tt < thickness; tt++) {
if (tt > 0) {
// clear out the array of pixels traced this go-around
Arrays.fill(traced, false);
// use the destination image as our new source
src = dest;
// decrement the trace pixel alpha-level
salpha -= Math.max(0, stepAlpha);
tpixel = (spixel | (salpha << 24));
}
for (int yy = 0; yy < hei; yy++) {
for (int xx = 0; xx < wid; xx++) {
// get the pixel we're checking
int argb = src.getRGB(xx, yy);
if ((argb & TRANS_MASK) != 0) {
// copy any pixel that isn't transparent
dest.setRGB(xx, yy, argb);
} else if (bordersNonTransparentPixel(
src, wid, hei, traced, xx, yy)) {
dest.setRGB(xx, yy, tpixel);
// note that we traced this pixel this pass so
// that it doesn't impact other-pixel borderedness
traced[(yy*wid)+xx] = true;
}
}
}
}
return dest;
}
/**
* Returns whether the given pixel is bordered by any non-transparent
* pixel.
*/
protected static boolean bordersNonTransparentPixel (
BufferedImage data, int wid, int hei, boolean[] traced, int x, int y)
{
// check the three-pixel row above the pixel
if (y > 0) {
for (int rxx = x - 1; rxx <= x + 1; rxx++) {
if (rxx < 0 || rxx >= wid || traced[((y-1)*wid)+rxx]) {
continue;
}
if ((data.getRGB(rxx, y - 1) & TRANS_MASK) != 0) {
return true;
}
}
}
// check the pixel to the left
if (x > 0 && !traced[(y*wid)+(x-1)]) {
if ((data.getRGB(x - 1, y) & TRANS_MASK) != 0) {
return true;
}
}
// check the pixel to the right
if (x < wid - 1 && !traced[(y*wid)+(x+1)]) {
if ((data.getRGB(x + 1, y) & TRANS_MASK) != 0) {
return true;
}
}
// check the three-pixel row below the pixel
if (y < hei - 1) {
for (int rxx = x - 1; rxx <= x + 1; rxx++) {
if (rxx < 0 || rxx >= wid || traced[((y+1)*wid)+rxx]) {
continue;
}
if ((data.getRGB(rxx, y + 1) & TRANS_MASK) != 0) {
return true;
}
}
}
return false;
}
/**
* Create an image using the alpha channel from the first and the RGB
* values from the second.
*/
public static BufferedImage composeMaskedImage (
ImageCreator isrc, BufferedImage mask, BufferedImage base)
{
int wid = base.getWidth();
int hei = base.getHeight();
Raster maskdata = mask.getData();
Raster basedata = base.getData();
// create a new image using the rasters if possible
if (maskdata.getNumBands() == 4 && basedata.getNumBands() >= 3) {
WritableRaster target =
basedata.createCompatibleWritableRaster(wid, hei);
// copy the alpha from the mask image
int[] adata = maskdata.getSamples(0, 0, wid, hei, 3, (int[]) null);
target.setSamples(0, 0, wid, hei, 3, adata);
// copy the RGB from the base image
for (int ii=0; ii < 3; ii++) {
int[] cdata = basedata.getSamples(
0, 0, wid, hei, ii, (int[]) null);
target.setSamples(0, 0, wid, hei, ii, cdata);
}
return new BufferedImage(mask.getColorModel(), target, true, null);
} else {
// otherwise composite them by rendering them with an alpha
// rule
BufferedImage target = isrc.createImage(
wid, hei, Transparency.TRANSLUCENT);
Graphics2D g2 = target.createGraphics();
try {
g2.drawImage(mask, 0, 0, null);
g2.setComposite(AlphaComposite.SrcIn);
g2.drawImage(base, 0, 0, null);
} finally {
g2.dispose();
}
return target;
}
}
/**
* Create a new image using the supplied shape as a mask from which to
* cut out pixels from the supplied image. Pixels inside the shape
* will be added to the final image, pixels outside the shape will be
* clear.
*/
public static BufferedImage composeMaskedImage (
ImageCreator isrc, Shape mask, BufferedImage base)
{
int wid = base.getWidth();
int hei = base.getHeight();
// alternate method for composition:
// 1. create WriteableRaster with base data
// 2. test each pixel with mask.contains() and set the alpha
// channel to fully-alpha if false
// 3. create buffered image from raster
// (I didn't use this method because it depends on the colormodel
// of the source image, and was booching when the souce image was
// a cut-up from a tileset, and it seems like it would take
// longer than the method we are using.
// But it's something to consider)
// composite them by rendering them with an alpha rule
BufferedImage target = isrc.createImage(
wid, hei, Transparency.TRANSLUCENT);
Graphics2D g2 = target.createGraphics();
try {
g2.setColor(Color.BLACK); // whatever, really
g2.fill(mask);
g2.setComposite(AlphaComposite.SrcIn);
g2.drawImage(base, 0, 0, null);
} finally {
g2.dispose();
}
return target;
}
/**
* Returns true if the supplied image contains a non-transparent pixel
* at the specified coordinates, false otherwise.
*/
public static boolean hitTest (BufferedImage image, int x, int y)
{
// it's only a hit if the pixel is non-transparent
int argb = image.getRGB(x, y);
return (argb >> 24) != 0;
}
/**
* Computes the bounds of the smallest rectangle that contains all
* non-transparent pixels of this image. This isn't extremely
* efficient, so you shouldn't be doing this anywhere exciting.
*/
public static void computeTrimmedBounds (
BufferedImage image, Rectangle tbounds)
{
// this could be more efficient, but it's run as a batch process
// and doesn't really take that long anyway
int width = image.getWidth(), height = image.getHeight();
int firstrow = -1, lastrow = -1, minx = width, maxx = 0;
for (int yy = 0; yy < height; yy++) {
int firstidx = -1, lastidx = -1;
for (int xx = 0; xx < width; xx++) {
// if this pixel is transparent, do nothing
int argb = image.getRGB(xx, yy);
if ((argb >> 24) == 0) {
continue;
}
// otherwise, if we've not seen a non-transparent pixel,
// make a note that this is the first non-transparent
// pixel in the row
if (firstidx == -1) {
firstidx = xx;
}
// keep track of the last non-transparent pixel we saw
lastidx = xx;
}
// if we saw no pixels on this row, we can bail now
if (firstidx == -1) {
continue;
}
// update our min and maxx
minx = Math.min(firstidx, minx);
maxx = Math.max(lastidx, maxx);
// otherwise keep track of the first row on which we see
// pixels and the last row on which we see pixels
if (firstrow == -1) {
firstrow = yy;
}
lastrow = yy;
}
// fill in the dimensions
if (firstrow != -1) {
tbounds.x = minx;
tbounds.y = firstrow;
tbounds.width = maxx - minx + 1;
tbounds.height = lastrow - firstrow + 1;
} else {
// Entirely blank image. Return 1x1 blank image.
tbounds.x = 0;
tbounds.y = 0;
tbounds.width = 1;
tbounds.height = 1;
}
}
/**
* Returns the estimated memory usage in bytes for the specified
* image.
*/
public static long getEstimatedMemoryUsage (BufferedImage image)
{
if (image != null) {
return getEstimatedMemoryUsage(image.getRaster());
} else {
return 0;
}
}
/**
* Returns the estimated memory usage in bytes for the specified
* raster.
*/
public static long getEstimatedMemoryUsage (Raster raster)
{
// we assume that the data buffer stores each element in a
// byte-rounded memory element; maybe the buffer is smarter about
// things than this, but we're better to err on the safe side
DataBuffer db = raster.getDataBuffer();
int bpe = (int)Math.ceil(
DataBuffer.getDataTypeSize(db.getDataType()) / 8f);
return bpe * db.getSize();
}
/**
* Returns the estimated memory usage in bytes for all buffered images
* in the supplied iterator.
*/
public static long getEstimatedMemoryUsage (Iterator iter)
{
long size = 0;
while (iter.hasNext()) {
BufferedImage image = (BufferedImage)iter.next();
size += getEstimatedMemoryUsage(image);
}
return size;
}
/**
* Obtains the default graphics configuration for this VM. If the JVM
* is in headless mode, this method will return null.
*/
protected static GraphicsConfiguration getDefGC ()
{
if (_gc == null) {
// obtain information on our graphics environment
try {
GraphicsEnvironment env =
GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice gd = env.getDefaultScreenDevice();
_gc = gd.getDefaultConfiguration();
} catch (HeadlessException e) {
// no problem, just return null
}
}
return _gc;
}
/** The graphics configuration for the default screen device. */
protected static GraphicsConfiguration _gc;
/** Used when seeking fully transparent pixels for outlining. */
protected static final int TRANS_MASK = (0xFF << 24);
/** Used when outlining. */
protected static final int RGB_MASK = 0x00FFFFFF;
}
@@ -0,0 +1,67 @@
//
// $Id: Mirage.java 4191 2006-06-13 22:42:20Z ray $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
/**
* Provides an interface via which images can be accessed in a way that
* allows them to optionally be located in video memory where that affords
* performance improvements.
*/
public interface Mirage
{
/**
* Renders this mirage at the specified position in the supplied
* graphics context.
*/
public void paint (Graphics2D gfx, int x, int y);
/**
* Returns the width of this mirage.
*/
public int getWidth ();
/**
* Returns the height of this mirage.
*/
public int getHeight ();
/**
* Returns true if this mirage contains a non-transparent pixel at the
* specified coordinate.
*/
public boolean hitTest (int x, int y);
/**
* Returns a snapshot of this mirage as a buffered image. The snapshot
* should <em>not</em> be modified by the caller.
*/
public BufferedImage getSnapshot ();
/**
* Returns an estimate of the memory consumed by this mirage's image
* raster data.
*/
public long getEstimatedMemoryUsage ();
}
@@ -0,0 +1,60 @@
//
// $Id: MirageIcon.java 4191 2006-06-13 22:42:20Z ray $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image;
import java.awt.Component;
import java.awt.Graphics2D;
import java.awt.Graphics;
import javax.swing.Icon;
/**
* Implements the Swing {@link Icon} interface with a mirage providing the
* image information.
*/
public class MirageIcon implements Icon
{
public MirageIcon (Mirage mirage)
{
_mirage = mirage;
}
// documentation inherited from interface
public void paintIcon (Component c, Graphics g, int x, int y)
{
_mirage.paint((Graphics2D)g, x, y);
}
// documentation inherited from interface
public int getIconWidth()
{
return _mirage.getWidth();
}
// documentation inherited from interface
public int getIconHeight()
{
return _mirage.getHeight();
}
protected Mirage _mirage;
}
@@ -0,0 +1,776 @@
//
// $Id: Quantize.java 4031 2006-04-18 20:35:28Z ray $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image;
/*
* @(#)Quantize.java 0.90 9/19/00 Adam Doppelt
*/
/**
* Calculates a reduced color
*
* Three Rings note: Code taken from
* <a href="http://www.gurge.com/amd/java/quantize/">Adam Doppelt</a>, who
* adapted it from other code. Feel the love.<p>
*
* RenderingHints is supposed to provide a way to block dithering, but I have
* not been able to get that to work. It always dithers, so we use this
* class instead.
* <p>
*
* The following modifications were added to the original code:
* - Made it work with image data with transparent pixels.
* - Clarified documentation of the main method.
* - Changed the 'QUICK' constant to false for better quantization.
* - Fixed an integer overflow that caused a bug quantizing large images.
*
* <p><p>
*
* Original headers follow:
* </pre>
*
*
*
* An efficient color quantization algorithm, adapted from the C++
* implementation quantize.c in <a
* href="http://www.imagemagick.org/">ImageMagick</a>. The pixels for
* an image are placed into an oct tree. The oct tree is reduced in
* size, and the pixels from the original image are reassigned to the
* nodes in the reduced tree.<p>
*
* Here is the copyright notice from ImageMagick:
*
* <pre>
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Permission is hereby granted, free of charge, to any person obtaining a %
% copy of this software and associated documentation files ("ImageMagick"), %
% to deal in ImageMagick without restriction, including without limitation %
% the rights to use, copy, modify, merge, publish, distribute, sublicense, %
% and/or sell copies of ImageMagick, and to permit persons to whom the %
% ImageMagick 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 ImageMagick. %
% %
% 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 %
% E. I. du Pont de Nemours and Company 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 ImageMagick or the use or other %
% dealings in ImageMagick. %
% %
% Except as contained in this notice, the name of the E. I. du Pont de %
% Nemours and Company shall not be used in advertising or otherwise to %
% promote the sale, use or other dealings in ImageMagick without prior %
% written authorization from the E. I. du Pont de Nemours and Company. %
% %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
</pre>
*
*
* @version 0.90 19 Sep 2000
* @author <a href="http://www.gurge.com/amd/">Adam Doppelt</a>
*/
public class Quantize {
/*
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% %
% %
% %
% QQQ U U AAA N N TTTTT IIIII ZZZZZ EEEEE %
% Q Q U U A A NN N T I ZZ E %
% Q Q U U AAAAA N N N T I ZZZ EEEEE %
% Q QQ U U A A N NN T I ZZ E %
% QQQQ UUU A A N N T IIIII ZZZZZ EEEEE %
% %
% %
% Reduce the Number of Unique Colors in an Image %
% %
% %
% Software Design %
% John Cristy %
% July 1992 %
% %
% %
% Copyright 1998 E. I. du Pont de Nemours and Company %
% %
% Permission is hereby granted, free of charge, to any person obtaining a %
% copy of this software and associated documentation files ("ImageMagick"), %
% to deal in ImageMagick without restriction, including without limitation %
% the rights to use, copy, modify, merge, publish, distribute, sublicense, %
% and/or sell copies of ImageMagick, and to permit persons to whom the %
% ImageMagick 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 ImageMagick. %
% %
% 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 %
% E. I. du Pont de Nemours and Company 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 ImageMagick or the use or other %
% dealings in ImageMagick. %
% %
% Except as contained in this notice, the name of the E. I. du Pont de %
% Nemours and Company shall not be used in advertising or otherwise to %
% promote the sale, use or other dealings in ImageMagick without prior %
% written authorization from the E. I. du Pont de Nemours and Company. %
% %
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
% Realism in computer graphics typically requires using 24 bits/pixel to
% generate an image. Yet many graphic display devices do not contain
% the amount of memory necessary to match the spatial and color
% resolution of the human eye. The QUANTIZE program takes a 24 bit
% image and reduces the number of colors so it can be displayed on
% raster device with less bits per pixel. In most instances, the
% quantized image closely resembles the original reference image.
%
% A reduction of colors in an image is also desirable for image
% transmission and real-time animation.
%
% Function Quantize takes a standard RGB or monochrome images and quantizes
% them down to some fixed number of colors.
%
% For purposes of color allocation, an image is a set of n pixels, where
% each pixel is a point in RGB space. RGB space is a 3-dimensional
% vector space, and each pixel, pi, is defined by an ordered triple of
% red, green, and blue coordinates, (ri, gi, bi).
%
% Each primary color component (red, green, or blue) represents an
% intensity which varies linearly from 0 to a maximum value, cmax, which
% corresponds to full saturation of that color. Color allocation is
% defined over a domain consisting of the cube in RGB space with
% opposite vertices at (0,0,0) and (cmax,cmax,cmax). QUANTIZE requires
% cmax = 255.
%
% The algorithm maps this domain onto a tree in which each node
% represents a cube within that domain. In the following discussion
% these cubes are defined by the coordinate of two opposite vertices:
% The vertex nearest the origin in RGB space and the vertex farthest
% from the origin.
%
% The tree's root node represents the the entire domain, (0,0,0) through
% (cmax,cmax,cmax). Each lower level in the tree is generated by
% subdividing one node's cube into eight smaller cubes of equal size.
% This corresponds to bisecting the parent cube with planes passing
% through the midpoints of each edge.
%
% The basic algorithm operates in three phases: Classification,
% Reduction, and Assignment. Classification builds a color
% description tree for the image. Reduction collapses the tree until
% the number it represents, at most, the number of colors desired in the
% output image. Assignment defines the output image's color map and
% sets each pixel's color by reclassification in the reduced tree.
% Our goal is to minimize the numerical discrepancies between the original
% colors and quantized colors (quantization error).
%
% Classification begins by initializing a color description tree of
% sufficient depth to represent each possible input color in a leaf.
% However, it is impractical to generate a fully-formed color
% description tree in the classification phase for realistic values of
% cmax. If colors components in the input image are quantized to k-bit
% precision, so that cmax= 2k-1, the tree would need k levels below the
% root node to allow representing each possible input color in a leaf.
% This becomes prohibitive because the tree's total number of nodes is
% 1 + sum(i=1,k,8k).
%
% A complete tree would require 19,173,961 nodes for k = 8, cmax = 255.
% Therefore, to avoid building a fully populated tree, QUANTIZE: (1)
% Initializes data structures for nodes only as they are needed; (2)
% Chooses a maximum depth for the tree as a function of the desired
% number of colors in the output image (currently log2(colormap size)).
%
% For each pixel in the input image, classification scans downward from
% the root of the color description tree. At each level of the tree it
% identifies the single node which represents a cube in RGB space
% containing the pixel's color. It updates the following data for each
% such node:
%
% n1: Number of pixels whose color is contained in the RGB cube
% which this node represents;
%
% n2: Number of pixels whose color is not represented in a node at
% lower depth in the tree; initially, n2 = 0 for all nodes except
% leaves of the tree.
%
% Sr, Sg, Sb: Sums of the red, green, and blue component values for
% all pixels not classified at a lower depth. The combination of
% these sums and n2 will ultimately characterize the mean color of a
% set of pixels represented by this node.
%
% E: The distance squared in RGB space between each pixel contained
% within a node and the nodes' center. This represents the quantization
% error for a node.
%
% Reduction repeatedly prunes the tree until the number of nodes with
% n2 > 0 is less than or equal to the maximum number of colors allowed
% in the output image. On any given iteration over the tree, it selects
% those nodes whose E count is minimal for pruning and merges their
% color statistics upward. It uses a pruning threshold, Ep, to govern
% node selection as follows:
%
% Ep = 0
% while number of nodes with (n2 > 0) > required maximum number of colors
% prune all nodes such that E <= Ep
% Set Ep to minimum E in remaining nodes
%
% This has the effect of minimizing any quantization error when merging
% two nodes together.
%
% When a node to be pruned has offspring, the pruning procedure invokes
% itself recursively in order to prune the tree from the leaves upward.
% n2, Sr, Sg, and Sb in a node being pruned are always added to the
% corresponding data in that node's parent. This retains the pruned
% node's color characteristics for later averaging.
%
% For each node, n2 pixels exist for which that node represents the
% smallest volume in RGB space containing those pixel's colors. When n2
% > 0 the node will uniquely define a color in the output image. At the
% beginning of reduction, n2 = 0 for all nodes except a the leaves of
% the tree which represent colors present in the input image.
%
% The other pixel count, n1, indicates the total number of colors
% within the cubic volume which the node represents. This includes n1 -
% n2 pixels whose colors should be defined by nodes at a lower level in
% the tree.
%
% Assignment generates the output image from the pruned tree. The
% output image consists of two parts: (1) A color map, which is an
% array of color descriptions (RGB triples) for each color present in
% the output image; (2) A pixel array, which represents each pixel as
% an index into the color map array.
%
% First, the assignment phase makes one pass over the pruned color
% description tree to establish the image's color map. For each node
% with n2 > 0, it divides Sr, Sg, and Sb by n2 . This produces the
% mean color of all pixels that classify no lower than this node. Each
% of these colors becomes an entry in the color map.
%
% Finally, the assignment phase reclassifies each pixel in the pruned
% tree to identify the deepest node containing the pixel's color. The
% pixel's value in the pixel array becomes the index of this node's mean
% color in the color map.
%
% With the permission of USC Information Sciences Institute, 4676 Admiralty
% Way, Marina del Rey, California 90292, this code was adapted from module
% ALCOLS written by Paul Raveling.
%
% The names of ISI and USC are not used in advertising or publicity
% pertaining to distribution of the software without prior specific
% written permission from ISI.
%
*/
final static boolean QUICK = false;
final static int MAX_RGB = 255;
final static int MAX_NODES = 266817;
final static int MAX_TREE_DEPTH = 8;
// these are precomputed in advance
static int SQUARES[];
static int SHIFT[];
static {
SQUARES = new int[MAX_RGB + MAX_RGB + 1];
for (int i= -MAX_RGB; i <= MAX_RGB; i++) {
SQUARES[i + MAX_RGB] = i * i;
}
SHIFT = new int[MAX_TREE_DEPTH + 1];
for (int i = 0; i < MAX_TREE_DEPTH + 1; ++i) {
SHIFT[i] = 1 << (15 - i);
}
}
/**
* Reduce the image to the given number of colors.
*
* @param pixels an in/out parameter that should initially contain
* [A]RGB values but that will contain color palette indicies upon return.
*
* @return The new color palette.
*/
public static int[] quantizeImage(int pixels[][], int max_colors) {
Cube cube = new Cube(pixels, max_colors);
cube.classification();
cube.reduction();
cube.assignment();
return cube.colormap;
}
static class Cube {
int pixels[][];
int max_colors;
int colormap[];
// do we have transparent pixels?
boolean hasTrans = false;
Node root;
int depth;
// counter for the number of colors in the cube. this gets
// recalculated often.
int colors;
// counter for the number of nodes in the tree
int nodes;
Cube(int pixels[][], int max_colors) {
this.pixels = pixels;
this.max_colors = max_colors;
int i = max_colors;
// tree_depth = log max_colors
// 4
for (depth = 1; i != 0; depth++) {
i /= 4;
}
if (depth > 1) {
--depth;
}
if (depth > MAX_TREE_DEPTH) {
depth = MAX_TREE_DEPTH;
} else if (depth < 2) {
depth = 2;
}
root = new Node(this);
}
/*
* Procedure Classification begins by initializing a color
* description tree of sufficient depth to represent each
* possible input color in a leaf. However, it is impractical
* to generate a fully-formed color description tree in the
* classification phase for realistic values of cmax. If
* colors components in the input image are quantized to k-bit
* precision, so that cmax= 2k-1, the tree would need k levels
* below the root node to allow representing each possible
* input color in a leaf. This becomes prohibitive because the
* tree's total number of nodes is 1 + sum(i=1,k,8k).
*
* A complete tree would require 19,173,961 nodes for k = 8,
* cmax = 255. Therefore, to avoid building a fully populated
* tree, QUANTIZE: (1) Initializes data structures for nodes
* only as they are needed; (2) Chooses a maximum depth for
* the tree as a function of the desired number of colors in
* the output image (currently log2(colormap size)).
*
* For each pixel in the input image, classification scans
* downward from the root of the color description tree. At
* each level of the tree it identifies the single node which
* represents a cube in RGB space containing It updates the
* following data for each such node:
*
* number_pixels : Number of pixels whose color is contained
* in the RGB cube which this node represents;
*
* unique : Number of pixels whose color is not represented
* in a node at lower depth in the tree; initially, n2 = 0
* for all nodes except leaves of the tree.
*
* total_red/green/blue : Sums of the red, green, and blue
* component values for all pixels not classified at a lower
* depth. The combination of these sums and n2 will
* ultimately characterize the mean color of a set of pixels
* represented by this node.
*/
void classification() {
int pixels[][] = this.pixels;
int width = pixels.length;
int height = pixels[0].length;
// convert to indexed color
for (int x = width; x-- > 0; ) {
for (int y = height; y-- > 0; ) {
int pixel = pixels[x][y];
int alpha = (pixel >> 24) & 0xFF;
if (alpha != 255) {
hasTrans = true;
continue; // don't add transparent pixels to the cube
}
int red = (pixel >> 16) & 0xFF;
int green = (pixel >> 8) & 0xFF;
int blue = (pixel >> 0) & 0xFF;
// a hard limit on the number of nodes in the tree
if (nodes > MAX_NODES) {
System.out.println("pruning");
root.pruneLevel();
--depth;
}
// walk the tree to depth, increasing the
// number_pixels count for each node
Node node = root;
for (int level = 1; level <= depth; ++level) {
int id = (((red > node.mid_red ? 1 : 0) << 0) |
((green > node.mid_green ? 1 : 0) << 1) |
((blue > node.mid_blue ? 1 : 0) << 2));
if (node.child[id] == null) {
new Node(node, id, level);
}
node = node.child[id];
node.number_pixels += SHIFT[level];
}
++node.unique;
node.total_red += red;
node.total_green += green;
node.total_blue += blue;
}
}
// if we have transparent pixels, that cuts into the number
// of other colors we can use.
if (hasTrans) {
this.max_colors--;
}
}
/*
* reduction repeatedly prunes the tree until the number of
* nodes with unique > 0 is less than or equal to the maximum
* number of colors allowed in the output image.
*
* When a node to be pruned has offspring, the pruning
* procedure invokes itself recursively in order to prune the
* tree from the leaves upward. The statistics of the node
* being pruned are always added to the corresponding data in
* that node's parent. This retains the pruned node's color
* characteristics for later averaging.
*/
void reduction() {
long threshold = 1;
while (colors > max_colors) {
colors = 0;
threshold = root.reduce(threshold, Long.MAX_VALUE);
}
}
/**
* The result of a closest color search.
*/
static class Search {
int distance;
int color_number;
}
/*
* Procedure assignment generates the output image from the
* pruned tree. The output image consists of two parts: (1) A
* color map, which is an array of color descriptions (RGB
* triples) for each color present in the output image; (2) A
* pixel array, which represents each pixel as an index into
* the color map array.
*
* First, the assignment phase makes one pass over the pruned
* color description tree to establish the image's color map.
* For each node with n2 > 0, it divides Sr, Sg, and Sb by n2.
* This produces the mean color of all pixels that classify no
* lower than this node. Each of these colors becomes an entry
* in the color map.
*
* Finally, the assignment phase reclassifies each pixel in
* the pruned tree to identify the deepest node containing the
* pixel's color. The pixel's value in the pixel array becomes
* the index of this node's mean color in the color map.
*/
void assignment() {
colormap = new int[colors];
colors = 0;
root.colormap();
int pixels[][] = this.pixels;
int width = pixels.length;
int height = pixels[0].length;
Search search = new Search();
int transPad = hasTrans ? 1 : 0;
// convert to indexed color
for (int x = width; x-- > 0; ) {
for (int y = height; y-- > 0; ) {
int pixel = pixels[x][y];
int alpha = (pixel >> 24) & 0xFF;
if (alpha != 255) {
pixels[x][y] = 0; // transparent
continue;
}
int red = (pixel >> 16) & 0xFF;
int green = (pixel >> 8) & 0xFF;
int blue = (pixel >> 0) & 0xFF;
// walk the tree to find the cube containing that color
Node node = root;
for ( ; ; ) {
int id = (((red > node.mid_red ? 1 : 0) << 0) |
((green > node.mid_green ? 1 : 0) << 1) |
((blue > node.mid_blue ? 1 : 0) << 2) );
if (node.child[id] == null) {
break;
}
node = node.child[id];
}
if (QUICK) {
// if QUICK is set, just use that
// node. Strictly speaking, this isn't
// necessarily best match.
pixels[x][y] = node.color_number + transPad;
} else {
// Find the closest color.
search.distance = Integer.MAX_VALUE;
node.parent.closestColor(red, green, blue, search);
pixels[x][y] = search.color_number + transPad;
}
}
}
// expand the colormap by one to account for the transparent
if (hasTrans) {
int[] newcmap = new int[colormap.length + 1];
System.arraycopy(colormap, 0, newcmap, 1, colormap.length);
colormap = newcmap;
}
}
/**
* A single Node in the tree.
*/
static class Node {
Cube cube;
// parent node
Node parent;
// child nodes
Node child[];
int nchild;
// our index within our parent
int id;
// our level within the tree
int level;
// our color midpoint
int mid_red;
int mid_green;
int mid_blue;
// the pixel count for this node and all children
long number_pixels;
// the pixel count for this node
int unique;
// the sum of all pixels contained in this node
int total_red;
int total_green;
int total_blue;
// used to build the colormap
int color_number;
Node(Cube cube) {
this.cube = cube;
this.parent = this;
this.child = new Node[8];
this.id = 0;
this.level = 0;
this.number_pixels = Long.MAX_VALUE;
this.mid_red = (MAX_RGB + 1) >> 1;
this.mid_green = (MAX_RGB + 1) >> 1;
this.mid_blue = (MAX_RGB + 1) >> 1;
}
Node(Node parent, int id, int level) {
this.cube = parent.cube;
this.parent = parent;
this.child = new Node[8];
this.id = id;
this.level = level;
// add to the cube
++cube.nodes;
if (level == cube.depth) {
++cube.colors;
}
// add to the parent
++parent.nchild;
parent.child[id] = this;
// figure out our midpoint
int bi = (1 << (MAX_TREE_DEPTH - level)) >> 1;
mid_red = parent.mid_red + ((id & 1) > 0 ? bi : -bi);
mid_green = parent.mid_green + ((id & 2) > 0 ? bi : -bi);
mid_blue = parent.mid_blue + ((id & 4) > 0 ? bi : -bi);
}
/**
* Remove this child node, and make sure our parent
* absorbs our pixel statistics.
*/
void pruneChild() {
--parent.nchild;
parent.unique += unique;
parent.total_red += total_red;
parent.total_green += total_green;
parent.total_blue += total_blue;
parent.child[id] = null;
--cube.nodes;
cube = null;
parent = null;
}
/**
* Prune the lowest layer of the tree.
*/
void pruneLevel() {
if (nchild != 0) {
for (int id = 0; id < 8; id++) {
if (child[id] != null) {
child[id].pruneLevel();
}
}
}
if (level == cube.depth) {
pruneChild();
}
}
/**
* Remove any nodes that have fewer than threshold
* pixels. Also, as long as we're walking the tree:
*
* - figure out the color with the fewest pixels
* - recalculate the total number of colors in the tree
*/
long reduce(long threshold, long next_threshold) {
if (nchild != 0) {
for (int id = 0; id < 8; id++) {
if (child[id] != null) {
next_threshold = child[id].reduce(threshold, next_threshold);
}
}
}
if (number_pixels <= threshold) {
pruneChild();
} else {
if (unique != 0) {
cube.colors++;
}
if (number_pixels < next_threshold) {
next_threshold = number_pixels;
}
}
return next_threshold;
}
/*
* colormap traverses the color cube tree and notes each
* colormap entry. A colormap entry is any node in the
* color cube tree where the number of unique colors is
* not zero.
*/
void colormap() {
if (nchild != 0) {
for (int id = 0; id < 8; id++) {
if (child[id] != null) {
child[id].colormap();
}
}
}
if (unique != 0) {
int r = ((total_red + (unique >> 1)) / unique);
int g = ((total_green + (unique >> 1)) / unique);
int b = ((total_blue + (unique >> 1)) / unique);
cube.colormap[cube.colors] = ((( 0xFF) << 24) |
((r & 0xFF) << 16) |
((g & 0xFF) << 8) |
((b & 0xFF) << 0));
color_number = cube.colors++;
}
}
/* ClosestColor traverses the color cube tree at a
* particular node and determines which colormap entry
* best represents the input color.
*/
void closestColor(int red, int green, int blue, Search search) {
if (nchild != 0) {
for (int id = 0; id < 8; id++) {
if (child[id] != null) {
child[id].closestColor(red, green, blue, search);
}
}
}
if (unique != 0) {
int color = cube.colormap[color_number];
int distance = distance(color, red, green, blue);
if (distance < search.distance) {
search.distance = distance;
search.color_number = color_number;
}
}
}
/**
* Figure out the distance between this node and som color.
*/
final static int distance(int color, int r, int g, int b) {
return (SQUARES[((color >> 16) & 0xFF) - r + MAX_RGB] +
SQUARES[((color >> 8) & 0xFF) - g + MAX_RGB] +
SQUARES[((color >> 0) & 0xFF) - b + MAX_RGB]);
}
public String toString() {
StringBuilder buf = new StringBuilder();
if (parent == this) {
buf.append("root");
} else {
buf.append("node");
}
buf.append(' ');
buf.append(level);
buf.append(" [");
buf.append(mid_red);
buf.append(',');
buf.append(mid_green);
buf.append(',');
buf.append(mid_blue);
buf.append(']');
return new String(buf);
}
}
}
}
@@ -0,0 +1,145 @@
//
// $Id$
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2005 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.image.BufferedImage;
import com.threerings.media.Log;
/**
* Draws a mirage combined with an arbitrary AffineTransform.
*/
public class TransformedMirage
implements Mirage
{
/**
* Constructor.
*/
public TransformedMirage (Mirage base, AffineTransform transform)
{
_base = base;
// clone the transform so that it doesn't get changed on us.
_transform = (AffineTransform) transform.clone();
computeTransformedBounds();
_transform.preConcatenate(
AffineTransform.getTranslateInstance(-_bounds.x, -_bounds.y));
}
// documentation inherited from interface Mirage
public void paint (Graphics2D gfx, int x, int y)
{
AffineTransform otrans = gfx.getTransform();
gfx.translate(x, y);
gfx.transform(_transform);
_base.paint(gfx, 0, 0);
gfx.setTransform(otrans);
}
// documentation inherited from interface Mirage
public int getWidth ()
{
return _bounds.width;
}
// documentation inherited from interface Mirage
public int getHeight ()
{
return _bounds.height;
}
// documentation inherited from interface Mirage
public boolean hitTest (int x, int y)
{
Point p = new Point(x, y);
try {
_transform.createInverse().transform(p, p);
return _base.hitTest(p.x, p.y);
} catch (NoninvertibleTransformException nte) {
// grumble, grumble
// TODO: log something?
return ImageUtil.hitTest(getSnapshot(), x, y);
}
}
// documentation inherited from interface Mirage
public BufferedImage getSnapshot ()
{
BufferedImage baseSnap = _base.getSnapshot();
BufferedImage img = new BufferedImage(_bounds.width, _bounds.height,
baseSnap.getType());
Graphics2D gfx = (Graphics2D) img.getGraphics();
try {
gfx.transform(_transform);
gfx.drawImage(baseSnap, 0, 0, null);
} finally {
gfx.dispose();
}
return img;
}
// documentation inherited from interface Mirage
public long getEstimatedMemoryUsage ()
{
return _base.getEstimatedMemoryUsage();
}
/**
* Compute the bounds of the base Mirage after it has been
* transformed.
*/
protected void computeTransformedBounds ()
{
int w = _base.getWidth();
int h = _base.getHeight();
Point[] points = new Point[] {
new Point(0, 0), new Point(w, 0), new Point(0, h), new Point(w, h) };
_transform.transform(points, 0, points, 0, 4);
int minX, minY, maxX, maxY;
minX = minY = Integer.MAX_VALUE;
maxX = maxY = Integer.MIN_VALUE;
for (int ii=0; ii < 4; ii++) {
minX = Math.min(minX, points[ii].x);
maxX = Math.max(maxX, points[ii].x);
minY = Math.min(minY, points[ii].y);
maxY = Math.max(maxY, points[ii].y);
}
_bounds = new Rectangle(minX, minY, maxX - minX, maxY - minY);
}
/** The base mirage. */
protected Mirage _base;
/** Our transformed bounds. */
protected Rectangle _bounds;
/** The transform we apply when painting the base mirage. */
protected AffineTransform _transform;
}
@@ -0,0 +1,202 @@
//
// $Id: VolatileMirage.java 4191 2006-06-13 22:42:20Z ray $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import com.samskivert.util.StringUtil;
/**
* A mirage implementation which allows the image to be maintained in
* video memory and rebuilt from some source image or images in the event
* that our target screen resolution changes or we are flushed from video
* memory for some other reason.
*/
public abstract class VolatileMirage implements Mirage
{
/**
* Informs the base class of its image manager and image bounds.
*/
protected VolatileMirage (ImageManager imgr, Rectangle bounds)
{
_imgr = imgr;
_bounds = bounds;
}
// documentation inherited from interface
public void paint (Graphics2D gfx, int x, int y)
{
// create our volatile image for the first time if necessary
if (_image == null) {
createVolatileImage();
}
// int renders = 0;
// do {
// // validate that our image is compatible with the target GC
// switch (_image.validate(_imgr.getGraphicsConfiguration())) {
// case VolatileImage.IMAGE_RESTORED:
// refreshVolatileImage(); // need to rerender it
// break;
// case VolatileImage.IMAGE_INCOMPATIBLE:
// createVolatileImage(); // need to recreate it
// break;
// }
// // now we can render it
// gfx.drawImage(_image, x, y, null);
// renders++;
// // don't try forever
// } while (_image.contentsLost() && (renders < 10));
if (IMAGE_DEBUG) {
gfx.setColor(new Color(_image.getRGB(_bounds.width/2,
_bounds.height/2)));
gfx.fillRect(x, y, _bounds.width, _bounds.height);
} else {
gfx.drawImage(_image, x, y, null);
}
// TODO: note number of attempted renders for performance
}
/**
* Returns the x offset into our source image, which is generally zero but
* may be non-zero for a mirage that obtains its data from a region of its
* source image.
*/
public int getX ()
{
return _bounds.x;
}
/**
* Returns the y offset into our source image, which is generally zero but
* may be non-zero for a mirage that obtains its data from a region of its
* source image.
*/
public int getY ()
{
return _bounds.y;
}
// documentation inherited from interface
public int getWidth ()
{
return _bounds.width;
}
// documentation inherited from interface
public int getHeight ()
{
return _bounds.height;
}
// documentation inherited from interface
public boolean hitTest (int x, int y)
{
// return ImageUtil.hitTest(_image.getSnapshot(), x, y);
return ImageUtil.hitTest(_image, x, y);
}
// documentation inherited from interface
public long getEstimatedMemoryUsage ()
{
return ImageUtil.getEstimatedMemoryUsage(_image.getRaster());
}
// documentation inherited from interface
public BufferedImage getSnapshot ()
{
// return _image.getSnapshot();
return _image;
}
/**
* Creates our volatile image from the information in our source
* image.
*/
protected void createVolatileImage ()
{
// release any previous volatile image we might hold
if (_image != null) {
_image.flush();
}
// create a new, compatible, volatile image
// _image = _imgr.createVolatileImage(
// _bounds.width, _bounds.height, getTransparency());
_image = _imgr.createImage(
_bounds.width, _bounds.height, getTransparency());
// render our source image into the volatile image
refreshVolatileImage();
}
/**
* Returns the transparency that should be used when creating our
* volatile image.
*/
protected abstract int getTransparency ();
/**
* Rerenders our volatile image from the its source image data.
*/
protected abstract void refreshVolatileImage ();
/**
* Generates a string representation of this instance.
*/
public String toString ()
{
StringBuilder buf = new StringBuilder("[");
toString(buf);
return buf.append("]").toString();
}
/**
* Generates a string representation of this instance.
*/
protected void toString (StringBuilder buf)
{
buf.append("bounds=").append(StringUtil.toString(_bounds));
}
/** The image manager with whom we interoperate. */
protected ImageManager _imgr;
/** The bounds of the region of our source image which we desire for
* this mirage (possibly the whole thing). */
protected Rectangle _bounds;
/** Our volatile image which lives in video memory and can go away at
* any time. */
// protected VolatileImage _image;
protected BufferedImage _image;
/** Turns off image rendering for testing. */
protected static final boolean IMAGE_DEBUG = false;
}
@@ -0,0 +1,54 @@
//
// $Id: DumpColorPository.java 4191 2006-06-13 22:42:20Z ray $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image.tools;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Iterator;
import com.threerings.media.image.ColorPository;
/**
* Simple tool for dumping a serialized color pository.
*/
public class DumpColorPository
{
public static void main (String[] args)
{
if (args.length == 0) {
System.err.println("Usage: DumpColorPository colorpos.dat");
System.exit(-1);
}
try {
ColorPository pos = ColorPository.loadColorPository(
new FileInputStream(args[0]));
Iterator iter = pos.enumerateClasses();
while (iter.hasNext()) {
System.out.println(iter.next());
}
} catch (IOException ioe) {
ioe.printStackTrace(System.err);
}
}
}
@@ -0,0 +1,78 @@
//
// $Id: ColorPositoryParser.java 4191 2006-06-13 22:42:20Z ray $
//
// Narya library - tools for developing networked games
// Copyright (C) 2002-2004 Three Rings Design, Inc., All Rights Reserved
// http://www.threerings.net/code/narya/
//
// This library is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation; either version 2.1 of the License, or
// (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package com.threerings.media.image.tools.xml;
import java.io.Serializable;
import org.xml.sax.Attributes;
import org.apache.commons.digester.Digester;
import org.apache.commons.digester.Rule;
import com.samskivert.xml.SetPropertyFieldsRule;
import com.threerings.tools.xml.CompiledConfigParser;
import com.threerings.media.image.ColorPository.ClassRecord;
import com.threerings.media.image.ColorPository.ColorRecord;
import com.threerings.media.image.ColorPository;
/**
* Parses the XML color repository definition and creates a {@link
* ColorPository} instance that reflects its contents.
*/
public class ColorPositoryParser extends CompiledConfigParser
{
// documentation inherited
protected Serializable createConfigObject ()
{
return new ColorPository();
}
// documentation inherited
protected void addRules (Digester digest)
{
// create and configure class record instances
String prefix = "colors/class";
digest.addObjectCreate(prefix, ClassRecord.class.getName());
digest.addRule(prefix, new SetPropertyFieldsRule());
digest.addSetNext(prefix, "addClass", ClassRecord.class.getName());
// create and configure color record instances
prefix += "/color";
digest.addRule(prefix, new Rule() {
public void begin (String namespace, String name,
Attributes attributes) throws Exception {
// we want to inherit settings from the color class when
// creating the record, so we do some custom stuff
ColorRecord record = new ColorRecord();
ClassRecord clrec = (ClassRecord)digester.peek();
record.starter = clrec.starter;
digester.push(record);
}
public void end (String namespace, String name) throws Exception {
digester.pop();
}
});
digest.addRule(prefix, new SetPropertyFieldsRule());
digest.addSetNext(prefix, "addColor", ColorRecord.class.getName());
}
}