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:
@@ -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;
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user