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,332 @@
//
// $Id: AbstractMedia.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;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.PathIterator;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import com.samskivert.util.ObserverList;
import com.samskivert.util.StringUtil;
/**
* Something that can be rendered on the media panel.
*/
public abstract class AbstractMedia
implements Shape
{
/** A {@link #_renderOrder} value at or above which, indicates that this
* media is in the HUD (heads up display) and should not scroll when the
* view scrolls. */
public static final int HUD_LAYER = 65536;
/**
* Instantiate an abstract media object.
*/
public AbstractMedia (Rectangle bounds)
{
_bounds = bounds;
}
/**
* Called periodically by this media's manager to give it
* a chance to do its thing.
*
* @param tickStamp a time stamp associated with this tick.
* <em>Note:</em> this is not obtained from a call to {@link
* System#currentTimeMillis} and cannot be compared to timestamps
* obtained there from.
*/
public abstract void tick (long tickStamp);
/**
* Called by the appropriate manager to request that the
* media render itself with the given graphics context. The
* media may wish to inspect the clipping region that has been set
* on the graphics context to render itself more efficiently. This
* method will only be called after it has been established that this
* media's bounds intersect the clipping region.
*/
public abstract void paint (Graphics2D gfx);
/**
* Called when the appropriate media manager has been paused for some
* length of time and is then unpaused. Media should adjust any time stamps
* that are maintained internally forward by the delta so that time
* maintains the illusion of flowing smoothly forward.
*/
public void fastForward (long timeDelta)
{
// adjust our first tick stamp
_firstTick += timeDelta;
}
/**
* Invalidate the media's bounding rectangle for later painting.
*/
public void invalidate ()
{
if (_mgr != null) {
_mgr.getRegionManager().invalidateRegion(_bounds);
}
}
/**
* Set the location.
*/
public void setLocation (int x, int y)
{
_bounds.x = x;
_bounds.y = y;
}
/**
* Returns a rectangle containing all the pixels rendered by this media.
*/
public Rectangle getBounds ()
{
return _bounds;
}
// documentation inherited from interface Shape
public Rectangle2D getBounds2D ()
{
return _bounds;
}
// documentation inherited from interface Shape
public boolean contains (double x, double y)
{
return _bounds.contains(x, y);
}
// documentation inherited from interface Shape
public boolean contains (Point2D p)
{
return _bounds.contains(p);
}
// documentation inherited from interface Shape
public boolean intersects (double x, double y, double w, double h)
{
return _bounds.intersects(x, y, w, h);
}
// documentation inherited from interface Shape
public boolean intersects (Rectangle2D r)
{
return _bounds.intersects(r);
}
// documentation inherited from interface Shape
public boolean contains (double x, double y, double w, double h)
{
return _bounds.contains(x, y, w, h);
}
// documentation inherited from interface Shape
public boolean contains (Rectangle2D r)
{
return _bounds.contains(r);
}
// documentation inherited from interface Shape
public PathIterator getPathIterator (AffineTransform at)
{
return _bounds.getPathIterator(at);
}
// documentation inherited from interface Shape
public PathIterator getPathIterator (AffineTransform at, double flatness)
{
return _bounds.getPathIterator(at, flatness);
}
/**
* Sets the render order associated with this media. Media
* can be rendered in two layers; those with negative render order and
* those with positive render order. In the same layer, they
* will be rendered according to their render order's cardinal value
* (least to greatest). Those with the same render order value will be
* rendered in arbitrary order.
*
* <p>This method may not be called during a tick.
*
* @see #HUD_LAYER
*/
public void setRenderOrder (int renderOrder)
{
if (_renderOrder != renderOrder) {
_renderOrder = renderOrder;
if (_mgr != null) {
_mgr.renderOrderDidChange(this);
invalidate();
}
}
}
/**
* Returns the render order of this media element.
*/
public int getRenderOrder ()
{
return _renderOrder;
}
/**
* Queues the supplied notification up to be dispatched to this
* abstract media's observers.
*/
public void queueNotification (ObserverList.ObserverOp amop)
{
if (_observers != null) {
if (_mgr != null) {
_mgr.queueNotification(_observers, amop);
} else {
Log.warning("Have no manager, dropping notification " +
"[media=" + this + ", op=" + amop + "].");
}
}
}
/**
* Called by the {@link AbstractMediaManager} when we are in a
* {@link VirtualMediaPanel} that just scrolled.
*/
public void viewLocationDidChange (int dx, int dy)
{
if (_renderOrder >= HUD_LAYER) {
setLocation(_bounds.x + dx, _bounds.y + dy);
}
}
/**
* Dumps this media to a String object.
*/
public String toString ()
{
StringBuilder buf = new StringBuilder();
buf.append(StringUtil.shortClassName(this));
buf.append("[");
toString(buf);
return buf.append("]").toString();
}
/**
* Initialize the media.
*/
protected final void init (AbstractMediaManager manager)
{
_mgr = manager;
init();
}
/**
* Called when the media has had its manager set.
* Derived classes may override this method, but should be sure to
* call <code>super.init()</code>.
*/
protected void init ()
{
}
/**
* Prior to the first call to {@link #tick} on an abstract media, this
* method is called by the {@link AbstractMediaManager}. It is called
* during the normal tick cycle, immediately prior to the first call
* to {@link #tick}.
*
* <p><em>Note:</em> It is imperative that
* <code>super.willStart()</code> is called by any entity that
* overrides this method because the {@link AbstractMediaManager}
* depends on the setting of the {@link #_firstTick} value to know
* whether or not to call this method.
*/
protected void willStart (long tickStamp)
{
_firstTick = tickStamp;
}
/**
* Called by the media manager after the media is removed from service.
* Derived classes may override this method, but should be sure to
* call <code>super.shutdown()</code>.
*/
protected void shutdown ()
{
invalidate();
_mgr = null;
}
/**
* Add the specified observer to this media element.
*/
protected void addObserver (Object obs)
{
if (_observers == null) {
_observers = new ObserverList(ObserverList.FAST_UNSAFE_NOTIFY);
}
_observers.add(obs);
}
/**
* Remove the specified observer from this media element.
*/
protected void removeObserver (Object obs)
{
if (_observers != null) {
_observers.remove(obs);
}
}
/**
* This should be overridden by derived classes (which should be sure
* to call <code>super.toString()</code>) to append the derived class
* specific information to the string buffer.
*/
protected void toString (StringBuilder buf)
{
buf.append("bounds=").append(StringUtil.toString(_bounds));
buf.append(", renderOrder=").append(_renderOrder);
}
/** The layer in which to render. */
protected int _renderOrder = 0;
/** The bounds of the media's rendering area. */
protected Rectangle _bounds;
/** Our manager. */
protected AbstractMediaManager _mgr;
/** Our observers. */
protected ObserverList _observers = null;
/** The tick stamp associated with our first call to {@link #tick}.
* This is set up automatically in {@link #willStart}. */
protected long _firstTick;
}
@@ -0,0 +1,335 @@
//
// $Id: AbstractMediaManager.java 3551 2005-05-13 22:22:56Z andrzej $
//
// 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;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.util.ArrayList;
import java.util.Comparator;
import com.samskivert.util.ObserverList.ObserverOp;
import com.samskivert.util.ObserverList;
import com.samskivert.util.SortableArrayList;
import com.samskivert.util.Tuple;
/**
* Manages, ticks, and paints {@link AbstractMedia}.
*/
public abstract class AbstractMediaManager
implements MediaConstants
{
/**
* Default constructor.
*/
public AbstractMediaManager (MediaPanel panel)
{
_panel = panel;
_remgr = panel.getRegionManager();
}
/**
* Returns region manager in use by this manager.
*/
public RegionManager getRegionManager ()
{
return _remgr;
}
/**
* Returns the media panel with which we are coordinating.
*/
public MediaPanel getMediaPanel ()
{
return _panel;
}
/**
* Must be called every frame so that the media can be properly
* updated.
*/
public void tick (long tickStamp)
{
_tickStamp = tickStamp;
tickAllMedia(tickStamp);
dispatchNotifications();
// we clear our tick stamp when we're about to be painted, this
// lets us handle situations when yet more media is slipped in
// between our being ticked and our being painted
}
/**
* This will always be called prior to the {@link #paint} calls for a
* particular tick. Because it is possible that there will be no dirty
* regions and thus no calls to {@link #paint} this method exists so
* that the media manager can guarantee that it will be notified when
* all ticking is complete and the painting phase has begun.
*/
public void willPaint ()
{
// now that we're done ticking, we can safely clear this
_tickStamp = 0;
}
/**
* Renders all registered media in the given layer that intersect
* the supplied clipping rectangle to the given graphics context.
*
* @param layer the layer to render; one of {@link #FRONT}, {@link
* #BACK}, or {@link #ALL}. The front layer contains all animations
* with a positive render order; the back layer contains all
* animations with a negative render order; all, both.
*/
public void paint (Graphics2D gfx, int layer, Shape clip)
{
for (int ii = 0, nn = _media.size(); ii < nn; ii++) {
AbstractMedia media = (AbstractMedia)_media.get(ii);
int order = media.getRenderOrder();
try {
if (((layer == ALL) ||
(layer == FRONT && order >= 0) ||
(layer == BACK && order < 0)) &&
clip.intersects(media.getBounds())) {
media.paint(gfx);
}
} catch (Exception e) {
Log.warning("Failed to render media " +
"[media=" + media + ", e=" + e + "].");
Log.logStackTrace(e);
}
}
}
/**
* If the manager is paused for some length of time, it should
* be fast forwarded by the appropriate number of milliseconds. This
* allows media to smoothly pick up where they left off rather than
* abruptly jumping into the future, thinking that some outrageous
* amount of time passed since their last tick.
*/
public void fastForward (long timeDelta)
{
if (_tickStamp > 0) {
Log.warning("Egads! Asked to fastForward() during a tick.");
Thread.dumpStack();
}
for (int ii=0, nn=_media.size(); ii < nn; ii++) {
((AbstractMedia) _media.get(ii)).fastForward(timeDelta);
}
}
/**
* Returns true if the specified media is being managed by this media
* manager.
*/
public boolean isManaged (AbstractMedia media)
{
return _media.contains(media);
}
/**
* Called by a {@link VirtualMediaPanel} when the view that contains our
* media is scrolled.
*
* @param dx the scrolled distance in the x direction (in pixels).
* @param dy the scrolled distance in the y direction (in pixels).
*/
public void viewLocationDidChange (int dx, int dy)
{
// let our media know
for (int ii = 0, ll = _media.size(); ii < ll; ii++) {
((AbstractMedia)_media.get(ii)).viewLocationDidChange(dx, dy);
}
}
/**
* Called by a {@link AbstractMedia} when its render order has changed.
*/
public void renderOrderDidChange (AbstractMedia media)
{
if (_tickStamp > 0) {
Log.warning("Egads! Render order changed during a tick.");
Thread.dumpStack();
}
_media.remove(media);
_media.insertSorted(media, RENDER_ORDER);
}
/**
* Calls {@link AbstractMedia#tick} on all media to give them a chance
* to move about, change their look, generate dirty regions, and so
* forth.
*/
protected void tickAllMedia (long tickStamp)
{
// we use _tickpos so that it can be adjusted if media is added or
// removed during the tick dispatch
for (_tickpos = 0; _tickpos < _media.size(); _tickpos++) {
tickMedia((AbstractMedia) _media.get(_tickpos), tickStamp);
}
_tickpos = -1;
}
/**
* Inserts the specified media into this manager, return true on
* success.
*/
protected boolean insertMedia (AbstractMedia media)
{
if (_media.contains(media)) {
Log.warning("Attempt to insert media more than once " +
"[media=" + media + "].");
Thread.dumpStack();
return false;
}
media.init(this);
int ipos = _media.insertSorted(media, RENDER_ORDER);
// if we've started our tick but have not yet painted our media,
// we need to take care that this newly added media will be ticked
// before our upcoming render
if (_tickStamp > 0L) {
if (_tickpos == -1) {
// if we're done with our own call to tick(), we
// definitely need to tick this new media
tickMedia(media, _tickStamp);
} else if (ipos <= _tickpos) {
// otherwise, we're in the middle of our call to tick()
// and we only need to tick this guy if he's being
// inserted before our current tick position (if he's
// inserted after our current position, we'll get to him
// as part of this tick iteration)
_tickpos++;
tickMedia(media, _tickStamp);
}
}
return true;
}
/** A helper function used to call {@link AbstractMedia#tick}. */
protected final void tickMedia (AbstractMedia media, long tickStamp)
{
if (media._firstTick == 0L) {
media.willStart(tickStamp);
}
media.tick(tickStamp);
}
/**
* Removes the specified media from this manager, return true on
* success.
*/
protected boolean removeMedia (AbstractMedia media)
{
int mpos = _media.indexOf(media);
if (mpos != -1) {
_media.remove(mpos);
media.invalidate();
media.shutdown();
// if we're in the middle of ticking, we need to adjust the
// _tickpos if necessary
if (mpos <= _tickpos) {
_tickpos--;
}
return true;
}
Log.warning("Attempt to remove media that wasn't inserted " +
"[media=" + media + "].");
return false;
}
/**
* Clears all media from the manager and calls {@link
* AbstractMedia#shutdown} on each. This does not invalidate the
* media's vacated bounds; it is assumed that it will be ok.
*/
protected void clearMedia ()
{
if (_tickStamp > 0) {
Log.warning("Egads! Requested to clearMedia() during a tick.");
Thread.dumpStack();
}
for (int ii=_media.size() - 1; ii >= 0; ii--) {
AbstractMedia media = (AbstractMedia) _media.remove(ii);
media.shutdown();
}
}
/**
* Queues the notification for dispatching after we've ticked all the
* media.
*/
public void queueNotification (ObserverList observers, ObserverOp event)
{
_notify.add(new Tuple(observers, event));
}
/**
* Dispatches all queued media notifications.
*/
protected void dispatchNotifications ()
{
for (int ii = 0, nn = _notify.size(); ii < nn; ii++) {
Tuple tuple = (Tuple)_notify.get(ii);
((ObserverList)tuple.left).apply((ObserverOp)tuple.right);
}
_notify.clear();
}
/** The media panel we're working with. */
protected MediaPanel _panel;
/** The region manager. */
protected RegionManager _remgr;
/** List of observers to notify at the end of the tick. */
protected ArrayList _notify = new ArrayList();
/** Our render-order sorted list of media. */
protected SortableArrayList _media = new SortableArrayList();
/** The position in our media list that we're ticking in the middle of
* a call to {@link #tick} otherwise -1. */
protected int _tickpos = -1;
/** The tick stamp if the manager is in the midst of a call to {@link
* #tick}, else <code>0</code>. */
protected long _tickStamp;
/** Used to sort media by render order. */
protected static final Comparator RENDER_ORDER = new Comparator() {
public int compare (Object o1, Object o2) {
int result = (((AbstractMedia)o1)._renderOrder -
((AbstractMedia)o2)._renderOrder);
return (result != 0) ? result :
// find some other way to keep them stable relative to
// each other
o1.hashCode() - o2.hashCode();
}
};
}
@@ -0,0 +1,502 @@
//
// $Id: ActiveRepaintManager.java 4181 2006-06-07 21:54:12Z 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;
import java.applet.Applet;
import java.awt.Component;
import java.awt.Graphics2D;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.Window;
import javax.swing.CellRendererPane;
import javax.swing.JComponent;
import javax.swing.JEditorPane;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.RepaintManager;
import javax.swing.SwingUtilities;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;
import com.samskivert.util.ListUtil;
import com.samskivert.util.RunAnywhere;
import com.samskivert.util.StringUtil;
/**
* Used to get Swing's repainting to jive with our active rendering
* strategy.
*
* @see FrameManager
*/
public class ActiveRepaintManager extends RepaintManager
{
/**
* Components that are rooted in this component (which must be a {@link
* Window} or an {@link Applet}) will be rendered into the offscreen buffer
* managed by the frame manager. Other components will be rendered into
* separate offscreen buffers and repainted in the normal Swing manner.
*/
public ActiveRepaintManager (Component root)
{
_root = root;
}
// documentation inherited
public synchronized void addInvalidComponent (JComponent comp)
{
Component vroot = null;
if (DEBUG) {
Log.info("Maybe invalidating " + toString(comp) + ".");
}
// locate the validation root for this component
for (Component c = comp; c != null; c = c.getParent()) {
// if the component is not part of an active widget hierarcy,
// we can stop now; if the component is a cell render pane,
// we're apparently supposed to ignore it as wel
if (!c.isDisplayable() || c instanceof CellRendererPane) {
return;
}
// skip non-Swing components
if (!(c instanceof JComponent)) {
continue;
}
// if we find our validate root, we can stop looking; NOTE:
// JTextField incorrectly claims to be a validate root thereby
// fucking up the program something serious; we jovially
// ignore it's claims here and restore order to the universe;
// see bug #403550 for more fallout from Sun's fuckup
if (!(c instanceof JTextField) &&
!(c instanceof JScrollPane) &&
((JComponent)c).isValidateRoot()) {
vroot = c;
break;
}
}
// if we found no validation root we can abort as this component
// is not part of any valid widget hierarchy
if (vroot == null) {
if (DEBUG) {
Log.info("Skipping vrootless component: " + toString(comp));
}
return;
}
// make sure that the component is actually in a window or applet
// that is showing
if (getRoot(vroot) == null) {
if (DEBUG) {
Log.info("Skipping rootless component " +
"[comp=" + toString(comp) +
", vroot=" + toString(vroot) + "].");
}
return;
}
// add the invalid component to our list and we'll validate it on
// the next frame
if (!ListUtil.containsRef(_invalid, vroot)) {
if (DEBUG) {
Log.info("Invalidating " + toString(vroot) + ".");
}
_invalid = ListUtil.add(_invalid, vroot);
// on the mac, components frequently do not repaint themselves
// after being invalidated so we have to force a repaint from
// the validation roon on down
if (RunAnywhere.isMacOS() && vroot instanceof JComponent) {
addDirtyRegion((JComponent)vroot, 0, 0,
vroot.getWidth(), vroot.getHeight());
}
}
}
// documentation inherited
public synchronized void addDirtyRegion (
JComponent comp, int x, int y, int width, int height)
{
// ignore invalid requests
if ((width <= 0) || (height <= 0) || (comp == null) ||
(comp.getWidth() <= 0) || (comp.getHeight() <= 0)) {
// Log.info("Skipping bogus region " + comp.getClass().getName() +
// ", x=" + x + ", y=" + y + ", width=" + width + ", height=" + height + ".");
return;
}
// if this component is already dirty, simply expand their
// existing dirty rectangle
Rectangle drect = (Rectangle)_dirty.get(comp);
if (drect != null) {
drect.add(x, y);
drect.add(x+width, y+height);
return;
}
// make sure this component has a valid root
if (getRoot(comp) == null) {
// Log.info("Skipping rootless repaint " + comp + ".");
return;
}
if (DEBUG) {
Log.info("Dirtying component [comp=" + toString(comp) +
", drect=" + StringUtil.toString(
new Rectangle(x, y, width, height)) + "].");
}
// if we made it this far, we can queue up a dirty region for this
// component to be repainted on the next tick
_dirty.put(comp, new Rectangle(x, y, width, height));
}
/**
* Returns the root component for the supplied component or null if it
* is not part of a rooted hierarchy or if any parent along the way is
* found to be hidden or without a peer.
*/
protected Component getRoot (Component comp)
{
for (Component c = comp; c != null; c = c.getParent()) {
boolean hidden = !c.isDisplayable();
// on the mac, the JRootPane is invalidated before it is
// visible and is never again invalidated or repainted, so we
// punt and allow all invisible components to be invalidated
// and revalidated
if (!RunAnywhere.isMacOS()) {
hidden |= !c.isVisible();
}
if (hidden) {
return null;
}
if (c instanceof Window || c instanceof Applet) {
return c;
}
}
return null;
}
// documentation inherited
public synchronized Rectangle getDirtyRegion (JComponent comp)
{
Rectangle drect = (Rectangle)_dirty.get(comp);
// copy the rectangle if we found one, otherwise create an empty
// rectangle because we don't want them leaving empty handed
return (drect == null) ?
new Rectangle(0, 0, 0, 0) : new Rectangle(drect);
}
// documentation inherited
public synchronized void markCompletelyClean (JComponent comp)
{
_dirty.remove(comp);
}
/**
* Validates the invalid components that have been queued up since the
* last frame tick.
*/
public void validateComponents ()
{
// swap out our invalid array
Object[] invalid = null;
synchronized (this) {
invalid = _invalid;
_invalid = null;
}
// if there's nothing to validate, we're home free
if (invalid == null) {
return;
}
// validate everything therein
int icount = invalid.length;
for (int ii = 0; ii < icount; ii++) {
if (invalid[ii] != null) {
if (DEBUG) {
Log.info("Validating " + invalid[ii]);
}
((Component)invalid[ii]).validate();
}
}
}
/**
* Paints the components that have become dirty since the last tick.
*
* @return true if any components were painted.
*/
public boolean paintComponents (Graphics g, FrameManager fmgr)
{
synchronized (this) {
// exit now if there are no dirty rectangles to paint
if (_dirty.isEmpty()) {
return false;
}
// otherwise, swap our hashmaps
HashMap tmap = _spare;
_spare = _dirty;
_dirty = tmap;
}
// scan through the list, looking for components for whom a parent
// component is also dirty. in such a case, the dirty rectangle
// for the parent component is expanded to contain the dirty
// rectangle of the child and the child is removed from the
// repaint list (painting the parent will repaint the child)
Iterator iter = _spare.entrySet().iterator();
PRUNE:
while (iter.hasNext()) {
Entry entry = (Entry)iter.next();
JComponent comp = (JComponent)entry.getKey();
Rectangle drect = (Rectangle)entry.getValue();
int x = comp.getX() + drect.x, y = comp.getY() + drect.y;
// climb up the parent hierarchy, looking for the first opaque
// parent as well as the root component
for (Component c = comp.getParent(); c != null; c = c.getParent()) {
// stop looking for combinable parents for non-visible or
// non-JComponents
if (!c.isVisible() || !c.isDisplayable() ||
!(c instanceof JComponent)) {
break;
}
// check to see if this parent is dirty
Rectangle prect = (Rectangle)_spare.get(c);
if (prect != null) {
// that we were going to merge it with its parent and
// blow it away
drect.x = x;
drect.y = y;
if (DEBUG) {
Log.info("Found dirty parent " +
"[comp=" + toString(comp) +
", drect=" + StringUtil.toString(drect) +
", pcomp=" + toString(c) +
", prect=" + StringUtil.toString(prect) +
"].");
}
prect.add(drect);
if (DEBUG) {
Log.info("New prect " + StringUtil.toString(prect));
}
// remove the child component and be on our way
iter.remove();
continue PRUNE;
}
// translate the coordinates into this component's
// coordinate system
x += c.getX();
y += c.getY();
}
}
// now paint each of the dirty components, by setting the clipping
// rectangle appropriately and calling paint() on the associated
// root component
iter = _spare.entrySet().iterator();
while (iter.hasNext()) {
Entry entry = (Entry)iter.next();
JComponent comp = (JComponent)entry.getKey();
Rectangle drect = (Rectangle)entry.getValue();
// get the root component, adjust the clipping (dirty)
// rectangle and obtain the bounds of the client in absolute
// coordinates
Component root = null, ocomp = null;
// start with the components bounds which we'll switch to the
// opaque parent component's bounds if and when we find one
_cbounds.setBounds(0, 0, comp.getWidth(), comp.getHeight());
// climb up the parent hierarchy, looking for the first opaque
// parent as well as the root component
for (Component c = comp; c != null; c = c.getParent()) {
if (!c.isVisible() || !c.isDisplayable()) {
break;
}
if (c instanceof JComponent) {
// make a note of the first opaque parent we find
if (ocomp == null && ((JComponent)c).isOpaque()) {
ocomp = c;
// we need to obtain the opaque parent's coordinates
// in the root coordinate system for when we repaint
_cbounds.setBounds(
0, 0, ocomp.getWidth(), ocomp.getHeight());
}
} else {
// oh god the hackery. apparently the fscking
// JEditorPane wraps a heavy weight component around
// every swing component it uses when doing forms
Component tp = c.getParent();
if (!(tp instanceof JEditorPane)) {
root = c;
break;
}
}
// translate the coordinates into this component's
// coordinate system
drect.x += c.getX();
drect.y += c.getY();
_cbounds.x += c.getX();
_cbounds.y += c.getY();
// clip the dirty region to the bounds of this component
SwingUtilities.computeIntersection(
c.getX(), c.getY(), c.getWidth(), c.getHeight(), drect);
}
// if we found no opaque parent, just paint the component
// itself (this seems to happen with the top-level layered
// pane)
if (ocomp == null) {
ocomp = comp;
}
// if this component is rooted in our frame, repaint it into
// the supplied graphics instance
if (root == _root) {
if (DEBUG) {
Log.info("Repainting [comp=" + toString(comp) +
StringUtil.toString(_cbounds) +
", ocomp=" + toString(ocomp) +
", drect=" + StringUtil.toString(drect) + "].");
}
g.setClip(drect);
g.translate(_cbounds.x, _cbounds.y);
try {
// some components are ill-behaved and may throw an
// exception while painting themselves, and so we
// needs must deal with these fellows gracefully
ocomp.paint(g);
} catch (Exception e) {
Log.warning("Exception while painting component " +
"[comp=" + ocomp + ", e=" + e + "].");
Log.logStackTrace(e);
}
g.translate(-_cbounds.x, -_cbounds.y);
// we also need to repaint any components in this layer
// that are above our freshly repainted component
fmgr.renderLayers((Graphics2D)g, ocomp, _cbounds, _clipped);
} else if (root != null) {
if (DEBUG) {
Log.info("Repainting old-school " +
"[comp=" + toString(comp) +
", ocomp=" + toString(ocomp) +
", root=" + toString(root) +
", bounds=" + StringUtil.toString(_cbounds) +
"].");
dumpHierarchy(comp);
}
// otherwise, repaint with standard swing double buffers
Image obuf = getOffscreenBuffer(
ocomp, _cbounds.width, _cbounds.height);
Graphics og = null, cg = null;
try {
og = obuf.getGraphics();
ocomp.paint(og);
cg = ocomp.getGraphics();
cg.drawImage(obuf, 0, 0, null);
} finally {
if (og != null) {
og.dispose();
}
if (cg != null) {
cg.dispose();
}
}
}
}
// clear out the mapping of dirty components
_spare.clear();
return true;
}
/**
* Used to dump a component when debugging.
*/
protected static String toString (Component comp)
{
return comp.getClass().getName() +
StringUtil.toString(comp.getBounds());
}
/**
* Dumps the containment hierarchy for the supplied component.
*/
protected static void dumpHierarchy (Component comp)
{
for (String indent = ""; comp != null; indent += " ") {
Log.info(indent + toString(comp));
comp = comp.getParent();
}
}
/** The root of our interface. */
protected Component _root;
/** A list of invalid components. */
protected Object[] _invalid;
/** A mapping of invalid rectangles for each widget that is dirty. */
protected HashMap _dirty = new HashMap();
/** A spare hashmap that we swap in while repainting dirty components
* in the old hashmap. */
protected HashMap _spare = new HashMap();
/** Used to compute dirty components' bounds. */
protected Rectangle _cbounds = new Rectangle();
/** Used when rendering "layered" components. */
protected boolean[] _clipped = new boolean[] { true };
/** We debug so much that we have to make it easy to enable and
* disable debug logging. Yay! */
protected static final boolean DEBUG = false;
}
@@ -0,0 +1,148 @@
//
// $Id: BackFrameManager.java 4181 2006-06-07 21:54:12Z 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;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Rectangle;
import java.awt.image.VolatileImage;
/**
* A {@link FrameManager} extension that uses a volatile off-screen image
* to do its rendering.
*/
public class BackFrameManager extends FrameManager
{
// documentation inherited
protected void paint (long tickStamp)
{
// start out assuming we can do an incremental render
boolean incremental = true;
do {
GraphicsConfiguration gc = _window.getGraphicsConfiguration();
// create our off-screen buffer if necessary
if (_backimg == null || _backimg.getWidth() != _window.getWidth() ||
_backimg.getHeight() != _window.getHeight()) {
createBackBuffer(gc);
}
// make sure our back buffer hasn't disappeared
int valres = _backimg.validate(gc);
// if we've changed resolutions, recreate the buffer
if (valres == VolatileImage.IMAGE_INCOMPATIBLE) {
Log.info("Back buffer incompatible, recreating.");
createBackBuffer(gc);
}
// if the image wasn't A-OK, we need to rerender the whole
// business rather than just the dirty parts
if (valres != VolatileImage.IMAGE_OK) {
// Log.info("Lost back buffer, redrawing " + valres);
if (_bgfx != null) {
_bgfx.dispose();
}
_bgfx = (Graphics2D)_backimg.getGraphics();
if (_fgfx != null) {
_fgfx.dispose();
_fgfx = null;
}
incremental = false;
}
// dirty everything if we're not incrementally rendering
if (!incremental) {
_root.getRootPane().revalidate();
_root.getRootPane().repaint();
}
if (!paint(_bgfx)) {
return;
}
// we cache our frame's graphics object so that we can avoid
// instantiating a new one on every tick
if (_fgfx == null) {
_fgfx = (Graphics2D)_window.getGraphics();
}
_fgfx.drawImage(_backimg, 0, 0, null);
// if we loop through a second time, we'll need to rerender
// everything
incremental = false;
} while (_backimg.contentsLost());
}
// documentation inherited
protected void restoreFromBack (Rectangle dirty)
{
if (_fgfx == null || _backimg == null) {
return;
}
// Log.info("Restoring from back " + StringUtil.toString(dirty) + ".");
_fgfx.setClip(dirty);
_fgfx.drawImage(_backimg, 0, 0, null);
_fgfx.setClip(null);
}
/**
* Creates the off-screen buffer used to perform double buffered
* rendering of the animated panel.
*/
protected void createBackBuffer (GraphicsConfiguration gc)
{
// if we have an old image, clear it out
if (_backimg != null) {
_backimg.flush();
_bgfx.dispose();
}
// create the offscreen buffer
int width = _window.getWidth(), height = _window.getHeight();
_backimg = gc.createCompatibleVolatileImage(width, height);
// fill the back buffer with white
_bgfx = (Graphics2D)_backimg.getGraphics();
_bgfx.fillRect(0, 0, width, height);
// clear out our frame graphics in case that became invalid for
// the same reasons our back buffer became invalid
if (_fgfx != null) {
_fgfx.dispose();
_fgfx = null;
}
// Log.info("Created back buffer [" + width + "x" + height + "].");
}
/** The image used to render off-screen. */
protected VolatileImage _backimg;
/** The graphics object from our back buffer. */
protected Graphics2D _bgfx;
/** The graphics object from our frame. */
protected Graphics2D _fgfx;
}
@@ -0,0 +1,101 @@
//
// $Id: FlipFrameManager.java 4181 2006-06-07 21:54:12Z 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;
import java.awt.AWTException;
import java.awt.BufferCapabilities;
import java.awt.Graphics2D;
import java.awt.ImageCapabilities;
import java.awt.Rectangle;
import java.awt.image.BufferStrategy;
/**
* A {@link FrameManager} extension that uses a flip-buffer (via {@link
* BufferStrategy} to do its rendering.
*/
public class FlipFrameManager extends FrameManager
{
// documentation inherited
protected void paint (long tickStamp)
{
// create our buffer strategy if we don't already have one
if (_bufstrat == null) {
BufferCapabilities cap = new BufferCapabilities(
new ImageCapabilities(true), new ImageCapabilities(true),
BufferCapabilities.FlipContents.COPIED);
try {
_window.createBufferStrategy(2, cap);
} catch (AWTException ae) {
Log.warning("Failed creating flip bufstrat: " + ae + ".");
// fall back to one without custom capabilities
_window.createBufferStrategy(2);
}
_bufstrat = _window.getBufferStrategy();
}
// start out assuming we can do an incremental render
boolean incremental = true;
do {
Graphics2D gfx = null;
try {
gfx = (Graphics2D)_bufstrat.getDrawGraphics();
// dirty everything if we're not incrementally rendering
if (!incremental) {
Log.info("Doing non-incremental render; contents lost " +
"[lost=" + _bufstrat.contentsLost() +
", rest=" + _bufstrat.contentsRestored() + "].");
_root.getRootPane().revalidate();
_root.getRootPane().repaint();
}
// request to paint our participants and components and bail
// if they paint nothing
if (!paint(gfx)) {
return;
}
// flip our buffer to visible
_bufstrat.show();
// if we loop through a second time, we'll need to rerender
// everything
incremental = false;
} finally {
if (gfx != null) {
gfx.dispose();
}
}
} while (_bufstrat.contentsLost());
}
// documentation inherited
protected void restoreFromBack (Rectangle dirty)
{
// nothing doing
}
/** The buffer strategy used to do our rendering. */
protected BufferStrategy _bufstrat;
}
@@ -0,0 +1,111 @@
package com.threerings.media;
import java.awt.Component;
import com.threerings.media.FrameManager;
import com.threerings.media.FrameParticipant;
public abstract class FrameInterval
implements FrameParticipant
{
/**
* Constructor - registers the interval as a frame participant
*/
public FrameInterval (FrameManager mgr)
{
_mgr = mgr;
}
// documentation inhertied from FrameParticipant
public Component getComponent ()
{
return null;
}
// documentation inherited from FrameParticipant
public boolean needsPaint ()
{
return false;
}
// documentation inherited from FrameParticipant
public void tick (long tickStamp)
{
if (_nextTime == -1) {
// First time through
_nextTime = tickStamp + _initDelay;
} else if (tickStamp >= _nextTime) {
// If we're repeating, set the next time to run, otherwise, reset
if (_repeatDelay != 0L) {
_nextTime += _repeatDelay;
} else {
_nextTime = -1;
cancel();
}
expired();
}
}
/**
*
* The main method where your interval should do its work.
*
*/
public abstract void expired ();
/**
* Schedule the interval to execute once, after the specified delay.
* Supersedes any previous schedule that this Interval may have had.
*/
public final void schedule (long delay)
{
schedule(delay, 0L);
}
/**
* Schedule the interval to execute repeatedly, with the same delay.
* Supersedes any previous schedule that this Interval may have had.
*/
public final void schedule (long delay, boolean repeat)
{
schedule(delay, repeat ? delay : 0L);
}
/**
* Schedule the interval to execute repeatedly with the specified
* initial delay and repeat delay.
* Supersedes any previous schedule that this Interval may have had.
*/
public final void schedule (long initialDelay, long repeatDelay)
{
if (!_mgr.isRegisteredFrameParticipant(this)) {
_mgr.registerFrameParticipant(this);
}
_repeatDelay = repeatDelay;
_initDelay = initialDelay;
_nextTime = -1;
}
/**
* Cancel the current schedule, and ensure that any expirations that
* are queued up but have not yet run do not run.
*/
public final void cancel ()
{
_mgr.removeFrameParticipant(this);
}
/** Time of the next expiration. */
protected long _nextTime;
/** Time between expirations. */
protected long _repeatDelay;
/** Time between expirations. */
protected long _initDelay;
/** The context whose FrameManager we are using. */
protected FrameManager _mgr;
}
@@ -0,0 +1,846 @@
//
// $Id: FrameManager.java 4181 2006-06-07 21:54:12Z 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;
import java.applet.Applet;
import java.awt.Component;
import java.awt.Graphics2D;
import java.awt.GraphicsDevice;
import java.awt.KeyEventDispatcher;
import java.awt.KeyboardFocusManager;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.KeyEvent;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.awt.EventQueue;
import javax.swing.JLayeredPane;
import javax.swing.RepaintManager;
import javax.swing.JRootPane;
import javax.swing.RootPaneContainer;
import javax.swing.event.AncestorEvent;
import javax.swing.event.AncestorListener;
import com.samskivert.swing.RuntimeAdjust;
import com.samskivert.util.ListUtil;
import com.samskivert.util.RunAnywhere;
import com.samskivert.util.StringUtil;
import com.threerings.media.timer.MediaTimer;
import com.threerings.media.timer.SystemMediaTimer;
import com.threerings.media.util.TrailingAverage;
import com.threerings.util.unsafe.Unsafe;
/**
* Provides a central point from which the computation for each "frame" or tick
* can be dispatched. This assumed that the application structures its activity
* around the rendering of each frame, which is a common architecture for
* games. The animation and sprite support provided by other classes in this
* package are structured for use in an application that uses a frame manager
* to tick everything once per frame.
*
* <p> The frame manager goes through a simple two part procedure every frame:
*
* <ul>
* <li> Ticking all of the frame participants: in {@link
* FrameParticipant#tick}, any processing that need be performed during this
* frame should be performed. Care should be taken not to execute code that
* will take unduly long, instead such processing should be broken up so that
* it can be performed in small pieces every frame (or performed on a separate
* thread with the results safely communicated back to the frame participants
* for incorporation into the rendering loop).
*
* <li> Painting the user interface hierarchy: the top-level component (the
* frame) is painted (via a call to {@link JRootPane#paint}) into a flip buffer
* (if supported, an off-screen buffer if not). Updates that were computed
* during the tick should be rendered in this call to paint. The paint call
* will propagate down to all components in the UI hierarchy, some of which may
* be {@link FrameParticipant}s and will have prepared themselves for their
* upcoming painting in the previous call to {@link
* FrameParticipant#tick}. When the call to paint completes, the flip buffer is
* flipped and the process starts all over again. </ul>
*
* <p> The ticking and rendering takes place on the AWT thread so as to
* avoid the need for complicated coordination between AWT event handler
* code and frame code. However, this means that all AWT (and Swing) event
* handlers <em>must not</em> perform any complicated processing. After
* each frame, control of the AWT thread is given back to the AWT which
* processes all pending AWT events before giving the frame manager an
* opportunity to process the next frame. Thus the convenience of
* everything running on the AWT thread comes with the price of requiring
* that AWT event handlers not block or perform any intensive processing.
* In general, this is a sensible structure for an application anyhow, so
* this organization tends to be preferable to an organization where the
* AWT and frame threads are separate and must tread lightly so as not to
* collide.
*
* <p> Note: the way that <code>JScrollPane</code> goes about improving
* performance when scrolling complicated contents cannot work with active
* rendering. If you use a <code>JScrollPane</code> in an application that
* uses the frame manager, you should either use the provided {@link
* SafeScrollPane} or set your scroll panes' viewports to
* <code>SIMPLE_SCROLL_MODE</code>.
*/
public abstract class FrameManager
{
/**
* Normally, the frame manager will repaint any component in a {@link
* JLayeredPane} layer (popups, overlays, etc.) that overlaps a frame
* participant on every tick because the frame participant <em>could</em>
* have changed underneath the overlay which would require that the overlay
* be repainted. If the application knows that the frame participant
* beneath the overlay will never change, it can have its overlay implement
* this interface and avoid the expense of forcibly fully repainting the
* overlay on every frame.
*/
public static interface SafeLayerComponent
{
}
/**
* Creates a frame manager that will use a {@link SystemMediaTimer} to
* obtain timing information, which is available on every platform, but
* returns inaccurate time stamps on many platforms.
*
* @see #newInstance(Window, RootPaneContainer, MediaTimer)
*/
public static FrameManager newInstance (
Window window, RootPaneContainer root)
{
// first try creating a PerfTimer which is the best if we're using
// JDK1.4.2
MediaTimer timer = null;
try {
timer = (MediaTimer)Class.forName(PERF_TIMER).newInstance();
} catch (Throwable t) {
Log.info("Can't use PerfTimer (" + t + ") reverting to " +
"System.currentTimeMillis() based timer.");
timer = new SystemMediaTimer();
}
return newInstance(window, root, timer);
}
/**
* Constructs a frame manager that will do its rendering to the
* supplied frame. It is likely that the caller will want to have put
* the frame into full-screen exclusive mode prior to providing it to
* the frame manager so that the frame manager can take advantage of
* optimizations available in that mode.
*
* @see GraphicsDevice#setFullScreenWindow
*/
public static FrameManager newInstance (
Window window, RootPaneContainer root, MediaTimer timer)
{
FrameManager fmgr;
if (_useFlip.getValue()) {
Log.info("Creating flip frame manager.");
fmgr = new FlipFrameManager();
} else {
Log.info("Creating back frame manager.");
fmgr = new BackFrameManager();
}
fmgr.init(window, root, timer);
return fmgr;
}
/**
* Initializes this frame manager and prepares it for operation.
*/
protected void init (
Window window, RootPaneContainer root, MediaTimer timer)
{
_window = window;
_root = root;
if (window instanceof ManagedJFrame) {
((ManagedJFrame)window).init(this);
}
_timer = timer;
// set up our custom repaint manager
_remgr = new ActiveRepaintManager(_window);
RepaintManager.setCurrentManager(_remgr);
// turn off double buffering for the whole business because we
// handle repaints
_remgr.setDoubleBufferingEnabled(false);
if (DEBUG_EVENTS) {
addTestListeners();
}
}
/**
* Adds a variety of listeners to the frame in order to provide
* visibility into the various events received by the frame.
*/
protected void addTestListeners ()
{
// add a test window listener
_window.addWindowListener(new WindowListener() {
public void windowActivated (WindowEvent e) {
Log.info("Window activated [evt=" + e + "].");
}
public void windowClosed (WindowEvent e) {
Log.info("Window closed [evt=" + e + "].");
}
public void windowClosing (WindowEvent e) {
Log.info("Window closing [evt=" + e + "].");
}
public void windowDeactivated (WindowEvent e) {
Log.info("Window deactivated [evt=" + e + "].");
}
public void windowDeiconified (WindowEvent e) {
Log.info("Window deiconified [evt=" + e + "].");
}
public void windowIconified (WindowEvent e) {
Log.info("Window iconified [evt=" + e + "].");
}
public void windowOpened (WindowEvent e) {
Log.info("Window opened [evt=" + e + "].");
}
});
// add a component listener
_window.addComponentListener(new ComponentListener() {
public void componentHidden (ComponentEvent e) {
Log.info("Window component hidden [evt=" + e + "].");
}
public void componentShown (ComponentEvent e) {
Log.info("Window component shown [evt=" + e + "].");
}
public void componentMoved (ComponentEvent e) {
Log.info("Window component moved [evt=" + e + "].");
}
public void componentResized (ComponentEvent e) {
Log.info("Window component resized [evt=" + e + "].");
}
});
// add test ancestor focus listener
_root.getRootPane().addAncestorListener(
new AncestorListener() {
public void ancestorAdded (AncestorEvent e) {
Log.info("Root pane ancestor added [e=" + e + "].");
}
public void ancestorRemoved (AncestorEvent e) {
Log.info("Root pane ancestor removed [e=" + e + "].");
}
public void ancestorMoved (AncestorEvent e) {
Log.info("Root pane ancestor moved [e=" + e + "].");
}
});
// add test key event dispatcher
KeyboardFocusManager.getCurrentKeyboardFocusManager().
addKeyEventDispatcher(new KeyEventDispatcher() {
public boolean dispatchKeyEvent (KeyEvent e) {
// if ((e.getModifiersEx() & KeyEvent.ALT_DOWN_MASK) != 0 &&
// e.getKeyCode() == KeyEvent.VK_TAB) {
// Log.info("Detected alt-tab key event " +
// "[e=" + e + "].");
// // attempt to eat the event so that windows
// // doesn't alt-tab into unhappy land
// e.consume();
// return true;
// }
return false;
}
});
}
/**
* Instructs the frame manager to target the specified number of
* frames per second. If the computation and rendering for a frame are
* completed with time to spare, the frame manager will wait until the
* proper time to begin processing for the next frame. If a frame
* takes longer than its alotted time, the frame manager will
* immediately begin processing on the next frame.
*/
public void setTargetFrameRate (int fps)
{
// compute the number of milliseconds per frame
_millisPerFrame = 1000/fps;
}
/**
* Registers a frame participant. The participant will be given the
* opportunity to do processing and rendering on each frame.
*/
public void registerFrameParticipant (FrameParticipant participant)
{
Object[] nparts = ListUtil.testAndAddRef(_participants, participant);
if (nparts == null) {
Log.warning("Refusing to add duplicate frame participant! " +
participant);
} else {
_participants = nparts;
}
}
/**
* Returns true if the specified participant is registered.
*/
public boolean isRegisteredFrameParticipant (FrameParticipant participant)
{
return ListUtil.containsRef(_participants, participant);
}
/**
* Removes a frame participant.
*/
public void removeFrameParticipant (FrameParticipant participant)
{
ListUtil.clearRef(_participants, participant);
}
/**
* Returns a millisecond granularity time stamp using the {@link
* MediaTimer} with which this frame manager was configured.
* <em>Note:</em> this should only be called from the AWT thread.
*/
public long getTimeStamp ()
{
return _timer.getElapsedMillis();
}
/**
* Starts up the per-frame tick
*/
public void start ()
{
if (_ticker == null) {
_ticker = new Ticker();
_ticker.start();
_lastTickStamp = 0;
}
}
/**
* Stops the per-frame tick.
*/
public synchronized void stop ()
{
if (_ticker != null) {
_ticker.cancel();
_ticker = null;
}
}
/**
* Returns true if the tick interval is be running (not necessarily at
* that instant, but in general).
*/
public synchronized boolean isRunning ()
{
return (_ticker != null);
}
/**
* Returns the number of ticks executed in the last second.
*/
public int getPerfTicks ()
{
return Math.round(_fps[1]);
}
/**
* Returns the number of ticks requested in the last second.
*/
public int getPerfTries ()
{
return Math.round(_fps[0]);
}
/**
* Returns debug performance metrics.
*/
public TrailingAverage[] getPerfMetrics ()
{
if (_metrics == null) {
_metrics = new TrailingAverage[] {
new TrailingAverage(150),
new TrailingAverage(150),
new TrailingAverage(150) };
}
return _metrics;
}
/**
* Called to perform the frame processing and rendering.
*/
protected void tick (long tickStamp)
{
long start = 0L, paint = 0L;
if (MediaPanel._perfDebug.getValue()) {
start = paint = _timer.getElapsedMicros();
}
// if our frame is not showing (or is impossibly sized), don't try
// rendering anything
if (_window.isShowing() &&
_window.getWidth() > 0 && _window.getHeight() > 0) {
// tick our participants
tickParticipants(tickStamp);
paint = _timer.getElapsedMicros();
// repaint our participants and components
paint(tickStamp);
}
if (MediaPanel._perfDebug.getValue()) {
long end = _timer.getElapsedMicros();
getPerfMetrics()[1].record((int)(paint-start)/100);
getPerfMetrics()[2].record((int)(end-paint)/100);
}
}
/**
* Called once per frame to invoke {@link FrameParticipant#tick} on
* all of our frame participants.
*/
protected void tickParticipants (long tickStamp)
{
long gap = tickStamp - _lastTickStamp;
if (_lastTickStamp != 0 && gap > (HANG_DEBUG ? HANG_GAP : BIG_GAP)) {
Log.debug("Long tick delay [delay=" + gap + "ms].");
}
_lastTickStamp = tickStamp;
// validate any invalid components
try {
_remgr.validateComponents();
} catch (Throwable t) {
Log.warning("Failure validating components.");
Log.logStackTrace(t);
}
// tick all of our frame participants
for (int ii = 0; ii < _participants.length; ii++) {
FrameParticipant part = (FrameParticipant)_participants[ii];
if (part == null) {
continue;
}
try {
long start = 0L;
if (HANG_DEBUG) {
start = System.currentTimeMillis();
}
part.tick(tickStamp);
if (HANG_DEBUG) {
long delay = (System.currentTimeMillis() - start);
if (delay > HANG_GAP) {
Log.info("Whoa nelly! Ticker took a long time " +
"[part=" + part + ", time=" + delay + "ms].");
}
}
} catch (Throwable t) {
Log.warning("Frame participant choked during tick " +
"[part=" + StringUtil.safeToString(part) + "].");
Log.logStackTrace(t);
}
}
}
/**
* Called once per frame to invoke {@link Component#paint} on all of
* our frame participants' components and all dirty components managed
* by our {@link ActiveRepaintManager}.
*/
protected abstract void paint (long tickStamp);
/**
* Paints our frame participants and any dirty components via the
* repaint manager.
*
* @return true if anything was painted, false if not.
*/
protected boolean paint (Graphics2D gfx)
{
// paint our frame participants (which want to be handled
// specially)
int painted = 0;
for (int ii = 0; ii < _participants.length; ii++) {
FrameParticipant part = (FrameParticipant)_participants[ii];
if (part == null) {
continue;
}
Component pcomp = part.getComponent();
if (pcomp == null || !part.needsPaint()) {
continue;
}
long start = 0L;
if (HANG_DEBUG) {
start = System.currentTimeMillis();
}
// get the bounds of this component
pcomp.getBounds(_tbounds);
// the bounds adjustment we're about to call will add in the
// components initial bounds offsets, so we remove them here
_tbounds.setLocation(0, 0);
// convert them into top-level coordinates; also note that if
// this component does not have a valid or visible root, we
// don't want to paint it either
if (getRoot(pcomp, _tbounds) == null) {
continue;
}
try {
// render this participant; we don't set the clip because
// frame participants are expected to handle clipping
// themselves; otherwise we might pointlessly set the clip
// here, creating a few Rectangle objects in the process,
// only to have the frame participant immediately set the
// clip to something more sensible
gfx.translate(_tbounds.x, _tbounds.y);
pcomp.paint(gfx);
gfx.translate(-_tbounds.x, -_tbounds.y);
painted++;
} catch (Throwable t) {
String ptos = StringUtil.safeToString(part);
Log.warning("Frame participant choked during paint " +
"[part=" + ptos + "].");
Log.logStackTrace(t);
}
// render any components in our layered pane that are not in
// the default layer
_clipped[0] = false;
renderLayers(gfx, pcomp, _tbounds, _clipped);
if (HANG_DEBUG) {
long delay = (System.currentTimeMillis() - start);
if (delay > HANG_GAP) {
Log.warning("Whoa nelly! Painter took a long time " +
"[part=" + part + ", time=" + delay + "ms].");
}
}
}
// repaint any widgets that have declared they need to be
// repainted since the last tick
boolean pcomp = _remgr.paintComponents(gfx, this);
// let the caller know if anybody painted anything
return ((painted > 0) || pcomp);
}
/**
* Called by the {@link ManagedJFrame} when our window was hidden and
* reexposed.
*/
protected abstract void restoreFromBack (Rectangle dirty);
/**
* Renders all components in all {@link JLayeredPane} layers that
* intersect the supplied bounds.
*/
protected void renderLayers (Graphics2D g, Component pcomp,
Rectangle bounds, boolean[] clipped)
{
JLayeredPane lpane =
JLayeredPane.getLayeredPaneAbove(pcomp);
if (lpane != null) {
renderLayer(g, bounds, lpane, clipped, JLayeredPane.PALETTE_LAYER);
renderLayer(g, bounds, lpane, clipped, JLayeredPane.MODAL_LAYER);
renderLayer(g, bounds, lpane, clipped, JLayeredPane.POPUP_LAYER);
renderLayer(g, bounds, lpane, clipped, JLayeredPane.DRAG_LAYER);
}
}
/**
* Renders all components in the specified layer of the supplied
* layered pane that intersect the supplied bounds.
*/
protected void renderLayer (Graphics2D g, Rectangle bounds,
JLayeredPane pane, boolean[] clipped,
Integer layer)
{
// stop now if there are no components in that layer
int ccount = pane.getComponentCountInLayer(layer.intValue());
if (ccount == 0) {
return;
}
// render them up
Component[] comps = pane.getComponentsInLayer(layer.intValue());
for (int ii = 0; ii < ccount; ii++) {
Component comp = comps[ii];
if (!comp.isVisible() || comp instanceof SafeLayerComponent) {
continue;
}
// if this overlay does not intersect the component we just
// rendered, we don't need to repaint it
_tbounds.setBounds(0, 0, comp.getWidth(), comp.getHeight());
getRoot(comp, _tbounds);
if (!_tbounds.intersects(bounds)) {
continue;
}
// if the clipping region has not yet been set during this
// render pass, the time has come to do so
if (!clipped[0]) {
g.setClip(bounds);
clipped[0] = true;
}
// translate into the components coordinate system and render
g.translate(_tbounds.x, _tbounds.y);
try {
comp.paint(g);
} catch (Exception e) {
Log.warning("Component choked while rendering.");
Log.logStackTrace(e);
}
g.translate(-_tbounds.x, -_tbounds.y);
}
}
// documentation inherited
public void checkpoint (String name, int ticks)
{
Log.info("Frames in last second: " + ticks);
}
/**
* Returns the root component for the supplied component or null if it
* is not part of a rooted hierarchy or if any parent along the way is
* found to be hidden or without a peer. Along the way, it adjusts the
* supplied component-relative rectangle to be relative to the
* returned root component.
*/
public static Component getRoot (Component comp, Rectangle rect)
{
for (Component c = comp; c != null; c = c.getParent()) {
if (!c.isVisible() || !c.isDisplayable()) {
return null;
}
if (c instanceof Window || c instanceof Applet) {
return c;
}
rect.x += c.getX();
rect.y += c.getY();
}
return null;
}
/** Used to effect periodic calls to {@link #tick}. */
protected class Ticker extends Thread
{
public void run ()
{
Log.info("Frame manager ticker running " +
"[sleepGran=" + _sleepGranularity.getValue() + "].");
while (_running) {
long start = 0L;
if (MediaPanel._perfDebug.getValue()) {
start = _timer.getElapsedMicros();
}
Unsafe.sleep(_sleepGranularity.getValue());
long woke = _timer.getElapsedMicros();
if (start > 0L) {
getPerfMetrics()[0].record((int)(woke-start)/100);
int elapsed = (int)(woke-start);
if (elapsed > _sleepGranularity.getValue()*1500) {
Log.warning("Long tick [elapsed=" + elapsed + "us].");
}
}
// work around sketchy bug on WinXP that causes the clock
// to leap into the past from time to time
if (woke < _lastAttempt) {
Log.warning("Zoiks! We've leapt into the past, coping " +
"as best we can [dt=" +
(woke - _lastAttempt) + "].");
_lastAttempt = woke;
}
if (woke - _lastAttempt >= _millisPerFrame * 1000) {
_lastAttempt = woke;
if (testAndSet()) {
EventQueue.invokeLater(_awtTicker);
}
// else: drop the frame
}
}
}
public void cancel ()
{
_running = false;
}
protected final synchronized boolean testAndSet ()
{
_tries++;
if (!_ticking) {
_ticking = true;
return true;
}
return false;
}
protected final synchronized void clearTicking (long elapsed)
{
if (++_ticks == 100) {
long time = (elapsed - _lastTick);
_fps[0] = _tries * 1000f / time;
_fps[1] = _ticks * 1000f / time;
_lastTick = elapsed;
_ticks = _tries = 0;
}
_ticking = false;
}
/** Used to invoke the call to {@link #tick} on the AWT event
* queue thread. */
protected Runnable _awtTicker = new Runnable ()
{
public void run ()
{
long elapsed = _timer.getElapsedMillis();
try {
tick(elapsed);
} finally {
clearTicking(elapsed);
}
}
};
/** Used to stick a fork in our ticker when desired. */
protected transient boolean _running = true;
/** Used to detect when we need to drop frames. */
protected boolean _ticking;
/** The time at which we last attempted to tick. */
protected long _lastAttempt;
/** Used to compute metrics. */
protected int _tries, _ticks, _time;
/** Used to compute metrics. */
protected long _lastTick;
};
/** The window into which we do our rendering. */
protected Window _window;
/** Provides access to our Swing bits. */
protected RootPaneContainer _root;
/** Used to obtain timing measurements. */
protected MediaTimer _timer;
/** Our custom repaint manager. */
protected ActiveRepaintManager _remgr;
/** The number of milliseconds per frame (14 by default, which gives
* an fps of ~71). */
protected long _millisPerFrame = 14;
/** Used to track big delays in calls to our tick method. */
protected long _lastTickStamp;
/** The thread that dispatches our frame ticks. */
protected Ticker _ticker;
/** Used to track and report frames per second. */
protected float[] _fps = new float[2];
/** Used to track performance metrics. */
protected TrailingAverage[] _metrics;
/** A temporary bounds rectangle used to avoid lots of object creation. */
protected Rectangle _tbounds = new Rectangle();
/** Used to lazily set the clip when painting popups and other
* "layered" components. */
protected boolean[] _clipped = new boolean[1];
/** The entites that are ticked each frame. */
protected Object[] _participants = new Object[4];
/** If we don't get ticked for 500ms, that's worth complaining about. */
protected static final long BIG_GAP = 500L;
/** If we don't get ticked for 100ms and we're hang debugging,
* complain. */
protected static final long HANG_GAP = 100L;
/** Enable this to log warnings when ticking or painting takes too
* long. */
protected static final boolean HANG_DEBUG = false;
/** A debug hook that toggles debug rendering of sprite paths. */
protected static RuntimeAdjust.BooleanAdjust _useFlip =
new RuntimeAdjust.BooleanAdjust(
"When active a flip-buffer will be used to manage our " +
"rendering, otherwise a volatile back buffer is used " +
"[requires restart]", "narya.media.frame",
// back buffer rendering doesn't work on the Mac, so we
// default to flip buffer on that platform; we still allow it
// to be toggled so that we can easily test things when they
// release new JVMs
MediaPrefs.config, RunAnywhere.isMacOS());
/** Allows us to tweak the sleep granularity. */
protected static RuntimeAdjust.IntAdjust _sleepGranularity =
new RuntimeAdjust.IntAdjust(
"The number of milliseconds slept before checking to see if " +
"it's time to queue up a new frame tick.", "narya.media.sleep_gran",
MediaPrefs.config, RunAnywhere.isWindows() ? 10 : 7);
/** Whether to enable AWT event debugging for the frame. */
protected static final boolean DEBUG_EVENTS = false;
/** The name of the high-performance timer class we attempt to load. */
protected static final String PERF_TIMER =
"com.threerings.media.timer.PerfTimer";
}
@@ -0,0 +1,64 @@
//
// $Id: FrameParticipant.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;
import java.awt.Component;
/**
* Provides a mechanism for participating in the frame tick managed by the
* {@link FrameManager}.
*/
public interface FrameParticipant
{
/**
* This is called on all registered frame participants, one for every
* frame. Following the tick the interface will be rendered, so
* participants can prepare themselves for their upcoming render in
* this method (making use of the timestamp provided for the frame if
* choreography is desired between different participants).
*/
public void tick (long tickStamp);
/**
* Called immediately prior to {@link #getComponent} and then {@link
* Component#paint} on said component, to determine whether or not
* this frame participant needs to be painted.
*/
public boolean needsPaint ();
/**
* If a frame participant wishes also to be actively rendered every
* frame rather than use passive rendering (which for Swing, at least,
* is hijacked when using the frame manager such that we take care of
* repainting dirty Swing components every frame into our off-screen
* buffer), it can return a component here which will have {@link
* Component#paint} called on it once per frame with a translated but
* unclipped graphics object.
*
* <p> Because clipping is expensive in terms of rectangle object
* allocation, frame participants are given the opportunity to do
* their own clipping because they are likely to want to clip to a
* more fine grained region than their entire bounds. If a particpant
* does not wish to be actively rendered, it can safely return null.
*/
public Component getComponent ();
}
@@ -0,0 +1,175 @@
//
// $Id: HourglassView.java 18862 2005-01-27 00:34:51Z tedv $
package com.threerings.media;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import javax.swing.JComponent;
import com.samskivert.util.Interval;
import com.samskivert.util.ResultListener;
import com.threerings.media.image.Mirage;
import com.threerings.media.util.MultiFrameImage;
/**
* Displays an hourglass with the sand level representing the amount of
* time remaining.
*/
public class HourglassView extends TimerView
{
/**
* Constructs an hourglass view.
*/
public HourglassView (FrameManager fmgr, JComponent host, int x, int y,
Mirage glassImage, Mirage topImage, Rectangle topRect,
Mirage botImage, Rectangle botRect,
MultiFrameImage sandTrickle)
{
this(fmgr, host, x, y, glassImage, topImage, topRect, new Point(0, 0),
botImage, botRect, new Point(0, 0), sandTrickle);
}
/**
* Constructs an hourglass view.
*/
public HourglassView (
FrameManager fmgr, JComponent host, int x, int y, Mirage glassImage,
Mirage topImage, Rectangle topRect, Point topOff,
Mirage botImage, Rectangle botRect, Point botOff,
MultiFrameImage sandTrickle)
{
super(fmgr, host, new Rectangle(x, y, glassImage.getWidth(),
glassImage.getHeight()));
// Store relevant coordinate data
_topRect = topRect;
_topOff = topOff;
_botRect = botRect;
_botOff = botOff;
// Save hourglass images
_hourglass = glassImage;
_sandTop = topImage;
_sandBottom = botImage;
_sandTrickle = sandTrickle;
// Initialize the falling grain of sand
_sandY = 0;
// Percentage changes smaller than one pixel in the hourglass
// itself definitely won't be noticed
_changeThreshold = 1.0f / _bounds.height;
}
// documentation inherited
public void changeComplete (float complete)
{
super.changeComplete(complete);
setSandTrickleY();
}
// documentation inherited
public void tick (long tickStamp)
{
// Let the parent handle its stuff
super.tick(tickStamp);
// check if the sand should be updated
if (_enabled && _running && tickStamp > _sandStamp) {
// update the sand frame
setSandTrickleY();
_sandFrame = (_sandFrame + 1) % _sandTrickle.getFrameCount();
_sandStamp = tickStamp + SAND_RATE;
// Make sure the timer gets repainted
invalidate();
}
}
// documentation inherited
public void paint (Graphics2D gfx, float completed)
{
// Handle processing from parent class
super.paint(gfx, completed);
// Paint the hourglass
gfx.translate(_bounds.x, _bounds.y);
_hourglass.paint(gfx, 0, 0);
// Paint the remaining top sand level
Shape oclip = gfx.getClip();
int top = _topRect.y + (int)(_topRect.height * completed);
gfx.clipRect(_topRect.x, top, _topRect.width,
_topRect.height - (top-_topRect.y));
_sandTop.paint(gfx, _topOff.x, _topOff.y);
// paint the current sand frame
gfx.setClip(oclip);
int sandtop = _topRect.y + _topRect.height;
if (_sandY < _botRect.height) {
gfx.clipRect(_botRect.x, sandtop, _botRect.width, _sandY);
}
_sandTrickle.paintFrame(gfx, _sandFrame,
_botRect.x + (_botRect.width - _sandTrickle.getWidth(_sandFrame))/2,
sandtop);
gfx.setClip(oclip);
// Paint the current bottom sand level
top = getSandBottomTop(completed);
gfx.clipRect(_botRect.x, top, _botRect.width,
_botRect.height-(top-_botRect.y));
_sandBottom.paint(gfx, _botOff.x, _botOff.y);
gfx.setClip(oclip);
// untranslate
gfx.translate(-_bounds.x, -_bounds.y);
}
/**
* Set the y position of the trickle.
*/
protected void setSandTrickleY ()
{
_sandY = (int) (_botRect.height * Math.min(1f, (_complete / .025)));
}
/**
* Returns the current top coordinate of the bottom sand pile.
*/
protected int getSandBottomTop (float completed)
{
return _botRect.y + _botRect.height -
(int)(_botRect.height * completed);
}
/** The bounds of the sand within the top and bottom sand images. */
protected Rectangle _topRect, _botRect;
/** Offsets at which to render the sand images. */
protected Point _topOff, _botOff;
/** Our images. */
protected Mirage _hourglass, _sandTop, _sandBottom;
/** The sand trickle. */
protected MultiFrameImage _sandTrickle;
/** The last time the sand updated moved. */
protected long _sandStamp;
/** The next sand frame to be painted. */
protected int _sandFrame;
/** The position of the top of the sand. */
protected int _sandY;
/** How often the sand frame updates. */
protected static final long SAND_RATE = 80;
}
@@ -0,0 +1,183 @@
//
// $Id: IconManager.java 3749 2005-11-09 04:00:16Z 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;
import java.io.IOException;
import java.util.Properties;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import com.samskivert.util.ConfigUtil;
import com.samskivert.util.LRUHashMap;
import com.samskivert.util.StringUtil;
import com.threerings.media.image.ImageUtil;
import com.threerings.media.tile.TileIcon;
import com.threerings.media.tile.TileManager;
import com.threerings.media.tile.TileSet;
/**
* Manages the creation of icons from tileset images. The icon manager is
* provided with a configuration file, which maps icon set identifiers to
* uniform tilesets and provides the metric information for said tilesets.
* UI code can subsequently request icons from the icon manager based on
* icon set identifier and index.
*
* <p> The configuration might look like the following:
*
* <pre>
* arrows.path = /rsrc/media/icons/arrows.png
* arrows.metrics = 20, 25 # icons that are 20 pixels wide and 25 pixels tall
*
* smileys.path = /rsrc/media/icons/smileys.png
* smileys.metrics = 16, 16 # icons that are 16 pixels square
* </pre>
*
* A user could then request an <code>arrows</code> icon like so:
*
* <pre>
* Icon icon = iconmgr.getIcon("arrows", 2);
* </pre>
*/
public class IconManager
{
/**
* Creates an icon manager that will obtain tilesets from the supplied
* tile manager and which will load its configuration information from
* the specified properties file.
*
* @param tmgr the tile manager to use when fetching tilesets.
* @param configPath the path (relative to the classpath) from which
* the icon manager configuration can be loaded.
*
* @exception IOException thrown if an error occurs loading the
* configuration file.
*/
public IconManager (TileManager tmgr, String configPath)
throws IOException
{
this(tmgr, ConfigUtil.loadProperties(configPath));
}
/**
* Creates an icon manager that will obtain tilesets from the supplied
* tile manager and which will read its configuration information from
* the supplied properties file.
*/
public IconManager (TileManager tmgr, Properties config)
{
// save these for later
_tilemgr = tmgr;
_config = config;
}
/**
* If icon images should be loaded from a set of resource bundles
* rather than the classpath, that set can be set here.
*/
public void setSource (String resourceSet)
{
_rsrcSet = resourceSet;
}
/**
* Fetches the icon with the specified index from the named icon set.
*/
public Icon getIcon (String iconSet, int index)
{
try {
// see if the tileset is already loaded
TileSet set = (TileSet)_icons.get(iconSet);
// load it up if not
if (set == null) {
String path = _config.getProperty(iconSet + PATH_SUFFIX);
if (StringUtil.isBlank(path)) {
throw new Exception("No path specified for icon set");
}
String metstr = _config.getProperty(iconSet + METRICS_SUFFIX);
if (StringUtil.isBlank(metstr)) {
throw new Exception("No metrics specified for icon set");
}
int[] metrics = StringUtil.parseIntArray(metstr);
if (metrics == null || metrics.length != 2) {
throw new Exception("Invalid icon set metrics " +
"[metrics=" + metstr + "]");
}
// load up the tileset
if (_rsrcSet == null) {
set = _tilemgr.loadTileSet(
path, metrics[0], metrics[1]);
} else {
set = _tilemgr.loadTileSet(
_rsrcSet, path, metrics[0], metrics[1]);
}
// cache it
_icons.put(iconSet, set);
}
// fetch the appropriate image and create an image icon
return new TileIcon(set.getTile(index));
} catch (Exception e) {
Log.warning("Unable to load icon [iconSet=" + iconSet +
", index=" + index + ", error=" + e + "].");
}
// return an error icon
return new ImageIcon(ImageUtil.createErrorImage(32, 32));
}
/** The tile manager we use to load tilesets. */
protected TileManager _tilemgr;
/** Our configuration information. */
protected Properties _config;
/** The resource bundle from which we load icon images, or null if
* they should be loaded from the classpath. */
protected String _rsrcSet;
/** A cache of our icon tilesets. */
protected LRUHashMap _icons = new LRUHashMap(ICON_CACHE_SIZE);
/** The suffix we append to an icon set name to obtain the tileset
* image path configuration parameter. */
protected static final String PATH_SUFFIX = ".path";
/** The suffix we append to an icon set name to obtain the tileset
* metrics configuration parameter. */
protected static final String METRICS_SUFFIX = ".metrics";
/** The maximum number of icon tilesets that may be cached at once. */
protected static final int ICON_CACHE_SIZE = 10;
}
+63
View File
@@ -0,0 +1,63 @@
//
// $Id: Log.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;
/**
* A placeholder class that contains a reference to the log object used by
* the media services package.
*/
public class Log
{
public static final String PACKAGE = "media";
public static com.samskivert.util.Log log =
new com.samskivert.util.Log(PACKAGE);
/** Convenience function. */
public static void debug (String message)
{
log.debug(message);
}
/** Convenience function. */
public static void info (String message)
{
log.info(message);
}
/** Convenience function. */
public static void warning (String message)
{
log.warning(message);
}
/** Convenience function. */
public static void logStackTrace (Throwable t)
{
log.logStackTrace(com.samskivert.util.Log.WARNING, t);
}
public static int getLevel ()
{
return com.samskivert.util.Log.getLevel(PACKAGE);
}
}
@@ -0,0 +1,117 @@
//
// $Id: ManagedJFrame.java 3285 2004-12-28 03:48:40Z 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;
import java.awt.Graphics;
import java.awt.GraphicsConfiguration;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.Shape;
import javax.swing.JFrame;
import com.samskivert.util.StringUtil;
import com.threerings.media.Log;
/**
* When using the {@link FrameManager}, one must use this top-level frame
* class.
*/
public class ManagedJFrame extends JFrame
{
/**
* Constructs a managed frame with no title.
*/
public ManagedJFrame ()
{
this("");
}
/**
* Constructs a managed frame with the specified title.
*/
public ManagedJFrame (String title)
{
super(title);
}
/**
* Constructs a managed frame in the specified graphics configuration.
*/
public ManagedJFrame (GraphicsConfiguration gc)
{
super(gc);
}
/**
* Called by our frame manager when it's ready to go.
*/
public void init (FrameManager fmgr)
{
_fmgr = fmgr;
}
/**
* Returns the frame manager managing this frame.
*/
public FrameManager getFrameManager ()
{
return _fmgr;
}
/**
* We catch paint requests and forward them on to the repaint
* infrastructure.
*/
public void paint (Graphics g)
{
update(g);
}
/**
* We catch update requests and forward them on to the repaint
* infrastructure.
*/
public void update (Graphics g)
{
Shape clip = g.getClip();
Rectangle dirty;
if (clip != null) {
dirty = clip.getBounds();
} else {
dirty = getRootPane().getBounds();
// account for our frame insets
Insets insets = getInsets();
dirty.x += insets.left;
dirty.y += insets.top;
}
if (_fmgr != null) {
_fmgr.restoreFromBack(dirty);
} else {
Log.info("Dropping AWT dirty rect " + StringUtil.toString(dirty) +
" [clip=" + StringUtil.toString(clip) + "].");
}
}
protected FrameManager _fmgr;
}
@@ -0,0 +1,37 @@
//
// $Id: MediaConstants.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;
/**
* Constants specific to the media package.
*/
public interface MediaConstants
{
/** Identifies the back "layer" of sprites and animations. */
public static final int BACK = 1;
/** Identifies the front "layer" of sprites and animations. */
public static final int FRONT = 2;
/** Identifies the all "layers" of sprites and animations. */
public static final int ALL = 3;
}
@@ -0,0 +1,783 @@
//
// $Id: MediaPanel.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;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import javax.swing.JComponent;
import javax.swing.SwingUtilities;
import javax.swing.event.AncestorEvent;
import javax.swing.event.MouseInputAdapter;
import java.util.Arrays;
import java.util.ArrayList;
import com.samskivert.swing.Controller;
import com.samskivert.swing.Label;
import com.samskivert.swing.RuntimeAdjust;
import com.samskivert.swing.event.AncestorAdapter;
import com.samskivert.swing.event.CommandEvent;
import com.samskivert.util.IntListUtil;
import com.samskivert.util.StringUtil;
import com.threerings.media.timer.MediaTimer;
import com.threerings.media.animation.Animation;
import com.threerings.media.animation.AnimationManager;
import com.threerings.media.sprite.ButtonSprite;
import com.threerings.media.sprite.Sprite;
import com.threerings.media.sprite.SpriteManager;
import com.threerings.media.sprite.action.ActionSprite;
import com.threerings.media.sprite.action.ArmingSprite;
import com.threerings.media.sprite.action.CommandSprite;
import com.threerings.media.sprite.action.DisableableSprite;
import com.threerings.media.sprite.action.HoverSprite;
/**
* Provides a useful extensible framework for rendering animated displays
* that use sprites and animations. Sprites and animations can be added to
* this panel and they will automatically be ticked and rendered (see
* {@link #addSprite} and {@link #addAnimation}).
*
* <p> To facilitate optimized sprite and animation rendering, the panel
* provides a dirty region manager which is used to only repaint dirtied
* regions on each frame. Derived classes can add dirty regions to the
* scene and/or augment the dirty regions created by moving sprites and
* changing animations.
*
* <p> Sprite and animation rendering is done in two layers: front and
* back. Callbacks are provided to render behind the back layer ({@link
* #paintBehind}), in between front and back ({@link #paintBetween}) and
* in front of the front layer ({@link #paintInFront}).
*
* <p> The animated panel automatically registers with the {@link
* FrameManager} to participate in the frame tick. It only does so while
* it is a visible part of the UI hierarchy, so animations and sprites are
* paused while the animated panel is hidden.
*/
public class MediaPanel extends JComponent
implements FrameParticipant, MediaConstants
{
/**
* Constructs a media panel.
*/
public MediaPanel (FrameManager framemgr)
{
// keep this for later
_framemgr = framemgr;
setOpaque(true); // our repaints shouldn't cause other jcomponents to
// create our region manager
_remgr = new RegionManager();
// create our animation and sprite managers
_animmgr = new AnimationManager(this);
_spritemgr = new SpriteManager(this);
// participate in the frame when we're visible
addAncestorListener(new AncestorAdapter() {
public void ancestorAdded (AncestorEvent event) {
_framemgr.registerFrameParticipant(MediaPanel.this);
}
public void ancestorRemoved (AncestorEvent event) {
_framemgr.removeFrameParticipant(MediaPanel.this);
}
});
}
/**
* Get the bounds of the viewport, in media coordinates. For the base
* MediaPanel, this will always be (0, 0, width, height).
*/
public Rectangle getViewBounds ()
{
return new Rectangle(getWidth(), getHeight());
}
/**
* Returns a reference to the animation manager used by this media
* panel.
*/
public AnimationManager getAnimationManager ()
{
return _animmgr;
}
/**
* Returns a reference to the sprite manager used by this media panel.
*/
public SpriteManager getSpriteManager ()
{
return _spritemgr;
}
/**
* Pauses the sprites and animations that are currently active on this
* media panel. Also stops listening to the frame tick while paused.
*/
public void setPaused (boolean paused)
{
// sanity check
if ((paused && (_pauseTime != 0)) ||
(!paused && (_pauseTime == 0))) {
Log.warning("Requested to pause when paused or vice-versa " +
"[paused=" + paused + "].");
return;
}
if (_paused = paused) {
// make a note of our pause time
_pauseTime = _framemgr.getTimeStamp();
} else {
// let the animation and sprite managers know that we just
// warped into the future
long delta = _framemgr.getTimeStamp() - _pauseTime;
_animmgr.fastForward(delta);
_spritemgr.fastForward(delta);
// clear out our pause time
_pauseTime = 0;
}
}
/**
* Returns a reference to the region manager that is used to
* accumulate dirty regions each frame.
*/
public RegionManager getRegionManager ()
{
return _remgr;
}
/**
* Returns a timestamp from the {@link MediaTimer} used to track time
* intervals for this media panel. <em>Note:</em> this should only be
* called from the AWT thread.
*/
public long getTimeStamp ()
{
return _framemgr.getTimeStamp();
}
// documentation inherited from interface
public void getPerformanceStatus (StringBuilder buf)
{
}
/**
* Adds a sprite to this panel.
*/
public void addSprite (Sprite sprite)
{
_spritemgr.addSprite(sprite);
if (((sprite instanceof ActionSprite) ||
(sprite instanceof HoverSprite)) && (_actionSpriteCount++ == 0)) {
if (_actionHandler == null) {
_actionHandler = createActionSpriteHandler();
}
addMouseListener(_actionHandler);
addMouseMotionListener(_actionHandler);
}
}
/**
* @return true if the sprite is already added to this panel.
*/
public boolean isManaged (Sprite sprite)
{
return _spritemgr.isManaged(sprite);
}
/**
* Removes a sprite from this panel.
*/
public void removeSprite (Sprite sprite)
{
_spritemgr.removeSprite(sprite);
if (((sprite instanceof ActionSprite) ||
(sprite instanceof HoverSprite)) && (--_actionSpriteCount == 0)) {
removeMouseListener(_actionHandler);
removeMouseMotionListener(_actionHandler);
}
}
/**
* Removes all sprites from this panel.
*/
public void clearSprites ()
{
_spritemgr.clearMedia();
if (_actionHandler != null) {
removeMouseListener(_actionHandler);
removeMouseMotionListener(_actionHandler);
_actionSpriteCount = 0;
}
}
/**
* Adds an animation to this panel. Animations are automatically
* removed when they finish.
*/
public void addAnimation (Animation anim)
{
_animmgr.registerAnimation(anim);
}
/**
* @return true if the animation is already added to this panel.
*/
public boolean isManaged (Animation anim)
{
return _animmgr.isManaged(anim);
}
/**
* Aborts a currently running animation and removes it from this
* panel. Animations are normally automatically removed when they
* finish.
*/
public void abortAnimation (Animation anim)
{
_animmgr.unregisterAnimation(anim);
}
/**
* Removes all animations from this panel.
*/
public void clearAnimations ()
{
_animmgr.clearMedia();
}
// documentation inherited from interface
public void tick (long tickStamp)
{
// bail if ticking is currently disabled
if (_paused) {
return;
}
// let derived classes do their business
willTick(tickStamp);
// now tick our animations and sprites
_animmgr.tick(tickStamp);
_spritemgr.tick(tickStamp);
// if performance debugging is enabled,
if (_perfDebug.getValue()) {
if (_perfLabel == null) {
_perfLabel = new Label(
"", Label.OUTLINE, Color.white, Color.black,
new Font("Arial", Font.PLAIN, 10));
}
if (_perfRect == null) {
_perfRect = new Rectangle(5, 5, 0, 0);
}
StringBuilder perf = new StringBuilder();
perf.append("[FPS: ");
perf.append(_framemgr.getPerfTicks()).append("/");
perf.append(_framemgr.getPerfTries());
perf.append(" PM:");
StringUtil.toString(perf, _framemgr.getPerfMetrics());
// perf.append(" MP:").append(_dirtyPerTick);
perf.append("]");
String perfStatus = perf.toString();
if (!_perfStatus.equals(perfStatus)) {
_perfStatus = perfStatus;
_perfLabel.setText(perfStatus);
Graphics2D gfx = (Graphics2D)getGraphics();
if (gfx != null) {
_perfLabel.layout(gfx);
gfx.dispose();
// make sure the region we dirty contains the old and
// the new text (which we ensure by never letting the
// rect shrink)
Dimension psize = _perfLabel.getSize();
_perfRect.width = Math.max(_perfRect.width, psize.width);
_perfRect.height = Math.max(_perfRect.height, psize.height);
dirtyScreenRect(new Rectangle(_perfRect));
}
}
}
// let derived classes do their business
didTick(tickStamp);
// make a note that the next paint will correspond to a call to
// tick()
_tickPaintPending = true;
}
/**
* Derived classes can override this method and perform computation
* prior to the ticking of the sprite and animation managers.
*/
protected void willTick (long tickStamp)
{
}
/**
* Derived classes can override this method and perform computation
* subsequent to the ticking of the sprite and animation managers.
*/
protected void didTick (long tickStamp)
{
}
// documentation inherited from interface
public boolean needsPaint ()
{
// compute our average dirty regions per tick
if (_tick++ == 99) {
_tick = 0;
int dirty = IntListUtil.sum(_dirty);
Arrays.fill(_dirty, 0);
_dirtyPerTick = (float)dirty/100;
}
// if we have no dirty regions, clear our pending tick indicator
// because we're not going to get painted
boolean needsPaint = _remgr.haveDirtyRegions();
if (!needsPaint) {
_tickPaintPending = false;
}
// regardless of whether or not we paint, we need to let our
// abstract media managers know that we've gotten to the point of
// painting because they need to remain prepared to deal with
// media changes that happen any time between the tick() and the
// paint() and thus need to know when paint() happens
_animmgr.willPaint();
_spritemgr.willPaint();
return needsPaint;
}
// documentation inherited from interface
public Component getComponent ()
{
return this;
}
// documentation inherited
public void setOpaque (boolean opaque)
{
if (!opaque) {
Log.warning("Media panels shouldn't be setOpaque(false).");
Thread.dumpStack();
}
super.setOpaque(true);
}
// documentation inherited
public void repaint (long tm, int x, int y, int width, int height)
{
if (width > 0 && height > 0) {
dirtyScreenRect(new Rectangle(x, y, width, height));
}
}
// documentation inherited
public void paint (Graphics g)
{
Graphics2D gfx = (Graphics2D)g;
// no use in painting if we're not showing or if we've not yet
// been validated
if (!isValid() || !isShowing()) {
return;
}
// if this isn't a tick paint, then we need to grab the clipping
// rectangle and mark it as dirty
if (!_tickPaintPending) {
Shape clip = g.getClip();
if (clip == null) {
// mark the whole component as dirty
repaint();
} else {
dirtyScreenRect(clip.getBounds());
}
// we used to bail out here and not render until our next
// tick, but it turns out that we need to render here because
// Swing may have repainted our parent over us and expect that
// we're going to paint ourselves on top of whatever it just
// painted, so we go ahead and paint now to avoid flashing
} else {
_tickPaintPending = false;
}
// if we have no invalid rects, there's no need to repaint
if (!_remgr.haveDirtyRegions()) {
return;
}
// get our dirty rectangles and delegate the main painting to a
// method that can be more easily overridden
Rectangle[] dirty = _remgr.getDirtyRegions();
_dirty[_tick] = dirty.length;
try {
paint(gfx, dirty);
} catch (Throwable t) {
Log.warning(this + " choked in paint(" + dirty + ").");
Log.logStackTrace(t);
}
// render our performance debugging if it's enabled
if (_perfRect != null && _perfDebug.getValue()) {
gfx.setClip(null);
_perfLabel.render(gfx, _perfRect.x, _perfRect.y);
}
}
/**
* Performs the actual painting of the media panel. Derived methods
* can override this method if they wish to perform pre- and/or
* post-paint activities or if they wish to provide their own painting
* mechanism entirely.
*/
protected void paint (Graphics2D gfx, Rectangle[] dirty)
{
int dcount = dirty.length;
for (int ii = 0; ii < dcount; ii++) {
Rectangle clip = dirty[ii];
// sanity-check the dirty rectangle
if (clip == null) {
Log.warning("Found null dirty rect painting media panel?!");
Thread.dumpStack();
continue;
}
// constrain this dirty region to the bounds of the component
constrainToBounds(clip);
// ignore rectangles that were reduced to nothingness
if (clip.width == 0 || clip.height == 0) {
continue;
}
// clip to this dirty region
clipToDirtyRegion(gfx, clip);
// paint the region
paintDirtyRect(gfx, clip);
}
}
/**
* Paints all the layers of the specified dirty region.
*/
protected void paintDirtyRect (Graphics2D gfx, Rectangle rect)
{
// paint the behind the scenes stuff
paintBehind(gfx, rect);
// paint back sprites and animations
paintBits(gfx, AnimationManager.BACK, rect);
// paint the between the scenes stuff
paintBetween(gfx, rect);
// paint front sprites and animations
paintBits(gfx, AnimationManager.FRONT, rect);
// paint anything in front
paintInFront(gfx, rect);
}
/**
* Called by the main rendering code to constrain this dirty rectangle
* to the bounds of the media panel. If a derived class is using dirty
* rectangles that live in some sort of virtual coordinate system,
* they'll want to override this method and constraint the rectangles
* properly.
*/
protected void constrainToBounds (Rectangle dirty)
{
SwingUtilities.computeIntersection(
0, 0, getWidth(), getHeight(), dirty);
}
/**
* This is called to clip the rendering output to the supplied dirty
* region. This should use {@link Graphics#setClip} because the
* clipping region will need to be replaced as we iterate through our
* dirty regions. By default, a region is assumed to represent screen
* coordinates, but if a derived class wishes to maintain dirty
* regions in non-screen coordinates, it can override this method to
* properly clip to the dirty region.
*/
protected void clipToDirtyRegion (Graphics2D gfx, Rectangle dirty)
{
// Log.info("MP: Clipping to [clip=" + StringUtil.toString(dirty) + "].");
gfx.setClip(dirty);
}
/**
* Called to mark the specified rectangle (in screen coordinates) as
* dirty. The rectangle will be bent, folded and mutilated, so be sure
* you're not passing a rectangle into this method that is being used
* elsewhere.
*
* <p> If derived classes wish to convert from screen coordinates to
* some virtual coordinate system to be used by their repaint manager,
* this is the place to do it.
*/
protected void dirtyScreenRect (Rectangle rect)
{
_remgr.addDirtyRegion(rect);
}
/**
* Paints behind all sprites and animations. The supplied invalid
* rectangle should be redrawn in the supplied graphics context.
* Sub-classes should override this method to do the actual rendering
* for their display.
*/
protected void paintBehind (Graphics2D gfx, Rectangle dirtyRect)
{
}
/**
* Paints between the front and back layer of sprites and animations.
* The supplied invalid rectangle should be redrawn in the supplied
* graphics context. Sub-classes should override this method to do the
* actual rendering for their display.
*/
protected void paintBetween (Graphics2D gfx, Rectangle dirtyRect)
{
}
/**
* Paints in front of all sprites and animations. The supplied invalid
* rectangle should be redrawn in the supplied graphics context.
* Sub-classes should override this method to do the actual rendering
* for their display.
*/
protected void paintInFront (Graphics2D gfx, Rectangle dirtyRect)
{
}
/**
* Renders the sprites and animations that intersect the supplied
* dirty region in the specified layer. Derived classes can override
* this method if they need to do custom sprite or animation rendering
* (if they need to do special sprite z-order handling, for example).
* The clipping region will already be set appropriately.
*/
protected void paintBits (Graphics2D gfx, int layer, Rectangle dirty)
{
if (layer == FRONT) {
_spritemgr.paint(gfx, layer, dirty);
_animmgr.paint(gfx, layer, dirty);
} else {
_animmgr.paint(gfx, layer, dirty);
_spritemgr.paint(gfx, layer, dirty);
}
}
/**
* Creates the mouse listener that will handle action sprites and their
* variants.
*/
protected ActionSpriteHandler createActionSpriteHandler ()
{
return new ActionSpriteHandler();
}
/** The frame manager with whom we register. */
protected FrameManager _framemgr;
/** The animation manager in use by this panel. */
protected AnimationManager _animmgr;
/** The sprite manager in use by this panel. */
protected SpriteManager _spritemgr;
/** Used to accumulate and merge dirty regions on each tick. */
protected RegionManager _remgr;
/** Used to correlate tick()s with paint()s. */
protected boolean _tickPaintPending = false;
/** Whether we're currently paused. */
protected boolean _paused;
/** Used to track the clock time at which we were paused. */
protected long _pauseTime;
/** Used to keep metrics. */
protected int[] _dirty = new int[200];
/** Used to keep metrics. */
protected int _tick;
/** Used to keep metrics. */
protected float _dirtyPerTick;
/** Handles ActionSprite/HoverSprite/ArmingSprite manipulation. */
protected class ActionSpriteHandler extends MouseInputAdapter
{
// documentation inherited
public void mousePressed (MouseEvent me)
{
if (_activeSprite == null) {
// see if we can find one
Sprite s = getHit(me);
if (s instanceof ActionSprite) {
_activeSprite = s;
}
}
if (_activeSprite instanceof ArmingSprite) {
((ArmingSprite) _activeSprite).setArmed(true);
}
}
// documentation inherited
public void mouseReleased (MouseEvent me)
{
if (_activeSprite instanceof ArmingSprite) {
((ArmingSprite)_activeSprite).setArmed(false);
}
if ((_activeSprite instanceof ActionSprite) &&
_activeSprite.hitTest(me.getX(), me.getY())) {
ActionEvent event;
if (_activeSprite instanceof CommandSprite) {
CommandSprite cs = (CommandSprite) _activeSprite;
event = new CommandEvent(
MediaPanel.this, cs.getActionCommand(),
cs.getCommandArgument(), me.getWhen(),
me.getModifiers());
} else {
ActionSprite as = (ActionSprite) _activeSprite;
event = new ActionEvent(
MediaPanel.this, ActionEvent.ACTION_PERFORMED,
as.getActionCommand(), me.getWhen(), me.getModifiers());
}
Controller.postAction(event);
}
if (!(_activeSprite instanceof HoverSprite)) {
_activeSprite = null;
}
mouseMoved(me);
}
// documentation inherited
public void mouseDragged (MouseEvent me)
{
if (_activeSprite instanceof ArmingSprite) {
((ArmingSprite) _activeSprite).setArmed(
_activeSprite.hitTest(me.getX(), me.getY()));
}
}
// documentation inherited
public void mouseMoved (MouseEvent me)
{
Sprite s = getHit(me);
if (_activeSprite == s) {
return;
}
if (_activeSprite instanceof HoverSprite) {
((HoverSprite) _activeSprite).setHovered(false);
}
_activeSprite = s;
if (_activeSprite instanceof HoverSprite) {
((HoverSprite) _activeSprite).setHovered(true);
}
}
/**
* Utility method, get the highest non-disabled action/hover sprite.
*/
protected Sprite getHit (MouseEvent me)
{
ArrayList list = new ArrayList();
_spritemgr.getHitSprites(list, me.getX(), me.getY());
for (int ii = 0, nn = list.size(); ii < nn; ii++) {
Object o = list.get(ii);
if ((o instanceof HoverSprite || o instanceof ActionSprite) &&
(!(o instanceof DisableableSprite) ||
((DisableableSprite) o).isEnabled())) {
return (Sprite) o;
}
}
return null;
}
/** The active hover sprite, or action sprite. */
protected Sprite _activeSprite;
}
/** The action sprite handler, or null for none. */
protected ActionSpriteHandler _actionHandler;
/** The number of action/hover sprites being managed. */
protected int _actionSpriteCount;
// used to render performance metrics
protected String _perfStatus = "";
protected Label _perfLabel;
protected static Rectangle _perfRect;
/** A debug hook that toggles FPS rendering. */
protected static RuntimeAdjust.BooleanAdjust _perfDebug =
new RuntimeAdjust.BooleanAdjust(
"Toggles frames per second and dirty regions per tick rendering.",
"narya.media.fps_display", MediaPrefs.config, false) {
protected void adjusted (boolean newValue) {
// clear out some things if we're turned off
if (!newValue) {
_perfRect = null;
}
}
};
}
@@ -0,0 +1,35 @@
//
// $Id: MediaPrefs.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;
import com.samskivert.util.Config;
/**
* Provides access to runtime configuration parameters for the media
* package and its subpackages.
*/
public class MediaPrefs
{
/** Used to load our preferences from a properties file and map them
* to the persistent Java preferences repository. */
public static Config config = new Config("rsrc/config/media");
}
@@ -0,0 +1,165 @@
//
// $Id: RegionManager.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;
import java.awt.EventQueue;
import java.awt.Rectangle;
import java.util.ArrayList;
import com.samskivert.util.StringUtil;
/**
* Manages regions (rectangles) that are invalidated in the process of
* ticking animations and sprites and generally doing other display
* related business.
*/
public class RegionManager
{
/**
* Invalidates the specified region.
*/
public void invalidateRegion (int x, int y, int width, int height)
{
if (isValidSize(width, height)) {
addDirtyRegion(new Rectangle(x, y, width, height));
}
}
/**
* Invalidates the specified region (the supplied rectangle will be
* cloned as the region manager fiddles with the rectangles it uses
* internally).
*/
public void invalidateRegion (Rectangle rect)
{
if (isValidSize(rect.width, rect.height)) {
addDirtyRegion((Rectangle)rect.clone());
}
}
/**
* Adds the supplied rectangle to the dirty regions. Control of the
* rectangle is given to the region manager as it may choose to bend,
* fold or mutilate it later. If you don't want the region manager
* messing with your rectangle, use {@link #invalidateRegion}.
*/
public void addDirtyRegion (Rectangle rect)
{
// make sure we're on an AWT thread
if (!EventQueue.isDispatchThread()) {
Log.warning("Oi! Region dirtied on non-AWT thread " +
"[rect=" + rect + "].");
Thread.dumpStack();
}
// sanity check
if (rect == null) {
Log.warning("Attempt to dirty a null rect!?");
Thread.dumpStack();
return;
}
// more sanity checking
long x = rect.x, y = rect.y;
if ((Math.abs(x) > Integer.MAX_VALUE/2) ||
(Math.abs(y) > Integer.MAX_VALUE/2)) {
Log.warning("Requested to dirty questionable region " +
"[rect=" + StringUtil.toString(rect) + "].");
if (Log.getLevel() == Log.log.DEBUG) {
Thread.dumpStack();
}
return; // Let's not do it!
}
if (isValidSize(rect.width, rect.height)) {
// Log.info("Invalidating " + StringUtil.toString(rect));
_dirty.add(rect);
}
}
/** Used to ensure our dirty regions are not invalid. */
protected final boolean isValidSize (int width, int height)
{
if (width < 0 || height < 0) {
Log.warning("Attempt to add invalid dirty region?! " +
"[size=" + width + "x" + height + "].");
Thread.dumpStack();
return false;
} else if (width == 0 || height == 0) {
// no need to complain about zero sized rectangles, just
// ignore them
return false;
} else {
return true;
}
}
/**
* Returns true if dirty regions have been accumulated since the last
* call to {@link #getDirtyRegions}.
*/
public boolean haveDirtyRegions ()
{
return (_dirty.size() > 0);
}
/**
* Merges all outstanding dirty regions into a single list of
* rectangles and returns that to the caller. Interally, the list of
* accumulated dirty regions is cleared out and prepared for the next
* frame.
*/
public Rectangle[] getDirtyRegions ()
{
ArrayList merged = new ArrayList();
for (int ii = _dirty.size() - 1; ii >= 0; ii--) {
// pop the next rectangle from the dirty list
Rectangle mr = (Rectangle)_dirty.remove(ii);
// merge in any overlapping rectangles
for (int jj = ii - 1; jj >= 0; jj--) {
Rectangle r = (Rectangle)_dirty.get(jj);
if (mr.intersects(r)) {
// remove the overlapping rectangle from the list
_dirty.remove(jj);
ii--;
// grow the merged dirty rectangle
mr.add(r);
}
}
// add the merged rectangle to the list
merged.add(mr);
}
Rectangle[] rects = new Rectangle[merged.size()];
merged.toArray(rects);
return rects;
}
/** A list of dirty rectangles. */
protected ArrayList _dirty = new ArrayList();
}
@@ -0,0 +1,89 @@
//
// $Id: SafeScrollPane.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;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import javax.swing.JComponent;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
/**
* A scroll pane that is safe to use in frame managed views.
*/
public class SafeScrollPane extends JScrollPane
{
public SafeScrollPane ()
{
}
public SafeScrollPane (Component view)
{
super(view);
}
public SafeScrollPane (Component view, int owidth, int oheight)
{
super(view);
if (owidth != 0 || oheight != 0) {
_override = new Dimension(owidth, oheight);
}
}
// documentation inherited
public Dimension getPreferredSize ()
{
Dimension d = super.getPreferredSize();
if (_override != null) {
if (_override.width != 0) {
d.width = _override.width;
}
if (_override.height != 0) {
d.height = _override.height;
}
}
return d;
}
protected JViewport createViewport ()
{
JViewport vp = new JViewport() {
public void setViewPosition (Point p) {
super.setViewPosition(p);
// simple scroll mode results in setViewPosition causing
// our view to become invalid, but nothing ever happens to
// queue up a revalidate for said view, so we have to do
// it here
Component c = getView();
if (c instanceof JComponent) {
((JComponent)c).revalidate();
}
}
};
vp.setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
return vp;
}
protected Dimension _override;
}
@@ -0,0 +1,513 @@
//
// $Id: HourglassView.java 18697 2005-01-19 01:18:47Z tedv $
package com.threerings.media;
import java.awt.Component;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import javax.swing.JComponent;
import javax.swing.event.AncestorEvent;
import com.samskivert.swing.event.AncestorAdapter;
import com.samskivert.util.ResultListener;
/**
* A generic timer class that can be rendered on screen.
*
* NOTE: This base class doesn't actually render anything, but it's still
* useful for triggering user supplied callback functions. Derived classes
* are more than welcome to setup their own rendering, of course.
*/
public class TimerView
implements FrameParticipant, HierarchyListener
{
/**
* Constructs a timer view that fires at the default rate.
*/
public TimerView (FrameManager fmgr, JComponent host, Rectangle bounds)
{
// Cache the input arguments
_fmgr = fmgr;
_host = host;
_bounds = new Rectangle(bounds);
// Watch for changes in the timer's state so it can effectively
// register or unregister with the frame manager as necessary
_host.addHierarchyListener(this);
checkFrameParticipation();
}
/**
* Sets whether this timer should be rendered.
*/
public void setEnabled (boolean enabled)
{
if (_enabled != enabled)
{
_enabled = enabled;
invalidate();
}
}
/**
* Get the amount of time it takes to render digital changes in the
* completion state.
*/
public long getTransitionTime ()
{
return _transitionTime;
}
/**
* Set the amount of time it takes to render digital changes in the
* completion state (probably due to a timer reset).
*/
public void setTransitionTime (long time)
{
_transitionTime = Math.max(0, time);
}
/**
* Setup a warning to trigger after the timer is "warnPercent"
* or more completed. If "warner" is non-null, it will be
* called at that time.
*/
public void setWarning (float warnPercent, ResultListener warner)
{
// This warning hasn't triggered yet
_warned = false;
// Here are the details
_warnPercent = warnPercent;
_warner = warner;
}
/**
* Remove any warning this timer might have had.
*/
public void removeWarning ()
{
// Don't trigger any warnings
_warned = false;
_warnPercent = -1;
_warner = null;
}
/** Test if the timer is running right now. */
public boolean running ()
{
return _running;
}
/**
* Start the timer running from the specified percentage complete,
* to expire at the specified time.
*
* @param startPercent a value in [0f, 1f) indicating how much
* hourglass time has already elapsed when the timer starts.
* @param duration The time interval over which the timer would
* run if it started at 0%.
* @param finisher a listener that will be notified when the timer
* finishes, or null if nothing should be notified.
*/
public void start (float startPercent, long duration,
ResultListener finisher)
{
// Sanity check input arguments
if (startPercent < 0.0f || startPercent >= 1.0f) {
throw new IllegalArgumentException(
"Invalid starting percent " + startPercent);
}
if (duration < 0) {
throw new IllegalArgumentException("Invalid duration " + duration);
}
// Stop any current processing
stop();
// Record the timer's full duration and effective start time
_duration = duration;
// Change the completion percent and make sure the starting
// time gets updated on the next tick
changeComplete(startPercent);
_start = Long.MIN_VALUE;
// Thank you sir; would you kindly take a chair in the waiting room?
_finisher = finisher;
// The warning and completion handlers haven't been triggered yet
_warned = false;
_completed = false;
// Start things running
_running = true;
}
/**
* Reset the timer.
*/
public void reset ()
{
// Stop processing permanently
stop();
// Reset the completion percent
changeComplete(0f);
}
/**
* Stop the timer.
*/
public void stop ()
{
// Stop processing
pause();
// Prevent it from being unpaused
_lastUpdate = Long.MIN_VALUE;
}
/**
* Pause the timer from processing.
*/
public void pause ()
{
// Stop processing
_running = false;
}
/**
* Unpause the timer.
*/
public void unpause ()
{
// Don't unpause the timer if it wasn't paused to begin with
if (_lastUpdate == Long.MIN_VALUE || _start == Long.MIN_VALUE) {
return;
}
// Adjust the starting time when the timer next ticks
_processUnpause = true;
// Start things running again
_running = true;
}
/**
* Generate an unexpected change in the timer's completion and
* set up the necessary details to render interpolation from the old
* to new states
*/
public void changeComplete (float complete)
{
// Store the new state
_complete = complete;
// Determine the percentage difference between the "actual"
// completion state and the completion amount to render during
// the smooth interpolation
_renderOffset = _renderComplete - _complete;
// When the timer next ticks, find out when this interpolation
// should start
_renderOffsetTime = Long.MIN_VALUE;
}
/**
* Updates the timer for the current inputted time.
*/
protected void update (long now)
{
}
/**
* Test if the warning was triggered and needs to be processed.
*/
protected boolean triggeredWarning ()
{
// Trigger a warning if it exists, it hasn't occured yet,
// and the timer has passed the warning threshold.
return ((_warnPercent >= 0f) &&
!_warned && (_warnPercent <= _complete));
}
/**
* Test if the completion was triggered and needs to be processed.
*/
protected boolean triggeredCompleted ()
{
return (!_completed && (1.0 <= _complete));
}
/**
* Handle the trigger of the warning.
*/
protected void handleWarning ()
{
// Remember that this warning was handled
_warned = true;
// Execute the warning listener if one was supplied
if (_warner != null) {
_warner.requestCompleted(this);
}
}
/**
* Handle the trigger of the timer's completion.
*/
protected void handleCompleted ()
{
// Remember that the completion was handled
_completed = true;
// Stop the timer
stop();
// Handle the trigger function if one was supplied
if (_finisher != null) {
_finisher.requestCompleted(this);
}
}
/**
* Invalidates this view's bounds via the host component.
*/
protected void invalidate ()
{
// Schedule the timer's location on screen to get repainted
_invalidated = true;
_host.repaint(_bounds.x, _bounds.y, _bounds.width, _bounds.height);
}
/**
* Renders the timer to the given graphics context if enabled.
*/
public void render (Graphics2D gfx)
{
// Paint the timer if its enabled
if (_enabled) {
paint(gfx, _renderComplete);
}
_invalidated = false;
}
/**
* Paint the timer into the given graphics context at the inputted
* percent complete (0f means just started, 1f means just finished).
*/
public void paint (Graphics2D gfx, float complete)
{
// Inheriting classes will want to implement their own
// version of this function. Remember to call this one though!
// Remember the completion level the last time the timer was painted
_paintComplete = complete;
}
// documentation inherited
public void tick (long now)
{
if (!_enabled) {
return;
}
// Initialize the starting time if necessary
if (_start == Long.MIN_VALUE) {
_start = now - Math.round(_duration * _complete);
}
// Initialize the timestamp of the rendering error if necessary
if (_renderOffsetTime == Long.MIN_VALUE) {
_renderOffsetTime = now;
}
// If the timer was just unpaused, handle the updates that
// need a timestamp
if (_processUnpause) {
_processUnpause = false;
_start += now - _lastUpdate;
}
// Update the completion and triggers when the timer is running
if (running())
{
// Figure out how what percent the timer has completed
_lastUpdate = now;
_complete = ((float) (_lastUpdate - _start)) / _duration;
// Handle warning trigger if necessary
if (triggeredWarning()) {
handleWarning();
}
// Handle completion trigger if necessary
if (triggeredCompleted()) {
handleCompleted();
}
}
// Add error to the render completion percent if the interpolation
// hasn't finished yet
_renderComplete = _complete;
float error = _renderOffset;
if (_renderOffsetTime + _transitionTime > now) {
_renderComplete += _renderOffset *
(1f - (now - _renderOffsetTime) / (float) _transitionTime);
_renderComplete = Math.max(0f, Math.min(1f, _renderComplete));
}
// Possibly orce a repaint if the render level changed (highly
// probable but not guaranteed)
if (_renderComplete != _paintComplete)
{
// Always force a repaint when changing to or from a boundary
if (_renderComplete <= 0f || _paintComplete <= 0f ||
_renderComplete >= 1f || _paintComplete >= 1f)
{
invalidate();
}
// Also force a repaint if the completion state sufficiently
// changed
else if (Math.abs(_renderComplete - _paintComplete) >
_changeThreshold)
{
invalidate();
}
}
}
// documentation inherited
public boolean needsPaint ()
{
// Always paint if the timer was invalidated
return _invalidated;
}
// documentation inherited
public Component getComponent ()
{
return null;
}
// documentation inherited
public void hierarchyChanged (HierarchyEvent e)
{
checkFrameParticipation();
}
/** Check that the frame knows about the timer. */
public void checkFrameParticipation ()
{
// Determine whether or not the timer should participate in
// media ticks
boolean participate = _host.isShowing();
// Start participating if necessary
if (participate && !_participating)
{
_fmgr.registerFrameParticipant(this);
_participating = true;
}
// Stop participating if necessary
else if (!participate && _participating)
{
_fmgr.removeFrameParticipant(this);
_participating = false;
}
}
/** The frame manager that manages our animated view. */
protected FrameManager _fmgr;
/** The media panel containing the view. */
protected JComponent _host;
/** The screen coordinates of the timer's bounding box. */
protected Rectangle _bounds;
/** Whether to render the timer. */
protected boolean _enabled = true;
/** Whether the timer is running. */
protected boolean _running = false;
/** Whether the timer is participating in media tick updates. */
protected boolean _participating = false;
/** The last time the timer updated while running. */
protected long _lastUpdate = Long.MIN_VALUE;
/** The total amount of time in the timer when it was 0% done. */
protected long _duration;
/** The timestamp when the timer effectively started. */
protected long _start;
/** The percent of the duration time completed. */
protected float _complete = 0f;
/** The difference between the old rendered completion percent and the
* new completion percent the last time the completion percent had
* an unexpected change. In other words, this is the greatest amount
* that _renderComplete will differ from _complete during an
* state interpolation.
*/
protected float _renderOffset = 0f;
/** The timestamp of _renderOffset. */
protected long _renderOffsetTime = Long.MIN_VALUE;
/** The amount of time it takes to fully interpolation a render
* transition from completion 0.0 to 1.0. */
protected long _transitionTime = 0;
/** The completion percent at which to render the timer. */
protected float _renderComplete = 0f;
/** The completion percent last time the timer was repainted. */
protected float _paintComplete = -1;
/**
* The timer will not invalidate itself until the completion level
* to render changes by at least this much from the previous time it
* was invalidated.
*/
protected float _changeThreshold = 0.0f;
/** True if the timer has been invalidated since its last repaint. */
protected boolean _invalidated;
/** Trigger the warning when at least this much time has elapsed,
* or -1 for no warning. */
protected float _warnPercent = -1;
/** True if the warning has already been processed. */
protected boolean _warned;
/** True if the completion has already been processed. */
protected boolean _completed;
/** True if the code should process everything required for an unpause
* on the next tick(). */
protected boolean _processUnpause = false;
/** A listener to be notified when the timer finishes. */
protected ResultListener _finisher;
/** A listener to be notified when the warning time occurs. */
protected ResultListener _warner;
/** The default update date. */
protected static final long DEFAULT_RATE = 100L;
}
@@ -0,0 +1,18 @@
//
// $Id: ViewTracker.java 4191 2006-06-13 22:42:20Z ray $
package com.threerings.media;
/**
* An interface used by entities that wish to respond to the scrolling of a
* {@link VirtualMediaPanel}.
*
* @see VirtualMediaPanel#addViewTracker
*/
public interface ViewTracker
{
/**
* Called by a {@link VirtualMediaPanel} when it scrolls.
*/
public void viewLocationDidChange (int dx, int dy);
}
@@ -0,0 +1,463 @@
//
// $Id: VirtualMediaPanel.java 3310 2005-01-24 23:08:21Z 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;
import java.awt.Graphics2D;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.util.ArrayList;
import javax.swing.SwingUtilities;
import com.samskivert.util.RunAnywhere;
import com.threerings.media.image.ImageUtil;
import com.threerings.media.image.Mirage;
import com.threerings.media.util.MathUtil;
import com.threerings.media.util.Pathable;
/**
* Extends the base media panel with the notion of a virtual coordinate
* system. All entities in the virtual media panel have virtual
* coordinates and the virtual media panel displays a window into that
* virtual view. The panel can be made to scroll by adjusting the view
* offset slightly at the start of each tick and it will efficiently copy
* the unmodified view data and generate repaint requests for the exposed
* regions.
*/
public class VirtualMediaPanel extends MediaPanel
{
/** The code for the pathable following mode wherein we keep the view
* centered on the pathable's location. */
public static final byte CENTER_ON_PATHABLE = 0;
/** The code for the pathable following mode wherein we ensure that
* the marked pathable is always kept within the visible bounds of the
* view. */
public static final byte ENCLOSE_PATHABLE = 1;
/** The code for the pathable following mode wherein we set the upper-left
* corner of the view to the coordinates of the pathable. */
public static final byte TRACK_PATHABLE = 2;
/**
* Constructs a virtual media panel.
*/
public VirtualMediaPanel (FrameManager framemgr)
{
super(framemgr);
}
/**
* Set a background image to tile the background of the media panel.
*/
public void setBackground (Mirage background)
{
_background = background;
}
/**
* Sets the upper-left coordinate of the view port in virtual
* coordinates. The view will be as efficient as possible about
* repainting itself to achieve this new virtual location (meaning
* that if we need only to move one pixel to the left, it will use
* {@link Graphics#copyArea} to move our rendered view over one pixel
* and generate a dirty region for the exposed area). The new location
* will not take effect until the view is {@link #tick}ed, so only the
* last call to this method during a tick will have any effect.
*/
public void setViewLocation (int x, int y)
{
// make a note of our new x and y offsets
_nx = x;
_ny = y;
}
/**
* Returns the bounds of the viewport in virtual coordinates. The
* returned rectangle must <em>not</em> be modified.
*/
public Rectangle getViewBounds ()
{
return _vbounds;
}
/**
* Adds an entity that will be informed when the view scrolls.
*/
public void addViewTracker (ViewTracker tracker)
{
_trackers.add(tracker);
}
/**
* Removes an entity from the view trackers list.
*/
public void removeViewTracker (ViewTracker tracker)
{
_trackers.remove(tracker);
}
/**
* Instructs the view to follow the supplied pathable; ensuring that
* the view's coordinates are adjusted according to the follow mode.
*
* @param pable the pathable to follow.
* @param followMode the strategy for keeping the pathable in view.
*/
public void setFollowsPathable (Pathable pable, byte followMode)
{
_fmode = followMode;
_fpath = pable;
trackPathable(); // immediately update our location
}
/**
* Clears out the pathable that was being enclosed or followed due to
* a previous call to {@link #setFollowsPathable}.
*/
public void clearPathable ()
{
_fpath = null;
_fmode = (byte) -1;
}
/**
* We overload this to translate mouse events into the proper
* coordinates before they are dispatched to any of the mouse
* listeners.
*/
protected void processMouseEvent (MouseEvent event)
{
event.translatePoint(_vbounds.x, _vbounds.y);
super.processMouseEvent(event);
}
/**
* We overload this to translate mouse events into the proper
* coordinates before they are dispatched to any of the mouse
* listeners.
*/
protected void processMouseMotionEvent (MouseEvent event)
{
event.translatePoint(_vbounds.x, _vbounds.y);
super.processMouseMotionEvent(event);
}
/**
* We overload this to translate mouse events into the proper
* coordinates before they are dispatched to any of the mouse
* listeners.
*/
protected void processMouseWheelEvent (MouseWheelEvent event)
{
event.translatePoint(_vbounds.x, _vbounds.y);
super.processMouseWheelEvent(event);
}
// documentation inherited
protected void dirtyScreenRect (Rectangle rect)
{
// translate the screen rect into happy coordinates
rect.translate(_vbounds.x, _vbounds.y);
_remgr.addDirtyRegion(rect);
}
// documentation inherited
public void doLayout ()
{
super.doLayout();
// we need to obtain our absolute screen coordinates to work
// around the Windows copyArea() bug
findRootBounds();
}
// documentation inherited
public void setBounds (int x, int y, int width, int height)
{
super.setBounds(x, y, width, height);
// keep track of the size of the viewport
_vbounds.width = getWidth();
_vbounds.height = getHeight();
// we need to obtain our absolute screen coordinates to work
// around the Windows copyArea() bug
findRootBounds();
}
/**
* Determines the absolute screen coordinates at which this panel is
* located and stores them for reference later when rendering. This
* is necessary in order to work around the Windows
* <code>copyArea()</code> bug.
*/
protected void findRootBounds ()
{
_abounds.setLocation(0, 0);
FrameManager.getRoot(this, _abounds);
}
// documentation inherited
protected void didTick (long tickStamp)
{
super.didTick(tickStamp);
int width = getWidth(), height = getHeight();
// adjusts our view location to track any pathable we might be
// tracking
trackPathable();
// if we have a new target location, we'll need to generate dirty
// regions for the area exposed by the scrolling
if (_nx != _vbounds.x || _ny != _vbounds.y) {
// determine how far we'll be moving on this tick
int dx = _nx - _vbounds.x, dy = _ny - _vbounds.y;
// Log.info("Scrolling into place [n=(" + _nx + ", " + _ny +
// "), t=(" + _vbounds.x + ", " + _vbounds.y +
// "), d=(" + dx + ", " + dy +
// "), width=" + width + ", height=" + height + "].");
// Mac OS X's redraw breaks on scrolling, so we repaint the
// entire panel
if (RunAnywhere.isMacOS()) {
_remgr.invalidateRegion(_nx, _ny, width, height);
} else {
_dx = dx;
_dy = dy;
// these are used to prevent the vertical strip from
// overlapping the horizontal strip
int sy = _ny, shei = height;
// and add invalid rectangles for the exposed areas
if (dy > 0) {
shei = Math.max(shei - dy, 0);
_remgr.invalidateRegion(_nx, _ny + height - dy, width, dy);
} else if (dy < 0) {
sy -= dy;
_remgr.invalidateRegion(_nx, _ny, width, -dy);
}
if (dx > 0) {
_remgr.invalidateRegion(_nx + width - dx, sy, dx, shei);
} else if (dx < 0) {
_remgr.invalidateRegion(_nx, sy, -dx, shei);
}
}
// now go ahead and update our location so that changes in
// between here and the call to paint() for this tick don't
// booch everything
_vbounds.x = _nx; _vbounds.y = _ny;
// let derived classes react if they so desire
viewLocationDidChange(dx, dy);
}
}
/**
* Called during our tick when we have adjusted the view location. The
* {@link #_vbounds} will already have been updated to reflect our new
* view coordinates.
*
* @param dx the delta scrolled in the x direction (in pixels).
* @param dy the delta scrolled in the y direction (in pixels).
*/
protected void viewLocationDidChange (int dx, int dy)
{
if (_perfRect != null) {
Rectangle sdirty = new Rectangle(_perfRect);
sdirty.translate(-dx, -dy);
dirtyScreenRect(sdirty);
}
// inform our view trackers
for (int ii = 0, ll = _trackers.size(); ii < ll; ii++) {
((ViewTracker)_trackers.get(ii)).viewLocationDidChange(dx, dy);
}
// let our sprites and animations know what's up
_animmgr.viewLocationDidChange(dx, dy);
_spritemgr.viewLocationDidChange(dx, dy);
}
/**
* Implements the standard pathable tracking support. Derived classes
* may wish to override this if they desire custom tracking
* functionality.
*/
protected void trackPathable ()
{
// if we're tracking a pathable, adjust our view coordinates
if (_fpath == null) {
return;
}
int width = getWidth(), height = getHeight();
int nx = _vbounds.x, ny = _vbounds.y;
// figure out where to move
switch (_fmode) {
case TRACK_PATHABLE:
nx = _fpath.getX();
ny = _fpath.getY();
break;
case CENTER_ON_PATHABLE:
nx = _fpath.getX() - width/2;
ny = _fpath.getY() - height/2;
break;
case ENCLOSE_PATHABLE:
Rectangle bounds = _fpath.getBounds();
if (nx > bounds.x) {
nx = bounds.x;
} else if (nx + width < bounds.x + bounds.width) {
nx = bounds.x + bounds.width - width;
}
if (ny > bounds.y) {
ny = bounds.y;
} else if (ny + height < bounds.y + bounds.height) {
ny = bounds.y + bounds.height - height;
}
break;
default:
Log.warning("Eh? Set to invalid pathable mode " +
"[mode=" + _fmode + "].");
break;
}
// Log.info("Tracking pathable [mode=" + _fmode +
// ", pable=" + _fpath + ", nx=" + nx + ", ny=" + ny + "].");
setViewLocation(nx, ny);
}
// documentation inherited
protected void paint (Graphics2D gfx, Rectangle[] dirty)
{
// if we're scrolling, go ahead and do the business
if (_dx != 0 || _dy != 0) {
int width = getWidth(), height = getHeight();
int cx = (_dx > 0) ? _dx : 0;
int cy = (_dy > 0) ? _dy : 0;
// set the clip to the bounds of the component (we can't
// assume the clip is anything sensible upon entry to paint()
// because the frame manager expects us to set our own clip)
gfx.setClip(0, 0, width, height);
// on windows, attempting to call copyArea() on a translated
// graphics context results in boochness; so we have to
// untranslate the graphics context, do our copyArea() and
// then translate it back
if (RunAnywhere.isWindows()) {
gfx.translate(-_abounds.x, -_abounds.y);
gfx.copyArea(_abounds.x + cx, _abounds.y + cy,
width - Math.abs(_dx),
height - Math.abs(_dy), -_dx, -_dy);
gfx.translate(_abounds.x, _abounds.y);
} else if (RunAnywhere.isMacOS()) {
try {
gfx.copyArea(cx, cy,
width - Math.abs(_dx),
height - Math.abs(_dy), -_dx, -_dy);
} catch (Exception e) {
// HACK when it throws an exception trying to do the
// copy area, just repaint everything
dirty = new Rectangle[] { new Rectangle(_vbounds) };
}
} else {
gfx.copyArea(cx, cy,
width - Math.abs(_dx),
height - Math.abs(_dy), -_dx, -_dy);
}
// and clear out our scroll deltas
_dx = 0; _dy = 0;
}
// translate into happy space
gfx.translate(-_vbounds.x, -_vbounds.y);
// now do the actual painting
super.paint(gfx, dirty);
// translate back out of happy space
gfx.translate(_vbounds.x, _vbounds.y);
}
// documentation inherited
protected void constrainToBounds (Rectangle dirty)
{
SwingUtilities.computeIntersection(
_vbounds.x, _vbounds.y, getWidth(), getHeight(), dirty);
}
// documentation inherited
protected void paintBehind (Graphics2D gfx, Rectangle dirtyRect)
{
// if we have a background image specified, tile it!
if (_background != null) {
// make sure it's aligned
int iw = _background.getWidth();
int ih = _background.getHeight();
int lowx = iw * MathUtil.floorDiv(dirtyRect.x, iw);
int lowy = ih * MathUtil.floorDiv(dirtyRect.y, ih);
ImageUtil.tileImage(gfx, _background, lowx, lowy,
dirtyRect.width + (dirtyRect.x - lowx),
dirtyRect.height + (dirtyRect.y - lowy));
}
}
/** Our viewport bounds in virtual coordinates. */
protected Rectangle _vbounds = new Rectangle();
/** Our target offsets to be effected on the next tick. */
protected int _nx, _ny;
/** Our scroll offsets. */
protected int _dx, _dy;
/** Our tiling background image. */
protected Mirage _background;
/** The mode we're using when following a pathable. */
protected byte _fmode = -1;
/** The pathable being followed. */
protected Pathable _fpath;
/** We need to know our absolute coordinates in order to work around
* the Windows copyArea() bug. */
protected Rectangle _abounds = new Rectangle();
/** A list of entities to be informed when the view scrolls. */
protected ArrayList _trackers = new ArrayList();
}
@@ -0,0 +1,109 @@
//
// $Id$
//
// 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;
import java.awt.Rectangle;
import javax.swing.BoundedRangeModel;
import javax.swing.DefaultBoundedRangeModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import com.threerings.media.util.MathUtil;
/**
* Provides a {@link BoundedRangeModel} interface to a virtual media panel
* so that it can easily be wired up to scroll bars or other scrolling
* controls.
*/
public class VirtualRangeModel
implements ChangeListener
{
/**
* Creates a virtual media panel range model that will interact with
* the supplied virtual media panel.
*/
public VirtualRangeModel (VirtualMediaPanel panel)
{
_panel = panel;
// listen to our range models and scroll our badself
_hrange.addChangeListener(this);
_vrange.addChangeListener(this);
}
/**
* Informs the virtual range model the extent of the area over which
* we can scroll.
*/
public void setScrollableArea (int x, int y, int width, int height)
{
Rectangle vb = _panel.getViewBounds();
int hmax = x + width, vmax = y + height, value;
if (width > vb.width) {
value = MathUtil.bound(x, _hrange.getValue(), hmax - vb.width);
_hrange.setRangeProperties(value, vb.width, x, hmax, false);
} else {
// Let's center it and lock it down.
int newx = x - (vb.width - width)/2;
_hrange.setRangeProperties(newx, 0, newx, newx, false);
}
if (height > vb.height) {
value = MathUtil.bound(y, _vrange.getValue(), vmax - vb.height);
_vrange.setRangeProperties(value, vb.height, y, vmax, false);
} else {
// Let's center it and lock it down.
int newy = y - (vb.height - height)/2;
_vrange.setRangeProperties(newy, 0, newy, newy, false);
}
}
/**
* Returns a range model that controls the scrollability of the scene
* in the horizontal direction.
*/
public BoundedRangeModel getHorizModel ()
{
return _hrange;
}
/**
* Returns a range model that controls the scrollability of the scene
* in the vertical direction.
*/
public BoundedRangeModel getVertModel ()
{
return _vrange;
}
// documentation inherited from interface ChangeListener
public void stateChanged (ChangeEvent e)
{
_panel.setViewLocation(_hrange.getValue(), _vrange.getValue());
}
protected VirtualMediaPanel _panel;
protected BoundedRangeModel _hrange = new DefaultBoundedRangeModel();
protected BoundedRangeModel _vrange = new DefaultBoundedRangeModel();
}
@@ -0,0 +1,169 @@
//
// $Id: Animation.java 3822 2006-01-27 03:28:04Z 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.animation;
import java.awt.Rectangle;
import com.samskivert.util.ObserverList;
import com.threerings.media.AbstractMedia;
/**
* The animation class is an abstract class that should be extended to
* provide animation functionality. It is generally used in conjunction
* with an {@link AnimationManager}.
*/
public abstract class Animation extends AbstractMedia
{
/**
* Constructs an animation.
*
* @param bounds the animation rendering bounds.
*/
public Animation (Rectangle bounds)
{
super(bounds);
}
/**
* Returns true if the animation has finished all of its business,
* false if not.
*/
public boolean isFinished ()
{
return _finished;
}
/**
* If this animation has run to completion, it can be reset to prepare
* it for another go.
*/
public void reset ()
{
_finished = false;
}
// documentation inherited
public void setLocation (int x, int y)
{
if (_bounds.x == x && _bounds.y == y) {
return; // no-op
}
// start with our current bounds
Rectangle dirty = new Rectangle(_bounds);
// move ourselves
super.setLocation(x, y);
// grow the dirty rectangle to incorporate our new bounds and pass
// the dirty region to our region manager
if (_mgr != null) {
// if our new bounds intersect our old bounds, grow a single
// dirty rectangle to incorporate them both
if (_bounds.intersects(dirty)) {
dirty.add(_bounds);
} else {
// otherwise invalidate our new bounds separately
_mgr.getRegionManager().invalidateRegion(_bounds);
}
_mgr.getRegionManager().addDirtyRegion(dirty);
}
}
// documentation inherited
protected void willStart (long tickStamp)
{
super.willStart(tickStamp);
queueNotification(new AnimStartedOp(this, tickStamp));
}
/**
* Called when the animation is finished and the animation manager is
* about to remove it from service.
*/
protected void willFinish (long tickStamp)
{
queueNotification(new AnimCompletedOp(this, tickStamp));
}
/**
* Called when the animation is finished and the animation manager has
* removed it from service.
*/
protected void didFinish (long tickStamp)
{
}
/**
* Adds an animation observer to this animation's list of observers.
*/
public void addAnimationObserver (AnimationObserver obs)
{
addObserver(obs);
}
/**
* Removes an animation observer from this animation's list of observers.
*/
public void removeAnimationObserver (AnimationObserver obs)
{
removeObserver(obs);
}
/** Whether the animation is finished. */
protected boolean _finished = false;
/** Used to dispatch {@link AnimationObserver#animationStarted}. */
protected static class AnimStartedOp implements ObserverList.ObserverOp
{
public AnimStartedOp (Animation anim, long when) {
_anim = anim;
_when = when;
}
public boolean apply (Object observer) {
((AnimationObserver)observer).animationStarted(_anim, _when);
return true;
}
protected Animation _anim;
protected long _when;
}
/** Used to dispatch {@link AnimationObserver#animationCompleted}. */
protected static class AnimCompletedOp implements ObserverList.ObserverOp
{
public AnimCompletedOp (Animation anim, long when) {
_anim = anim;
_when = when;
}
public boolean apply (Object observer) {
((AnimationObserver)observer).animationCompleted(_anim, _when);
return true;
}
protected Animation _anim;
protected long _when;
}
}
@@ -0,0 +1,38 @@
//
// $Id: AnimationAdapter.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.animation;
/**
* An adapter class for {@link AnimationObserver}.
*/
public class AnimationAdapter implements AnimationObserver
{
// documentation inherited from interface
public void animationStarted (Animation anim, long when)
{
}
// documentation inherited from interface
public void animationCompleted (Animation anim, long when)
{
}
}
@@ -0,0 +1,68 @@
//
// $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.animation;
import java.awt.Rectangle;
import java.util.ArrayList;
import com.samskivert.swing.util.SwingUtil;
import com.threerings.media.Log;
/**
* A utility class for positioning animations such that they don't overlap,
* as best as possible.
*/
public class AnimationArranger
{
/**
* Position the specified animation so that it is not overlapping
* any still-running animations previously passed to this method.
*/
public void positionAvoidAnimation (Animation anim, Rectangle viewBounds)
{
Rectangle abounds = new Rectangle(anim.getBounds());
ArrayList avoidables = (ArrayList) _avoidAnims.clone();
// if we are able to place it somewhere, do so
if (SwingUtil.positionRect(abounds, viewBounds, avoidables)) {
anim.setLocation(abounds.x, abounds.y);
}
// add the animation to the list of avoidables
_avoidAnims.add(anim);
// keep an eye on it so that we can remove it when it's finished
anim.addAnimationObserver(_avoidAnimObs);
}
/** The animations that other animations may wish to avoid. */
protected ArrayList _avoidAnims = new ArrayList();
/** Automatically removes avoid animations when they're done. */
protected AnimationAdapter _avoidAnimObs = new AnimationAdapter() {
public void animationCompleted (Animation anim, long when) {
if (!_avoidAnims.remove(anim)) {
Log.warning("Couldn't remove avoid animation?! " + anim + ".");
}
}
};
}
@@ -0,0 +1,260 @@
//
// $Id: AnimationFrameSequencer.java 3310 2005-01-24 23:08:21Z 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.animation;
import com.samskivert.util.ObserverList;
import com.threerings.media.util.FrameSequencer;
import com.threerings.media.util.MultiFrameImage;
/**
* Used to control animation timing when displaying an animation.
*/
public interface AnimationFrameSequencer extends FrameSequencer
{
/**
* Called after init to set the animation.
*/
public void setAnimation (Animation anim);
/**
* A sequencer that can step through a series of frames in any order
* and speed and notify (via {@link SequencedAnimationObserver}) when
* specific frames are reached.
*/
public static class MultiFunction implements AnimationFrameSequencer
{
/**
* Creates the simplest multifunction frame sequencer.
*
* @param sequence the ordering to display the frames.
* @param msPerFrame the number of ms to display each frame.
* @param loop if false, the sequencer will report the end of
* the animation after progressing through all of the frames once,
* otherwise it will loop indefinitely.
*/
public MultiFunction (int[] sequence, long msPerFrame, boolean loop)
{
_length = sequence.length;
_sequence = sequence;
setDelay(msPerFrame);
_marks = new boolean[_length];
_loop = loop;
}
/**
* Creates a fully-specified multifunction frame sequencer.
*
* @param sequence the ordering to display the frames.
* @param msPerFrame the number of ms to display each frame.
* @param marks if true for a frame, a FrameReachedEvent will
* be generated when that frame is reached.
* @param loop if false, the sequencer will report the end of
* the animation after progressing through all of the frames once,
* otherwise it will loop indefinitely.
*/
public MultiFunction (
int[] sequence, long[] msPerFrame, boolean[] marks, boolean loop)
{
_length = sequence.length;
if ((_length != msPerFrame.length) || (_length != marks.length)) {
throw new IllegalArgumentException(
"All MultiFunction FrameSequencer arrays must be " +
"the same length.");
}
_sequence = sequence;
_delays = msPerFrame;
_marks = marks;
_loop = loop;
}
/**
* Set the delay to use.
*/
public void setDelay (long msPerFrame)
{
_delays = null;
_delay = msPerFrame;
}
/**
* Set the length of the sequence we are to use, or 0 means set
* it to the length of the sequence array that we were constructed with.
*/
public void setLength (int len)
{
_length = (len == 0) ? _sequence.length : len;
if (_curIdx >= _length) {
_curIdx = 0;
}
}
/**
* Do we reset the animation on init? Default is true.
*/
public void setResetOnInit (boolean resets)
{
_resets = resets;
}
// documentation inherited from interface
public void init (MultiFrameImage source)
{
int framecount = source.getFrameCount();
// let's make sure our frames are valid
for (int ii=0; ii < _length; ii++) {
if (_sequence[ii] >= framecount) {
throw new IllegalArgumentException(
"MultiFunction FrameSequencer was initialized " +
"with a MultiFrameImage with not enough frames " +
"to match the sequence it was constructed with.");
}
}
if (_resets) {
_lastStamp = 0L;
_curIdx = 0;
}
}
// documentation inherited from interface
public void setAnimation (Animation anim)
{
_animation = anim;
}
// documentation inherited from interface
public int tick (long tickStamp)
{
// obtain our starting timestamp if we don't already have one
if (_lastStamp == 0L) {
_lastStamp = tickStamp;
// we might need to notify on the first frame
checkNotify(tickStamp);
}
// we may have rushed through more than one frame since the last
// tick, but we want to always notify even if we never displayed
long curdelay = getDelay(_curIdx);
while (tickStamp >= (_lastStamp + curdelay)) {
_lastStamp += curdelay;
_curIdx++;
if (_curIdx == _length) {
if (_loop) {
_curIdx = 0;
} else {
return -1;
}
}
checkNotify(tickStamp);
// get the delay for checking the next frame
curdelay = getDelay(_curIdx);
}
// return the right frame
return _sequence[_curIdx];
}
// documentation inherited from interface
public void fastForward (long timeDelta)
{
// this method should be called "unpause"
_lastStamp += timeDelta;
}
/**
* Get the delay to use for the specified frame.
*/
protected final long getDelay (int index)
{
return (_delays == null) ? _delay : _delays[index];
}
/**
* Check to see if we need to notify that we've reached a marked frame.
*/
protected void checkNotify (long tickStamp)
{
if (_marks[_curIdx]) {
_animation.queueNotification(
new FrameReachedOp(_animation, tickStamp,
_sequence[_curIdx], _curIdx));
}
}
/** The current sequence index. */
protected int _curIdx = 0;
/** The length of our animation sequence. */
protected int _length;
/** Do we reset on init? */
protected boolean _resets = true;
/** The sequence of frames to display. */
protected int[] _sequence;
/** The corresponding delay for each frame, in ms. */
protected long[] _delays;
/** Or a single delay for all frames. */
protected long _delay;
/** Whether to send a FrameReachedEvent on a sequence index. */
protected boolean[] _marks;
/** Does the animation loop? */
protected boolean _loop;
/** The time at which we were last ticked. */
protected long _lastStamp;
/** The animation that we're sequencing for. */
protected Animation _animation;
/** Used to dispatch {@link SequencedAnimationObserver#frameReached}. */
protected static class FrameReachedOp implements ObserverList.ObserverOp
{
public FrameReachedOp (Animation anim, long when,
int frameIdx, int frameSeq) {
_anim = anim;
_when = when;
_frameIdx = frameIdx;
_frameSeq = frameSeq;
}
public boolean apply (Object observer) {
if (observer instanceof SequencedAnimationObserver) {
((SequencedAnimationObserver)observer).frameReached(
_anim, _when, _frameIdx, _frameSeq);
}
return true;
}
protected Animation _anim;
protected long _when;
protected int _frameIdx, _frameSeq;
}
}
}
@@ -0,0 +1,81 @@
//
// $Id: AnimationManager.java 3347 2005-02-14 03:00:58Z 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.animation;
import com.threerings.media.AbstractMediaManager;
import com.threerings.media.MediaPanel;
/**
* Manages a collection of animations, ticking them when the animation
* manager itself is ticked and generating events when animations finish
* and suchlike.
*/
public class AnimationManager extends AbstractMediaManager
{
/**
* Construct and initialize the animation manager which readies itself
* to manage animations.
*/
public AnimationManager (MediaPanel panel)
{
super(panel);
}
/**
* Registers the given {@link Animation} with the animation manager
* for ticking and painting.
*/
public void registerAnimation (Animation anim)
{
insertMedia(anim);
}
/**
* Un-registers the given {@link Animation} from the animation
* manager. The bounds of the animation will automatically be
* invalidated so that they are properly rerendered in the absence of
* the animation.
*/
public void unregisterAnimation (Animation anim)
{
removeMedia(anim);
}
// documentation inherited
protected void tickAllMedia (long tickStamp)
{
super.tickAllMedia(tickStamp);
for (int ii = _media.size() - 1; ii >= 0; ii--) {
Animation anim = (Animation)_media.get(ii);
if (!anim.isFinished()) {
continue;
}
// as the anim is finished, remove it and notify observers
anim.willFinish(tickStamp);
unregisterAnimation(anim);
anim.didFinish(tickStamp);
// Log.info("Removed finished animation " + anim + ".");
}
}
}
@@ -0,0 +1,39 @@
//
// $Id: AnimationObserver.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.animation;
/**
* An interface to be implemented by classes that would like to observe an
* {@link Animation} and be notified of interesting events relating to it.
*/
public interface AnimationObserver
{
/**
* Called the first time this animation is ticked.
*/
public void animationStarted (Animation anim, long when);
/**
* Called when the observed animation has completed.
*/
public void animationCompleted (Animation anim, long when);
}
@@ -0,0 +1,295 @@
//
// $Id: AnimationSequencer.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.animation;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.util.ArrayList;
import com.samskivert.util.StringUtil;
import com.threerings.media.Log;
/**
* An animation that provides facilities for adding a sequence of
* animations that are fired after a fixed time interval has elapsed or
* after previous animations in the sequence have completed. Facilities
* are also provided for running code upon the completion of animations in
* the sequence.
*/
public class AnimationSequencer extends Animation
{
/**
* Constructs an animation sequencer with the expectation that
* animations will be added via subsequent calls to {@link
* #addAnimation}.
*
* @param animmgr the animation manager to which to add our animations
* when they are ready to start.
*/
public AnimationSequencer (AnimationManager animmgr)
{
super(new Rectangle());
_animmgr = animmgr;
}
/**
* Adds the supplied animation to the sequence with the given
* parameters. Note that care should be taken if this is called after
* the animation sequence has begun firing animations. Do not add new
* animations after the final animation in the sequence has been
* started or you run the risk of attempting to add an animation to
* the sequence after it thinks that it has finished (in which case
* this method will fail).
*
* @param anim the animation to be sequenced, or null if the
* completion action should be run immediately when this "animation"
* is ready to fired.
* @param delta the number of milliseconds following the
* <em>start</em> of the previous animation in the queue that this
* animation should be started; 0 if it should be started
* simultaneously with its predecessor int the queue; -1 if it should
* be started when its predecessor has completed.
* @param completionAction a runnable to be executed when this
* animation completes.
*/
public void addAnimation (
Animation anim, long delta, Runnable completionAction)
{
// sanity check
if (_finished) {
throw new IllegalStateException(
"Animation added to finished sequencer");
}
// if this guy is triggering on a previous animation, grab that
// good fellow here
AnimRecord trigger = null;
if (delta == -1) {
if (_queued.size() > 0) {
// if there are queued animations we want the most
// recently queued animation
trigger = (AnimRecord)_queued.get(_queued.size()-1);
} else if (_running.size() > 0) {
// otherwise, if there are running animations, we want the
// last one in that list
trigger = (AnimRecord)_running.get(_running.size()-1);
}
// otherwise we have no trigger, we'll just start ASAP
}
AnimRecord arec = new AnimRecord(
anim, delta, trigger, completionAction);
// Log.info("Queued " + arec + ".");
_queued.add(arec);
}
/**
* Clears out the animations being managed by this sequencer.
*/
public void clear ()
{
_queued.clear();
_lastStamp = 0;
}
// documentation inherited
public void tick (long tickStamp)
{
if (_lastStamp == 0) {
_lastStamp = tickStamp;
}
// add all animations whose time has come
while (_queued.size() > 0) {
AnimRecord arec = (AnimRecord)_queued.get(0);
if (!arec.readyToFire(tickStamp, _lastStamp)) {
// if it's not time to add this animation, all subsequent
// animations must surely wait as well
break;
}
// remove it from queued and put it on the running list
_queued.remove(0);
_running.add(arec);
// note that we've advanced to the next animation
_lastStamp = tickStamp;
// fire in the hole!
arec.fire(tickStamp);
}
// we're done when both lists are empty
// boolean finished = _finished;
_finished = ((_queued.size() + _running.size()) == 0);
// if (!finished && _finished) {
// Log.info("Finishing sequence at " + (tickStamp%10000) + ".");
// }
}
// documentation inherited
public void paint (Graphics2D gfx)
{
// don't care
}
// documentation inherited
public void fastForward (long timeDelta)
{
_lastStamp += timeDelta;
}
// documentation inherited
public void viewLocationDidChange (int dx, int dy)
{
super.viewLocationDidChange(dx, dy);
// track cumulative view location changes so that we can adjust our
// animations before we start them
_vdx += dx;
_vdy += dy;
}
/**
* Called when the time comes to start an animation. Derived classes
* may override this method and pass the animation on to their
* animation manager and do whatever else they need to do with
* operating animations. The default implementation simply adds them
* to the media panel supplied when we were constructed.
*
* @param anim the animation to be displayed.
* @param tickStamp the timestamp at which this animation was fired.
*/
protected void startAnimation (Animation anim, long tickStamp)
{
// account for any view scrolling that happened before this animation
// was actually added to the view
if (_vdx != 0 || _vdy != 0) {
anim.viewLocationDidChange(_vdx, _vdy);
}
_animmgr.registerAnimation(anim);
}
protected class AnimRecord extends AnimationAdapter
{
public AnimRecord (Animation anim, long delta, AnimRecord trigger,
Runnable completionAction) {
_anim = anim;
_delta = delta;
_trigger = trigger;
_completionAction = completionAction;
}
public boolean readyToFire (long now, long lastStamp)
{
if (_delta == -1) {
// if we have no trigger, that means we should start
// immediately, otherwise we wait until our trigger is no
// longer running (they are guaranteed not to be still
// queued at this point because readyToFire is only called
// on an animation after all animations previous in the
// queue have been started)
return (_trigger == null) ?
true : !_running.contains(_trigger);
} else {
return (lastStamp + _delta <= now);
}
}
public void fire (long when)
{
// Log.info("Firing " + this + " at " + (when%10000) + ".");
// if we have an animation, start it up and await its
// completion
if (_anim != null) {
startAnimation(_anim, when);
_anim.addAnimationObserver(this);
} else {
// since there's no animation, we need to fire our
// completion routine immediately
fireCompletion(when);
}
}
public void fireCompletion (long when)
{
// Log.info("Completing " + this + " at " + (when%10000) + ".");
// call the completion action, if there is one
if (_completionAction != null) {
try {
_completionAction.run();
} catch (Throwable t) {
Log.logStackTrace(t);
}
}
// make a note that this animation is complete
_running.remove(this);
// kids, don't try this at home; we call tick() immediately so
// that any animations triggered on the completion of previous
// animations can trigger on the completion of this animation
// rather than having to wait until the next tick to do so
tick(when);
}
public void animationCompleted (Animation anim, long when)
{
fireCompletion(when);
}
public String toString ()
{
return "[anim=" + StringUtil.shortClassName(_anim) +
((_anim == null) ? "" : ("/" + _anim.hashCode())) +
", action=" + _completionAction +
", delta=" + _delta + ", trig=" + (_trigger != null) + "]";
}
protected Animation _anim;
protected Runnable _completionAction;
protected long _delta;
protected AnimRecord _trigger;
}
/** The animation manager in which we run animations. */
protected AnimationManager _animmgr;
/** Animations that have not been fired. */
protected ArrayList _queued = new ArrayList();
/** Animations that are currently running. */
protected ArrayList _running = new ArrayList();
/** The timestamp at which we fired the last animation. */
protected long _lastStamp;
/** Used to track view scrolling while animations are in limbo. */
protected int _vdx, _vdy;
}
@@ -0,0 +1,95 @@
//
// $Id: AnimationWaiter.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.animation;
/**
* An abstract class that simplifies a common animation usage case in
* which a number of animations are created and the creator would like to
* be able to perform specific actions when each animation has completed
* (see {@link #animationDidFinish} and/or when all animations are
* finished (see {@link #allAnimationsFinished}.)
*/
public abstract class AnimationWaiter
implements AnimationObserver
{
/**
* Adds an animation to the animation waiter for observation.
*/
public void addAnimation (Animation anim)
{
anim.addAnimationObserver(this);
_animCount++;
}
/**
* Adds the supplied animations to the animation waiter for
* observation.
*/
public void addAnimations (Animation[] anims)
{
int acount = anims.length;
for (int ii = 0; ii < acount; ii++) {
addAnimation(anims[ii]);
}
}
// documentation inherited from interface
public void animationStarted (Animation anim, long when)
{
}
// documentation inherited from interface
public void animationCompleted (Animation anim, long when)
{
// note that the animation is finished
animationDidFinish(anim);
_animCount--;
// let derived classes know when all is done
if (_animCount == 0) {
allAnimationsFinished();
}
}
/**
* Called when an animation being observed by the waiter has
* completed its business. Derived classes may wish to override
* this method to engage in their unique antics.
*/
protected void animationDidFinish (Animation anim)
{
// nothing for now
}
/**
* Called when all animations being observed by the waiter have
* completed their business. Derived classes may wish to override
* this method to engage in their unique antics.
*/
protected void allAnimationsFinished ()
{
// nothing for now
}
/** The number of animations. */
protected int _animCount;
}
@@ -0,0 +1,67 @@
//
// $Id: BlankAnimation.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.animation;
import java.awt.Graphics2D;
import java.awt.Rectangle;
/**
* Displays nothing, but does so for a specified amount of time. Useful
* when you want to get an animation completed event in some period of
* time but don't actually need to display anything.
*/
public class BlankAnimation extends Animation
{
public BlankAnimation (long duration)
{
super(new Rectangle(0, 0, 0, 0));
_duration = duration;
}
// documentation inherited
public void tick (long timestamp)
{
if (_start == 0) {
// initialize our starting time
_start = timestamp;
}
// check whether we're done
_finished = (timestamp - _start >= _duration);
}
// documentation inherited
public void fastForward (long timeDelta)
{
if (_start > 0) {
_start += timeDelta;
}
}
// documentation inherited
public void paint (Graphics2D gfx)
{
// nothing doing
}
protected long _duration, _start;
}
@@ -0,0 +1,87 @@
//
// $Id: BlendAnimation.java 4191 2006-06-13 22:42:20Z ray $
package com.threerings.media.animation;
import java.awt.AlphaComposite;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import com.threerings.media.image.Mirage;
import com.threerings.media.util.LinearTimeFunction;
import com.threerings.media.util.TimeFunction;
/**
* Blends between a series of images using alpha.
*/
public class BlendAnimation extends Animation
{
/**
* Blends from the starting image through each successive image in the
* specified amount of time (blending between each image takes place
* in <code>delay</code> milliseconds).
*/
public BlendAnimation (int x, int y, Mirage[] images, int delay)
{
super(new Rectangle(x, y, images[0].getWidth(), images[0].getHeight()));
_images = images;
int fades = images.length-1;
_tfunc = new LinearTimeFunction(0, 100 * fades, delay * fades);
}
// documentation inherited
public void tick (long timestamp)
{
// check to see if our blend level has changed
int level = _tfunc.getValue(timestamp);
if (level == _level) {
return;
}
// stop if we reach the end
if (level == 100*(_images.length-1)) {
_finished = true;
}
_level = level;
invalidate();
}
// documentation inherited
public void fastForward (long timeDelta)
{
_tfunc.fastForward(timeDelta);
}
// documentation inherited
public void paint (Graphics2D gfx)
{
int index = _level / 100;
float alpha = 1f - (_level % 100) / 100f;
Composite ocomp = gfx.getComposite();
gfx.setComposite(AlphaComposite.getInstance(
AlphaComposite.SRC_OVER, alpha));
_images[index].paint(gfx, _bounds.x, _bounds.y);
if (index < _images.length-1) {
gfx.setComposite(AlphaComposite.getInstance(
AlphaComposite.SRC_OVER, 1f-alpha));
_images[index+1].paint(gfx, _bounds.x, _bounds.y);
}
gfx.setComposite(ocomp);
}
/** The images between which we are blending. */
protected Mirage[] _images;
/** The time function we're using to time our blends. */
protected TimeFunction _tfunc;
/** Our current blend level and image index all wrapped into one. */
protected int _level;
/** The alpha composite used to render our current image. */
protected AlphaComposite _currentComp;
/** The alpha composite used to render our "next" image. */
protected AlphaComposite _nextComp;
}
@@ -0,0 +1,120 @@
//
// $Id: BobbleAnimation.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.animation;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import com.samskivert.util.RandomUtil;
import com.threerings.media.image.Mirage;
/**
* An animation that bobbles an image around within a specific horizontal
* and vertical pixel range for a given period of time.
*/
public class BobbleAnimation extends Animation
{
/**
* Constructs a bobble animation.
*
* @param image the image to animate.
* @param sx the starting x-position.
* @param sy the starting y-position.
* @param rx the horizontal bobble range.
* @param ry the vertical bobble range.
* @param duration the time to animate in milliseconds.
*/
public BobbleAnimation (
Mirage image, int sx, int sy, int rx, int ry, int duration)
{
super(new Rectangle(sx - rx, sy - ry, sx + image.getWidth() + rx,
sy + image.getHeight() + ry));
// save things off
_image = image;
_sx = sx;
_sy = sy;
_rx = rx;
_ry = ry;
// calculate animation ending time
_duration = duration;
}
// documentation inherited
public void tick (long tickStamp)
{
// grab our ending time on our first tick
if (_end == 0L) {
_end = tickStamp + _duration;
}
_finished = (tickStamp >= _end);
invalidate();
// calculate the latest position
int dx = RandomUtil.getInt(_rx);
int dy = RandomUtil.getInt(_ry);
_x = (_sx + dx) * ((RandomUtil.getInt(2) == 0) ? -1 : 1);
_y = (_sy + dy) * ((RandomUtil.getInt(2) == 0) ? -1 : 1);
}
// documentation inherited
public void fastForward (long timeDelta)
{
_end += timeDelta;
}
// documentation inherited
public void paint (Graphics2D gfx)
{
_image.paint(gfx, _x, _y);
}
// documentation inherited
protected void toString (StringBuilder buf)
{
super.toString(buf);
buf.append(", x=").append(_x);
buf.append(", y=").append(_y);
buf.append(", rx=").append(_rx);
buf.append(", ry=").append(_ry);
buf.append(", sx=").append(_sx);
buf.append(", sy=").append(_sy);
}
/** The current position of the image. */
protected int _x, _y;
/** The horizontal and vertical bobbling range. */
protected int _rx, _ry;
/** The starting position. */
protected int _sx, _sy;
/** The image to animate. */
protected Mirage _image;
/** Animation ending timing information. */
protected long _duration, _end;
}
@@ -0,0 +1,336 @@
//
// $Id: ExplodeAnimation.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.animation;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import com.samskivert.util.RandomUtil;
import com.samskivert.util.StringUtil;
import com.threerings.media.image.Mirage;
/**
* An animation that displays an object exploding into chunks, fading out
* as they fly apart. The animation ends when all chunks have exited the
* animation bounds, or when the given delay time (if any is specified)
* has elapsed.
*/
public class ExplodeAnimation extends Animation
{
/**
* A class that describes an explosion's attributes.
*/
public static class ExplodeInfo
{
/** The bounds within which to animate. */
public Rectangle bounds;
/** The number of image chunks on each axis. */
public int xchunk, ychunk;
/** The maximum chunk velocity on each axis in pixels per
* millisecond. */
public float xvel, yvel;
/** The y-axis chunk acceleration in pixels per millisecond. */
public float yacc;
/** The chunk rotational velocity in rotations per millisecond. */
public float rvel;
/** The animation length in milliseconds, or -1 if the animation
* should continue until all pieces are outside the bounds. */
public long delay;
/** Returns a string representation of this instance. */
public String toString ()
{
return StringUtil.fieldsToString(this);
}
}
/**
* Constructs an explode animation with the chunks represented as
* filled rectangles of the specified color.
*
* @param color the color to render the chunks in.
* @param info the explode info object.
* @param x the x-position of the object.
* @param y the y-position of the object.
* @param width the width of the object.
* @param height the height of the object.
*/
public ExplodeAnimation (
Color color, ExplodeInfo info, int x, int y, int width, int height)
{
super(info.bounds);
_color = color;
init(info, x, y, width, height);
}
/**
* Constructs an explode animation with the chunks represented as
* portions of the actual image.
*
* @param image the image to animate.
* @param info the explode info object.
* @param x the x-position of the object.
* @param y the y-position of the object.
* @param width the width of the object.
* @param height the height of the object.
*/
public ExplodeAnimation (
Mirage image, ExplodeInfo info, int x, int y, int width, int height)
{
super(info.bounds);
_image = image;
init(info, x, y, width, height);
}
/**
* Initializes the animation with the attributes of the given explode
* info object.
*/
protected void init (ExplodeInfo info, int x, int y, int width, int height)
{
_info = info;
_ox = x;
_oy = y;
_owid = width;
_ohei = height;
_info.rvel = (float)((2.0f * Math.PI) * _info.rvel);
_chunkcount = (_info.xchunk * _info.ychunk);
_cxpos = new int[_chunkcount];
_cypos = new int[_chunkcount];
_sxvel = new float[_chunkcount];
_syvel = new float[_chunkcount];
// determine chunk dimensions
_cwid = _owid / _info.xchunk;
_chei = _ohei / _info.ychunk;
_hcwid = _cwid / 2;
_hchei = _chei / 2;
// initialize all chunks
for (int ii = 0; ii < _chunkcount; ii++) {
// initialize chunk position
int xpos = ii % _info.xchunk;
int ypos = ii / _info.xchunk;
_cxpos[ii] = _ox + (xpos * _cwid);
_cypos[ii] = _oy + (ypos * _chei);
// initialize chunk velocity
_sxvel[ii] = RandomUtil.getFloat(_info.xvel) *
((xpos < (_info.xchunk / 2)) ? -1.0f : 1.0f);
_syvel[ii] = -(RandomUtil.getFloat(_info.yvel));
}
// initialize the chunk rotation angle
_angle = 0.0f;
}
// documentation inherited
public void fastForward (long timeDelta)
{
if (_start > 0) {
_start += timeDelta;
_end += timeDelta;
}
}
// documentation inherited
public void tick (long timestamp)
{
if (_start == 0) {
// initialize our starting time
_start = timestamp;
if (_info.delay != -1) {
_end = _start + _info.delay;
}
}
// figure out the distance the chunks have travelled
long msecs = timestamp - _start;
if (_info.delay != -1) {
// calculate the alpha level with which to render the chunks
float pctdone = msecs / (float)_info.delay;
_alpha = Math.max(0.1f, Math.min(1.0f, 1.0f - pctdone));
}
// move all chunks and check whether any remain to be animated
int inside = 0;
for (int ii = 0; ii < _chunkcount; ii++) {
// determine the chunk travel distance
int xtrav = (int)(_sxvel[ii] * msecs);
int ytrav = (int)
((_syvel[ii] * msecs) + (0.5f * _info.yacc * (msecs * msecs)));
// determine the chunk movement direction
int xpos = ii % _info.xchunk;
int ypos = ii / _info.xchunk;
// update the chunk position
_cxpos[ii] = _ox + (xpos * _cwid) + xtrav;
_cypos[ii] = _oy + (ypos * _chei) + ytrav;
// note whether this chunk is still within our bounds
_wrect.setBounds(_cxpos[ii], _cypos[ii], _cwid, _chei);
if (_bounds.intersects(_wrect)) {
inside++;
}
}
// increment the rotation angle
_angle += _info.rvel;
// note whether we're finished
_finished = (inside == 0) || (_info.delay != -1 && timestamp >= _end);
// dirty ourselves
invalidate();
}
// documentation inherited
public void paint (Graphics2D gfx)
{
Shape oclip = gfx.getClip();
Composite ocomp = null;
if (_info.delay != -1) {
// set the alpha composite to reflect the current fade-out
ocomp = gfx.getComposite();
gfx.setComposite(
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, _alpha));
}
for (int ii = 0; ii < _chunkcount; ii++) {
// get the chunk position within the image
int xpos = ii % _info.xchunk;
int ypos = ii / _info.xchunk;
// calculate image chunk offset
int xoff = -(xpos * _cwid);
int yoff = -(ypos * _chei);
// translate the origin to center on the chunk
int tx = _cxpos[ii] + _hcwid, ty = _cypos[ii] + _hchei;
gfx.translate(tx, ty);
// set up the desired rotation
gfx.rotate(_angle);
if (_image != null) {
// draw the image chunk
gfx.clipRect(-_hcwid, -_hchei, _cwid, _chei);
_image.paint(gfx, -_hcwid + xoff, -_hchei + yoff);
} else {
// draw the color chunk
gfx.setColor(_color);
gfx.fillRect(-_hcwid, -_hchei, _cwid, _chei);
}
// restore the original transform and clip
gfx.rotate(-_angle);
gfx.translate(-tx, -ty);
gfx.setClip(oclip);
}
if (_info.delay != -1) {
// restore the original composite
gfx.setComposite(ocomp);
}
}
// documentation inherited
protected void toString (StringBuilder buf)
{
super.toString(buf);
buf.append(", ox=").append(_ox);
buf.append(", oy=").append(_oy);
buf.append(", cwid=").append(_cwid);
buf.append(", chei=").append(_chei);
buf.append(", hcwid=").append(_hcwid);
buf.append(", hchei=").append(_hchei);
buf.append(", chunkcount=").append(_chunkcount);
buf.append(", info=").append(_info);
}
/** The current chunk rotation. */
protected float _angle;
/** The starting x-axis velocity of each chunk. */
protected float[] _sxvel;
/** The starting y-axis velocity of each chunk. */
protected float[] _syvel;
/** The current x-axis position of each chunk. */
protected int[] _cxpos;
/** The current y-axis position of each chunk. */
protected int[] _cypos;
/** The individual chunk dimensions in pixels. */
protected int _cwid, _chei;
/** The individual chunk dimensions in pixels, halved for handy use in
* repeated calculations. */
protected int _hcwid, _hchei;
/** The total number of image chunks. */
protected int _chunkcount;
/** The explode info. */
protected ExplodeInfo _info;
/** The exploding object position and dimensions. */
protected int _ox, _oy, _owid, _ohei;
/** The color to render the object chunks in if we're using a color. */
protected Color _color;
/** The image to animate if we're using an image. */
protected Mirage _image;
/** The starting animation time. */
protected long _start;
/** The ending animation time. */
protected long _end;
/** The percent alpha with which to render the chunks. */
protected float _alpha;
/** A reusable working rectangle. */
protected Rectangle _wrect = new Rectangle();
}
@@ -0,0 +1,85 @@
//
// $Id: FadeAnimation.java 3427 2005-03-24 04:50:52Z 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.animation;
import java.awt.AlphaComposite;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import com.threerings.media.Log;
import com.threerings.media.effects.FadeEffect;
/**
* An animation that displays an image fading from one alpha level to
* another in specified increments over time. The animation is finished
* when the specified target alpha is reached.
*/
public abstract class FadeAnimation extends Animation
{
/**
* Constructs a fade animation.
*
* @param bounds our bounds.
* @param alpha the starting alpha.
* @param step the alpha amount to step by each millisecond.
* @param target the target alpha level.
*/
protected FadeAnimation (
Rectangle bounds, float alpha, float step, float target)
{
super(bounds);
_effect = new FadeEffect(alpha, step, target);
}
// documentation inherited
public void tick (long timestamp)
{
if (_effect.tick(timestamp)) {
_finished = _effect.finished();
invalidate();
}
}
// documentation inherited
public void paint (Graphics2D gfx)
{
_effect.beforePaint(gfx);
paintAnimation(gfx);
_effect.afterPaint(gfx);
}
// documentation inherited
protected void willStart (long tickStamp)
{
super.willStart(tickStamp);
_effect.init(tickStamp);
}
/**
* Here is where derived animations actually render their image.
*/
protected abstract void paintAnimation (Graphics2D gfx);
/** This handles the main work of fading. */
protected FadeEffect _effect;
}
@@ -0,0 +1,52 @@
//
// $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.animation;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import com.threerings.media.image.Mirage;
/**
* Fades an image in or out by varying the alpha level during rendering.
*/
public class FadeImageAnimation extends FadeAnimation
{
/**
* Creates an image fading animation.
*/
public FadeImageAnimation (Mirage image, int x, int y,
float alpha, float step, float target)
{
super(new Rectangle(x, y, image.getWidth(), image.getHeight()),
alpha, step, target);
_image = image;
}
// documentation inherited
protected void paintAnimation (Graphics2D gfx)
{
_image.paint(gfx, _bounds.x, _bounds.y);
}
protected Mirage _image;
}
@@ -0,0 +1,99 @@
//
// $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.animation;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import com.samskivert.swing.Label;
import com.samskivert.swing.util.SwingUtil;
import com.threerings.media.Log;
/**
* Does something extraordinary.
*/
public class FadeLabelAnimation extends FadeAnimation
{
/**
* Creates a label fading animation.
*/
public FadeLabelAnimation (Label label, int x, int y,
float alpha, float step, float target)
{
super(new Rectangle(x, y, 0, 0), alpha, step, target);
_label = label;
}
/**
* Indicates that our label should be rendered with antialiased text.
*/
public void setAntiAliased (boolean antiAliased)
{
_antiAliased = antiAliased;
}
// documentation inherited
protected void init ()
{
super.init();
// if our label is not yet laid out, do the deed
if (!_label.isLaidOut()) {
Graphics2D gfx = (Graphics2D)_mgr.getMediaPanel().getGraphics();
if (gfx != null) {
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
_antiAliased ?
RenderingHints.VALUE_ANTIALIAS_ON :
RenderingHints.VALUE_ANTIALIAS_OFF);
_label.layout(gfx);
gfx.dispose();
}
}
// size the bounds to fit our label
Dimension size = _label.getSize();
_bounds.width = size.width;
_bounds.height = size.height;
}
// documentation inherited
protected void paintAnimation (Graphics2D gfx)
{
Object ohints = null;
if (_antiAliased) {
ohints = SwingUtil.activateAntiAliasing(gfx);
}
_label.render(gfx, _bounds.x, _bounds.y);
if (_antiAliased) {
SwingUtil.restoreAntiAliasing(gfx, ohints);
}
}
/** The label we are rendering. */
protected Label _label;
/** Whether or not to use anti-aliased rendering. */
protected boolean _antiAliased;
}
@@ -0,0 +1,248 @@
//
// $Id: FloatingTextAnimation.java 3212 2004-11-12 00:33:38Z 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.animation;
import java.awt.AlphaComposite;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import com.samskivert.swing.Label;
public class FloatingTextAnimation extends Animation
{
/**
* Constructs an animation for the given text centered at the given
* coordinates.
*/
public FloatingTextAnimation (Label label, int x, int y)
{
this(label, x, y, DEFAULT_FLOAT_PERIOD);
}
/**
* Constructs an animation for the given text centered at the given
* coordinates. The animation will float up the screen for 30 pixels.
*/
public FloatingTextAnimation (Label label, int x, int y, long floatPeriod)
{
this(label, x, y, x, y - DELTA_Y, floatPeriod);
}
/**
* Constructs an animation for the given text starting at the given
* coordinates and floating toward the specified coordinates.
*/
public FloatingTextAnimation (Label label, int sx, int sy,
int destx, int desty, long floatPeriod)
{
super(new Rectangle(sx, sy, label.getSize().width,
label.getSize().height));
// save things off
_label = label;
_startX = _x = sx;
_startY = _y = sy;
_destx = destx;
_desty = desty;
_floatPeriod = floatPeriod;
// calculate our deltas
_dx = (destx - sx);
_dy = (desty - sy);
// initialize our starting alpha
_alpha = 1.0f;
}
/**
* Called to change the direction 180 degrees.
*/
public void flipDirection ()
{
_dx = -_dx;
_dy = -_dy;
}
/**
* Returns the label used to render this score animation.
*/
public Label getLabel ()
{
return _label;
}
// documentation inherited
public void setLocation (int x, int y)
{
super.setLocation(x, y);
// update our destination coordinates
_destx += (x - _startX);
_desty += (y - _startY);
// update our initial coordinates
_startX = _x = x;
_startY = _y = y;
// recalculate our deltas
_dx = (_destx - x);
_dy = (_desty - y);
}
/**
* Sets the duration of this score animation to the specified time in
* milliseconds. This should be called before the animation is added
* to the animation manager.
*/
public void setFloatPeriod (long floatPeriod)
{
_floatPeriod = floatPeriod;
}
// documentation inherited
public void tick (long timestamp)
{
boolean invalid = false;
if (_start == 0) {
// initialize our starting time
_start = timestamp;
// we need to make sure to invalidate ourselves initially
invalid = true;
}
long fadeDelay = _floatPeriod/2;
long fadePeriod = _floatPeriod - fadeDelay;
// figure out the current alpha
long msecs = timestamp - _start;
float oalpha = _alpha;
if (msecs > fadeDelay) {
long rmsecs = msecs - fadeDelay;
_alpha = Math.max(1.0f - (rmsecs / (float)fadePeriod), 0.0f);
_alpha = Math.min(_alpha, 1.0f);
}
// get the alpha composite
_comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, _alpha);
// determine the new y-position of the score
float pctdone = (float)msecs / _floatPeriod;
int ox = _x, oy = _y;
_x = _startX + (int)(_dx * pctdone);
_y = _startY + (int)(_dy * pctdone);
// only update our location and dirty ourselves if we actually
// moved or our alpha changed
if (ox != _x || oy != _y) {
// dirty our old location
invalidate();
_bounds.setLocation(_x, _y);
invalid = true;
} else if (oalpha != _alpha) {
invalid = true;
}
if (invalid) {
// dirty our current location
invalidate();
}
// note whether we're done
_finished = (msecs >= _floatPeriod);
}
// documentation inherited
public void fastForward (long timeDelta)
{
if (_start > 0) {
_start += timeDelta;
}
}
// documentation inherited
public void paint (Graphics2D gfx)
{
Composite ocomp = gfx.getComposite();
if (_comp != null) {
gfx.setComposite(_comp);
}
paintLabels(gfx, _x, _y);
gfx.setComposite(ocomp);
}
/**
* Derived classes may wish to extend score animation and render more than
* just the standard single label.
*
* @param x the upper left coordinate of the animation.
* @param y the upper left coordinate of the animation.
*/
protected void paintLabels (Graphics2D gfx, int x, int y)
{
_label.render(gfx, x, y);
}
// documentation inherited
protected void toString (StringBuilder buf)
{
super.toString(buf);
buf.append(", x=").append(_x);
buf.append(", y=").append(_y);
buf.append(", alpha=").append(_alpha);
}
/** The starting animation time. */
protected long _start;
/** The label we're animating. */
protected Label _label;
/** The starting coordinates of the score. */
protected int _startX, _startY;
/** The current coordinates of the score. */
protected int _x, _y;
/** The destination coordinates towards which the animation travels. */
protected int _destx, _desty;
/** The distance to be traveled by the score animation. */
protected int _dx, _dy;
/** The duration for which we float up the screen. */
protected long _floatPeriod;
/** The current alpha level used to render the score. */
protected float _alpha;
/** The composite used to render the score. */
protected Composite _comp;
/** The time in milliseconds during which the score is visible. */
protected static final long DEFAULT_FLOAT_PERIOD = 1500L;
/** The total vertical distance the score travels. */
protected static final int DELTA_Y = 30;
}
@@ -0,0 +1,150 @@
//
// $Id: GleamAnimation.java 4191 2006-06-13 22:42:20Z ray $
package com.threerings.media.animation;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Transparency;
import com.threerings.media.sprite.Sprite;
import com.threerings.media.sprite.SpriteManager;
import com.threerings.media.util.LinearTimeFunction;
import com.threerings.media.util.TimeFunction;
/**
* Washes all non-transparent pixels in a sprite with a particular color
* (by compositing them with the solid color with progressively higher
* alpha values) and then back again.
*/
public class GleamAnimation extends Animation
{
/**
* Creates a gleam animation with the supplied sprite. The sprite will
* be faded to the specified color and then back again. The sprite may
* be already added to the supplied sprite manager or not, but when
* the animation is complete, it will have been added.
*
* @param fadeIn if true, the sprite itself will be faded in as we
* fade up to the gleam color and the gleam color will fade out,
* leaving just the sprite imagery.
*/
public GleamAnimation (SpriteManager spmgr, Sprite sprite, Color color,
int upmillis, int downmillis, boolean fadeIn)
{
super(sprite.getBounds());
_spmgr = spmgr;
_sprite = sprite;
_color = color;
_upfunc = new LinearTimeFunction(0, 750, upmillis);
_downfunc = new LinearTimeFunction(750, 0, downmillis);
_fadeIn = fadeIn;
}
// documentation inherited
public void tick (long timestamp)
{
int alpha;
if (_upfunc != null) {
if ((alpha = _upfunc.getValue(timestamp)) == 750) {
_upfunc = null;
}
} else if (_downfunc != null) {
if ((alpha = _downfunc.getValue(timestamp)) == 0) {
_downfunc = null;
}
} else {
_finished = true;
_spmgr.addSprite(_sprite);
return;
}
if (_alpha != alpha) {
_alpha = alpha;
invalidate();
}
}
// documentation inherited
public void fastForward (long timeDelta)
{
if (_upfunc != null) {
_upfunc.fastForward(timeDelta);
} else if (_downfunc != null) {
_downfunc.fastForward(timeDelta);
}
}
// documentation inherited
public void paint (Graphics2D gfx)
{
// TODO: recreate our off image if the sprite bounds changed; we
// also need to change the bounds of our animation which might
// require some jockeying (especially if we shrink)
if (_offimg == null) {
_offimg = gfx.getDeviceConfiguration().createCompatibleImage(
_bounds.width, _bounds.height, Transparency.TRANSLUCENT);
}
// create a mask image with our sprite and the appropriate color
Graphics2D ogfx = (Graphics2D)_offimg.getGraphics();
try {
ogfx.setColor(_color);
ogfx.fillRect(0, 0, _bounds.width, _bounds.height);
ogfx.setComposite(AlphaComposite.DstAtop);
ogfx.translate(-_sprite.getX(), -_sprite.getY());
_sprite.paint(ogfx);
} finally {
ogfx.dispose();
}
Composite ocomp = null;
Composite ncomp = AlphaComposite.getInstance(
AlphaComposite.SRC_OVER, _alpha/1000f);
// if we're fading the sprite in on the way up, set our alpha
// composite before we render the sprite
if (_fadeIn && _upfunc != null) {
ocomp = gfx.getComposite();
gfx.setComposite(ncomp);
}
// next render the sprite
_sprite.paint(gfx);
// if we're not fading in, we still need to alpha the white bits
if (ocomp == null) {
ocomp = gfx.getComposite();
gfx.setComposite(ncomp);
}
// now alpha composite our mask atop the sprite
gfx.drawImage(_offimg, _sprite.getX(), _sprite.getY(), null);
gfx.setComposite(ocomp);
}
// documentation inherited
protected void willStart (long tickStamp)
{
super.willStart(tickStamp);
// remove the sprite we're fiddling with from the manager; we'll
// add it back when we're done
if (_spmgr.isManaged(_sprite)) {
_spmgr.removeSprite(_sprite);
}
}
protected SpriteManager _spmgr;
protected Sprite _sprite;
protected Color _color;
protected Image _offimg;
protected boolean _fadeIn;
protected TimeFunction _upfunc;
protected TimeFunction _downfunc;
protected int _alpha = -1;
}
@@ -0,0 +1,134 @@
//
// $Id: MultiFrameAnimation.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.animation;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import com.threerings.media.util.FrameSequencer;
import com.threerings.media.util.MultiFrameImage;
/**
* Animates a sequence of image frames in place with a particular frame
* rate.
*/
public class MultiFrameAnimation extends Animation
{
/**
* Creates a multi-frame animation with the specified source image
* frames and the specified target frame rate (in frames per second).
*
* @param frames the source frames of the animation.
* @param fps the target number of frames per second.
* @param loop whether the animation should loop indefinitely or
* finish after one shot.
*/
public MultiFrameAnimation (
MultiFrameImage frames, double fps, boolean loop)
{
this(frames, new FrameSequencer.ConstantRate(fps, loop));
}
/**
* Creates a multi-frame animation with the specified source image
* frames and the specified target frame rate (in frames per second).
*/
public MultiFrameAnimation (MultiFrameImage frames, FrameSequencer seeker)
{
// we'll set up our bounds via setLocation() and in reset()
super(new Rectangle());
_frames = frames;
_seeker = seeker;
// reset ourselves to start things off
reset();
}
// documentation inherited
public Rectangle getBounds ()
{
// fill in the bounds with our current animation frame's bounds
return _bounds;
}
/**
* If this animation has run to completion, it can be reset to prepare
* it for another go.
*/
public void reset ()
{
super.reset();
// set the frame number to -1 so that we don't ignore the
// transition to frame zero on the first call to tick()
_fidx = -1;
// reset our frame sequencer
_seeker.init(_frames);
if (_seeker instanceof AnimationFrameSequencer) {
((AnimationFrameSequencer) _seeker).setAnimation(this);
}
}
// documentation inherited
public void tick (long tickStamp)
{
int fidx = _seeker.tick(tickStamp);
if (fidx == -1) {
_finished = true;
} else if (fidx != _fidx) {
// update our frame index and bounds
setFrameIndex(fidx);
// and have ourselves repainted
invalidate();
}
}
/**
* Sets the frame index and updates our dimensions.
*/
protected void setFrameIndex (int fidx)
{
_fidx = fidx;
_bounds.width = _frames.getWidth(_fidx);
_bounds.height = _frames.getHeight(_fidx);
}
// documentation inherited
public void paint (Graphics2D gfx)
{
_frames.paintFrame(gfx, _fidx, _bounds.x, _bounds.y);
}
// documentation inherited
public void fastForward (long timeDelta)
{
_seeker.fastForward(timeDelta);
}
protected MultiFrameImage _frames;
protected FrameSequencer _seeker;
protected int _fidx;
}
@@ -0,0 +1,134 @@
//
// $Id: RainAnimation.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.animation;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import com.samskivert.util.RandomUtil;
/**
* An animation that displays raindrops spattering across an image.
*/
public class RainAnimation extends Animation
{
/**
* Constructs a rain animation with reasonable defaults for the number
* of raindrops and their dimensions.
*
* @param bounds the bounding rectangle for the animation.
* @param duration the number of seconds the animation should last.
*/
public RainAnimation (Rectangle bounds, long duration)
{
super(bounds);
init(duration, DEFAULT_COUNT, DEFAULT_WIDTH, DEFAULT_HEIGHT);
}
/**
* Constructs a rain animation.
*
* @param bounds the bounding rectangle for the animation.
* @param duration the number of seconds the animation should last.
* @param count the number of raindrops to render in each frame.
* @param wid the width of a raindrop's bounding rectangle.
* @param hei the height of a raindrop's bounding rectangle.
*/
public RainAnimation (
Rectangle bounds, long duration, int count, int wid, int hei)
{
super(bounds);
init(duration, count, wid, hei);
}
protected void init (long duration, int count, int wid, int hei)
{
// save things off
_count = count;
_wid = wid;
_hei = hei;
// create the raindrop array
_drops = new int[count];
// calculate ending time
_duration = duration;
}
// documentation inherited
public void tick (long tickStamp)
{
// grab our ending time on our first tick
if (_end == 0L) {
_end = tickStamp + _duration;
}
_finished = (tickStamp >= _end);
// calculate the latest raindrop locations
for (int ii = 0; ii < _count; ii++) {
int x = RandomUtil.getInt(_bounds.width);
int y = RandomUtil.getInt(_bounds.height);
_drops[ii] = (x << 16 | y);
}
invalidate();
}
// documentation inherited
public void fastForward (long timeDelta)
{
_end += timeDelta;
}
// documentation inherited
public void paint (Graphics2D gfx)
{
gfx.setColor(Color.white);
for (int ii = 0; ii < _count; ii++) {
int x = _drops[ii] >> 16;
int y = _drops[ii] & 0xFFFF;
gfx.drawLine(x, y, x + _wid, y + _hei);
}
}
/** The number of raindrops. */
protected static final int DEFAULT_COUNT = 300;
/** The raindrop streak dimensions. */
protected static final int DEFAULT_WIDTH = 13;
protected static final int DEFAULT_HEIGHT = 10;
/** The number of raindrops. */
protected int _count;
/** The dimensions of each raindrop's bounding rectangle. */
protected int _wid, _hei;
/** The raindrop locations. */
protected int[] _drops;
/** Animation ending timing information. */
protected long _duration, _end;
}
@@ -0,0 +1,192 @@
//
// $Id: ScaleAnimation.java 3123 2004-09-18 22:58:39Z mdb $
package com.threerings.media.animation;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Image;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import com.threerings.media.image.Mirage;
import com.threerings.media.sprite.Sprite;
import com.threerings.media.sprite.SpriteManager;
import com.threerings.media.util.LinearTimeFunction;
import com.threerings.media.util.TimeFunction;
/**
* Animates an image changing size about its center point.
*/
public class ScaleAnimation extends Animation
{
/**
* Creates a scale animation with the supplied image. If the image's
* size would ever be 0 or less, it is not drawn.
*
* @param image The image to paint.
*
* @param center The screen coordinates of the pixel upon which the
* image's center should always be rendered.
*
* @param startScale The amount to scale the image when it is rendered
* at time 0.
*
* @param endScale The amount to scale the image at the final frame
* of animation.
*
* @param duration The time in milliseconds the anim takes to complete.
*/
public ScaleAnimation (Mirage image, Point center,
float startScale, float endScale, int duration)
{
super(getBounds(Math.max(startScale, endScale), center, image));
// Save inputted variables
_image = image;
_bufferedImage = _image.getSnapshot();
_center = new Point(center);
_startScale = startScale;
_endScale = endScale;
_duration = duration;
// Hack the LinearTimeFunction to use fixed point rationals
//
// FIXME: This class doesn't seem to be saving me a lot of
// work, since I have to repackage the outputs into floats
// anyway. Find some way to make the LinearTimeFunction do
// more of this work for us, or write a new class that does.
// Maybe IntLinearTimeFunction and FloatLinearTimeFunction
// classes would be useful.
_scaleFunc = new LinearTimeFunction(0, 10000, duration);
}
/**
* Java wants the first call in a constructor to be super()
* if it exists at all, so we have to trick it with this function.
*
* Oh, and this function computes how big the bounding box needs
* to be to bound the inputted image scaled to the inputted size
* centered around the inputted center poitn.
*/
public static Rectangle getBounds (float scale, Point center, Mirage image)
{
Point size = getSize(scale, image);
Point corner = getCorner(center, size);
return new Rectangle(corner.x, corner.y, size.x, size.y);
}
/**
* Compute the bounds to use to render the animation's image
* right now.
*/
public Rectangle getBounds ()
{
return getBounds(_scale, _center, _image);
}
/** Computes the width and height to which an image should be scaled. */
public static Point getSize (float scale, Mirage image)
{
int width = Math.max(0, Math.round(image.getWidth() * scale));
int height = Math.max(0, Math.round(image.getHeight() * scale));
return new Point(width, height);
}
/**
* Computes the upper left corner where the image should be drawn,
* given the center and dimensions to which the image should be scaled.
*/
public static Point getCorner (Point center, Point size)
{
return new Point(center.x - size.x/2, center.y - size.y/2);
}
// documentation inherited
public void tick (long tickStamp)
{
// Compute the new scaling value
float weight = _scaleFunc.getValue(tickStamp) / 10000.0f;
float scale = ((1.0f - weight) * _startScale) +
(( weight) * _endScale);
// Update the animation if the scaling changes
if (_scale != scale)
{
_scale = scale;
invalidate();
}
// Check if the animation completed
if (weight >= 1.0f) {
_finished = true;
}
}
// documentation inherited
public void fastForward (long timeDelta)
{
_scaleFunc.fastForward(timeDelta);
}
// documentation inherited
public void paint (Graphics2D gfx)
{
// Compute the bounding box to render this image
Rectangle bounds = getBounds();
// Paint nothing if the image was scaled to nothing
if (bounds.width <= 0 || bounds.height <= 0) {
return;
}
// Smooth out the image scaling
//
// FIXME: Should this be turned off when the painting is done?
gfx.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
RenderingHints.VALUE_INTERPOLATION_BILINEAR);
// Paint the image scaled to this location
gfx.drawImage(_bufferedImage,
bounds.x,
bounds.y,
bounds.x + bounds.width,
bounds.y + bounds.height,
0,
0,
_bufferedImage.getWidth(),
_bufferedImage.getHeight(),
null);
}
/** The image to scale. */
protected Mirage _image;
/** The image converted to a format Graphics2D likes, and cached. */
protected BufferedImage _bufferedImage;
/** The center pixel to render the image around. */
protected Point _center;
/** The amount of time the animation should last. */
//XXX Is this needed?
protected long _duration;
/** The amount to scale the image at the start of the animation. */
protected float _startScale;
/** The amount to scale the image at the end of the animation. */
protected float _endScale;
/** The current amount of scaling to render. */
protected float _scale;
/** Computes the image scaling to use at the specified time. */
protected TimeFunction _scaleFunc;
}
@@ -0,0 +1,35 @@
//
// $Id: SequencedAnimationObserver.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.animation;
/**
* Extends the animation observer interface with extra goodies.
*/
public interface SequencedAnimationObserver extends AnimationObserver
{
/**
* Called when the observed animation -- previously configured with an
* {@link AnimationFrameSequencer} -- reached the specified frame.
*/
public void frameReached (Animation anim, long when,
int frameIdx, int frameSeq);
}
@@ -0,0 +1,257 @@
//
// $Id: SparkAnimation.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.animation;
import java.awt.AlphaComposite;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import com.samskivert.util.RandomUtil;
import com.threerings.media.animation.Animation;
import com.threerings.media.image.Mirage;
import com.threerings.media.Log;
/**
* Displays a set of spark images originating from a specified position
* and flying outward in random directions, fading out as they go, for a
* specified period of time.
*/
public class SparkAnimation extends Animation
{
/**
* Constructs a spark animation with the supplied parameters.
*
* @param bounds the bounding rectangle for the animation.
* @param x the starting x-position for the sparks.
* @param y the starting y-position for the sparks.
* @param xjog the maximum X distance by which to "jog" the initial spark
* positions, or 0 if no jogging is desired.
* @param yjog the maximum Y distance by which to "jog" the initial spark
* positions, or 0 if no jogging is desired.
* @param minxvel the minimum starting x-velocity of the sparks.
* @param minyvel the minimum starting y-velocity of the sparks.
* @param maxxvel the maximum x-velocity of the sparks.
* @param maxyvel the maximum y-velocity of the sparks.
* @param xacc the x axis acceleration, or 0 if none is desired.
* @param yacc the y axis acceleration, or 0 if none is desired.
* @param images the spark images to be animated.
* @param delay the duration of the animation in milliseconds.
* @param fade do the fade thing
*/
public SparkAnimation (Rectangle bounds, int x, int y, int xjog, int yjog,
float minxvel, float minyvel,
float maxxvel, float maxyvel,
float xacc, float yacc,
Mirage[] images, long delay, boolean fade)
{
super(bounds);
// save things off
_xacc = xacc;
_yacc = yacc;
_images = images;
_delay = delay;
_fade = fade;
// initialize various things
_icount = images.length;
_ox = new int[_icount];
_oy = new int[_icount];
_xpos = new int[_icount];
_ypos = new int[_icount];
_sxvel = new float[_icount];
_syvel = new float[_icount];
for (int ii = 0; ii < _icount; ii++) {
// initialize spark position
_ox[ii] = x +
((xjog == 0) ? 0 : RandomUtil.getInt(xjog) * randomDirection());
_oy[ii] = y +
((yjog == 0) ? 0 : RandomUtil.getInt(yjog) * randomDirection());
// Choose random X and Y axis velocities between the inputted
// bounds
_sxvel[ii] = minxvel + RandomUtil.getFloat(1) * (maxxvel - minxvel);
_syvel[ii] = minyvel + RandomUtil.getFloat(1) * (maxyvel - minyvel);
// If accelerationes were given, make the starting velocities
// move against that acceleration; otherwise pick directions
// at random
if (_xacc > 0) {
_sxvel[ii] = -_sxvel[ii];
} else if (_xacc == 0) {
_sxvel[ii] *= randomDirection();
}
if (_yacc > 0) {
_syvel[ii] = -_syvel[ii];
} else if (_yacc == 0) {
_syvel[ii] *= randomDirection();
}
}
if (_fade) {
_alpha = 1.0f;
_comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, _alpha);
}
}
/**
* Returns at random -1 for negative direction or +1 for positive.
*/
protected int randomDirection ()
{
return (RandomUtil.getInt(2) == 0) ? -1 : 1;
}
// documentation inherited
protected void willStart (long stamp)
{
super.willStart(stamp);
_start = stamp;
}
// documentation inherited
public void fastForward (long timeDelta)
{
_start += timeDelta;
}
// documentation inherited
public void tick (long timestamp)
{
// figure out the distance the chunks have travelled
long msecs = Math.max(timestamp - _start, 0);
long msecsSq = msecs * msecs;
// calculate the alpha level with which to render the chunks
if (_fade) {
float pctdone = msecs / (float)_delay;
_alpha = Math.max(0.1f, Math.min(1.0f, 1.0f - pctdone));
_comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, _alpha);
}
// assume all sparks have moved outside the bounds
boolean allOutside = true;
// move all sparks and check whether any remain to be animated
for (int ii = 0; ii < _icount; ii++) {
// determine the travel distance
int xtrav = (int)
((_sxvel[ii] * msecs) + (0.5f * _xacc * msecsSq));
int ytrav = (int)
((_syvel[ii] * msecs) + (0.5f * _yacc * msecsSq));
// update the position
_xpos[ii] = _ox[ii] + xtrav;
_ypos[ii] = _oy[ii] + ytrav;
// check to see if any are still in. Stop looking
// when we find one
if (allOutside && _bounds.intersects(_xpos[ii], _ypos[ii],
_images[ii].getWidth(), _images[ii].getHeight())) {
allOutside = false;
}
}
// note whether we're finished
_finished = allOutside || (msecs >= _delay);
// dirty ourselves
// TODO: only do this if at least one spark actually moved
invalidate();
}
// documentation inherited
public void paint (Graphics2D gfx)
{
Shape oclip = gfx.getClip();
gfx.clip(_bounds);
Composite ocomp = gfx.getComposite();
if (_fade) {
// set the alpha composite to reflect the current fade-out
gfx.setComposite(_comp);
}
// draw all sparks
for (int ii = 0; ii < _icount; ii++) {
_images[ii].paint(gfx, _xpos[ii], _ypos[ii]);
}
// restore the original gfx settings
gfx.setComposite(ocomp);
gfx.setClip(oclip);
}
// documentation inherited
protected void toString (StringBuilder buf)
{
super.toString(buf);
buf.append(", ox=").append(_ox);
buf.append(", oy=").append(_oy);
buf.append(", alpha=").append(_alpha);
}
/** The spark images we're animating. */
protected Mirage[] _images;
/** The number of images we're animating. */
protected int _icount;
/** The x axis acceleration in pixels per millisecond. */
protected float _xacc;
/** The y axis acceleration in pixels per millisecond. */
protected float _yacc;
/** The starting x-axis velocity of each chunk. */
protected float[] _sxvel;
/** The starting y-axis velocity of each chunk. */
protected float[] _syvel;
/** The starting 'jog' positions for each spark. */
protected int[] _ox, _oy;
/** The current positions of each spark. */
protected int[] _xpos, _ypos;
/** The starting animation time. */
protected long _start;
/** Whether or not we should fade the sparks out. */
protected boolean _fade;
/** The percent alpha with which to render the images. */
protected float _alpha;
/** The alpha composite with which to render the images. */
protected AlphaComposite _comp;
/** The duration of the spark animation in milliseconds. */
protected long _delay;
}
@@ -0,0 +1,102 @@
//
// $Id: SpriteAnimation.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.animation;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import com.threerings.media.sprite.Sprite;
import com.threerings.media.sprite.SpriteManager;
import com.threerings.media.sprite.PathObserver;
import com.threerings.media.util.Path;
public class SpriteAnimation extends Animation
implements PathObserver
{
/**
* Constructs a sprite animation for the given sprite. The first time
* the animation is ticked, the sprite will be added to the given
* sprite manager and started along the supplied path.
*/
public SpriteAnimation (SpriteManager spritemgr, Sprite sprite, Path path)
{
super(new Rectangle());
// save things off
_spritemgr = spritemgr;
_sprite = sprite;
_path = path;
// set up our sprite business
_sprite.addSpriteObserver(this);
}
/**
* Derived classes can override tick if they want to end the animation
* in some other way than the sprite completing a path.
*/
public void tick (long timestamp)
{
// start the sprite moving on its path on our first tick
if (_path != null) {
_spritemgr.addSprite(_sprite);
_sprite.move(_path);
_path = null;
}
}
// documentation inherited
public void paint (Graphics2D gfx)
{
// nothing for now
}
// documentation inherited from interface
public void pathCancelled (Sprite sprite, Path path)
{
_finished = true;
}
// documentation inherited from interface
public void pathCompleted (Sprite sprite, Path path, long when)
{
_finished = true;
}
// documentation inherited
protected void didFinish (long tickStamp)
{
super.didFinish(tickStamp);
_spritemgr.removeSprite(_sprite);
_sprite = null;
}
/** The sprite associated with this animation. */
protected Sprite _sprite;
/** The sprite manager managing our sprite. */
protected SpriteManager _spritemgr;
/** The path along which we'll move our sprite. */
protected Path _path;
}
@@ -0,0 +1,125 @@
//
// $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.effects;
import java.awt.AlphaComposite;
import java.awt.Composite;
import java.awt.Graphics2D;
/**
* Handles the math and timing of doing a fade effect in a sprite or animation.
*/
public class FadeEffect
{
public FadeEffect (float alpha, float step, float target)
{
if ((step == 0f) || (alpha > target && step > 0f) ||
(alpha < target && step < 0f)) {
throw new IllegalArgumentException("Step specified is illegal: " +
"Fade would never finish (start=" + alpha + ", step=" + step +
", target=" + target + ")");
}
// save things off
_startAlpha = alpha;
_step = step;
_target = target;
// create the initial composite
_alpha = Math.max(0f, Math.min(1f, _startAlpha));
_comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, _alpha);
}
public void init (long tickStamp)
{
_finished = false;
_initStamp = tickStamp;
}
public boolean finished ()
{
return _finished;
}
public float getAlpha ()
{
return _alpha;
}
public boolean tick (long tickStamp)
{
// figure out the current alpha
long msecs = tickStamp - _initStamp;
float alpha = _startAlpha + (msecs * _step);
_finished = (_startAlpha < _target) ? (alpha >= _target)
: (alpha <= _target);
if (alpha < 0.0f) {
alpha = 0.0f;
} else if (alpha > 1.0f) {
alpha = 1.0f;
}
if (_alpha != alpha) {
_alpha = alpha;
_comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, _alpha);
return true;
}
// return false, unless we're finished
return _finished;
}
public void beforePaint (Graphics2D gfx)
{
_ocomp = gfx.getComposite();
gfx.setComposite(_comp);
}
public void afterPaint (Graphics2D gfx)
{
gfx.setComposite(_ocomp);
}
/** The composite used to render the image with the current alpha. */
protected Composite _comp;
/** The composite in effect outside our faded render. */
protected Composite _ocomp;
/** The current alpha of the image. */
protected float _alpha;
/** The target alpha. */
protected float _target;
/** The alpha step per millisecond. */
protected float _step;
/** The starting alpha. */
protected float _startAlpha;
/** Time zero. */
protected long _initStamp;
/** True when we're finished. */
protected boolean _finished;
}
@@ -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());
}
}
@@ -0,0 +1,178 @@
//
// $Id: MidiPlayer.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.sound;
import java.io.BufferedInputStream;
import java.io.InputStream;
import javax.sound.midi.MetaEventListener;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.Sequencer;
import javax.sound.midi.Synthesizer;
/**
* Plays midi/rmf sounds using Java's sequencer, which is susceptible
* to the accuracy of System.currentTimeMillis() and so currently sounds
* like "ass" under Windows.
*/
public class MidiPlayer extends MusicPlayer
implements MetaEventListener
{
// documentation inherited
public void init ()
throws Exception
{
_sequencer = MidiSystem.getSequencer();
_sequencer.open();
if (_sequencer instanceof Synthesizer) {
_channels = ((Synthesizer) _sequencer).getChannels();
}
}
// documentation inherited
public void shutdown ()
{
_sequencer.close();
}
// documentation inherited
public void start (InputStream stream)
throws Exception
{
_sequencer.setSequence(new BufferedInputStream(stream));
_sequencer.start();
_sequencer.addMetaEventListener(this);
}
// documentation inherited
public void stop ()
{
_sequencer.removeMetaEventListener(this);
_sequencer.stop();
}
// documentation inherited
public void setVolume (float volume)
{
if (_channels != null) {
int setting = (int) (volume * 127.0);
for (int ii=0; ii < _channels.length; ii++) {
_channels[ii].controlChange(VOLUME_CONTROL, setting);
}
}
}
// documentation inherited from interface MetaEventListener
public void meta (MetaMessage msg)
{
if (msg.getType() == END_OF_TRACK) {
_musicListener.musicStopped();
}
}
// STUFF FROM ANOTHER TIME
// /**
// * Get a list of alternate midi devices.
// */
// public MidiDevice.Info[] getAlternateMidiDevices ()
// {
// ArrayList infos = new ArrayList();
// CollectionUtil.addAll(infos, MidiSystem.getMidiDeviceInfo());
//
// // remove the synth/seqs, leaving only hardware midi thingies
// for (Iterator iter=infos.iterator(); iter.hasNext(); ) {
// try {
// MidiDevice dev = MidiSystem.getMidiDevice(
// (MidiDevice.Info) iter.next());
// if ((dev instanceof Sequencer) ||
// (dev instanceof Synthesizer)) {
// iter.remove();
// }
// } catch (MidiUnavailableException mue) {
// iter.remove();
// }
// }
//
// return (MidiDevice.Info[]) infos.toArray(
// new MidiDevice.Info[infos.size()]);
// }
//
// /**
// * Attempt to use the alternate midi device for output.
// * Return true if we're using it.
// */
// public boolean useAlternateDevice (MidiDevice.Info devinfo)
// {
// Log.info("Trying alternate device: " + devinfo);
// try {
// MidiDevice dev = MidiSystem.getMidiDevice(devinfo);
// Receiver rec = dev.getReceiver();
// if (rec == null) {
// Log.info("Got no device!");
// return false;
// }
// _stoppingSong = true;
// _sequencer.stop();
// _sequencer.close();
//
// Receiver old = _sequencer.getTransmitter().getReceiver();
// Log.info("Old receiver: " + old);
// if (old != null) {
// old.close();
// }
// _sequencer.open();
//
// // THIS DOESN'T WORK.
// // See bug #4347135, specifically notes on the bottom.
// _sequencer.getTransmitter().setReceiver(rec);
// playTopSong();
//
// // possibly shut down an old receiver
// if (_receiver != null) {
// _receiver.close();
// }
// // set the new receiver
// _receiver = rec;
//
// return true;
//
// } catch (MidiUnavailableException mue) {
// Log.warning("Use of alternate device failed [e=" + mue +
// ", device=" + devinfo + "].");
// return false;
// }
// }
/** This is apparently the midi code for end of track. Wack. */
protected static final int END_OF_TRACK = 47;
/** The midi control for volume is 7. Ooooooo. */
protected static final int VOLUME_CONTROL = 7;
/** The sequencer. */
protected Sequencer _sequencer;
/** The channels in the sequencer, which we'll use to fuxor volumes. */
protected MidiChannel[] _channels;
}
@@ -0,0 +1,135 @@
//
// $Id: ModPlayer.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.sound;
import java.io.DataInputStream;
import java.io.InputStream;
import micromod.MicroMod;
import micromod.Module;
import micromod.ModuleLoader;
import micromod.output.JavaSoundOutputDevice;
import micromod.output.OutputDeviceException;
import micromod.output.converters.SS16LEAudioFormatConverter;
import micromod.resamplers.LinearResampler;
/**
* A player that plays .mod format music.
*/
public class ModPlayer extends MusicPlayer
{
// documentation inherited
public void init ()
throws Exception
{
_device = new NaryaSoundDevice();
_device.start();
}
// documentation inherited
public void shutdown ()
{
_device.stop();
}
// documentation inherited
public void start (InputStream stream)
throws Exception
{
Module module = ModuleLoader.read(new DataInputStream(stream));
final MicroMod mod = new MicroMod(
module, _device, new LinearResampler());
_player = new Thread("narya mod player") {
public void run () {
while (mod.getSequenceLoopCount() == 0) {
mod.doRealTimePlayback();
try {
Thread.sleep(20);
} catch (InterruptedException ie) {
// WFCares
}
if (_player != Thread.currentThread()) {
// we were stopped!
return;
}
}
_device.drain();
_musicListener.musicStopped();
}
};
_player.setDaemon(true);
_player.start();
}
// documentation inherited
public void stop ()
{
_player = null;
}
// documentation inherited
public void setVolume (float vol)
{
_device.setVolume(vol);
}
/**
* A class that allows us to access the dataline so we can adjust
* the volume.
*/
protected static class NaryaSoundDevice extends JavaSoundOutputDevice
{
public NaryaSoundDevice ()
throws OutputDeviceException
{
super(new SS16LEAudioFormatConverter(), 44100, 1000);
}
/**
* Adjust the volume of the line that we're sending our mod data to.
*/
public void setVolume (float vol)
{
SoundManager.adjustVolume(sourceDataLine, vol);
}
/**
* Access the drain method of the line.
*/
public void drain ()
{
sourceDataLine.drain();
}
}
/** The thread that does the work. */
protected Thread _player;
/** The sound output device. */
protected NaryaSoundDevice _device;
}
@@ -0,0 +1,148 @@
//
// $Id: Mp3Player.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.sound;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioInputStream;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import com.threerings.media.Log;
/**
* Plays mp3 files. Depends on three external jar files that aren't even
* imported here:
* tritonus_share.jar
* tritonus_mp3.jar
* javalayer.jar
*/
public class Mp3Player extends MusicPlayer
{
// documentation inherited
public void init ()
{
// TODO: some stuff needs to move here, like setting up the line
// but we don't yet know the audio format, so I need to figure that
// out (the format might always be known..).
}
// documentation inherited
public void shutdown ()
{
}
// documentation inherited
public void start (final InputStream stream)
throws Exception
{
// TODO: some stuff needs to come out of here and into init/shutdown
// but we'll deal with all that later, d00d.
_player = new Thread("narya mp3 relay") {
public void run () {
AudioInputStream inStream = null;
try {
inStream = AudioSystem.getAudioInputStream(
new BufferedInputStream(stream, BUFFER_SIZE));
} catch (Exception e) {
Log.warning("MP3 fuckola. [e=" + e + "].");
return;
}
AudioFormat sourceFormat = inStream.getFormat();
AudioFormat.Encoding targetEnc =
AudioFormat.Encoding.PCM_SIGNED;
inStream = AudioSystem.getAudioInputStream(
targetEnc, inStream);
AudioFormat format = inStream.getFormat();
DataLine.Info info = new DataLine.Info(
SourceDataLine.class, format);
try {
_line = (SourceDataLine) AudioSystem.getLine(info);
_line.open(format);
} catch (LineUnavailableException lue) {
Log.warning("MP3 line unavailable: " + lue);
return;
}
_line.start();
byte[] data = new byte[BUFFER_SIZE];
int count = 0;
while (count >= 0) {
try {
count = inStream.read(data, 0, data.length);
} catch (IOException ioe) {
Log.warning("Error reading MP3: " + ioe);
break;
}
if (count >= 0) {
_line.write(data, 0, count);
}
if (_player != Thread.currentThread()) {
return;
}
}
_line.drain();
_line.close();
_musicListener.musicStopped();
}
};
_player.setDaemon(true);
//_player.setPriority(_player.getPriority() + 1);
_player.start();
}
// documentation inherited
public void stop ()
{
_player = null;
}
// documentation inherited
public void setVolume (float volume)
{
// TODO : line won't be null when we initialize it in the right place
if (_line != null) {
SoundManager.adjustVolume(_line, volume);
}
}
/** The thread that transfers data to the line. */
protected Thread _player;
/** The line that we play through. */
protected SourceDataLine _line;
/** The size of our buffer. */
protected static final int BUFFER_SIZE = 8192;
}
@@ -0,0 +1,353 @@
//
// $Id: SoundManager.java 3290 2004-12-29 21:56:58Z 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.sound;
import java.util.Iterator;
import java.util.LinkedList;
import com.samskivert.util.Config;
import com.samskivert.util.RandomUtil;
import com.samskivert.util.RunQueue;
import com.threerings.media.Log;
/**
* Manages the playing of audio files.
*/
// TODO:
// - fade music out when stopped?
// - be able to pause music
public class MusicManager
{
/**
* Constructs a music manager.
*
* @param smgr The soundManager we work with.
* @param runQueue the client event run queue.
*
*/
public MusicManager (SoundManager smgr, RunQueue runQueue)
{
_smgr = smgr;
_runQueue = runQueue;
}
/**
* Shut the damn thing off.
*/
public void shutdown ()
{
_musicStack.clear();
stopMusicPlayer();
}
/**
* Returns a string summarizing our volume settings and disabled sound
* types.
*/
public String summarizeState ()
{
StringBuilder buf = new StringBuilder();
buf.append("musicVol=").append(_musicVol);
return buf.append("]").toString();
}
/**
* Sets the volume for music.
*
* @param vol a volume parameter between 0f and 1f, inclusive.
*/
public void setMusicVolume (float vol)
{
float oldvol = _musicVol;
_musicVol = Math.max(0f, Math.min(1f, vol));
if (_musicPlayer != null) {
_musicPlayer.setVolume(_musicVol);
}
if ((oldvol == 0f) && (_musicVol != 0f)) {
playTopMusic();
} else if ((oldvol != 0f) && (_musicVol == 0f)) {
stopMusicPlayer();
}
}
/**
* Get the music volume.
*/
public float getMusicVolume ()
{
return _musicVol;
}
/**
* Start playing the specified music repeatedly.
*/
public void pushMusic (String pkgPath, String key)
{
pushMusic(pkgPath, key, -1);
}
/**
* Start playing music for the specified number of loops. If no other
* music is pushed, the specified music will play for the number of loops
* specified, and will then be popped off the stack.
*/
public void pushMusic (String pkgPath, String key, int numloops)
{
MusicKey mkey = new MusicKey(pkgPath, key, numloops);
// stop any existing playing music
if (_musicPlayer != null) {
_musicPlayer.stop();
handleMusicStopped();
}
// add the new song
_musicStack.addFirst(mkey);
// and play it
playTopMusic();
}
/**
* Remove the specified music from the playlist. If it is currently
* playing, it will be stopped and the previous song will be started.
*/
public void removeMusic (String pkgPath, String key)
{
MusicKey mkey = new MusicKey(pkgPath, key, -1);
if (!_musicStack.isEmpty()) {
MusicKey current = (MusicKey) _musicStack.getFirst();
// if we're currently playing this song..
if (mkey.equals(current)) {
// stop it
if (_musicPlayer != null) {
_musicPlayer.stop();
}
// remove it from the stack
_musicStack.removeFirst();
// start playing the next..
playTopMusic();
return;
} else {
// we aren't currently playing this song. Simply remove.
for (Iterator iter=_musicStack.iterator(); iter.hasNext(); ) {
if (key.equals(iter.next())) {
iter.remove();
return;
}
}
}
}
Log.debug("Sequence stopped that wasn't in the stack anymore " +
"[key=" + mkey + "].");
}
/**
* Start the specified sequence.
*/
protected void playTopMusic ()
{
if (_musicStack.isEmpty()) {
return;
}
// if the volume is off, we don't actually want to play anything
// but we want to at least decrement any loopers by one
// and keep them on the top of the queue
if (_musicVol == 0f) {
handleMusicStopped();
return;
}
MusicKey info = (MusicKey) _musicStack.getFirst();
Config c = _smgr.getConfig(info);
String[] names = c.getValue(info.key, (String[])null);
if ((names == null) || (names.length == 0)) {
Log.warning("No such music [key=" + info + "].");
_musicStack.removeFirst();
playTopMusic();
return;
}
String music = names[RandomUtil.getInt(names.length)];
Class playerClass = getMusicPlayerClass(music);
// if we don't have a player for this song, play the next song
if (playerClass == null) {
_musicStack.removeFirst();
playTopMusic();
return;
}
// shutdown the old player if we're switching music types
if (! playerClass.isInstance(_musicPlayer)) {
if (_musicPlayer != null) {
_musicPlayer.shutdown();
}
// set up the new player
try {
_musicPlayer = (MusicPlayer) playerClass.newInstance();
_musicPlayer.init(_playerListener);
} catch (Exception e) {
Log.warning("Unable to instantiate music player [class=" +
playerClass + ", e=" + e + "].");
// scrap it, try again with the next song
_musicPlayer = null;
_musicStack.removeFirst();
playTopMusic();
return;
}
_musicPlayer.setVolume(_musicVol);
}
// play!
String bundle = c.getValue("bundle", (String)null);
try {
// TODO: buffer for the music player?
_musicPlayer.start(_smgr._rmgr.getResource(bundle, music));
} catch (Exception e) {
Log.warning("Error playing music, skipping [e=" + e +
", bundle=" + bundle + ", music=" + music + "].");
_musicStack.removeFirst();
playTopMusic();
return;
}
}
/**
* Get the appropriate music player for the specified music file.
*/
protected static Class getMusicPlayerClass (String path)
{
path = path.toLowerCase();
// if (path.endsWith(".mid") || path.endsWith(".rmf")) {
// return MidiPlayer.class;
// } else if (path.endsWith(".mod")) {
// return ModPlayer.class;
// } else if (path.endsWith(".mp3")) {
// return Mp3Player.class;
// } else if (path.endsWith(".ogg")) {
// return OggPlayer.class;
// } else {
return null;
// }
}
/**
* Stop whatever song is currently playing and deal with the
* MusicKey associated with it.
*/
protected void handleMusicStopped ()
{
if (_musicStack.isEmpty()) {
return;
}
// see what was playing
MusicKey current = (MusicKey) _musicStack.getFirst();
// see how many times the song was to loop and act accordingly
switch (current.loops) {
default:
current.loops--;
break;
case 1:
// sorry charlie
_musicStack.removeFirst();
break;
}
}
/**
* Stop the current music player.
*/
protected void stopMusicPlayer ()
{
if (_musicPlayer != null) {
_musicPlayer.stop();
_musicPlayer.shutdown();
_musicPlayer = null;
}
}
/**
* A class that tracks the information about our playing music files.
*/
protected static class MusicKey extends SoundManager.SoundKey
{
/** How many times to loop, or -1 for forever. */
public int loops;
public MusicKey (String set, String path, int loops)
{
super((byte) -1, set, path);
this.loops = loops;
}
}
/** The sound manager we work with. */
protected SoundManager _smgr;
/** The client event run queue. */
protected RunQueue _runQueue;
/** Volume level for music. */
protected float _musicVol = 1f;
/** The stack of songs that we're playing. */
protected LinkedList _musicStack = new LinkedList();
/** The current music player, if any. */
protected MusicPlayer _musicPlayer;
/** Event listener for receiving information about a song ending. */
protected MusicPlayer.MusicEventListener _playerListener =
new MusicPlayer.MusicEventListener() {
public void musicStopped () {
_runQueue.postRunnable(new Runnable() {
public void run() {
handleMusicStopped();
playTopMusic();
}
});
}
};
}
@@ -0,0 +1,89 @@
//
// $Id: MusicPlayer.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.sound;
import java.io.InputStream;
/**
* Abstract music player.
*/
public abstract class MusicPlayer
{
/**
* A watcher interested in music events.
*/
public interface MusicEventListener
{
/**
* A callback that all players should use when the song they are
* playing has finished playing (completely).
*/
public void musicStopped();
}
/**
* Initialize the music player.
*/
public final void init (MusicEventListener musicListener)
throws Exception
{
_musicListener = musicListener;
init();
}
/**
* Do your init here.
*/
public void init ()
throws Exception
{
}
/**
* Shutdown and free all resources.
*/
public void shutdown ()
{
}
/**
* Start playing song data from the specified stream.
*/
public abstract void start (InputStream stream)
throws Exception;
/**
* Stop playing the specified song.
*/
public abstract void stop ();
/**
* Set the volume.
*
* @param volume 0f - 1f, inclusive.
*/
public abstract void setVolume (float volume);
/** Tell this guy about it when a song stops. */
protected MusicEventListener _musicListener;
}
@@ -0,0 +1,401 @@
//
// $Id: OggPlayer.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.sound;
import java.io.InputStream;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import com.jcraft.jorbis.*;
import com.jcraft.jogg.*;
/**
* Plays Ogg Vorbis streams.
*
* Hacked together from NASTY code from JOrbis.
*/
// TODO- this would need to be greatly cleaned up if we were serious about
// using it.
public class OggPlayer extends MusicPlayer
{
// documentation inherited
public void start (final InputStream stream)
{
_player = new Thread("narya ogg player") {
public void run () {
playStream(stream);
}
};
_player.setDaemon(true);
_player.start();
}
// documentation inherited
public void stop ()
{
_player = null;
}
static final int BUFSIZE=4096*2;
static int convsize=BUFSIZE*2;
static byte[] convbuffer=new byte[convsize];
SyncState oy;
StreamState os;
Page og;
Packet op;
Info vi;
Comment vc;
DspState vd;
Block vb;
byte[] buffer=null;
int bytes=0;
int format;
int rate=0;
int channels=0;
SourceDataLine outputLine=null;
int frameSizeInBytes;
int bufferLengthInBytes;
void init_jorbis(){
oy=new SyncState();
os=new StreamState();
og=new Page();
op=new Packet();
vi=new Info();
vc=new Comment();
vd=new DspState();
vb=new Block(vd);
buffer=null;
bytes=0;
oy.init();
}
SourceDataLine getOutputLine(int channels, int rate){
if(outputLine!=null || this.rate!=rate || this.channels!=channels){
if(outputLine!=null){
outputLine.drain();
outputLine.stop();
outputLine.close();
}
init_audio(channels, rate);
outputLine.start();
}
return outputLine;
}
void init_audio(int channels, int rate){
try {
//ClassLoader originalClassLoader=null;
//try{
// originalClassLoader=Thread.currentThread().getContextClassLoader();
// Thread.currentThread().setContextClassLoader(ClassLoader.getSystemClassLoader());
//}
//catch(Exception ee){
// System.out.println(ee);
//}
AudioFormat audioFormat =
new AudioFormat((float)rate,
16,
channels,
true, // PCM_Signed
false // littleEndian
);
DataLine.Info info =
new DataLine.Info(SourceDataLine.class,
audioFormat,
AudioSystem.NOT_SPECIFIED);
if (!AudioSystem.isLineSupported(info)) {
//System.out.println("Line " + info + " not supported.");
return;
}
try{
outputLine = (SourceDataLine) AudioSystem.getLine(info);
//outputLine.addLineListener(this);
outputLine.open(audioFormat);
}
catch (LineUnavailableException ex) {
System.out.println("Unable to open the sourceDataLine: " + ex);
return;
}
catch (IllegalArgumentException ex) {
System.out.println("Illegal Argument: " + ex);
return;
}
frameSizeInBytes = audioFormat.getFrameSize();
int bufferLengthInFrames = outputLine.getBufferSize()/frameSizeInBytes/2;
bufferLengthInBytes = bufferLengthInFrames * frameSizeInBytes;
//buffer = new byte[bufferLengthInBytes];
//if(originalClassLoader!=null)
// Thread.currentThread().setContextClassLoader(originalClassLoader);
this.rate=rate;
this.channels=channels;
}
catch(Exception ee){
System.out.println(ee);
}
}
protected void playStream (InputStream stream)
{
init_jorbis();
loop:
while (true) {
int eos = 0;
int index = oy.buffer(BUFSIZE);
buffer = oy.data;
try {
bytes = stream.read(buffer, index, BUFSIZE);
} catch (Exception e) {
System.err.println(e);
return;
}
oy.wrote(bytes);
if (oy.pageout(og) != 1) {
if (bytes < BUFSIZE) {
break;
}
System.err.println("Input does not appear to be an Ogg bitstream.");
return;
}
os.init(og.serialno());
os.reset();
vi.init();
vc.init();
if (os.pagein(og) < 0) {
// error; stream version mismatch perhaps
System.err.println("Error reading first page of Ogg bitstream data.");
return;
}
if (os.packetout(op) != 1) {
// no page? must not be vorbis
System.err.println("Error reading initial header packet.");
break;
// return;
}
if (vi.synthesis_headerin(vc, op) < 0) {
// error case; not a vorbis header
System.err.println("This Ogg bitstream does not contain Vorbis audio data.");
return;
}
int i=0;
while (i < 2) {
while (i < 2) {
int result = oy.pageout(og);
if (result==0) {
break; // Need more data
}
if (result==1) {
os.pagein(og);
while (i < 2) {
result = os.packetout(op);
if (result == 0) {
break;
}
if (result == -1) {
System.err.println("Corrupt secondary header. Exiting.");
//return;
break loop;
}
vi.synthesis_headerin(vc, op);
i++;
}
}
}
index = oy.buffer(BUFSIZE);
buffer = oy.data;
try {
bytes = stream.read(buffer, index, BUFSIZE);
}
catch(Exception e){
System.err.println(e);
return;
}
if (bytes == 0 && i < 2) {
System.err.println("End of file before finding all Vorbis headers!");
return;
}
oy.wrote(bytes);
}
convsize=BUFSIZE/vi.channels;
vd.synthesis_init(vi);
vb.init(vd);
double[][][] _pcm=new double[1][][];
float[][][] _pcmf=new float[1][][];
int[] _index=new int[vi.channels];
getOutputLine(vi.channels, vi.rate);
while (eos == 0) {
while (eos == 0) {
if (_player != Thread.currentThread()) {
//System.err.println("bye.");
try {
//outputLine.drain();
//outputLine.stop();
//outputLine.close();
stream.close();
} catch(Exception ee) {
}
return;
}
int result = oy.pageout(og);
if (result == 0) {
break; // need more data
}
if (result == -1) { // missing or corrupt data at this page position
// System.err.println("Corrupt or missing data in bitstream; continuing...");
} else {
os.pagein(og);
while (true) {
result = os.packetout(op);
if (result == 0) break; // need more data
if (result == -1) { // missing or corrupt data at this page position
// no reason to complain; already complained above
} else {
// we have a packet. Decode it
int samples;
if (vb.synthesis(op) == 0) { // test for success!
vd.synthesis_blockin(vb);
}
while ((samples =
vd.synthesis_pcmout(_pcmf, _index)) > 0) {
double[][] pcm = _pcm[0];
float[][] pcmf = _pcmf[0];
boolean clipflag = false;
int bout = Math.min(samples, convsize);
// convert doubles to 16 bit signed ints (host order) and
// interleave
for (i = 0; i < vi.channels; i++) {
int ptr = i*2;
//int ptr=i;
int mono = _index[i];
for (int j = 0; j < bout; j++) {
int val = (int)
(pcmf[i][mono+j] * 32767.);
if (val > 32767){
val = 32767;
clipflag = true;
} else if (val < -32768) {
val = -32768;
clipflag = true;
}
if (val < 0) {
val = val | 0x8000;
}
convbuffer[ptr] = (byte)(val);
convbuffer[ptr+1] = (byte)(val>>>8);
ptr += 2 * (vi.channels);
}
}
outputLine.write(convbuffer, 0,
2 * vi.channels * bout);
vd.synthesis_read(bout);
}
}
}
if (og.eos() != 0) {
eos=1;
}
}
}
if (eos==0) {
index = oy.buffer(BUFSIZE);
buffer = oy.data;
try {
bytes = stream.read(buffer,index,BUFSIZE);
} catch (Exception e) {
System.err.println(e);
return;
}
if (bytes == -1) {
break;
}
oy.wrote(bytes);
if (bytes==0) {
eos=1;
}
}
}
os.clear();
vb.clear();
vd.clear();
vi.clear();
}
oy.clear();
//System.err.println("Done.");
try {
if (stream != null) {
stream.close();
}
} catch (Exception e) {
}
}
public void setVolume (float volume)
{
// TODO
}
protected Thread _player;
}
@@ -0,0 +1,57 @@
//
// $Id: SoundCodes.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.sound;
import com.threerings.media.sound.SoundManager.SoundType;
/**
* A basic set of sound types.
*/
public interface SoundCodes
{
/**
* Alert sounds are the type of sounds a player would hear when
* getting a puzzle challenge.
*/
public static final SoundType ALERT = new SoundType("alert");
/**
* Feedback sounds are the type of sounds a player would here when
* clicking on buttons or performing an action.
*/
public static final SoundType FEEDBACK = new SoundType("feedback");
/**
* Ambient sounds are birds chirping, waves lapping, boats creaking.
*/
public static final SoundType AMBIENT = new SoundType("ambient");
/**
* Game alert sounds are used to indicate that it's a player's turn.
*/
public static final SoundType GAME_ALERT = new SoundType("game_alert");
/**
* General game sound effects.
*/
public static final SoundType GAME_FX =new SoundType("game_fx");
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,49 @@
//
// $Id: Sounds.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.sound;
/**
* A base class for sound repository classes. These would extend this
* class and define keys for the various sounds that are mapped in the
* properties file associated with that sound repository.
*/
public class Sounds
{
/** The name of the sound repository configuration file. */
public static final String PROP_NAME = "sounds";
/**
* Return the package path prefix of the supplied class.
*
* Generates the key for the sound repository configuration file in
* the package associated with the class. For example, if a the class
* <code>com.threerings.happy.fun.GameSounds</code> were supplied to
* this method, it would return
* <code>com/threerings/happy/fun/sounds/</code> which would reference
* a <code>sounds.properties</code> file in the
* <code>com.threerings.happy.fun</code> package.
*/
protected static String getPackagePath (Class clazz)
{
return clazz.getPackage().getName().replace('.', '/') + "/";
}
}
@@ -0,0 +1,384 @@
//
// $Id: ButtonSprite.java 3509 2005-04-20 17:15:36Z 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.sprite;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Shape;
import com.samskivert.swing.Label;
import com.samskivert.swing.util.SwingUtil;
import com.threerings.media.sprite.action.ArmingSprite;
import com.threerings.media.sprite.action.CommandSprite;
import com.threerings.media.sprite.action.DisableableSprite;
/**
* A sprite that acts as a button.
*/
public class ButtonSprite extends Sprite
implements CommandSprite, ArmingSprite, DisableableSprite
{
/** The normal, square button style. */
public static final int NORMAL = 0;
/** The rounded button style. */
public static final int ROUNDED = 1;
/**
* Constructs a button sprite.
*
* @param label the label to render on the button
* @param style the style of button to render (NORMAL or ROUNDED)
* @param backgroundColor the background color of the button
* @param alternateColor the alternate (outline) color
* @param actionCommand the button's command
* @param commandArgument the button's command argument
*/
public ButtonSprite (Label label, int style, Color backgroundColor,
Color alternateColor, String actionCommand, Object commandArgument)
{
_label = label;
_style = style;
_backgroundColor = backgroundColor;
_alternateColor = alternateColor;
_actionCommand = actionCommand;
_commandArgument = commandArgument;
}
/**
* Constructs a button sprite.
*
* @param label the label to render on the button
* @param style the style of button to render (NORMAL or ROUNDED)
* @param arcWidth the width of the corner arcs for rounded buttons
* @param arcHeight the height of the corner arcs for rounded buttons
* @param backgroundColor the background color of the button
* @param alternateColor the alternate (outline) color
* @param actionCommand the button's command
* @param commandArgument the button's command argument
*/
public ButtonSprite (Label label, int style, int arcWidth, int arcHeight,
Color backgroundColor, Color alternateColor, String actionCommand,
Object commandArgument)
{
_label = label;
_style = style;
_arcWidth = arcWidth;
_arcHeight = arcHeight;
_backgroundColor = backgroundColor;
_alternateColor = alternateColor;
_actionCommand = actionCommand;
_commandArgument = commandArgument;
}
/**
* Returns a reference to the label displayed by this sprite.
*/
public Label getLabel ()
{
return _label;
}
/**
* Updates this sprite's bounds after a change to the label.
*/
public void updateBounds ()
{
// invalidate the old...
invalidate();
// size the bounds to fit our label
Dimension size = _label.getSize();
_bounds.width = size.width + PADDING*2 +
(_style == ROUNDED ? _arcWidth : 0);
_bounds.height = size.height + PADDING*2;
// ...and the new
invalidate();
}
/**
* Sets the style of this button.
*/
public void setStyle (int style)
{
_style = style;
updateBounds();
}
/**
* Returns the style of this button.
*/
public int getStyle ()
{
return _style;
}
/**
* Sets the arc width for rounded buttons.
*/
public void setArcWidth (int arcWidth)
{
_arcWidth = arcWidth;
updateBounds();
}
/**
* Returns the arc width for rounded buttons.
*/
public int getArcWidth ()
{
return _arcWidth;
}
/**
* Sets the arc height for rounded buttons.
*/
public void setArcHeight (int arcHeight)
{
_arcHeight = arcHeight;
updateBounds();
}
/**
* Returns the arc height for rounded buttons.
*/
public int getArcHeight ()
{
return _arcHeight;
}
/**
* Sets the background color of this button.
*/
public void setBackgroundColor (Color backgroundColor)
{
_backgroundColor = backgroundColor;
}
/**
* Returns the background color of this button.
*/
public Color getBackgroundColor ()
{
return _backgroundColor;
}
/**
* Sets the action command generated by this button.
*/
public void setActionCommand (String actionCommand)
{
_actionCommand = actionCommand;
}
// documentation inherited from interface CommandSprite
public String getActionCommand ()
{
return _actionCommand;
}
/**
* Sets the command argument generated by this button.
*/
public void setCommandArgument (Object commandArgument)
{
_commandArgument = commandArgument;
}
// documentation inherited from interface CommandSprite
public Object getCommandArgument ()
{
return _commandArgument;
}
/**
* Sets whether or not this button is enabled.
*/
public void setEnabled (boolean enabled)
{
if (_enabled != enabled) {
_enabled = enabled;
invalidate();
}
}
// documentation inherited from interface DisableableSprite
public boolean isEnabled ()
{
return _enabled;
}
// documentation inherited from interface ArmingSprite
public void setArmed (boolean pressed)
{
if (_pressed != pressed) {
_pressed = pressed;
invalidate();
}
}
/**
* Checks whether or not this button appears pressed.
*/
public boolean isArmed ()
{
return _pressed;
}
// documentation inherited
protected void init ()
{
super.init();
// lay out the label if not already
if (!_label.isLaidOut()) {
_label.layout(_mgr.getMediaPanel());
}
updateBounds();
}
// documentation inherited
public void paint (Graphics2D gfx)
{
Color baseTextColor = _label.getTextColor(),
baseAlternateColor = _label.getAlternateColor();
if (!_enabled) {
_label.setTextColor(baseTextColor.darker());
_label.setAlternateColor(baseAlternateColor.darker());
}
switch (_style) {
case NORMAL:
gfx.setColor(_enabled ? _backgroundColor :
_backgroundColor.darker());
gfx.fill3DRect(_bounds.x, _bounds.y, _bounds.width,
_bounds.height, !_pressed);
_label.render(gfx, _bounds.x + (_pressed ? PADDING :
PADDING - 1), _bounds.y + (_pressed ? PADDING :
PADDING - 1));
break;
case ROUNDED:
Object aaState = SwingUtil.activateAntiAliasing(gfx);
// draw outline
gfx.setColor(_alternateColor);
gfx.fillRoundRect(_bounds.x, _bounds.y, _bounds.width,
_bounds.height, _arcWidth, _arcHeight);
// draw foreground
gfx.setColor(_enabled ? _backgroundColor :
_backgroundColor.darker());
int innerBoundsX = _bounds.x+1, innerBoundsY = _bounds.y+1,
innerBoundsWidth = _bounds.width-2,
innerBoundsHeight = _bounds.height-2,
innerBoundsArcWidth = _arcWidth-2,
innerBoundsArcHeight = _arcHeight-2;
gfx.fillRoundRect(innerBoundsX, innerBoundsY,
innerBoundsWidth, innerBoundsHeight,
innerBoundsArcWidth, innerBoundsArcHeight);
Color brighter = _enabled ? _backgroundColor.brighter() :
_backgroundColor, darker = _enabled ?
_backgroundColor.darker() :
_backgroundColor.darker().darker();
// draw the upper left/lower right corners (always dark)
gfx.setColor(darker);
gfx.drawArc(innerBoundsX, innerBoundsY, innerBoundsArcWidth,
innerBoundsArcHeight, 90, 90);
gfx.drawArc(innerBoundsX + innerBoundsWidth -
innerBoundsArcWidth - 1,
innerBoundsY + innerBoundsHeight -
innerBoundsArcHeight - 1,
innerBoundsArcWidth, innerBoundsArcHeight, 270, 90);
// draw the upper right (dark when pressed)
gfx.setColor(_pressed ? darker : brighter);
gfx.drawLine(innerBoundsX + innerBoundsArcWidth/2,
innerBoundsY, innerBoundsX + innerBoundsWidth -
innerBoundsArcWidth/2, innerBoundsY);
gfx.drawArc(innerBoundsX + innerBoundsWidth -
innerBoundsArcWidth - 1, innerBoundsY,
innerBoundsArcWidth, innerBoundsArcHeight, 0, 90);
gfx.drawLine(innerBoundsX + innerBoundsWidth - 1,
innerBoundsY + innerBoundsArcHeight/2,
innerBoundsX + innerBoundsWidth - 1, innerBoundsY +
innerBoundsHeight - innerBoundsArcHeight/2);
// draw the lower left (light when pressed)
gfx.setColor(_pressed ? brighter : darker);
gfx.drawLine(innerBoundsX, innerBoundsY +
innerBoundsArcHeight/2, innerBoundsX,
innerBoundsY + innerBoundsHeight -
innerBoundsArcHeight/2);
gfx.drawArc(innerBoundsX, innerBoundsY + innerBoundsHeight -
innerBoundsArcHeight - 1,
innerBoundsArcWidth, innerBoundsArcHeight, 180, 90);
gfx.drawLine(innerBoundsX + innerBoundsArcWidth/2,
innerBoundsY + innerBoundsHeight - 1,
innerBoundsX + innerBoundsWidth - innerBoundsArcWidth/2,
innerBoundsY + innerBoundsHeight - 1);
SwingUtil.restoreAntiAliasing(gfx, aaState);
_label.render(gfx, _bounds.x + PADDING + _arcWidth/2 -
(_pressed ? 2 : 1),
_bounds.y + PADDING + (_pressed ? 1 : 0));
break;
}
if (!_enabled) {
_label.setTextColor(baseTextColor);
_label.setAlternateColor(baseAlternateColor);
}
}
/** The number of pixels to add between the text and the border. */
protected static final int PADDING = 2;
/** The label associated with this sprite. */
protected Label _label;
/** The button style. */
protected int _style;
/** The width of the corner arcs for rounded rectangle buttons. */
protected int _arcWidth;
/** The height of the corner arcs for rounded rectangle buttons. */
protected int _arcHeight;
/** The action command generated by this button. */
protected String _actionCommand;
/** The command argument generated by this button. */
protected Object _commandArgument;
/** The background color of this sprite. */
protected Color _backgroundColor;
/** The alternate (outline) color of this sprite. */
protected Color _alternateColor;
/** Whether or not the button is currently enabled. */
protected boolean _enabled = true;
/** Whether or not the button is currently pressed. */
protected boolean _pressed;
}
@@ -0,0 +1,203 @@
package com.threerings.media.sprite;
import java.awt.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.Composite;
import com.threerings.media.util.Path;
public class FadableImageSprite extends OrientableImageSprite
{
/**
* Fades this sprite in over the specified duration after
* waiting for the specified delay.
*/
public void fadeIn (long delay, long duration)
{
setAlpha(0.0f);
_fadeStamp = 0;
_fadeDelay = delay;
_fadeInDuration = duration;
}
/**
* Puts this sprite on the specified path and fades it in over
* the specified duration.
*
* @param path the path to move along
* @param fadePortion the portion of time to spend fading in, from 0.0f
* (no time) to 1.0f (the entire time)
*/
public void moveAndFadeIn (Path path, long pathDuration, float fadePortion)
{
move(path);
setAlpha(0.0f);
_fadeInDuration = (long)(pathDuration*fadePortion);
}
/**
* Puts this sprite on the specified path and fades it out over
* the specified duration.
*
* @param path the path to move along
* @param pathDuration the duration of the path
* @param fadePortion the portion of time to spend fading out, from 0.0f
* (no time) to 1.0f (the entire time)
*/
public void moveAndFadeOut (Path path, long pathDuration, float fadePortion)
{
move(path);
setAlpha(1.0f);
_pathDuration = pathDuration;
_fadeOutDuration = (long)(pathDuration*fadePortion);
}
/**
* Puts this sprite on the specified path, fading it in over the specified
* duration at the beginning and fading it out at the end.
*
* @param path the path to move along
* @param pathDuration the duration of the path
* @param fadePortion the portion of time to spend fading in/out, from
* 0.0f (no time) to 1.0f (the entire time)
*/
public void moveAndFadeInAndOut (Path path, long pathDuration,
float fadePortion)
{
move(path);
setAlpha(0.0f);
_pathDuration = pathDuration;
_fadeInDuration = _fadeOutDuration = (long)(pathDuration*fadePortion);
}
// Documentation inherited.
public void tick (long tickStamp)
{
super.tick(tickStamp);
if (_fadeInDuration != -1) {
if (_path != null && (tickStamp-_pathStamp) <= _fadeInDuration) {
// fading in while moving
float alpha = (float)(tickStamp-_pathStamp)/_fadeInDuration;
if (alpha >= 1.0f) {
// fade-in complete
setAlpha(1.0f);
_fadeInDuration = -1;
} else {
setAlpha(alpha);
}
} else {
// fading in while stationary
if (_fadeStamp == 0) {
// store the time at which fade started
_fadeStamp = tickStamp;
}
if (tickStamp > _fadeStamp + _fadeDelay) {
// initial delay has passed
float alpha = (float)(tickStamp-_fadeStamp-_fadeDelay)/
_fadeInDuration;
if (alpha >= 1.0f) {
// fade-in complete
setAlpha(1.0f);
_fadeInDuration = -1;
} else {
setAlpha(alpha);
}
}
}
} else if (_fadeOutDuration != -1 && _pathStamp+_pathDuration-tickStamp
<= _fadeOutDuration) {
// fading out while moving
float alpha = (float)(_pathStamp+_pathDuration-tickStamp)/
_fadeOutDuration;
setAlpha(alpha);
}
}
// Documentation inherited.
public void pathCompleted (long timestamp)
{
super.pathCompleted(timestamp);
if (_fadeInDuration != -1) {
setAlpha(1.0f);
_fadeInDuration = -1;
} else if (_fadeOutDuration != -1) {
setAlpha(0.0f);
_fadeOutDuration = -1;
}
}
// Documentation inherited.
public void paint (Graphics2D gfx)
{
if (_alphaComposite.getAlpha() < 1.0f) {
Composite ocomp = gfx.getComposite();
gfx.setComposite(_alphaComposite);
super.paint(gfx);
gfx.setComposite(ocomp);
} else {
super.paint(gfx);
}
}
/**
* Sets the alpha value of this sprite.
*/
public void setAlpha (float alpha)
{
if (alpha < 0.0f) {
alpha = 0.0f;
} else if (alpha > 1.0f) {
alpha = 1.0f;
}
if (alpha != _alphaComposite.getAlpha()) {
_alphaComposite =
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha);
if (_mgr != null) {
_mgr.getRegionManager().invalidateRegion(_bounds);
}
}
}
/**
* Returns the alpha value of this sprite.
*/
public float getAlpha ()
{
return _alphaComposite.getAlpha();
}
/** The alpha composite. */
protected AlphaComposite _alphaComposite =
AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 1.0f);
/** If fading in, the fade-in duration (otherwise -1). */
protected long _fadeInDuration = -1;
/** If fading in without moving, the fade-in delay. */
protected long _fadeDelay;
/** The time at which fading started. */
protected long _fadeStamp = -1;
/** If fading out, the fade-out duration (otherwise -1). */
protected long _fadeOutDuration = -1;
/** If fading out, the path duration. */
protected long _pathDuration;
}
@@ -0,0 +1,331 @@
//
// $Id: ImageSprite.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.sprite;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import com.threerings.media.image.Mirage;
import com.threerings.media.util.MultiFrameImage;
import com.threerings.media.util.SingleFrameImageImpl;
/**
* Extends the sprite class to support rendering the sprite with one or
* more frames of image animation. Overrides various methods to provide
* correspondingly desirable functionality, e.g., {@link #hitTest} only
* reports a hit if the specified point is within a non-transparent pixel
* for the sprite's current image frame.
*/
public class ImageSprite extends Sprite
{
/** Default frame rate. */
public static final int DEFAULT_FRAME_RATE = 15;
/** Animation mode indicating no animation. */
public static final int NO_ANIMATION = 0;
/** Animation mode indicating movement cued animation. */
public static final int MOVEMENT_CUED = 1;
/** Animation mode indicating time based animation. */
public static final int TIME_BASED = 2;
/** Animation mode indicating sequential progressive animation.
* Frame 0 is guaranteed to be shown first for the full duration, and
* so on. */
public static final int TIME_SEQUENTIAL = 3;
/**
* Constructs an image sprite without any associated frames and with
* an invalid default initial location. The sprite should be populated
* with a set of frames used to display it via a subsequent call to
* {@link #setFrames}, and its location updated with {@link
* #setLocation}.
*/
public ImageSprite ()
{
this((MultiFrameImage)null);
}
/**
* Constructs an image sprite.
*
* @param frames the multi-frame image used to display the sprite.
*/
public ImageSprite (MultiFrameImage frames)
{
// initialize frame animation member data
_frames = frames;
_frameIdx = 0;
_animMode = NO_ANIMATION;
_frameDelay = 1000L/DEFAULT_FRAME_RATE;
}
/**
* Constructs an image sprite that will display the supplied single
* image when rendering itself.
*/
public ImageSprite (Mirage image)
{
this(new SingleFrameImageImpl(image));
}
// documentation inherited
protected void init ()
{
super.init();
// now that we have our sprite manager, we can lay ourselves out
// and initialize our frames
layout();
}
/**
* Returns true if the sprite's bounds contain the specified point,
* and if there is a non-transparent pixel in the sprite's image at
* the specified point, false if not.
*/
public boolean hitTest (int x, int y)
{
// first check to see that we're in the sprite's bounds and that
// we've got a frame image (if we've got no image, there's nothing
// to be hit)
if (!super.hitTest(x, y) || _frames == null) {
return false;
}
return _frames.hitTest(_frameIdx, x - _bounds.x, y - _bounds.y);
}
/**
* Sets the animation mode for this sprite. The available modes are:
*
* <ul>
* <li><code>TIME_BASED</code>: cues the animation based on a target
* frame rate (specified via {@link #setFrameRate}).
* <li><code>MOVEMENT_CUED</code>: ticks the animation to the next
* frame every time the sprite is moved along its path.
* <li><code>NO_ANIMATION</code>: disables animation.
* </ul>
*
* @param mode the desired animation mode.
*/
public void setAnimationMode (int mode)
{
_animMode = mode;
}
/**
* Sets the number of frames per second desired for the sprite
* animation. This is only used when the animation mode is
* <code>TIME_BASED</code>.
*
* @param fps the desired frames per second.
*/
public void setFrameRate (float fps)
{
_frameDelay = (long)(1000/fps);
}
/**
* Set the image to be used for this sprite.
*/
public void setMirage (Mirage mirage)
{
setFrames(new SingleFrameImageImpl(mirage));
}
/**
* Set the image array used to render the sprite.
*
* @param frames the sprite images.
*/
public void setFrames (MultiFrameImage frames)
{
if (frames == null) {
// Log.warning("Someone set up us the null frames! " +
// "[sprite=" + this + "].");
return;
}
// if these are the same frames we already had, no need to do a
// bunch of pointless business
if (frames == _frames) {
return;
}
// set and init our frames
_frames = frames;
_frameIdx = 0;
layout();
}
/**
* Instructs this sprite to lay out its current frame and any
* accoutrements.
*/
public void layout ()
{
if (_frames != null) {
setFrameIndex(_frameIdx, true);
}
}
/**
* Instructs the sprite to display the specified frame index.
*/
protected void setFrameIndex (int frameIdx, boolean forceUpdate)
{
// make sure we're displaying a valid frame
frameIdx = (frameIdx % _frames.getFrameCount());
// if this is the same frame we're already displaying and we're
// not being forced to update, we can stop now
if (frameIdx == _frameIdx && !forceUpdate) {
return;
} else {
_frameIdx = frameIdx;
}
// start with our old bounds
Rectangle dirty = new Rectangle(_bounds);
// determine our drawing offsets and rendered rectangle size
accomodateFrame(_frameIdx, _frames.getWidth(_frameIdx),
_frames.getHeight(_frameIdx));
// add our new bounds
dirty.add(_bounds);
// give the dirty rectangle to the region manager
if (_mgr != null) {
_mgr.getRegionManager().addDirtyRegion(dirty);
}
}
/**
* Must adjust the bounds to accomodate the our new frame. This
* includes changing the width and height to reflect the size of the
* new frame and also updating the render origin (if necessary) and
* calling {@link #updateRenderOrigin} to reflect those changes in the
* sprite's bounds.
*
* @param frameIdx the index of our new frame.
* @param width the width of the new frame.
* @param height the height of the new frame.
*/
protected void accomodateFrame (int frameIdx, int width, int height)
{
_bounds.width = width;
_bounds.height = height;
}
// documentation inherited
public void paint (Graphics2D gfx)
{
if (_frames != null) {
// // DEBUG: fill our background with an alpha'd rectangle
// Composite ocomp = gfx.getComposite();
// gfx.setComposite(ALPHA_BOUNDS);
// gfx.setColor(Color.blue);
// gfx.fill(_bounds);
// gfx.setComposite(ocomp);
// render our frame
_frames.paintFrame(gfx, _frameIdx, _bounds.x, _bounds.y);
} else {
super.paint(gfx);
}
}
// documentation inherited
public void tick (long timestamp)
{
// if we have no frames, we're hosulated (to use a Greenwell term)
if (_frames == null) {
return;
}
int fcount = _frames.getFrameCount();
boolean moved = false;
// move the sprite along toward its destination, if any
moved = tickPath(timestamp);
// increment the display image if performing image animation
int nfidx = _frameIdx;
switch (_animMode) {
case NO_ANIMATION:
// nothing doing
break;
case TIME_BASED:
nfidx = (int)((timestamp/_frameDelay) % fcount);
break;
case TIME_SEQUENTIAL:
if (_firstStamp == 0L) {
_firstStamp = timestamp;
}
nfidx = (int) (((timestamp - _firstStamp) / _frameDelay) % fcount);
break;
case MOVEMENT_CUED:
// update the frame if the sprite moved
if (moved) {
nfidx = (_frameIdx + 1) % fcount;
}
break;
}
// update our frame (which will do nothing if this is the same as
// our existing frame index)
setFrameIndex(nfidx, false);
}
// documentation inherited
protected void toString (StringBuilder buf)
{
super.toString(buf);
buf.append(", fidx=").append(_frameIdx);
}
/** The images used to render the sprite. */
protected MultiFrameImage _frames;
/** The current frame index to render. */
protected int _frameIdx;
/** What type of animation is desired for this sprite. */
protected int _animMode;
/** For how many milliseconds to display an animation frame. */
protected long _frameDelay;
/** The first timestamp seen (in TIME_SEQUENTIAL mode). */
protected long _firstStamp = 0L;
// /** DEBUG: The alpha level used when rendering our bounds. */
// protected static final Composite ALPHA_BOUNDS =
// AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.2f);
}
@@ -0,0 +1,115 @@
//
// $Id: LabelSprite.java 3542 2005-05-06 03:01:43Z 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.sprite;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import com.samskivert.swing.Label;
/**
* A sprite that uses a label to render itself. If the label has not been
* previously laid out (see {@link Label#layout}) it will be done when the
* sprite is added to a media panel. If the label is altered after the
* sprite is created, {@link #updateBounds} should be called.
*/
public class LabelSprite extends Sprite
{
/**
* Constructs a label sprite that renders itself with the specified
* label.
*/
public LabelSprite (Label label)
{
_label = label;
}
/**
* Returns the label displayed by this sprite.
*/
public Label getLabel ()
{
return _label;
}
/**
* Indicates that our label should be rendered with antialiased text.
*/
public void setAntiAliased (boolean antiAliased)
{
_antiAliased = antiAliased;
}
/**
* Updates the bounds of the sprite after a change to the label.
*/
public void updateBounds ()
{
Dimension size = _label.getSize();
_bounds.width = size.width;
_bounds.height = size.height;
}
// documentation inherited
protected void init ()
{
super.init();
// if our label is not yet laid out, do the deed
if (!_label.isLaidOut()) {
layoutLabel();
}
// size the bounds to fit our label
updateBounds();
}
// documentation inherited
public void paint (Graphics2D gfx)
{
_label.render(gfx, _bounds.x, _bounds.y);
}
/**
* Lays out our underlying label which must be done if the text is
* changed.
*/
protected void layoutLabel ()
{
Graphics2D gfx = (Graphics2D)_mgr.getMediaPanel().getGraphics();
if (gfx != null) {
gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
(_antiAliased) ?
RenderingHints.VALUE_ANTIALIAS_ON :
RenderingHints.VALUE_ANTIALIAS_OFF);
_label.layout(gfx);
gfx.dispose();
}
}
/** The label associated with this sprite. */
protected Label _label;
/** Whether or not to use anti-aliased rendering. */
protected boolean _antiAliased;
}
@@ -0,0 +1,198 @@
//
// $Id: OrientableImageSprite.java 3586 2005-06-05 16:41:10Z 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.sprite;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import java.awt.image.*;
import com.threerings.media.image.Mirage;
import com.threerings.media.util.MultiFrameImage;
/**
* An image sprite that uses AWT's rotation methods to render itself in
* different orientations.
*/
public class OrientableImageSprite extends ImageSprite
{
/**
* Creates a new orientable image sprite.
*/
public OrientableImageSprite ()
{}
/**
* Creates a new orientable image sprite.
*
* @param image the image to render
*/
public OrientableImageSprite (Mirage image)
{
super(image);
}
/**
* Creates a new orientable image sprite.
*
* @param frames the frames to render
*/
public OrientableImageSprite (MultiFrameImage frames)
{
super(frames);
}
/**
* Computes and returns the rotation transform for this
* sprite.
*
* @return the newly computed rotation transform
*/
private AffineTransform getRotationTransform ()
{
double theta;
switch (_orient) {
case NORTH:
default:
theta = 0.0;
break;
case SOUTH:
theta = Math.PI;
break;
case EAST:
theta = Math.PI*0.5;
break;
case WEST:
theta = -Math.PI*0.5;
break;
case NORTHEAST:
theta = Math.PI*0.25;
break;
case NORTHWEST:
theta = -Math.PI*0.25;
break;
case SOUTHEAST:
theta = Math.PI*0.75;
break;
case SOUTHWEST:
theta = -Math.PI*0.75;
break;
case NORTHNORTHEAST:
theta = -Math.PI*0.125;
break;
case NORTHNORTHWEST:
theta = Math.PI*0.125;
break;
case SOUTHSOUTHEAST:
theta = -Math.PI*0.875;
break;
case SOUTHSOUTHWEST:
theta = Math.PI*0.875;
break;
case EASTNORTHEAST:
theta = -Math.PI*0.375;
break;
case EASTSOUTHEAST:
theta = -Math.PI*0.625;
break;
case WESTNORTHWEST:
theta = Math.PI*0.375;
break;
case WESTSOUTHWEST:
theta = Math.PI*0.625;
break;
}
return AffineTransform.getRotateInstance(
theta,
(_ox - _oxoff) + _frames.getWidth(_frameIdx)/2,
(_oy - _oyoff) + _frames.getHeight(_frameIdx)/2
);
}
// Documentation inherited.
protected void accomodateFrame (int frameIdx, int width, int height)
{
Area area = new Area(
new Rectangle(
(_ox - _oxoff),
(_oy - _oyoff),
width,
height
)
);
area.transform(getRotationTransform());
_bounds = area.getBounds();
}
// Documentation inherited.
public void setOrientation (int orient)
{
super.setOrientation(orient);
layout();
}
// Documentation inherited.
public void paint (Graphics2D graphics)
{
AffineTransform at = graphics.getTransform();
graphics.transform(getRotationTransform());
if (_frames != null) {
_frames.paintFrame(
graphics,
_frameIdx,
_ox - _oxoff,
_oy - _oyoff
);
}
else {
super.paint(graphics);
}
graphics.setTransform(at);
}
}
@@ -0,0 +1,40 @@
//
// $Id: PathAdapter.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.sprite;
import com.threerings.media.util.Path;
/**
* An adapter class for {@link PathObserver}.
*/
public class PathAdapter implements PathObserver
{
// documentation inherited from interface
public void pathCancelled (Sprite sprite, Path path)
{
}
// documentation inherited from interface
public void pathCompleted (Sprite sprite, Path path, long when)
{
}
}
@@ -0,0 +1,49 @@
//
// $Id: PathObserver.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.sprite;
import com.threerings.media.util.Path;
/**
* An interface to be implemented by classes that would like to be
* notified when a sprite completes or cancels its path.
*/
public interface PathObserver
{
/**
* Called when a sprite's path is cancelled either because a new path
* was started or the path was explicitly cancelled with {@link
* Sprite#cancelMove}.
*/
public void pathCancelled (Sprite sprite, Path path);
/**
* Called when a sprite completes its traversal of a path.
*
* @param sprite the sprite that completed its path.
* @param path the path that was completed.
* @param when the tick stamp of the media tick on which the path was
* completed (see {@link SpriteManager#tick}) (this may not be in the
* same time domain as {@link System#currentTimeMillis}).
*/
public void pathCompleted (Sprite sprite, Path path, long when);
}
@@ -0,0 +1,462 @@
//
// $Id: Sprite.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.sprite;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
import com.samskivert.util.ObserverList;
import com.threerings.util.DirectionCodes;
import com.threerings.media.AbstractMedia;
import com.threerings.media.util.Path;
import com.threerings.media.util.Pathable;
import com.threerings.media.Log;
/**
* The sprite class represents a single moveable object in an animated
* view. A sprite has a position and orientation within the view, and can
* be moved along a path.
*/
public abstract class Sprite extends AbstractMedia
implements DirectionCodes, Pathable
{
/**
* Constructs a sprite with an initially invalid location. Because
* sprite derived classes generally want to get in on the business
* when a sprite's location is set, it is not safe to do so in the
* constructor because their derived methods will be called before
* their constructor has been called. Thus a sprite should be fully
* constructed and <em>then</em> its location should be set.
*/
public Sprite ()
{
this(0, 0);
}
/**
* Constructs a sprite with the supplied dimensions. Because
* sprite derived classes generally want to get in on the business
* when a sprite's location is set, it is not safe to do so in the
* constructor because their derived methods will be called before
* their constructor has been called. Thus a sprite should be fully
* constructed and <em>then</em> its location should be set.
*/
public Sprite (int width, int height)
{
super(new Rectangle(0, 0, width, height));
}
/**
* Returns the sprite's x position in screen coordinates. This is the
* x coordinate of the sprite's origin, not the upper left of its
* bounds.
*/
public int getX ()
{
return _ox;
}
/**
* Returns the sprite's y position in screen coordinates. This is the
* y coordinate of the sprite's origin, not the upper left of its
* bounds.
*/
public int getY ()
{
return _oy;
}
/**
* Returns the offset to the sprite's origin from the upper-left of
* the sprite's image.
*/
public int getXOffset ()
{
return _oxoff;
}
/**
* Returns the offset to the sprite's origin from the upper-left of
* the sprite's image.
*/
public int getYOffset ()
{
return _oyoff;
}
/**
* Returns the sprite's width in pixels.
*/
public int getWidth ()
{
return _bounds.width;
}
/**
* Returns the sprite's height in pixels.
*/
public int getHeight ()
{
return _bounds.height;
}
/**
* Sprites have an orientation in one of the eight cardinal
* directions: <code>NORTH</code>, <code>NORTHEAST</code>, etc.
* Derived classes can choose to override this member function and
* select a different set of images based on their orientation, or
* they can ignore the orientation information.
*
* @see DirectionCodes
*/
public void setOrientation (int orient)
{
_orient = orient;
}
/**
* Returns the sprite's orientation as one of the eight cardinal
* directions: <code>NORTH</code>, <code>NORTHEAST</code>, etc.
*
* @see DirectionCodes
*/
public int getOrientation ()
{
return _orient;
}
// documentation inherited
public void setLocation (int x, int y)
{
if (x == _ox && y == _oy) {
return; // no-op
}
// start with our current bounds
Rectangle dirty = new Rectangle(_bounds);
// move ourselves
_ox = x;
_oy = y;
// we need to update our draw position which is based on the size
// of our current bounds
updateRenderOrigin();
// grow the dirty rectangle to incorporate our new bounds and pass
// the dirty region to our region manager
if (_mgr != null) {
// if our new bounds intersect our old bounds, grow a single
// dirty rectangle to incorporate them both
if (_bounds.intersects(dirty)) {
dirty.add(_bounds);
} else {
// otherwise invalidate our new bounds separately
_mgr.getRegionManager().invalidateRegion(_bounds);
}
_mgr.getRegionManager().addDirtyRegion(dirty);
}
}
// documentation inherited
public void paint (Graphics2D gfx)
{
gfx.drawRect(_bounds.x, _bounds.y, _bounds.width-1, _bounds.height-1);
}
/**
* Paint the sprite's path, if any, to the specified graphics context.
*
* @param gfx the graphics context.
*/
public void paintPath (Graphics2D gfx)
{
if (_path != null) {
_path.paint(gfx);
}
}
/**
* Returns true if the sprite's bounds contain the specified point,
* false if not.
*/
public boolean contains (int x, int y)
{
return _bounds.contains(x, y);
}
/**
* Returns true if the sprite's bounds contain the specified point,
* false if not.
*/
public boolean hitTest (int x, int y)
{
return _bounds.contains(x, y);
}
/**
* Returns whether the sprite is inside the given shape in pixel
* coordinates.
*/
public boolean inside (Shape shape)
{
return shape.contains(_ox, _oy);
}
/**
* Returns whether the sprite's drawn rectangle intersects the given
* shape in pixel coordinates.
*/
public boolean intersects (Shape shape)
{
return shape.intersects(_bounds);
}
/**
* Returns true if this sprite is currently following a path, false if
* it is not.
*/
public boolean isMoving ()
{
return (_path != null);
}
/**
* Set the sprite's active path and start moving it along its merry
* way. If the sprite is already moving along a previous path the old
* path will be lost and the new path will begin to be traversed.
*
* @param path the path to follow.
*/
public void move (Path path)
{
// if there's a previous path, let it know that it's going away
cancelMove();
// save off this path
_path = path;
// we'll initialize it on our next tick thanks to a zero path stamp
_pathStamp = 0;
}
/**
* Cancels any path that the sprite may currently be moving along.
*/
public void cancelMove ()
{
if (_path != null) {
Path oldpath = _path;
_path = null;
oldpath.wasRemoved(this);
if (_observers != null) {
_observers.apply(new CancelledOp(this, oldpath));
}
}
}
/**
* Returns the path being followed by this sprite or null if the
* sprite is not following a path.
*/
public Path getPath ()
{
return _path;
}
/**
* Called by the active path when it begins.
*/
public void pathBeginning ()
{
// nothing for now
}
/**
* Called by the active path when it has completed.
*/
public void pathCompleted (long timestamp)
{
Path oldpath = _path;
_path = null;
oldpath.wasRemoved(this);
if (_observers != null) {
_observers.apply(new CompletedOp(this, oldpath, timestamp));
}
}
// documentation inherited
public void tick (long tickStamp)
{
tickPath(tickStamp);
}
/**
* Ticks any path assigned to this sprite.
*
* @return true if the path relocated the sprite as a result of this
* tick, false if it remained in the same position.
*/
protected boolean tickPath (long tickStamp)
{
if (_path == null) {
return false;
}
// initialize the path if we haven't yet
if (_pathStamp == 0) {
_path.init(this, _pathStamp = tickStamp);
}
// it's possible that as a result of init() the path completed and
// removed itself with a call to pathCompleted(), so we have to be
// careful here
return (_path == null) ? true : _path.tick(this, tickStamp);
}
// documentation inherited
public void fastForward (long timeDelta)
{
// fast forward any path we're following
if (_path != null) {
_path.fastForward(timeDelta);
}
}
/**
* Update the coordinates at which the sprite image is drawn to
* reflect the sprite's current position.
*/
protected void updateRenderOrigin ()
{
// our bounds origin may differ from the sprite's origin
_bounds.x = _ox - _oxoff;
_bounds.y = _oy - _oyoff;
}
/**
* Add a sprite observer to observe this sprite's events.
*
* @param obs the sprite observer.
*/
public void addSpriteObserver (Object obs)
{
addObserver(obs);
}
/**
* Remove a sprite observer.
*/
public void removeSpriteObserver (Object obs)
{
removeObserver(obs);
}
// documentation inherited
public void viewLocationDidChange (int dx, int dy)
{
if (_renderOrder >= HUD_LAYER) {
setLocation(_ox + dx, _oy + dy);
}
}
// documentation inherited
protected void shutdown ()
{
super.shutdown();
cancelMove(); // cancel any active path
}
// documentation inherited
protected void toString (StringBuilder buf)
{
super.toString(buf);
buf.append(", ox=").append(_ox);
buf.append(", oy=").append(_oy);
buf.append(", oxoff=").append(_oxoff);
buf.append(", oyoff=").append(_oyoff);
}
/** Used to dispatch {@link PathObserver#pathCancelled}. */
protected static class CancelledOp implements ObserverList.ObserverOp
{
public CancelledOp (Sprite sprite, Path path) {
_sprite = sprite;
_path = path;
}
public boolean apply (Object observer) {
if (observer instanceof PathObserver) {
((PathObserver)observer).pathCancelled(_sprite, _path);
}
return true;
}
protected Sprite _sprite;
protected Path _path;
}
/** Used to dispatch {@link PathObserver#pathCompleted}. */
protected static class CompletedOp implements ObserverList.ObserverOp
{
public CompletedOp (Sprite sprite, Path path, long when) {
_sprite = sprite;
_path = path;
_when = when;
}
public boolean apply (Object observer) {
if (observer instanceof PathObserver) {
((PathObserver)observer).pathCompleted(_sprite, _path, _when);
}
return true;
}
protected Sprite _sprite;
protected Path _path;
protected long _when;
}
/** The location of the sprite's origin in pixel coordinates. If the
* sprite positions itself via a hotspot that is not the upper left
* coordinate of the sprite's bounds, the offset to the hotspot should
* be maintained in {@link #_oxoff} and {@link #_oyoff}. */
protected int _ox = Integer.MIN_VALUE, _oy = Integer.MIN_VALUE;
/** The offsets from our upper left coordinate to our origin (or hot
* spot). Derived classes will need to update these values if the
* sprite's origin is not coincident with the upper left coordinate of
* its bounds. */
protected int _oxoff, _oyoff;
/** The orientation of this sprite. */
protected int _orient = NONE;
/** When moving, the path the sprite is traversing. */
protected Path _path;
/** The timestamp at which we started along our path. */
protected long _pathStamp;
}
@@ -0,0 +1,94 @@
//
// $Id: SpriteIcon.java 3310 2005-01-24 23:08:21Z 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.sprite;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.Icon;
/**
* Implements the icon interface, using a {@link Sprite} to render the
* icon image.
*/
public class SpriteIcon implements Icon
{
/**
* Creates a sprite icon that will use the supplied sprite to render
* itself. This sprite should not be used for anything else while
* being used in this icon because it will be "moved" when the icon is
* rendered. The sprite's origin will be set to the bottom center of
* the label.
*/
public SpriteIcon (Sprite sprite)
{
this(sprite, 0);
}
/**
* Creates a sprite icon that will use the supplied sprite to render
* itself. This sprite should not be used for anything else while
* being used in this icon because it will be "moved" when the icon is
* rendered. The sprite's origin will be set to the bottom center of
* the label.
*
* @param sprite the sprite to render in this label.
* @param padding the number of pixels of blank space to put on all
* four sides of the sprite.
*/
public SpriteIcon (Sprite sprite, int padding)
{
_sprite = sprite;
// the sprite should be ticked once so that we can safely paint it
_sprite.tick(System.currentTimeMillis());
_padding = padding;
}
// documentation inherited from interface
public void paintIcon (Component c, Graphics g, int x, int y)
{
// move the sprite to a "location" that results in its image being
// in the upper left of the rectangle we desire
_sprite.setLocation(x + _sprite.getXOffset() + _padding,
y + _sprite.getYOffset() + _padding);
_sprite.paint((Graphics2D)g);
}
// documentation inherited from interface
public int getIconWidth ()
{
return _sprite.getWidth() + 2*_padding;
}
// documentation inherited from interface
public int getIconHeight ()
{
return _sprite.getHeight() + 2*_padding;
}
/** The sprite used to render this icon. */
protected Sprite _sprite;
/** Used to put a bit of padding around the sprite image. */
protected int _padding;
}
@@ -0,0 +1,269 @@
//
// $Id: SpriteManager.java 3733 2005-10-13 19:00:08Z 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.sprite;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.util.Collections;
import java.util.List;
import java.util.Iterator;
import com.threerings.media.AbstractMediaManager;
import com.threerings.media.MediaPanel;
/**
* The sprite manager manages the sprites running about in the game.
*/
public class SpriteManager extends AbstractMediaManager
{
/** A predicate used to operate on sprites (see {@link #removeSprites}. */
public static interface Predicate
{
/** Returns true if this sprite is to be included by the predicate,
* false if it should be excluded. */
public boolean evaluate (Sprite sprite);
}
/**
* Construct and initialize the sprite manager.
*/
public SpriteManager (MediaPanel panel)
{
super(panel);
}
/**
* When an animated view processes its dirty rectangles, it may
* require an expansion of the dirty region which may in turn
* require the invalidation of more sprites than were originally
* invalid. In such cases, the animated view can call back to the
* sprite manager, asking it to append the sprites that intersect
* a particular region to the given list.
*
* @param list the list to fill with any intersecting sprites.
* @param shape the shape in which we have interest.
*/
public void getIntersectingSprites (List list, Shape shape)
{
int size = _media.size();
for (int ii = 0; ii < size; ii++) {
Sprite sprite = (Sprite)_media.get(ii);
if (sprite.intersects(shape)) {
list.add(sprite);
}
}
}
/**
* When an animated view is determining what entity in its view is
* under the mouse pointer, it may require a list of sprites that are
* "hit" by a particular pixel. The sprites' bounds are first checked
* and sprites with bounds that contain the supplied point are further
* checked for a non-transparent at the specified location.
*
* @param list the list to fill with any intersecting sprites, the
* sprites with the highest render order provided first.
* @param x the x (screen) coordinate to be checked.
* @param y the y (screen) coordinate to be checked.
*/
public void getHitSprites (List list, int x, int y)
{
for (int ii = _media.size() - 1; ii >= 0; ii--) {
Sprite sprite = (Sprite)_media.get(ii);
if (sprite.hitTest(x, y)) {
list.add(sprite);
}
}
}
/**
* Finds the sprite with the highest render order that hits the
* specified pixel.
*
* @param x the x (screen) coordinate to be checked
* @param y the y (screen) coordinate to be checked
* @return the highest sprite hit
*/
public Sprite getHighestHitSprite (int x, int y)
{
// since they're stored in lowest -> highest order..
for (int ii = _media.size() - 1; ii >= 0; ii--) {
Sprite sprite = (Sprite)_media.get(ii);
if (sprite.hitTest(x, y)) {
return sprite;
}
}
return null;
}
/**
* Add a sprite to the set of sprites managed by this manager.
*
* @param sprite the sprite to add.
*/
public void addSprite (Sprite sprite)
{
if (insertMedia(sprite)) {
// and invalidate the sprite's original position
sprite.invalidate();
}
}
/**
* Returns a list of all sprites registered with the sprite manager.
* The returned list is immutable, sprites should be added or removed
* using {@link #addSprite} or {@link #removeSprite}.
*/
public List getSprites ()
{
return Collections.unmodifiableList(_media);
}
/**
* Returns an iterator over our managed sprites. Do not call
* {@link Iterator#remove}.
*/
public Iterator enumerateSprites ()
{
return _media.iterator();
}
/**
* Removes the specified sprite from the set of sprites managed by
* this manager.
*
* @param sprite the sprite to remove.
*/
public void removeSprite (Sprite sprite)
{
removeMedia(sprite);
}
/**
* Removes all sprites that match the supplied predicate.
*/
public void removeSprites (Predicate pred)
{
int idxoff = 0;
for (int ii = 0, ll = _media.size(); ii < ll; ii++) {
Sprite sprite = (Sprite)_media.get(ii-idxoff);
if (pred.evaluate(sprite)) {
_media.remove(sprite);
sprite.invalidate();
sprite.shutdown();
// we need to preserve the original "index" relative to the
// current tick position, so we don't decrement ii directly
idxoff++;
if (ii <= _tickpos) {
_tickpos--;
}
}
}
}
/**
* Render the sprite paths to the given graphics context.
*
* @param gfx the graphics context.
*/
public void renderSpritePaths (Graphics2D gfx)
{
for (int ii=0, nn=_media.size(); ii < nn; ii++) {
Sprite sprite = (Sprite)_media.get(ii);
sprite.paintPath(gfx);
}
}
// NOTE- collision handling code is turned off for now. To re-implement,
// a new array should be kept with sprites sorted in some sort of x/y order
//
// /**
// * Check all sprites for collisions with others and inform any
// * sprite observers.
// */
// protected void handleCollisions ()
// {
// // gather a list of all sprite collisions
// int size = _sprites.size();
// for (int ii = 0; ii < size; ii++) {
// Sprite sprite = (Sprite)_sprites.get(ii);
// checkCollisions(ii, size, sprite);
// }
// }
//
// /**
// * Check a sprite for collision with any other sprites in the
// * sprite list and notify the sprite observers associated with any
// * sprites that do indeed collide.
// *
// * @param idx the starting sprite index.
// * @param size the total number of sprites.
// * @param sprite the sprite to check against other sprites for
// * collisions.
// */
// protected void checkCollisions (int idx, int size, Sprite sprite)
// {
// // TODO: make this handle quickly moving objects that may pass
// // through each other.
//
// // if we're the last sprite we know we've already handled any
// // collisions
// if (idx == (size - 1)) {
// return;
// }
//
// // calculate the x-position of the right edge of the sprite we're
// // checking for collisions
// Rectangle bounds = sprite.getBounds();
// int edgeX = bounds.x + bounds.width;
//
// for (int ii = (idx + 1); ii < size; ii++) {
// Sprite other = (Sprite)_sprites.get(ii);
// Rectangle obounds = other.getBounds();
// if (obounds.x > edgeX) {
// // since sprites are stored in the list sorted by
// // ascending x-position, we know this sprite and any
// // other sprites farther on in the list can't possibly
// // intersect with the sprite we're checking, so we're
// // done.
// return;
//
// } else if (obounds.intersects(bounds)) {
// sprite.notifyObservers(new CollisionEvent(sprite, other));
// }
// }
// }
// /** The comparator used to sort sprites by horizontal position. */
// protected static final Comparator SPRITE_COMP = new SpriteComparator();
//
// /** Used to sort sprites. */
// protected static class SpriteComparator implements Comparator
// {
// public int compare (Object o1, Object o2)
// {
// Sprite s1 = (Sprite)o1;
// Sprite s2 = (Sprite)o2;
// return (s2.getX() - s1.getX());
// }
// }
}
@@ -0,0 +1,16 @@
//
// $Id$
package com.threerings.media.sprite.action;
/**
* An Action sprite is a sprite that may be pressed to generate an
* ActionEvent that will be posted to the Controller hierarchy.
*/
public interface ActionSprite
{
/**
* @return the action command to submit if this sprite is clicked.
*/
public String getActionCommand ();
}
@@ -0,0 +1,16 @@
//
// $Id$
package com.threerings.media.sprite.action;
/**
* An ActionSprite that wishes to be notified of events when it is armed
* or not.
*/
public interface ArmingSprite extends ActionSprite
{
/**
* Render this sprite such that is is drawn "armed".
*/
public void setArmed (boolean armed);
}
@@ -0,0 +1,16 @@
//
// $Id$
package com.threerings.media.sprite.action;
/**
* Extends CommandSprite to be a sprite that posts CommandEvents to
* the Controller hierarchy.
*/
public interface CommandSprite extends ActionSprite
{
/**
* @return the argument to the action command.
*/
public Object getCommandArgument();
}
@@ -0,0 +1,16 @@
//
// $Id$
package com.threerings.media.sprite.action;
/**
* Indicates a Sprite that may or may not be enabled to receive
* action / hover / arming notifications.
*/
public interface DisableableSprite
{
/**
* @return true if this sprite is currently enabled.
*/
public boolean isEnabled ();
}
@@ -0,0 +1,16 @@
//
// $Id$
package com.threerings.media.sprite.action;
/**
* An interface indicating that a sprite wishes to be notified when
* the mouse hovers over it.
*/
public interface HoverSprite
{
/**
* Set the current hover state.
*/
public void setHovered (boolean hovered);
}
@@ -0,0 +1,73 @@
//
// $Id: IMImageProvider.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.tile;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import com.threerings.media.image.Colorization;
import com.threerings.media.image.ImageDataProvider;
import com.threerings.media.image.ImageManager;
import com.threerings.media.image.Mirage;
/**
* Provides images to a tileset given a reference to the image manager and
* an image data provider.
*/
public class IMImageProvider implements ImageProvider
{
public IMImageProvider (ImageManager imgr, ImageDataProvider dprov)
{
_imgr = imgr;
_dprov = dprov;
}
public IMImageProvider (ImageManager imgr, String rset)
{
_imgr = imgr;
_rset = rset;
}
// documentation inherited from interface
public BufferedImage getTileSetImage (String path, Colorization[] zations)
{
return _imgr.getImage(getImageKey(path), zations);
}
// documentation inherited from interface
public Mirage getTileImage (String path, Rectangle bounds,
Colorization[] zations)
{
return _imgr.getMirage(getImageKey(path), bounds, zations);
}
protected final ImageManager.ImageKey getImageKey (String path)
{
return (_dprov == null) ?
_imgr.getImageKey(_rset, path) :
_imgr.getImageKey(_dprov, path);
}
protected ImageManager _imgr;
protected ImageDataProvider _dprov;
protected String _rset;
}
@@ -0,0 +1,64 @@
//
// $Id: ImageProvider.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.tile;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import com.threerings.media.image.Colorization;
import com.threerings.media.image.ImageManager;
import com.threerings.media.image.Mirage;
/**
* Provides a generic interface via which tileset images may be loaded. In
* most cases, a running application will want to obtain images via the
* {@link ImageManager}, but in some circumstances a simpler image
* provider may be desirable to avoid the overhead of the image manager
* infrastructure when simple image loading is all that is desired.
*/
public interface ImageProvider
{
/**
* Returns the raw tileset image with the specified path.
*
* @param path the path that identifies the desired image (corresponds
* to the image path from the tileset).
* @param zations if non-null, colorizations to apply to the source
* image before returning it.
*/
public BufferedImage getTileSetImage (String path, Colorization[] zations);
/**
* Obtains the tile image with the specified path in the form of a
* {@link Mirage}. It should be cropped from the tileset image
* identified by the supplied path.
*
* @param path the path that identifies the desired image (corresponds
* to the image path from the tileset).
* @param bounds if non-null, the region of the image to be returned
* as a mirage. If null, the entire image should be returned.
* @param zations if non-null, colorizations to apply to the image
* before converting it into a mirage.
*/
public Mirage getTileImage (String path, Rectangle bounds,
Colorization[] zations);
}
@@ -0,0 +1,39 @@
//
// $Id: NoSuchTileSetException.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.tile;
/**
* Thrown when an attempt is made to retrieve a non-existent tile set from
* the tile set manager.
*/
public class NoSuchTileSetException extends Exception
{
public NoSuchTileSetException (String tileSetName)
{
super("No tile set named '" + tileSetName + "'");
}
public NoSuchTileSetException (int tileSetId)
{
super("No tile set with id '" + tileSetId + "'");
}
}
@@ -0,0 +1,232 @@
//
// $Id: ObjectTile.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.tile;
import java.awt.Dimension;
import java.awt.Point;
import com.samskivert.util.ListUtil;
import com.samskivert.util.StringUtil;
import com.threerings.util.DirectionUtil;
/**
* An object tile extends the base tile to provide support for objects
* whose image spans more than one unit tile.
*
* <p> An object tile is generally positioned based on its origin rather
* than the upper left of its image. Generally this origin is in the
* bottom center of the object image, but can be configured to be anywhere
* that the natural center point of "contact" is for the object. Note that
* this does not automatically adjust the semantics of {@link #paint}, it
* is just expected that the caller will account for the object tile's
* origin when painting, if appropriate.
*
* <p> An object tile has dimensions (in tile units) that represent its
* footprint or "shadow".
*/
public class ObjectTile extends Tile
{
/**
* Returns the object footprint width in tile units.
*/
public int getBaseWidth ()
{
return _base.width;
}
/**
* Returns the object footprint height in tile units.
*/
public int getBaseHeight ()
{
return _base.height;
}
/**
* Sets the object footprint in tile units.
*/
protected void setBase (int width, int height)
{
_base.width = width;
_base.height = height;
}
/**
* Returns the x offset into the tile image of the origin (which will
* be aligned with the bottom center of the origin tile) or
* <code>Integer.MIN_VALUE</code> if the origin is not explicitly
* specified and should be computed from the image size and tile
* footprint.
*/
public int getOriginX ()
{
return _origin.x;
}
/**
* Returns the y offset into the tile image of the origin (which will
* be aligned with the bottom center of the origin tile) or
* <code>Integer.MIN_VALUE</code> if the origin is not explicitly
* specified and should be computed from the image size and tile
* footprint.
*/
public int getOriginY ()
{
return _origin.y;
}
/**
* Sets the offset in pixels from the origin of the tile image to the
* origin of the object. The object will be rendered such that its
* origin is at the bottom center of its origin tile. If no origin is
* specified, the bottom of the image is aligned with the bottom of
* the origin tile and the left side of the image is aligned with the
* left edge of the left-most base tile.
*/
protected void setOrigin (int x, int y)
{
_origin.x = x;
_origin.y = y;
}
/**
* Returns this object tile's default render priority.
*/
public int getPriority ()
{
return _priority;
}
/**
* Sets this object tile's default render priority.
*/
protected void setPriority (int priority)
{
_priority = priority;
}
/**
* Configures the "spot" associated with this object.
*/
public void setSpot (int x, int y, byte orient)
{
_spot = new Point(x, y);
_sorient = orient;
}
/**
* Returns true if this object has a spot.
*/
public boolean hasSpot ()
{
return (_spot != null);
}
/**
* Returns the x-coordinate of the "spot" associated with this object.
*/
public int getSpotX ()
{
return (_spot == null) ? 0 : _spot.x;
}
/**
* Returns the x-coordinate of the "spot" associated with this object.
*/
public int getSpotY ()
{
return (_spot == null) ? 0 : _spot.y;
}
/**
* Returns the orientation of the "spot" associated with this object.
*/
public int getSpotOrient ()
{
return _sorient;
}
/**
* Returns the list of constraints associated with this object, or
* <code>null</code> if the object has no constraints.
*/
public String[] getConstraints ()
{
return _constraints;
}
/**
* Checks whether this object has the given constraint.
*/
public boolean hasConstraint (String constraint)
{
return (_constraints == null) ? false :
ListUtil.contains(_constraints, constraint);
}
/**
* Configures this object's constraints.
*/
public void setConstraints (String[] constraints)
{
_constraints = constraints;
}
// documentation inherited
public void toString (StringBuilder buf)
{
super.toString(buf);
buf.append(", base=").append(StringUtil.toString(_base));
buf.append(", origin=").append(StringUtil.toString(_origin));
buf.append(", priority=").append(_priority);
if (_spot != null) {
buf.append(", spot=").append(StringUtil.toString(_spot));
buf.append(", sorient=");
buf.append(DirectionUtil.toShortString(_sorient));
}
if (_constraints != null) {
buf.append(", constraints=").append(StringUtil.toString(
_constraints));
}
}
/** The object footprint width in unit tile units. */
protected Dimension _base = new Dimension(1, 1);
/** The offset from the origin of the tile image to the object's
* origin or MIN_VALUE if the origin should be calculated based on the
* footprint. */
protected Point _origin = new Point(Integer.MIN_VALUE, Integer.MIN_VALUE);
/** This object tile's default render priority. */
protected int _priority;
/** The coordinates of the "spot" associated with this object. */
protected Point _spot;
/** The orientation of the "spot" associated with this object. */
protected byte _sorient;
/** The list of constraints associated with this object. */
protected String[] _constraints;
}
@@ -0,0 +1,295 @@
//
// $Id: ObjectTileSet.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.tile;
import com.samskivert.util.ListUtil;
import com.samskivert.util.StringUtil;
import com.threerings.media.image.Colorization;
/**
* The object tileset supports the specification of object information for
* object tiles in addition to all of the features of the swiss army
* tileset.
*
* @see ObjectTile
*/
public class ObjectTileSet extends SwissArmyTileSet
implements RecolorableTileSet
{
/** A constraint prefix indicating that the object must have empty space in
* the suffixed direction (N, E, S, or W). */
public static final String SPACE = "SPACE_";
/** A constraint indicating that the object is a surface (e.g., table). */
public static final String SURFACE = "SURFACE";
/** A constraint indicating that the object must be placed on a surface. */
public static final String ON_SURFACE = "ON_SURFACE";
/** A constraint prefix indicating that the object is a wall facing the
* suffixed direction (N, E, S, or W). */
public static final String WALL = "WALL_";
/** A constraint prefix indicating that the object must be placed on a
* wall facing the suffixed direction (N, E, S, or W). */
public static final String ON_WALL = "ON_WALL_";
/** A constraint prefix indicating that the object must be attached to a
* wall facing the suffixed direction (N, E, S, or W). */
public static final String ATTACH = "ATTACH_";
/** The low suffix for walls and attachments. Low attachments can be placed
* on low or normal walls; normal attachments can only be placed on normal
* walls. */
public static final String LOW = "_LOW";
/**
* Sets the widths (in unit tile count) of the objects in this
* tileset. This must be accompanied by a call to {@link
* #setObjectHeights}.
*/
public void setObjectWidths (int[] objectWidths)
{
_owidths = objectWidths;
}
/**
* Sets the heights (in unit tile count) of the objects in this
* tileset. This must be accompanied by a call to {@link
* #setObjectWidths}.
*/
public void setObjectHeights (int[] objectHeights)
{
_oheights = objectHeights;
}
/**
* Sets the x offset in pixels to the image origin.
*/
public void setXOrigins (int[] xorigins)
{
_xorigins = xorigins;
}
/**
* Sets the y offset in pixels to the image origin.
*/
public void setYOrigins (int[] yorigins)
{
_yorigins = yorigins;
}
/**
* Sets the default render priorities for our object tiles.
*/
public void setPriorities (byte[] priorities)
{
_priorities = priorities;
}
/**
* Provides a set of colorization classes that apply to objects in
* this tileset.
*/
public void setColorizations (String[] zations)
{
_zations = zations;
}
/**
* Sets the x offset to the "spots" associated with our object tiles.
*/
public void setXSpots (short[] xspots)
{
_xspots = xspots;
}
/**
* Sets the y offset to the "spots" associated with our object tiles.
*/
public void setYSpots (short[] yspots)
{
_yspots = yspots;
}
/**
* Sets the orientation of the "spots" associated with our object
* tiles.
*/
public void setSpotOrients (byte[] sorients)
{
_sorients = sorients;
}
/**
* Sets the lists of constraints associated with our object tiles.
*/
public void setConstraints (String[][] constraints)
{
_constraints = constraints;
}
/**
* Returns the x coordinate of the spot associated with the specified
* tile index.
*/
public int getXSpot (int tileIdx)
{
return (_xspots == null) ? 0 : _xspots[tileIdx];
}
/**
* Returns the y coordinate of the spot associated with the specified
* tile index.
*/
public int getYSpot (int tileIdx)
{
return (_yspots == null) ? 0 : _yspots[tileIdx];
}
/**
* Returns the orientation of the spot associated with the specified
* tile index.
*/
public int getSpotOrient (int tileIdx)
{
return (_sorients == null) ? 0 : _sorients[tileIdx];
}
/**
* Returns the list of constraints associated with the specified tile
* index, or <code>null</code> if the index has no constraints.
*/
public String[] getConstraints (int tileIdx)
{
return (_constraints == null) ? null : _constraints[tileIdx];
}
/**
* Checks whether the tile at the specified index has the given constraint.
*/
public boolean hasConstraint (int tileIdx, String constraint)
{
return (_constraints == null) ? false :
ListUtil.contains(_constraints[tileIdx], constraint);
}
// documentation inherited from interface RecolorableTileSet
public String[] getColorizations ()
{
return _zations;
}
// documentation inherited
protected void toString (StringBuilder buf)
{
super.toString(buf);
buf.append(", owidths=").append(StringUtil.toString(_owidths));
buf.append(", oheights=").append(StringUtil.toString(_oheights));
buf.append(", xorigins=").append(StringUtil.toString(_xorigins));
buf.append(", yorigins=").append(StringUtil.toString(_yorigins));
buf.append(", prios=").append(StringUtil.toString(_priorities));
buf.append(", zations=").append(StringUtil.toString(_zations));
buf.append(", xspots=").append(StringUtil.toString(_xspots));
buf.append(", yspots=").append(StringUtil.toString(_yspots));
buf.append(", sorients=").append(StringUtil.toString(_sorients));
buf.append(", constraints=").append(StringUtil.toString(_constraints));
}
// documentation inherited
protected Colorization[] getColorizations (int tileIndex, Colorizer rizer)
{
Colorization[] zations = null;
if (rizer != null && _zations != null) {
zations = new Colorization[_zations.length];
for (int ii = 0; ii < _zations.length; ii++) {
zations[ii] = rizer.getColorization(ii, _zations[ii]);
}
}
return zations;
}
// documentation inherited
protected Tile createTile ()
{
return new ObjectTile();
}
// documentation inherited
protected void initTile (Tile tile, int tileIndex, Colorization[] zations)
{
super.initTile(tile, tileIndex, zations);
ObjectTile otile = (ObjectTile)tile;
if (_owidths != null) {
otile.setBase(_owidths[tileIndex], _oheights[tileIndex]);
}
if (_xorigins != null) {
otile.setOrigin(_xorigins[tileIndex], _yorigins[tileIndex]);
}
if (_priorities != null) {
otile.setPriority(_priorities[tileIndex]);
}
if (_xspots != null) {
otile.setSpot(_xspots[tileIndex], _yspots[tileIndex],
_sorients[tileIndex]);
}
if (_constraints != null) {
otile.setConstraints(_constraints[tileIndex]);
}
}
/** The width (in tile units) of our object tiles. */
protected int[] _owidths;
/** The height (in tile units) of our object tiles. */
protected int[] _oheights;
/** The x offset in pixels to the origin of the tile images. */
protected int[] _xorigins;
/** The y offset in pixels to the origin of the tile images. */
protected int[] _yorigins;
/** The default render priorities of our objects. */
protected byte[] _priorities;
/** Colorization classes that apply to our objects. */
protected String[] _zations;
/** The x offset to the "spots" associated with our tiles. */
protected short[] _xspots;
/** The y offset to the "spots" associated with our tiles. */
protected short[] _yspots;
/** The orientation of the "spots" associated with our tiles. */
protected byte[] _sorients;
/** Lists of constraints associated with our tiles. */
protected String[][] _constraints;
/** Increase this value when object's serialized state is impacted by
* a class change (modification of fields, inheritance). */
private static final long serialVersionUID = 2;
}
@@ -0,0 +1,16 @@
//
// $Id: RecolorableTileSet.java 4191 2006-06-13 22:42:20Z ray $
package com.threerings.media.tile;
/**
* Indicates that a tileset has recolorization classes defined.
*/
public interface RecolorableTileSet
{
/**
* Returns the colorization classes that should be used to recolor
* objects in this tileset.
*/
public String[] getColorizations ();
}
@@ -0,0 +1,77 @@
//
// $Id: SimpleCachingImageProvider.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.tile;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.io.IOException;
import com.samskivert.util.LRUHashMap;
import com.threerings.media.Log;
import com.threerings.media.image.BufferedMirage;
import com.threerings.media.image.Colorization;
import com.threerings.media.image.Mirage;
/**
* An image provider that can be used by command line tools to load images
* and provide them to tilesets when doing things like preprocessing
* tileset images.
*/
public abstract class SimpleCachingImageProvider implements ImageProvider
{
// documentation inherited from interface
public BufferedImage getTileSetImage (String path, Colorization[] zations)
{
BufferedImage image = (BufferedImage)_cache.get(path);
if (image == null) {
try {
image = loadImage(path);
_cache.put(path, image);
} catch (IOException ioe) {
Log.warning("Failed to load image [path=" + path +
", ioe=" + ioe + "].");
}
}
return image;
}
// documentation inherited from interface
public Mirage getTileImage (String path, Rectangle bounds,
Colorization[] zations)
{
// mostly fake it
BufferedImage tsimg = getTileSetImage(path, zations);
tsimg = tsimg.getSubimage(bounds.x, bounds.y,
bounds.width, bounds.height);
return new BufferedMirage(tsimg);
}
/**
* Derived classes must implement this method to actually load the raw
* source images.
*/
protected abstract BufferedImage loadImage (String path)
throws IOException;
protected LRUHashMap _cache = new LRUHashMap(10);
}
@@ -0,0 +1,207 @@
//
// $Id: SwissArmyTileSet.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.tile;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.Rectangle;
import java.io.IOException;
import java.io.ObjectInputStream;
import com.samskivert.util.StringUtil;
/**
* The swiss army tileset supports a diverse variety of tiles in the
* tileset image. Each row can contain varying numbers of tiles and each
* row can have its own width and height. Tiles can be separated from the
* edge of the tileset image by some border offset and can be separated
* from one another by a gap distance.
*/
public class SwissArmyTileSet extends TileSet
{
// documentation inherited
public int getTileCount ()
{
return _numTiles;
}
/**
* Sets the tile counts which are the number of tiles in each row of
* the tileset image. Each row can have an arbitrary number of tiles.
*/
public void setTileCounts (int[] tileCounts)
{
_tileCounts = tileCounts;
// compute our total tile count
computeTileCount();
}
/**
* Returns the tile count settings.
*/
public int[] getTileCounts ()
{
return _tileCounts;
}
/**
* Computes our total tile count from the individual counts for each
* row.
*/
protected void computeTileCount ()
{
// compute our number of tiles
_numTiles = 0;
for (int i = 0; i < _tileCounts.length; i++) {
_numTiles += _tileCounts[i];
}
}
/**
* Sets the tile widths for each row. Each row can have tiles of a
* different width.
*/
public void setWidths (int[] widths)
{
_widths = widths;
}
/**
* Returns the width settings.
*/
public int[] getWidths ()
{
return _widths;
}
/**
* Sets the tile heights for each row. Each row can have tiles of a
* different height.
*/
public void setHeights (int[] heights)
{
_heights = heights;
}
/**
* Returns the height settings.
*/
public int[] getHeights ()
{
return _heights;
}
/**
* Sets the offset in pixels of the upper left corner of the first
* tile in the first row. If the tileset image has a border, this can
* be set to account for it.
*/
public void setOffsetPos (Point offsetPos)
{
_offsetPos = offsetPos;
}
/**
* Sets the size of the gap between tiles (in pixels). If the tiles
* have space between them, this can be set to account for it.
*/
public void setGapSize (Dimension gapSize)
{
_gapSize = gapSize;
}
// documentation inherited
protected void toString (StringBuilder buf)
{
super.toString(buf);
buf.append(", widths=").append(StringUtil.toString(_widths));
buf.append(", heights=").append(StringUtil.toString(_heights));
buf.append(", tileCounts=").append(StringUtil.toString(_tileCounts));
buf.append(", offsetPos=").append(StringUtil.toString(_offsetPos));
buf.append(", gapSize=").append(StringUtil.toString(_gapSize));
}
// documentation inherited
protected Rectangle computeTileBounds (int tileIndex)
{
// find the row number containing the sought-after tile
int ridx, tcount, ty, tx;
ridx = tcount = 0;
// start tile image position at image start offset
tx = _offsetPos.x;
ty = _offsetPos.y;
while ((tcount += _tileCounts[ridx]) < tileIndex + 1) {
// increment tile image position by row height and gap distance
ty += (_heights[ridx++] + _gapSize.height);
}
// determine the horizontal index of this tile in the row
int xidx = tileIndex - (tcount - _tileCounts[ridx]);
// final image x-position is based on tile width and gap distance
tx += (xidx * (_widths[ridx] + _gapSize.width));
// Log.info("Computed tile bounds [tileIndex=" + tileIndex +
// ", ridx=" + ridx + ", xidx=" + xidx +
// ", tx=" + tx + ", ty=" + ty + "].");
// crop the tile-sized image chunk from the full image
return new Rectangle(tx, ty, _widths[ridx], _heights[ridx]);
}
private void readObject (ObjectInputStream in)
throws IOException, ClassNotFoundException
{
in.defaultReadObject();
// compute our total tile count
computeTileCount();
}
/** The number of tiles in each row. */
protected int[] _tileCounts;
/** The number of tiles in the tileset. */
protected int _numTiles;
/** The width of the tiles in each row in pixels. */
protected int[] _widths;
/** The height of the tiles in each row in pixels. */
protected int[] _heights;
/** The offset distance (x, y) in pixels from the top-left of the
* image to the start of the first tile image. */
protected Point _offsetPos = new Point();
/** The distance (x, y) in pixels between each tile in each row
* horizontally, and between each row of tiles vertically. */
protected Dimension _gapSize = new Dimension();
/** Increase this value when object's serialized state is impacted by
* a class change (modification of fields, inheritance). */
private static final long serialVersionUID = 1;
}
@@ -0,0 +1,178 @@
//
// $Id: Tile.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.tile;
import java.awt.Graphics2D;
import java.util.Arrays;
import com.threerings.media.image.Colorization;
import com.threerings.media.image.Mirage;
/**
* A tile represents a single square in a single layer in a scene.
*/
public class Tile // implements Cloneable
{
/** Used when caching tiles. */
public static class Key
{
public TileSet tileSet;
public int tileIndex;
public Colorization[] zations;
public Key (TileSet tileSet, int tileIndex, Colorization[] zations) {
this.tileSet = tileSet;
this.tileIndex = tileIndex;
this.zations = zations;
}
public boolean equals (Object other) {
if (other instanceof Key) {
Key okey = (Key)other;
return (tileSet == okey.tileSet &&
tileIndex == okey.tileIndex &&
Arrays.equals(zations, okey.zations));
} else {
return false;
}
}
public int hashCode () {
int code = (tileSet == null) ? tileIndex :
(tileSet.hashCode() ^ tileIndex);
int zcount = (zations == null) ? 0 : zations.length;
for (int ii = 0; ii < zcount; ii++) {
if (zations[ii] != null) {
code ^= zations[ii].hashCode();
}
}
return code;
}
}
/** The key associated with this tile. */
public Key key;
/**
* Configures this tile with its tile image.
*/
public void setImage (Mirage image)
{
if (_mirage != null) {
_totalTileMemory -= _mirage.getEstimatedMemoryUsage();
}
_mirage = image;
if (_mirage != null) {
_totalTileMemory += _mirage.getEstimatedMemoryUsage();
}
}
/**
* Returns the width of this tile.
*/
public int getWidth ()
{
return _mirage.getWidth();
}
/**
* Returns the height of this tile.
*/
public int getHeight ()
{
return _mirage.getHeight();
}
/**
* Returns the estimated memory usage of our underlying tile image.
*/
public long getEstimatedMemoryUsage ()
{
return _mirage.getEstimatedMemoryUsage();
}
/**
* Render the tile image at the specified position in the given
* graphics context.
*/
public void paint (Graphics2D gfx, int x, int y)
{
_mirage.paint(gfx, x, y);
}
/**
* Returns true if the specified coordinates within this tile contains
* a non-transparent pixel.
*/
public boolean hitTest (int x, int y)
{
return _mirage.hitTest(x, y);
}
// /**
// * Creates a shallow copy of this tile object.
// */
// public Object clone ()
// {
// try {
// return (Tile)super.clone();
// } catch (CloneNotSupportedException cnse) {
// String errmsg = "All is wrong with the universe: " + cnse;
// throw new RuntimeException(errmsg);
// }
// }
/**
* Return a string representation of this tile.
*/
public String toString ()
{
StringBuilder buf = new StringBuilder("[");
toString(buf);
return buf.append("]").toString();
}
/**
* This should be overridden by derived classes (which should be sure
* to call <code>super.toString()</code>) to append the derived class
* specific tile information to the string buffer.
*/
protected void toString (StringBuilder buf)
{
buf.append(_mirage.getWidth()).append("x");
buf.append(_mirage.getHeight());
}
/** Decrement total tile memory by our value. */
protected void finalize ()
{
if (_mirage != null) {
_totalTileMemory -= _mirage.getEstimatedMemoryUsage();
}
}
/** Our tileset image. */
protected Mirage _mirage;
/** Used to track total (estimated) memory in use by tiles. */
protected static long _totalTileMemory = 0L;
}
@@ -0,0 +1,64 @@
//
// $Id: TileIcon.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.tile;
import java.awt.Component;
import java.awt.Graphics2D;
import java.awt.Graphics;
import javax.swing.Icon;
/**
* Implements the icon interface, using a {@link Tile} to render the icon
* image.
*/
public class TileIcon implements Icon
{
/**
* Creates a tile icon that will use the supplied tile to render itself.
*/
public TileIcon (Tile tile)
{
_tile = tile;
}
// documentation inherited from interface
public void paintIcon (Component c, Graphics g, int x, int y)
{
_tile.paint((Graphics2D)g, x, y);
}
// documentation inherited from interface
public int getIconWidth ()
{
return _tile.getWidth();
}
// documentation inherited from interface
public int getIconHeight ()
{
return _tile.getHeight();
}
/** The tile used to render this icon. */
protected Tile _tile;
}
@@ -0,0 +1,247 @@
//
// $Id: TileManager.java 3392 2005-03-10 01:30:34Z 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.tile;
import java.lang.ref.SoftReference;
import java.util.HashMap;
import com.samskivert.io.PersistenceException;
import com.threerings.media.Log;
import com.threerings.media.image.ImageManager;
/**
* The tile manager provides a simplified interface for retrieving and
* caching tiles. Tiles can be loaded in two different ways. An
* application can load a tileset by hand, specifying the path to the
* tileset image and all of the tileset metadata necessary for extracting
* the image tiles, or it can provide a tileset repository which loads up
* tilesets using whatever repository mechanism is implemented by the
* supplied repository. In the latter case, tilesets are loaded by a
* unique identifier.
*
* <p> Loading tilesets by hand is intended for things like toolbar icons
* or games with a single set of tiles (think Stratego, for example).
* Loading tilesets from a repository supports games with vast numbers of
* tiles to which more tiles may be added on the fly (think the tiles for
* an isometric-display graphical MUD).
*/
public class TileManager
{
/**
* Creates a tile manager and provides it with a reference to the
* image manager from which it will load tileset images.
*
* @param imgr the image manager via which the tile manager will
* decode and cache images.
*/
public TileManager (ImageManager imgr)
{
_imgr = imgr;
_defaultProvider = new IMImageProvider(_imgr, (String)null);
}
/**
* Loads up a tileset from the specified image with the specified
* metadata parameters.
*/
public UniformTileSet loadTileSet (String imgPath, int width, int height)
{
return loadCachedTileSet("", imgPath, width, height);
}
/**
* Loads up a tileset from the specified image (located in the
* specified resource set) with the specified metadata parameters.
*/
public UniformTileSet loadTileSet (
String rset, String imgPath, int width, int height)
{
return loadTileSet(
getImageProvider(rset), rset, imgPath, width, height);
}
/**
*/
public UniformTileSet loadTileSet (
ImageProvider improv, String improvKey, String imgPath,
int width, int height)
{
UniformTileSet uts = loadCachedTileSet(
improvKey, imgPath, width, height);
uts.setImageProvider(improv);
return uts;
}
/**
* Returns an image provider that will load images from the specified
* resource set.
*/
public ImageProvider getImageProvider (String rset)
{
return new IMImageProvider(_imgr, rset);
}
/**
* Used to load and cache tilesets loaded via {@link #loadTileSet}.
*/
protected UniformTileSet loadCachedTileSet (
String bundle, String imgPath, int width, int height)
{
String key = bundle + "::" + imgPath;
SoftReference ref = (SoftReference) _handcache.get(key);
UniformTileSet uts = (ref == null) ? null
: (UniformTileSet) ref.get();
if (uts == null) {
uts = new UniformTileSet();
uts.setImageProvider(_defaultProvider);
uts.setImagePath(imgPath);
uts.setWidth(width);
uts.setHeight(height);
_handcache.put(key, new SoftReference(uts));
}
return uts;
}
/**
* Sets the tileset repository that will be used by the tile manager
* when tiles are requested by tileset id.
*/
public void setTileSetRepository (TileSetRepository setrep)
{
_setrep = setrep;
}
/**
* Returns the tileset repository currently in use.
*/
public TileSetRepository getTileSetRepository ()
{
return _setrep;
}
/**
* Returns the tileset with the specified id. Tilesets are fetched
* from the tileset repository supplied via {@link
* #setTileSetRepository}, and are subsequently cached.
*
* @param tileSetId the unique identifier for the desired tileset.
*
* @exception NoSuchTileSetException thrown if no tileset exists with
* the specified id or if an underlying error occurs with the tileset
* repository's persistence mechanism.
*/
public TileSet getTileSet (int tileSetId)
throws NoSuchTileSetException
{
// make sure we have a repository configured
if (_setrep == null) {
throw new NoSuchTileSetException(tileSetId);
}
try {
return _setrep.getTileSet(tileSetId);
} catch (PersistenceException pe) {
Log.warning("Failure loading tileset [id=" + tileSetId +
", error=" + pe + "].");
throw new NoSuchTileSetException(tileSetId);
}
}
/**
* Returns the tileset with the specified name.
*
* @throws NoSuchTileSetException if no tileset with the specified
* name is available via our configured tile set repository.
*/
public TileSet getTileSet (String name)
throws NoSuchTileSetException
{
// make sure we have a repository configured
if (_setrep == null) {
throw new NoSuchTileSetException(name);
}
try {
return _setrep.getTileSet(name);
} catch (PersistenceException pe) {
Log.warning("Failure loading tileset [name=" + name +
", error=" + pe + "].");
throw new NoSuchTileSetException(name);
}
}
/**
* Returns the {@link Tile} object with the specified fully qualified
* tile id.
*
* @see TileUtil#getFQTileId
*/
public Tile getTile (int fqTileId)
throws NoSuchTileSetException
{
return getTile(TileUtil.getTileSetId(fqTileId),
TileUtil.getTileIndex(fqTileId), null);
}
/**
* Returns the {@link Tile} object with the specified fully qualified
* tile id. The supplied colorizer will be used to recolor the tile.
*
* @see TileUtil#getFQTileId
*/
public Tile getTile (int fqTileId, TileSet.Colorizer rizer)
throws NoSuchTileSetException
{
return getTile(TileUtil.getTileSetId(fqTileId),
TileUtil.getTileIndex(fqTileId), rizer);
}
/**
* Returns the {@link Tile} object from the specified tileset at the
* specified index.
*
* @param tileSetId the tileset id.
* @param tileIndex the index of the tile to be retrieved.
*
* @return the tile object.
*/
public Tile getTile (int tileSetId, int tileIndex, TileSet.Colorizer rizer)
throws NoSuchTileSetException
{
TileSet set = getTileSet(tileSetId);
return set.getTile(tileIndex, rizer);
}
/** The entity through which we decode and cache images. */
protected ImageManager _imgr;
/** A cache of tilesets that have been loaded by hand. */
protected HashMap _handcache = new HashMap();
/** The tile set repository. */
protected TileSetRepository _setrep;
/** Used to load tileset images from the default resource source. */
protected ImageProvider _defaultProvider;
}

Some files were not shown because too many files have changed in this diff Show More