Split Nenya into proper Maven submodules (same treatment Narya got).

This commit is contained in:
Michael Bayne
2012-02-27 11:46:22 -08:00
parent 90146d517d
commit 5e2380eb24
645 changed files with 358 additions and 283 deletions
@@ -0,0 +1,33 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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;
import com.samskivert.util.Logger;
/**
* Contains a reference to the log object used by this library.
*/
public class NenyaLog
{
/** We dispatch our log messages through this logger. */
public static Logger log = Logger.getLogger("com.threerings.nenya");
}
@@ -0,0 +1,39 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
/**
* A mechanism for caching composited character action animations on disk.
*/
public interface ActionCache
{
/**
* Fetches from the cache a composited set of images for a particular character for a
* particular action.
*/
public ActionFrames getActionFrames (CharacterDescriptor descrip, String action);
/**
* Requests that the specified set of action frames for the specified character be cached.
*/
public void cacheActionFrames (CharacterDescriptor descrip, String action, ActionFrames frames);
}
@@ -0,0 +1,69 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
import com.threerings.util.DirectionCodes;
import com.threerings.media.image.Colorization;
/**
* Encapsulates a set of frames in each of {@link DirectionCodes#DIRECTION_COUNT} orientations
* that are used to render a character sprite.
*/
public interface ActionFrames
{
/**
* Returns the number of orientations available in this set of action frames.
*/
public int getOrientationCount ();
/**
* Returns the multi-frame image that comprises the frames for the specified orientation.
*/
public TrimmedMultiFrameImage getFrames (int orient);
/**
* Returns the x offset from the upper left of the image to the "origin" for this character
* frame. A sprite with location (x, y) will be rendered such that its origin is coincident
* with that location.
*/
public int getXOrigin (int orient, int frameIdx);
/**
* Returns the y offset from the upper left of the image to the "origin" for this character
* frame. A sprite with location (x, y) will be rendered such that its origin is coincident
* with that location.
*/
public int getYOrigin (int orient, int frameIdx);
/**
* Creates a clone of these action frames which will have the supplied colorizations applied
* to the frame images.
*/
public ActionFrames cloneColorized (Colorization[] zations);
/**
* Creates a clone of these action frames which will have the supplied translation applied to
* the frame images.
*/
public ActionFrames cloneTranslated (int dx, int dy);
}
@@ -0,0 +1,72 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
import java.io.Serializable;
import java.awt.Point;
/**
* The action sequence class describes a particular character animation
* sequence. An animation sequence consists of one or more frames of
* animation, renders at a particular frame rate, and has an origin point
* that specifies the location of the base of the character in relation to
* the bounds of the animation images.
*/
public class ActionSequence implements Serializable
{
/**
* Defines the name of the default action sequence. When component
* tilesets are loaded to build a set of composited images for a
* particular action sequence, a check is first made for a component
* tileset specific to the action sequence and then for the
* component's default tileset if the action specific tileset did not
* exist.
*/
public static final String DEFAULT_SEQUENCE = "default";
/** The action sequence name. */
public String name;
/** The number of frames per second to show when animating. */
public float framesPerSecond;
/** The position of the character's base for this sequence. */
public Point origin = new Point();
/** Orientation codes for the orientations available for this
* action. */
public int[] orients;
@Override
public String toString ()
{
return "[name=" + name + ", framesPerSecond=" + framesPerSecond +
", origin=" + origin +
", orients=" + (orients == null ? 0 : orients.length) + "]";
}
/** 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,34 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
import com.samskivert.util.PrefsConfig;
/**
* Provides access to runtime configuration parameters for this package and its subpackages.
*/
public class CastPrefs
{
/** Used to load our preferences from a properties file and map them to the persistent Java
* preferences repository. */
public static PrefsConfig config = new PrefsConfig("rsrc/config/cast");
}
@@ -0,0 +1,123 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
import java.util.Set;
import java.io.Serializable;
import com.samskivert.util.StringUtil;
import com.threerings.media.sprite.Sprite;
/**
* The character component represents a single component that can be composited with other
* character components to generate an image representing a complete character displayable in any
* of the eight compass directions as detailed in the {@link Sprite} class direction constants.
*/
public class CharacterComponent implements Serializable
{
/** The unique component identifier. */
public int componentId;
/** The component's name. */
public String name;
/** The class of components to which this one belongs. */
public ComponentClass componentClass;
/**
* Constructs a character component with the specified id of the specified class.
*/
public CharacterComponent (
int componentId, String name, ComponentClass compClass, FrameProvider fprov)
{
this.componentId = componentId;
this.name = name;
this.componentClass = compClass;
_frameProvider = fprov;
}
/**
* Returns the render priority appropriate for this component at the specified action and
* orientation.
*/
public int getRenderPriority (String action, int orientation)
{
return componentClass.getRenderPriority(action, name, orientation);
}
/**
* Returns the image frames for the specified action animation or null if no animation for the
* specified action is available for this component.
*
* @param type null for the normal action frames or one of the custom action sub-types:
* {@link StandardActions#SHADOW_TYPE}, etc.
*/
public ActionFrames getFrames (String action, String type)
{
return _frameProvider.getFrames(this, action, type);
}
/**
* Returns the path to the image frames for the specified action animation or null if no
* animation for the specified action is available for this component.
*
* @param type null for the normal action frames or one of the custom action sub-types:
* {@link StandardActions#SHADOW_TYPE}, etc.
*
* @param existentPaths the set of all paths for which there are valid frames.
*/
public String getFramePath (String action, String type, Set<String> existentPaths)
{
return _frameProvider.getFramePath(this, action, type, existentPaths);
}
@Override
public boolean equals (Object other)
{
if (other instanceof CharacterComponent) {
return componentId == ((CharacterComponent)other).componentId;
} else {
return false;
}
}
@Override
public int hashCode ()
{
return componentId;
}
@Override
public String toString ()
{
return StringUtil.fieldsToString(this);
}
/** The entity from which we obtain our animation frames. */
protected FrameProvider _frameProvider;
/** 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,162 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
import java.util.Arrays;
import java.awt.Point;
import com.samskivert.util.StringUtil;
import com.threerings.media.image.Colorization;
/**
* The character descriptor object details the components that are
* pieced together to create a single character image.
*/
public class CharacterDescriptor
{
/**
* Constructs a character descriptor.
*
* @param components the component ids of the individual components
* that make up this character.
* @param zations the colorizations to apply to each of the character
* component images when compositing actions (an array of
* colorizations for each component, elements of which can be null to
* make it easier to support per-component specialized colorizations).
* This can be null if image recolorization is not desired.
*/
public CharacterDescriptor (int[] components, Colorization[][] zations)
{
_components = components;
_zations = zations;
}
/**
* Returns an array of the component identifiers comprising the
* character described by this descriptor.
*/
public int[] getComponentIds ()
{
return _components;
}
/**
* Returns an array of colorization arrays to be applied to the
* components when compositing action images (one array per
* component).
*/
public Colorization[][] getColorizations ()
{
return _zations;
}
/**
* Updates the colorizations to be used by this character descriptor.
*/
public void setColorizations (Colorization[][] zations)
{
_zations = zations;
}
/**
* Returns the array of translations to be applied to the components
* when compositing action images.
*/
public Point[] getTranslations ()
{
return _xlations;
}
/**
* Updates the translations to be used by this character descriptor.
*/
public void setTranslations (Point[] xlations)
{
_xlations = xlations;
}
@Override
public int hashCode ()
{
int code = 0, clength = _components.length;
for (int ii = 0; ii < clength; ii++) {
code ^= _components[ii];
}
return code;
}
@Override
public boolean equals (Object other)
{
if (!(other instanceof CharacterDescriptor)) {
return false;
}
// both the component ids and the colorizations must be equal
CharacterDescriptor odesc = (CharacterDescriptor)other;
if (!Arrays.equals(_components, odesc._components)) {
return false;
}
Colorization[][] zations = odesc._zations;
if (zations == null && _zations == null) {
// if neither has colorizations, we're clear
return true;
} else if (zations == null || _zations == null) {
// if one has colorizations whilst the other doesn't, they
// can't be equal
return false;
}
// otherwise, all of the colorizations must be equal as well
int zlength = zations.length;
if (zlength != _zations.length) {
return false;
}
for (int ii = 0; ii < zlength; ii++) {
if (!Arrays.equals(_zations[ii], zations[ii])) {
return false;
}
}
return Arrays.equals(_xlations, odesc._xlations);
}
@Override
public String toString ()
{
return "[cids=" + StringUtil.toString(_components) +
", colors=" + StringUtil.toString(_zations) + "]";
}
/** The component identifiers comprising the character. */
protected int[] _components;
/** The colorizations to apply when compositing this character. */
protected Colorization[][] _zations;
/** The translations to apply when compositing this character. */
protected Point[] _xlations;
}
@@ -0,0 +1,465 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.awt.Point;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.samskivert.util.LRUHashMap;
import com.samskivert.util.StringUtil;
import com.samskivert.util.Throttle;
import com.samskivert.util.Tuple;
import com.samskivert.swing.RuntimeAdjust;
import com.threerings.util.DirectionCodes;
import com.threerings.media.image.Colorization;
import com.threerings.media.image.ImageManager;
import com.threerings.cast.CompositedActionFrames.ComponentFrames;
import com.threerings.cast.CompositedActionFrames.CompositedFramesKey;
import static com.threerings.cast.Log.log;
/**
* The character manager provides facilities for constructing sprites that
* are used to represent characters in a scene. It also handles the
* compositing and caching of composited character animations.
*/
public class CharacterManager
implements DirectionCodes
{
/**
* Sets the size of the cache used for composited animation frames. This must be called before
* the CharacterManager is created.
*/
public static void setCacheSize (int cacheKilobytes)
{
_runCacheSize = cacheKilobytes;
}
/**
* Constructs the character manager.
*/
public CharacterManager (ImageManager imgr, ComponentRepository crepo)
{
// keep these around
_imgr = imgr;
_crepo = crepo;
// populate our actions table
Iterator<ActionSequence> iter = crepo.enumerateActionSequences();
while (iter.hasNext()) {
ActionSequence action = iter.next();
_actions.put(action.name, action);
}
// create a cache for our composited action frames
log.debug("Creating action cache [size=" + _runCacheSize + "k].");
_frameCache = new LRUHashMap<CompositedFramesKey, CompositedMultiFrameImage>(
_runCacheSize * 1024, new LRUHashMap.ItemSizer<CompositedMultiFrameImage>() {
public int computeSize (CompositedMultiFrameImage value) {
return (int)value.getEstimatedMemoryUsage();
}
});
_frameCache.setTracking(true); // TODO
}
/**
* Returns the component repository being used by this manager.
*/
public ComponentRepository getComponentRepository ()
{
return _crepo;
}
/**
* Instructs the character manager to construct instances of this
* derived class of {@link CharacterSprite} when creating new sprites.
*
* @exception IllegalArgumentException thrown if the supplied class
* does not derive from {@link CharacterSprite}.
*/
public void setCharacterClass (Class<? extends CharacterSprite> charClass)
{
// make a note of it
_charClass = charClass;
}
/**
* Instructs the character manager to use the provided cache for
* composited action animations.
*/
public void setActionCache (ActionCache cache)
{
_acache = cache;
}
/**
* Returns a {@link CharacterSprite} representing the character
* described by the given {@link CharacterDescriptor}, or
* <code>null</code> if an error occurs.
*
* @param desc the character descriptor.
*/
public CharacterSprite getCharacter (CharacterDescriptor desc)
{
return getCharacter(desc, _charClass);
}
/**
* Returns a {@link CharacterSprite} representing the character
* described by the given {@link CharacterDescriptor}, or
* <code>null</code> if an error occurs.
*
* @param desc the character descriptor.
* @param charClass the {@link CharacterSprite} derived class that
* should be instantiated instead of the configured default (which is
* set via {@link #setCharacterClass}).
*/
public <T extends CharacterSprite> T getCharacter (CharacterDescriptor desc,
Class<T> charClass)
{
try {
T sprite = charClass.newInstance();
sprite.init(desc, this);
return sprite;
} catch (Exception e) {
log.warning("Failed to instantiate character sprite.", e);
return null;
}
}
/**
* Obtains the composited animation frames for the specified action for a
* character with the specified descriptor. The resulting composited
* animation will be cached.
*
* @exception NoSuchComponentException thrown if any of the components in
* the supplied descriptor do not exist.
* @exception IllegalArgumentException thrown if any of the components
* referenced in the descriptor do not support the specified action.
*/
public ActionFrames getActionFrames (
CharacterDescriptor descrip, String action)
throws NoSuchComponentException
{
Tuple<CharacterDescriptor, String> key = new Tuple<CharacterDescriptor, String>(descrip, action);
ActionFrames frames = _actionFrames.get(key);
if (frames == null) {
// this doesn't actually composite the images, but prepares an
// object to be able to do so
frames = createCompositeFrames(descrip, action);
_actionFrames.put(key, frames);
}
// periodically report our frame image cache performance
if (!_cacheStatThrottle.throttleOp()) {
long size = getEstimatedCacheMemoryUsage();
int[] eff = _frameCache.getTrackedEffectiveness();
log.debug("CharacterManager LRU [mem=" + (size / 1024) + "k" +
", size=" + _frameCache.size() + ", hits=" + eff[0] +
", misses=" + eff[1] + "].");
}
return frames;
}
/**
* Informs the character manager that the action sequence for the
* given character descriptor is likely to be needed in the near
* future and so any efforts that can be made to load it into the
* action sequence cache in advance should be undertaken.
*
* <p> This will eventually be revamped to spiffily load action
* sequences in the background.
*/
public void resolveActionSequence (CharacterDescriptor desc, String action)
{
try {
if (getActionFrames(desc, action) == null) {
log.warning("Failed to resolve action sequence " +
"[desc=" + desc + ", action=" + action + "].");
}
} catch (NoSuchComponentException nsce) {
log.warning("Failed to resolve action sequence " +
"[nsce=" + nsce + "].");
}
}
/**
* Returns the action sequence instance with the specified name or
* null if no such sequence exists.
*/
public ActionSequence getActionSequence (String action)
{
return _actions.get(action);
}
/**
* Returns the estimated memory usage in bytes for all images
* currently cached by the cached action frames.
*/
protected long getEstimatedCacheMemoryUsage ()
{
long size = 0;
Iterator<CompositedMultiFrameImage> iter = _frameCache.values().iterator();
while (iter.hasNext()) {
size += iter.next().getEstimatedMemoryUsage();
}
return size;
}
/**
* Generates the composited animation frames for the specified action
* for a character with the specified descriptor.
*
* @exception NoSuchComponentException thrown if any of the components
* in the supplied descriptor do not exist.
* @exception IllegalArgumentException thrown if any of the components
* referenced in the descriptor do not support the specified action.
*/
protected ActionFrames createCompositeFrames (
CharacterDescriptor descrip, String action)
throws NoSuchComponentException
{
int[] cids = descrip.getComponentIds();
int ccount = cids.length;
Colorization[][] zations = descrip.getColorizations();
Point[] xlations = descrip.getTranslations();
log.debug("Compositing action [action=" + action +
", descrip=" + descrip + "].");
// this will be used to construct any shadow layers
HashMap<String, ArrayList<TranslatedComponent>> shadows = null;
// maps components by class name for masks
HashMap<String, ArrayList<TranslatedComponent>> ccomps =
Maps.newHashMap();
// create colorized versions of all of the source action frames
ArrayList<ComponentFrames> sources = Lists.newArrayListWithCapacity(ccount);
for (int ii = 0; ii < ccount; ii++) {
ComponentFrames cframes = new ComponentFrames();
sources.add(cframes);
CharacterComponent ccomp = (cframes.ccomp = _crepo.getComponent(cids[ii]));
// load up the main component images
ActionFrames source = ccomp.getFrames(action, null);
if (source == null) {
String errmsg = "Cannot composite action frames; no such " +
"action for component [action=" + action +
", desc=" + descrip + ", comp=" + ccomp + "]";
throw new RuntimeException(errmsg);
}
source = (zations == null || zations[ii] == null) ?
source : source.cloneColorized(zations[ii]);
Point xlation = (xlations == null) ? null : xlations[ii];
cframes.frames = (xlation == null) ?
source : source.cloneTranslated(xlation.x, xlation.y);
// store the component with its translation under its class for masking
TranslatedComponent tcomp = new TranslatedComponent(ccomp, xlation);
ArrayList<TranslatedComponent> tcomps = ccomps.get(ccomp.componentClass.name);
if (tcomps == null) {
ccomps.put(ccomp.componentClass.name, tcomps = Lists.newArrayList());
}
tcomps.add(tcomp);
// if this component has a shadow, make a note of it
if (ccomp.componentClass.isShadowed()) {
if (shadows == null) {
shadows = Maps.newHashMap();
}
ArrayList<TranslatedComponent> shadlist = shadows.get(ccomp.componentClass.shadow);
if (shadlist == null) {
shadows.put(ccomp.componentClass.shadow, shadlist = Lists.newArrayList());
}
shadlist.add(tcomp);
}
}
// now create any necessary shadow layers
if (shadows != null) {
for (Map.Entry<String, ArrayList<TranslatedComponent>> entry : shadows.entrySet()) {
ComponentFrames scf = compositeShadow(action, entry.getKey(), entry.getValue());
if (scf != null) {
sources.add(scf);
}
}
}
// add any necessary masks
for (ComponentFrames cframes : sources) {
ArrayList<TranslatedComponent> mcomps = ccomps.get(cframes.ccomp.componentClass.mask);
if (mcomps != null) {
cframes.frames = compositeMask(action, cframes.ccomp, cframes.frames, mcomps);
}
}
// use those to create an entity that will lazily composite things
// together as they are needed
ComponentFrames[] cfvec = sources.toArray(new ComponentFrames[sources.size()]);
return new CompositedActionFrames(_imgr, _frameCache, action, cfvec);
}
protected ComponentFrames compositeShadow (
String action, String sclass, ArrayList<TranslatedComponent> scomps)
{
final ComponentClass cclass = _crepo.getComponentClass(sclass);
if (cclass == null) {
log.warning("Components reference non-existent shadow layer " +
"class [sclass=" + sclass +
", scomps=" + StringUtil.toString(scomps) + "].");
return null;
}
ComponentFrames cframes = new ComponentFrames();
// create a fake component for the shadow layer
cframes.ccomp = new CharacterComponent(-1, "shadow", cclass, null);
ArrayList<ComponentFrames> sources = Lists.newArrayList();
for (TranslatedComponent scomp : scomps) {
ComponentFrames source = new ComponentFrames();
source.ccomp = scomp.ccomp;
source.frames = scomp.getFrames(action, StandardActions.SHADOW_TYPE);
if (source.frames == null) {
// skip this shadow component
continue;
}
sources.add(source);
}
// if we ended up with no shadow, no problem!
if (sources.size() == 0) {
return null;
}
// create custom action frames that use a special compositing
// multi-frame image that does the necessary shadow magic
ComponentFrames[] svec = sources.toArray(new ComponentFrames[sources.size()]);
cframes.frames = new CompositedActionFrames(_imgr, _frameCache, action, svec) {
@Override
protected CompositedMultiFrameImage createFrames (int orient) {
return new CompositedShadowImage(
_imgr, _sources, _action, orient, cclass.shadowAlpha);
}
};
return cframes;
}
protected ActionFrames compositeMask (
String action, CharacterComponent ccomp, ActionFrames cframes,
ArrayList<TranslatedComponent> mcomps)
{
ArrayList<ComponentFrames> sources = Lists.newArrayList();
sources.add(new ComponentFrames(ccomp, cframes));
for (TranslatedComponent mcomp : mcomps) {
ActionFrames mframes = mcomp.getFrames(action, StandardActions.CROP_TYPE);
if (mframes != null) {
sources.add(new ComponentFrames(mcomp.ccomp, mframes));
}
}
if (sources.size() == 1) {
return cframes;
}
ComponentFrames[] mvec = sources.toArray(new ComponentFrames[sources.size()]);
return new CompositedActionFrames(_imgr, _frameCache, action, mvec) {
@Override
protected CompositedMultiFrameImage createFrames (int orient) {
return new CompositedMaskedImage(_imgr, _sources, _action, orient);
}
};
}
/** Combines a component with an optional translation for shadowing or masking. */
protected static class TranslatedComponent
{
public CharacterComponent ccomp;
public Point xlation;
public TranslatedComponent (CharacterComponent ccomp, Point xlation)
{
this.ccomp = ccomp;
this.xlation = xlation;
}
public ActionFrames getFrames (String action, String type)
{
ActionFrames frames = ccomp.getFrames(action, type);
return (frames == null || xlation == null) ?
frames : frames.cloneTranslated(xlation.x, xlation.y);
}
}
/** The image manager with whom we interact. */
protected ImageManager _imgr;
/** The component repository. */
protected ComponentRepository _crepo;
/** A table of our action sequences. */
protected Map<String, ActionSequence> _actions = Maps.newHashMap();
/** A table of composited action sequences (these don't reference the
* actual image data directly and thus take up little memory). */
protected Map<Tuple<CharacterDescriptor, String>, ActionFrames> _actionFrames =
Maps.newHashMap();
/** A cache of composited animation frames. */
protected LRUHashMap<CompositedFramesKey, CompositedMultiFrameImage> _frameCache;
/** The character class to be created. */
protected Class<? extends CharacterSprite> _charClass = CharacterSprite.class;
/** The action animation cache, if we have one. */
protected ActionCache _acache;
/** Throttle our cache status logging to once every 30 seconds. */
protected Throttle _cacheStatThrottle = new Throttle(1, 30000L);
/** 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 character manager LRU " +
"action cache [requires restart]", "narya.cast.action_cache_size",
CastPrefs.config, 32768);
/**
* Cache size to be used in this run. Adjusted by setCacheSize without affecting
* the stored value.
*/
protected static int _runCacheSize = _cacheSize.getValue();
}
@@ -0,0 +1,388 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import javax.swing.SwingUtilities;
import com.threerings.media.sprite.ImageSprite;
import static com.threerings.cast.Log.log;
/**
* A character sprite is a sprite that animates itself while walking about in a scene.
*/
public class CharacterSprite extends ImageSprite
implements StandardActions
{
/**
* Initializes this character sprite with the specified character descriptor and character
* manager. It will obtain animation data from the supplied character manager.
*/
public void init (CharacterDescriptor descrip, CharacterManager charmgr)
{
// keep track of this stuff
_descrip = descrip;
_charmgr = charmgr;
// sanity check our values
sanityCheckDescrip();
// assign an arbitrary starting orientation
_orient = SOUTHWEST;
// pass the buck to derived classes
didInit();
}
/**
* Called after this sprite has been initialized with its character descriptor and character
* manager. Derived classes can do post-init business here.
*/
protected void didInit ()
{
}
/**
* Reconfigures this sprite to use the specified character descriptor.
*/
public void setCharacterDescriptor (CharacterDescriptor descrip)
{
// keep the new descriptor
_descrip = descrip;
// sanity check our values
sanityCheckDescrip();
// update our action frames
updateActionFrames();
}
/**
* Specifies the action to use when the sprite is at rest. The default is
* <code>STANDING</code>.
*/
public void setRestingAction (String action)
{
_restingAction = action;
}
/**
* Returns the action to be used when the sprite is at rest. Derived classes may wish to
* override this method and vary the action based on external parameters (or randomly).
*/
public String getRestingAction ()
{
return _restingAction;
}
/**
* Specifies the action to use when the sprite is following a path. The default is
* <code>WALKING</code>.
*/
public void setFollowingPathAction (String action)
{
_followingPathAction = action;
}
/**
* Returns the action to be used when the sprite is following a path. Derived classes may wish
* to override this method and vary the action based on external parameters (or randomly).
*/
public String getFollowingPathAction ()
{
return _followingPathAction;
}
/**
* Sets the action sequence used when rendering the character, from the set of available
* sequences.
*/
public void setActionSequence (String action)
{
// sanity check
if (action == null) {
log.warning("Refusing to set null action sequence " + this + ".", new Exception());
return;
}
// no need to noop
if (action.equals(_action)) {
return;
}
_action = action;
updateActionFrames();
}
@Override
public void setOrientation (int orient)
{
if (orient < 0 || orient >= FINE_DIRECTION_COUNT) {
log.info("Refusing to set invalid orientation",
"sprite", this, "orient", orient, new Exception());
return;
}
int oorient = _orient;
super.setOrientation(orient);
if (_orient != oorient) {
_frames = null;
}
}
@Override
public boolean hitTest (int x, int y)
{
// the irect adjustments are to account for our decorations
return (_frames != null && _ibounds.contains(x, y) &&
_frames.hitTest(_frameIdx, x - _ibounds.x, y - _ibounds.y));
}
@Override
public void tick (long tickStamp)
{
// composite our action frames if something since the last call to
// tick caused them to become invalid
compositeActionFrames();
super.tick(tickStamp);
// composite our action frames if something during tick() caused them to become invalid
compositeActionFrames();
}
@Override
public void cancelMove ()
{
super.cancelMove();
halt();
}
@Override
public void pathBeginning ()
{
super.pathBeginning();
// enable walking animation
setAnimationMode(TIME_BASED);
setActionSequence(getFollowingPathAction());
}
@Override
public void pathCompleted (long timestamp)
{
super.pathCompleted(timestamp);
halt();
}
@Override
public void paint (Graphics2D gfx)
{
if (_frames != null) {
decorateBehind(gfx);
// paint the image using _ibounds rather than _bounds which
// has been modified to include the bounds of our decorations
_frames.paintFrame(gfx, _frameIdx, _ibounds.x, _ibounds.y);
decorateInFront(gfx);
} else {
super.paint(gfx);
}
}
/**
* Called to paint any decorations that should appear behind the character sprite image.
*/
protected void decorateBehind (Graphics2D gfx)
{
}
/**
* Called to paint any decorations that should appear in front of the character sprite image.
*/
protected void decorateInFront (Graphics2D gfx)
{
}
/**
* Rebuilds our action frames given our current character descriptor and action sequence. This
* is called when either of those two things changes.
*/
protected void updateActionFrames ()
{
// get a reference to the action sequence so that we can obtain
// our animation frames and configure our frames per second
ActionSequence actseq = _charmgr.getActionSequence(_action);
if (actseq == null) {
String errmsg = "No such action '" + _action + "'.";
throw new IllegalArgumentException(errmsg);
}
try {
// obtain our animation frames for this action sequence
_aframes = _charmgr.getActionFrames(_descrip, _action);
// clear out our frames so that we recomposite on next tick
_frames = null;
// update the sprite render attributes
setFrameRate(actseq.framesPerSecond);
} catch (NoSuchComponentException nsce) {
log.warning("Character sprite references non-existent component",
"sprite", this, "err", nsce);
} catch (Exception e) {
log.warning("Failed to obtain action frames",
"sprite", this, "descrip", _descrip, "action", _action, e);
}
}
/** Called to recomposite our action frames if needed. */
protected final void compositeActionFrames ()
{
if (_frames == null && _aframes != null) {
setFrames(_aframes.getFrames(_orient));
}
}
/**
* Makes it easier to track down problems with bogus character descriptors.
*/
protected void sanityCheckDescrip ()
{
if (_descrip.getComponentIds() == null ||
_descrip.getComponentIds().length == 0) {
log.warning("Invalid character descriptor [sprite=" + this +
", descrip=" + _descrip + "].", new Exception());
}
}
@Override
protected boolean tickPath (long tickStamp)
{
boolean moved = super.tickPath(tickStamp);
// composite our action frames if our path caused them to become invalid
compositeActionFrames();
return moved;
}
@Override
protected void updateRenderOrigin ()
{
super.updateRenderOrigin();
// adjust our image bounds to reflect the new location
_ibounds.x = _bounds.x + _ioff.x;
_ibounds.y = _bounds.y + _ioff.y;
}
@Override
protected void accomodateFrame (int frameIdx, int width, int height)
{
// this will update our width and height
super.accomodateFrame(frameIdx, width, height);
// we now need to update the render offset for this frame
if (_aframes == null) {
log.warning("Have no action frames! " + _aframes + ".");
} else {
_oxoff = _aframes.getXOrigin(_orient, frameIdx);
_oyoff = _aframes.getYOrigin(_orient, frameIdx);
}
// and cause those changes to be reflected in our bounds
updateRenderOrigin();
// start out with our bounds the same as our image bounds
_ibounds.setBounds(_bounds);
// now we can call down and incorporate the dimensions of any
// decorations that will be rendered along with our image
unionDecorationBounds(_bounds);
// compute our new render origin
_oxoff = _ox - _bounds.x;
_oyoff = _oy - _bounds.y;
// track the offset from our expanded bounds to our image bounds
_ioff.x = _ibounds.x - _bounds.x;
_ioff.y = _ibounds.y - _bounds.y;
}
/**
* Called by {@link #accomodateFrame} to give derived classes an opportunity to incorporate
* the bounds of any decorations that will be drawn along with this sprite. The
* {@link #_ibounds} rectangle will contain the bounds of the image that comprises the
* undecorated sprite. From that the position and size of decorations can be computed and
* unioned with the supplied bounds rectangle (most likely by using
* {@link SwingUtilities#computeUnion} which applies the union in place rather than creating a
* new rectangle).
*/
protected void unionDecorationBounds (Rectangle bounds)
{
}
/**
* Updates the sprite animation frame to reflect the cessation of movement and disables any
* further animation.
*/
protected void halt ()
{
// only do something if we're actually animating
if (_animMode != NO_ANIMATION) {
// disable animation
setAnimationMode(NO_ANIMATION);
// come to a halt looking settled and at peace
String rest = getRestingAction();
if (rest != null) {
setActionSequence(rest);
}
}
}
/** The action to use when at rest. */
protected String _restingAction = STANDING;
/** The action to use when following a path. */
protected String _followingPathAction = WALKING;
/** A reference to the descriptor for the character that we're visualizing. */
protected CharacterDescriptor _descrip;
/** A reference to the character manager that created us. */
protected CharacterManager _charmgr;
/** The action we are currently displaying. */
protected String _action;
/** The animation frames for the active action sequence in each orientation. */
protected ActionFrames _aframes;
/** The offset from the upper-left of the total sprite bounds to the upper-left of the image
* within those bounds. */
protected Point _ioff = new Point();
/** The bounds of the current sprite image. */
protected Rectangle _ibounds = new Rectangle();
}
@@ -0,0 +1,255 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
import java.io.Serializable;
import com.samskivert.util.ArrayIntSet;
import com.samskivert.util.ComparableArrayList;
import com.samskivert.util.StringUtil;
/**
* Denotes a class of components to which {@link CharacterComponent}
* objects belong. Examples include "Hat", "Head", and "Feet". A component
* class dictates a component's rendering priority so that components can
* be rendered in an order that causes them to overlap properly.
*
* <p> Components support render priority overrides for particular
* actions, orientations or combinations of actions and orientations. The
* system is currently structured with the expectation that the overrides
* will be relatively few (less than fifteen, say) for any given component
* class. A system that relied on many overrides for its components would
* want to implement a more scalable algorithm for determining which, if
* any, override matches a particular action and orientation combination.
*/
public class ComponentClass implements Serializable
{
/** Used to effect custom render orders for particular actions, orientations, etc. */
public static class PriorityOverride
implements Comparable<PriorityOverride>, Serializable
{
/** The overridden render priority value. */
public int renderPriority;
/** The action, if any, for which this override is appropriate. */
public String action;
/** The component, if any, for which this override is appropriate. */
public String component;
/** The orientations, if any, for which this override is appropriate. */
public ArrayIntSet orients;
/**
* Determines whether this priority override matches the specified
* action, orientation ant component combination.
*/
public boolean matches (String action, String component, int orient)
{
return (((orients == null) || orients.contains(orient)) &&
((this.component == null) || this.component.equals(component)) &&
((this.action == null) || this.action.equals(action)));
}
// documentation inherited from interface
public int compareTo (PriorityOverride po)
{
// overrides with both an action and an orientation should come first in the list
int pri = priority(), opri = po.priority();
if (pri == opri) {
return hashCode() - po.hashCode();
} else {
return pri - opri;
}
}
protected int priority ()
{
int priority = 0;
if (component != null) {
priority += 3; // Extra priority to things that are for specific components
}
if (action != null) {
priority++;
}
if (orients != null) {
priority++;
}
return priority;
}
@Override
public String toString ()
{
return "[pri=" + renderPriority + ", action=" + action + ", component=" + component +
", orients=" + orients + "]";
}
/** 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 component class name. */
public String name;
/** The default render priority. */
public int renderPriority;
/** The color classes to use when recoloring components of this class. May
* be null if a system does not use recolorable components. */
public String[] colors;
/** The class name of the layer from which this component class obtains a
* mask to limit rendering to certain areas. */
public String mask;
/** Indicates the class name of the shadow layer to which this component
* class contributes a shadow. */
public String shadow;
/** 1.0 for a normal component, the alpha value of the pre-composited
* shadow for the special "shadow" component class. */
public float shadowAlpha = 1.0f;
/** Whether or not components of this class will have translations applied. */
public boolean translate;
/**
* Creates an uninitialized instance suitable for unserialization or
* population during XML parsing.
*/
public ComponentClass ()
{
}
/**
* Returns the render priority appropriate for the specified action and orientation.
*
* This is deprecated; you should really use {@link #getRenderPriority(String, String, int)}
* to handle potential per-component priority overrides.
*/
@Deprecated
public int getRenderPriority (String action, int orientation)
{
return getRenderPriority(action, null, orientation);
}
/**
* Returns the render priority appropriate for the specified action, orientation and
* component.
*/
public int getRenderPriority (String action, String component, int orientation)
{
// because we expect there to be relatively few priority overrides, we simply search
// linearly through the list for the closest match
int ocount = (_overrides != null) ? _overrides.size() : 0;
for (int ii = 0; ii < ocount; ii++) {
PriorityOverride over = _overrides.get(ii);
// based on the way the overrides are sorted, the first match
// is the most specific and the one we want
if (over.matches(action, component, orientation)) {
return over.renderPriority;
}
}
return renderPriority;
}
/**
* Adds the supplied render priority override record to this component class.
*/
public void addPriorityOverride (PriorityOverride override)
{
if (_overrides == null) {
_overrides = new ComparableArrayList<PriorityOverride>();
}
_overrides.insertSorted(override);
}
/**
* Returns true if this component class contributes a shadow to a
* particular shadow layer. Note: this is different from <em>being</em> a
* shadow layer which is determined by calling {@link #isShadow}.
*/
public boolean isShadowed ()
{
return (shadow != null);
}
/**
* Returns true if this component class is a shadow layer rather than a normal component class.
*/
public boolean isShadow ()
{
return (shadowAlpha != 1.0f);
}
/**
* Classes with the same name are the same.
*/
@Override
public boolean equals (Object other)
{
if (other instanceof ComponentClass) {
return name.equals(((ComponentClass)other).name);
} else {
return false;
}
}
/**
* Hashcode is based on component class name.
*/
@Override
public int hashCode ()
{
return name.hashCode();
}
@Override
public String toString ()
{
StringBuilder buf = new StringBuilder("[");
buf.append("name=").append(name);
buf.append(", pri=").append(renderPriority);
if (colors != null) {
buf.append(", colors=").append(StringUtil.toString(colors));
}
if (mask != null) {
buf.append(", mask=").append(mask);
}
if (shadowAlpha != 1.0f) {
buf.append(", shadow=").append(shadowAlpha);
} else if (shadow != null) {
buf.append(", shadow=").append(shadow);
}
return buf.append("]").toString();
}
/** A list of render priority overrides. */
protected ComparableArrayList<PriorityOverride> _overrides;
/** Increase this value when object's serialized state is impacted by
* a class change (modification of fields, inheritance). */
private static final long serialVersionUID = 4;
}
@@ -0,0 +1,63 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
import com.samskivert.io.PersistenceException;
/**
* Brokers component ids. The component repository interface makes
* available a collection of components based on a unique identifier. The
* expectation is that a collection of components will be used to populate
* a repository and in that population process, component ids will be
* assigned to the components. The component id broker system provides a
* means by which named components can be mapped consistently to a set of
* component ids. Humans can then be responsible for assigning unique
* names to the components and the broker will ensure that those names map
* to unique ids that won't change if the repository is rebuilt from the
* source components.
*/
public interface ComponentIDBroker
{
/**
* Returns the unique identifier for the named component. If no
* identifier has yet been assigned to the specified named component,
* one should be assigned and returned.
*
* @param cclass the name of the class to which the component belongs.
* @param cname the name of the component.
*
* @exception PersistenceException thrown if an error occurs
* communicating with the underlying persistence mechanism used to
* store the name to id mappings.
*/
public int getComponentID (String cclass, String cname)
throws PersistenceException;
/**
* When the user of a component id broker is done obtaining component
* ids, it must call this method to give the component id broker an
* opportunity to flush any newly created component ids back to its
* persistent store.
*/
public void commit ()
throws PersistenceException;
}
@@ -0,0 +1,67 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
import java.util.Iterator;
/**
* Makes available a collection of character components and associated metadata. Character
* components are animated sequences that can be composited together to create a complete
* character visualization (imagine interchanging pairs of boots, torsos, hats, etc.).
*/
public interface ComponentRepository
{
/**
* Returns the {@link CharacterComponent} object for the given component identifier.
*/
public CharacterComponent getComponent (int componentId)
throws NoSuchComponentException;
/**
* Returns the {@link CharacterComponent} object with the given component class and name.
*/
public CharacterComponent getComponent (String className, String compName)
throws NoSuchComponentException;
/**
* Returns the {@link ComponentClass} with the specified name or null if none exists with that
* name.
*/
public ComponentClass getComponentClass (String className);
/**
* Iterates over the {@link ComponentClass} instances representing all available character
* component classes.
*/
public Iterator<ComponentClass> enumerateComponentClasses ();
/**
* Iterates over the {@link ActionSequence} instances representing every available action
* sequence.
*/
public Iterator<ActionSequence> enumerateActionSequences ();
/**
* Iterates over the component ids of all components in the specified class.
*/
public Iterator<Integer> enumerateComponentIds (ComponentClass compClass);
}
@@ -0,0 +1,201 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
import java.util.Map;
import com.samskivert.util.StringUtil;
import com.threerings.util.DirectionCodes;
import com.threerings.media.image.Colorization;
import com.threerings.media.image.ImageManager;
import com.threerings.media.util.MultiFrameImage;
/**
* An implementation of the {@link MultiFrameImage} interface that is used
* to lazily create composited character frames when they are requested.
*/
public class CompositedActionFrames
implements ActionFrames, DirectionCodes
{
/** Used to associate a {@link CharacterComponent} with its {@link
* ActionFrames} for a particular action. */
public static class ComponentFrames
{
public CharacterComponent ccomp;
public ActionFrames frames;
public ComponentFrames () {
}
public ComponentFrames (
CharacterComponent ccomp, ActionFrames frames) {
this.ccomp = ccomp;
this.frames = frames;
}
@Override
public String toString () {
return ccomp + ":" + frames;
}
}
/**
* Constructs a set of composited action frames with the supplied
* source frames and colorization configuration. The actual component
* frame images will not be composited until they are requested.
*/
public CompositedActionFrames (
ImageManager imgr, Map<CompositedFramesKey, CompositedMultiFrameImage> frameCache,
String action, ComponentFrames[] sources)
{
// sanity check
if (sources == null || sources.length == 0) {
String errmsg = "Requested to composite invalid set of source " +
"frames! [action=" + action +
", sources=" + StringUtil.toString(sources) + "].";
throw new RuntimeException(errmsg);
}
_imgr = imgr;
_frameCache = frameCache;
_sources = sources;
_action = action;
// the sources must all have the same orientation count, so we
// just use the first
_orientCount = _sources[0].frames.getOrientationCount();
}
// documentation inherited from interface
public int getOrientationCount ()
{
return _orientCount;
}
// documentation inherited from interface
public TrimmedMultiFrameImage getFrames (int orient)
{
_key.setOrient(orient);
CompositedMultiFrameImage cmfi =
_frameCache.get(_key);
if (cmfi == null) {
cmfi = createFrames(orient);
_frameCache.put(new CompositedFramesKey(orient), cmfi);
}
return cmfi;
}
// documentation inherited from interface
public int getXOrigin (int orient, int frameIdx)
{
CompositedMultiFrameImage cmfi = (CompositedMultiFrameImage)
getFrames(orient);
return cmfi.getXOrigin(frameIdx);
}
// documentation inherited from interface
public int getYOrigin (int orient, int frameIdx)
{
CompositedMultiFrameImage cmfi = (CompositedMultiFrameImage)
getFrames(orient);
return cmfi.getYOrigin(frameIdx);
}
// documentation inherited from interface
public ActionFrames cloneColorized (Colorization[] zations)
{
throw new RuntimeException("What you talkin' about Willis?");
}
// documentation inherited from interface
public ActionFrames cloneTranslated (int dx, int dy)
{
ComponentFrames[] tsources = new ComponentFrames[_sources.length];
for (int ii = 0; ii < _sources.length; ii++) {
tsources[ii] = new ComponentFrames(
_sources[ii].ccomp, _sources[ii].frames.cloneTranslated(dx, dy));
}
return new CompositedActionFrames(_imgr, _frameCache, _action, tsources);
}
/**
* Creates our underlying multi-frame image for a particular orientation.
*/
protected CompositedMultiFrameImage createFrames (int orient)
{
return new CompositedMultiFrameImage(_imgr, _sources, _action, orient);
}
/** Used to cache composited frames for a particular action and
* orientation. */
protected class CompositedFramesKey
{
public CompositedFramesKey (int orient) {
_orient = orient;
}
public void setOrient (int orient) {
_orient = orient;
}
public CompositedActionFrames getOwner () {
return CompositedActionFrames.this;
}
@Override
public boolean equals (Object other) {
CompositedFramesKey okey = (CompositedFramesKey)other;
return ((getOwner() == okey.getOwner()) &&
(_orient == okey._orient));
}
@Override
public int hashCode () {
return CompositedActionFrames.this.hashCode() ^ _orient;
}
protected int _orient;
}
/** The image manager from whom we can obtain prepared volatile images
* onto which to render our composited actions. */
protected ImageManager _imgr;
/** Used to cache our composited action frame images. */
protected Map<CompositedFramesKey, CompositedMultiFrameImage> _frameCache;
/** The action for which we're compositing frames. */
protected String _action;
/** The number of orientations. */
protected int _orientCount;
/** Our source components and action frames. */
protected ComponentFrames[] _sources;
/** Used to avoid creating a new key object every time we do a cache
* lookup. */
protected CompositedFramesKey _key = new CompositedFramesKey(0);
}
@@ -0,0 +1,142 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
import java.awt.AlphaComposite;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import com.threerings.media.image.ImageManager;
import com.threerings.media.image.VolatileMirage;
import com.threerings.cast.CompositedActionFrames.ComponentFrames;
/**
* Used to composite action frames with mask frames.
*/
public class CompositedMaskedImage extends CompositedMultiFrameImage
{
public CompositedMaskedImage (
ImageManager imgr, ComponentFrames[] sources, String action,
int orient)
{
super(imgr, sources, action, orient);
}
@Override
public int getWidth (int index) {
return _sources[0].frames.getFrames(_orient).getWidth(index);
}
@Override
public int getHeight (int index) {
return _sources[0].frames.getFrames(_orient).getHeight(index);
}
@Override
public int getXOrigin (int index) {
return _sources[0].frames.getXOrigin(_orient, index);
}
@Override
public int getYOrigin (int index) {
return _sources[0].frames.getYOrigin(_orient, index);
}
@Override
public void paintFrame (Graphics2D g, int index, int x, int y) {
_images[index].paint(g, x + getX(index), y + getY(index));
}
@Override
public boolean hitTest (int index, int x, int y) {
return _images[index].hitTest(x + getX(index), y + getY(index));
}
@Override
public void getTrimmedBounds (int index, Rectangle bounds) {
bounds.setBounds(getX(index), getY(index), _images[index].getWidth(),
_images[index].getHeight());
}
@Override
protected CompositedMirage createCompositedMirage (int index)
{
return new MaskedMirage(index);
}
/**
* Combines the image in the first source with the masks in the rest. */
protected class MaskedMirage extends CompositedVolatileMirage
{
public MaskedMirage (int index)
{
super(index);
}
@Override
protected Rectangle combineBounds (Rectangle bounds, Rectangle tbounds)
{
if (bounds.width == 0 && bounds.height == 0) {
bounds.setBounds(tbounds);
} else {
bounds = bounds.intersection(tbounds);
}
return bounds;
}
@Override
protected void refreshVolatileImage ()
{
Graphics2D g = (Graphics2D)_image.getGraphics();
try {
TrimmedMultiFrameImage source = _sources[0].frames.getFrames(_orient);
source.paintFrame(g, _index, -_bounds.x, -_bounds.y);
g.setComposite(AlphaComposite.DstIn);
for (int ii = 1; ii < _sources.length; ii++) {
TrimmedMultiFrameImage mask = _sources[ii].frames.getFrames(_orient);
mask.paintFrame(g, _index, -_bounds.x, -_bounds.y);
}
} finally {
// clean up after ourselves
if (g != null) {
g.dispose();
}
}
}
}
/**
* @return the x offset into the source image for the image at index
*/
protected int getX (int index) {
return ((VolatileMirage)_images[index]).getX();
}
/**
* @return the y offset into the source image for the image at index
*/
protected int getY (int index) {
return ((VolatileMirage)_images[index]).getY();
}
}
@@ -0,0 +1,39 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
import com.threerings.media.image.Mirage;
public interface CompositedMirage
extends Mirage
{
/**
* Returns the x offset into our image.
*/
public int getXOrigin ();
/**
* Returns the y offset into our image.
*/
public int getYOrigin ();
}
@@ -0,0 +1,323 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
import java.util.Arrays;
import java.util.Comparator;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import com.threerings.media.image.ImageManager;
import com.threerings.media.image.Mirage;
import com.threerings.media.image.VolatileMirage;
import com.threerings.cast.CompositedActionFrames.ComponentFrames;
import com.threerings.cast.bundle.BundledComponentRepository.TileSetFrameImage;
/**
* Used to composite the action frames for a particular orientation of a
* {@link CompositedActionFrames}.
*/
public class CompositedMultiFrameImage
implements TrimmedMultiFrameImage
{
public CompositedMultiFrameImage (
ImageManager imgr, ComponentFrames[] sources,
String action, int orient)
{
_imgr = imgr;
_sources = sources;
_action = action;
_orient = orient;
// create our frame images (which will do the compositing)
int fcount = sources[0].frames.getFrames(orient).getFrameCount();
_images = new CompositedMirage[fcount];
for (int ii = 0; ii < fcount; ii++) {
_images[ii] = createCompositedMirage(ii);
}
}
// documentation inherited
public int getFrameCount () {
return _images.length;
}
// documentation inherited from interface
public int getWidth (int index) {
return _images[index].getWidth();
}
// documentation inherited from interface
public int getHeight (int index) {
return _images[index].getHeight();
}
public int getXOrigin (int index)
{
return _images[index].getXOrigin();
}
public int getYOrigin (int index)
{
return _images[index].getYOrigin();
}
// documentation inherited from interface
public void paintFrame (Graphics2D g, int index, int x, int y) {
_images[index].paint(g, x, y);
}
// documentation inherited from interface
public boolean hitTest (int index, int x, int y) {
return _images[index].hitTest(x, y);
}
// documentation inherited from interface
public void getTrimmedBounds (int index, Rectangle bounds) {
bounds.setBounds(0, 0, getWidth(index), getHeight(index));
}
/**
* Returns the estimated memory usage of our composited frame images.
*/
public long getEstimatedMemoryUsage ()
{
long size = 0;
for (CompositedMirage element : _images) {
size += element.getEstimatedMemoryUsage();
}
return size;
}
/**
* Creates a composited image for the specified frame.
*/
protected CompositedMirage createCompositedMirage (int index)
{
if (_sources.length == 1 && _sources[0].frames instanceof TileSetFrameImage) {
TileSetFrameImage frames = (TileSetFrameImage)_sources[0].frames;
Rectangle tbounds = new Rectangle();
frames.getTrimmedBounds(_orient, index, tbounds);
int x = frames.getXOrigin(_orient, index) - tbounds.x;
int y = frames.getYOrigin(_orient, index) - tbounds.y;
return new SubmirageForwarder(frames.getTileMirage(_orient, index), x, y);
}
return new CompositedVolatileMirage(index);
}
/**
* A CompositedMirage that forwards all of its Mirage calls to a delegate Mirage.
*/
protected class SubmirageForwarder implements CompositedMirage {
public SubmirageForwarder(Mirage m, int x, int y) {
delegate = m;
this.x = x;
this.y = y;
}
public int getXOrigin ()
{
return x;
}
public int getYOrigin ()
{
return y;
}
public long getEstimatedMemoryUsage ()
{
return delegate.getEstimatedMemoryUsage();
}
public int getHeight ()
{
return delegate.getHeight();
}
public BufferedImage getSnapshot ()
{
return delegate.getSnapshot();
}
public int getWidth ()
{
return delegate.getWidth();
}
public boolean hitTest (int x, int y)
{
return delegate.hitTest(x, y);
}
public void paint (Graphics2D gfx, int x, int y)
{
delegate.paint(gfx, x, y);
}
protected int x, y;
protected Mirage delegate;
}
// documentation inherited
protected Mirage getFrame (int orient, int index)
{
return _images[index];
}
/** The image manager from whom we load our images. */
protected ImageManager _imgr;
/** The action frames from which we obtain our source imagery. */
protected ComponentFrames[] _sources;
/** The action we're compositing. */
protected String _action;
/** The orientation we're compositing. */
protected int _orient;
/** Our composited action frame images. */
protected CompositedMirage[] _images;
/**
* Used to create our mirage using the source action frame images.
*/
protected class CompositedVolatileMirage extends VolatileMirage
implements CompositedMirage, Comparator<ComponentFrames>
{
public CompositedVolatileMirage (int index)
{
super(CompositedMultiFrameImage.this._imgr,
new Rectangle(0, 0, 0, 0));
// keep this for later
_index = index;
// first we need to determine the bounds of the rectangle that
// will enclose all of our various components
Rectangle tbounds = new Rectangle();
int scount = _sources.length;
for (int ii = 0; ii < scount; ii++) {
TrimmedMultiFrameImage source =
_sources[ii].frames.getFrames(_orient);
source.getTrimmedBounds(index, tbounds);
_bounds = combineBounds(_bounds, tbounds);
}
if (_bounds.width <= 0) {
_bounds.width = 1;
}
if (_bounds.height <= 0) {
_bounds.height = 1;
}
// compute our new origin
_origin.x = (_sources[0].frames.getXOrigin(_orient, index) -
_bounds.x);
_origin.y = (_sources[0].frames.getYOrigin(_orient, index) -
_bounds.y);
// Log.info("New origin [x=" + _origin.x + ", y=" + _origin.y + "].");
// render our volatile image for the first time
createVolatileImage();
}
public int getXOrigin ()
{
return _origin.x;
}
public int getYOrigin ()
{
return _origin.y;
}
// documentation inherited from interface
public int compare (ComponentFrames cf1, ComponentFrames cf2)
{
return (cf1.ccomp.getRenderPriority(_action, _orient) -
cf2.ccomp.getRenderPriority(_action, _orient));
}
/**
* Combines the working bounds with a new set of bounds.
*/
protected Rectangle combineBounds (Rectangle bounds, Rectangle tbounds)
{
// the first one defines our initial bounds
if (bounds.width == 0 && bounds.height == 0) {
bounds.setBounds(tbounds);
} else {
bounds.add(tbounds);
}
return bounds;
}
@Override
protected int getTransparency ()
{
return Transparency.BITMASK;
}
@Override
protected void refreshVolatileImage ()
{
// long start = System.currentTimeMillis();
// sort the sources appropriately for this orientation
Arrays.sort(_sources, this);
// now render each of the components into a composited frame
int scount = _sources.length;
Graphics2D g = (Graphics2D)_image.getGraphics();
try {
for (int ii = 0; ii < scount; ii++) {
TrimmedMultiFrameImage source =
_sources[ii].frames.getFrames(_orient);
source.paintFrame(g, _index, -_bounds.x, -_bounds.y);
}
} finally {
// clean up after ourselves
if (g != null) {
g.dispose();
}
}
// Log.info("Composited [orient=" + _orient + ", index=" + _index +
// ", tbounds=" + StringUtil.toString(_bounds) + "].");
// long now = System.currentTimeMillis();
// Log.info("Composited " + scount + " frames in " +
// (now-start) + " millis.");
}
protected int _index;
protected Point _origin = new Point();
}
}
@@ -0,0 +1,111 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
import java.awt.AlphaComposite;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import com.threerings.media.image.ImageManager;
import com.threerings.media.image.VolatileMirage;
import com.threerings.cast.CompositedActionFrames.ComponentFrames;
/**
* Used to composite the special shadow action frames for a particular
* orientation of a {@link CompositedActionFrames}.
*/
public class CompositedShadowImage extends CompositedMultiFrameImage
{
public CompositedShadowImage (ImageManager imgr, ComponentFrames[] sources,
String action, int orient, float shadowAlpha)
{
super(imgr, sources, action, orient);
// create the appropriate alpha composite for rendering the shadow
_shadowAlpha = AlphaComposite.getInstance(
AlphaComposite.SRC_OVER, shadowAlpha);
}
@Override
public int getWidth (int index) {
return _sources[0].frames.getFrames(_orient).getWidth(index);
}
@Override
public int getHeight (int index) {
return _sources[0].frames.getFrames(_orient).getHeight(index);
}
@Override
public int getXOrigin (int index) {
return _sources[0].frames.getXOrigin(_orient, index);
}
@Override
public int getYOrigin (int index) {
return _sources[0].frames.getYOrigin(_orient, index);
}
@Override
protected CompositedMirage createCompositedMirage (int index) {
// Always use a CompositedVolatileMirage for ShadowImage since we need to draw into it.
return new CompositedVolatileMirage(index);
}
@Override
public void paintFrame (Graphics2D g, int index, int x, int y) {
Composite ocomp = g.getComposite();
g.setComposite(_shadowAlpha);
_images[index].paint(g, x + getX(index), y + getY(index));
g.setComposite(ocomp);
}
@Override
public boolean hitTest (int index, int x, int y) {
return _images[index].hitTest(x + getX(index), y + getY(index));
}
@Override
public void getTrimmedBounds (int index, Rectangle bounds) {
bounds.setBounds(getX(index), getY(index), _images[index].getWidth(),
_images[index].getHeight());
}
/**
* @return the x offset into the source image for the image at index
*/
protected int getX (int index) {
return ((VolatileMirage)_images[index]).getX();
}
/**
* @return the y offset into the source image for the image at index
*/
protected int getY (int index) {
return ((VolatileMirage)_images[index]).getY();
}
/** The alpha value at which we render our shadow. */
protected AlphaComposite _shadowAlpha;
}
@@ -0,0 +1,49 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
import java.util.Set;
/**
* Provides a mechanism where by a character component can obtain access
* to its image frames for a particular action in an on demand manner.
*/
public interface FrameProvider
{
/**
* Returns the animation frames (in the eight sprite directions) for
* the specified action of the specified component. May return null if
* the specified action does not exist for the specified component.
*/
public ActionFrames getFrames (
CharacterComponent component, String action, String type);
/**
* Returns the file path of the animation frames (in the eight sprite directions) for the
* specified action of the specified component. May return a path to the default action or
* null if the specified action does not exist for the specified component.
*
* @param existentPaths the set of all paths for which there are valid frames.
*/
public String getFramePath (
CharacterComponent component, String action, String type, Set<String> existentPaths);
}
@@ -0,0 +1,32 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
import com.samskivert.util.Logger;
/**
* Contains a reference to the log object used by the Cast package.
*/
public class Log
{
public static Logger log = Logger.getLogger("cast");
}
@@ -0,0 +1,50 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
/**
* Thrown when an attempt is made to retrieve a non-existent character
* component from the component repository.
*/
public class NoSuchComponentException extends Exception
{
public NoSuchComponentException (int componentId)
{
super("No such component [componentId=" + componentId + "]");
_componentId = componentId;
}
public NoSuchComponentException (
String componentClass, String componentName)
{
super("No such component [class=" + componentClass +
", name=" + componentName + "]");
_componentId = -1;
}
public int getComponentId ()
{
return _componentId;
}
protected int _componentId;
}
@@ -0,0 +1,42 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
/**
* Actions are referenced by name and this interface defines constants for
* standard actions and action suffixes used by the shadow and cropping
* support.
*/
public interface StandardActions
{
/** The name of the standard standing action. */
public static final String STANDING = "standing";
/** The name of the standard walking action. */
public static final String WALKING = "walking";
/** A special action sub-type for shadow imagery. */
public static final String SHADOW_TYPE = "shadow";
/** A special action sub-type for crop imagery. */
public static final String CROP_TYPE = "crop";
}
@@ -0,0 +1,41 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast;
import java.awt.Rectangle;
import com.threerings.media.util.MultiFrameImage;
/**
* Used to generate more memory efficient composited images in
* circumstances where we have trimmed underlying component images.
*/
public interface TrimmedMultiFrameImage extends MultiFrameImage
{
/**
* Fills in the minimum bounding rectangle for this image that
* contains all non-transparent pixels. If this information is
* unavailable, the bounds of the entire image may be returned in
* exchange for improved performance.
*/
public void getTrimmedBounds (int index, Rectangle bounds);
}
@@ -0,0 +1,153 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast.builder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.threerings.cast.ComponentClass;
import com.threerings.cast.ComponentRepository;
/**
* The builder model represents the current state of the character the
* user is building.
*/
public class BuilderModel
{
/**
* Constructs a builder model.
*/
public BuilderModel (ComponentRepository crepo)
{
gatherComponentInfo(crepo);
}
/**
* Adds a builder model listener.
*
* @param l the listener.
*/
public void addListener (BuilderModelListener l)
{
if (!_listeners.contains(l)) {
_listeners.add(l);
}
}
/**
* Notifies all model listeners that the builder model has changed.
*/
protected void notifyListeners (int event)
{
int size = _listeners.size();
for (int ii = 0; ii < size; ii++) {
_listeners.get(ii).modelChanged(event);
}
}
/**
* Returns a list of the available component classes.
*/
public List<ComponentClass> getComponentClasses ()
{
return Collections.unmodifiableList(_classes);
}
/**
* Returns the list of components available in the specified class.
*/
public List<Integer> getComponents (ComponentClass cclass)
{
List<Integer> list = _components.get(cclass);
if (list == null) {
list = Lists.newArrayList();
}
return list;
}
/**
* Returns the selected components in an array.
*/
public int[] getSelectedComponents ()
{
int[] values = new int[_selected.size()];
Iterator<Integer> iter = _selected.values().iterator();
for (int ii = 0; iter.hasNext(); ii++) {
values[ii] = iter.next().intValue();
}
return values;
}
/**
* Sets the selected component for the given component class.
*/
public void setSelectedComponent (ComponentClass cclass, int cid)
{
_selected.put(cclass, Integer.valueOf(cid));
notifyListeners(BuilderModelListener.COMPONENT_CHANGED);
}
/**
* Gathers component class and component information from the
* character manager for later reference by others.
*/
protected void gatherComponentInfo (ComponentRepository crepo)
{
// get the list of all component classes
Iterators.addAll(_classes, crepo.enumerateComponentClasses());
for (int ii = 0; ii < _classes.size(); ii++) {
// get the list of components available for this class
ComponentClass cclass = _classes.get(ii);
Iterator<Integer> iter = crepo.enumerateComponentIds(cclass);
while (iter.hasNext()) {
Integer cid = iter.next();
ArrayList<Integer> clist = _components.get(cclass);
if (clist == null) {
_components.put(cclass, clist = Lists.newArrayList());
}
clist.add(cid);
}
}
}
/** The currently selected character components. */
protected HashMap<ComponentClass, Integer> _selected = Maps.newHashMap();
/** The hashtable of available component ids for each class. */
protected HashMap<ComponentClass, ArrayList<Integer>> _components = Maps.newHashMap();
/** The list of all available component classes. */
protected ArrayList<ComponentClass> _classes = Lists.newArrayList();
/** The model listeners. */
protected ArrayList<BuilderModelListener> _listeners = Lists.newArrayList();
}
@@ -0,0 +1,40 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast.builder;
/**
* The builder model listener interface should be implemented by
* classes that would like to be notified when the builder model is
* changed.
*
* @see BuilderModel
*/
public interface BuilderModelListener
{
/**
* Called by the {@link BuilderModel} when the model is changed.
*/
public void modelChanged (int event);
/** Notification event constants. */
public static final int COMPONENT_CHANGED = 0;
}
@@ -0,0 +1,63 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast.builder;
import javax.swing.BorderFactory;
import javax.swing.JPanel;
import com.samskivert.swing.HGroupLayout;
import com.samskivert.swing.VGroupLayout;
import com.threerings.cast.CharacterManager;
import com.threerings.cast.ComponentRepository;
/**
* The builder panel presents the user with an overview of a composited
* character and facilities for altering the individual components that
* comprise the character's display image.
*/
public class BuilderPanel extends JPanel
{
/**
* Constructs the builder panel.
*/
public BuilderPanel (CharacterManager charmgr,
ComponentRepository crepo, String cprefix)
{
setLayout(new VGroupLayout());
// give ourselves a wee bit of a border
setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
HGroupLayout gl = new HGroupLayout(HGroupLayout.STRETCH);
gl.setOffAxisPolicy(HGroupLayout.STRETCH);
// create the builder model
BuilderModel model = new BuilderModel(crepo);
// create the component selection and sprite display panels
JPanel sub = new JPanel(gl);
sub.add(new ComponentPanel(model, cprefix));
sub.add(new SpritePanel(charmgr, model));
add(sub);
}
}
@@ -0,0 +1,110 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast.builder;
import java.util.List;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import com.samskivert.swing.HGroupLayout;
import com.samskivert.swing.VGroupLayout;
import com.threerings.cast.ComponentClass;
/**
* The class editor displays a label and a slider that allow the user to
* select the desired component for a given component class.
*/
public class ClassEditor extends JPanel implements ChangeListener
{
/**
* Constructs a class editor.
*/
public ClassEditor (BuilderModel model, ComponentClass cclass, List<Integer> components)
{
_model = model;
_components = components;
_cclass = cclass;
VGroupLayout vgl = new VGroupLayout(VGroupLayout.STRETCH);
vgl.setOffAxisPolicy(VGroupLayout.STRETCH);
setLayout(vgl);
HGroupLayout hgl = new HGroupLayout();
hgl.setJustification(HGroupLayout.LEFT);
JPanel sub = new JPanel(hgl);
sub.add(new JLabel(cclass.name + ": "));
sub.add(_clabel = new JLabel("0"));
add(sub);
// create the slider allowing selection of available components
int max = components.size() - 1;
JSlider slider = new JSlider(JSlider.HORIZONTAL, 0, max, 0);
slider.setSnapToTicks(true);
slider.addChangeListener(this);
add(slider);
// set the starting component for this class
setSelectedComponent(0);
}
// documentation inherited
public void stateChanged (ChangeEvent e)
{
JSlider source = (JSlider)e.getSource();
if (!source.getValueIsAdjusting()) {
int val = source.getValue();
// update the model with the newly selected component
setSelectedComponent(val);
// update the label with the new value
_clabel.setText(Integer.toString(val));
}
}
/**
* Sets the selected component in the builder model for the
* component class associated with this editor to the component at
* the given index in this editor's list of available components.
*/
protected void setSelectedComponent (int idx)
{
int cid = _components.get(idx).intValue();
_model.setSelectedComponent(_cclass, cid);
}
/** The component class associated with this editor. */
protected ComponentClass _cclass;
/** The components selectable via this editor. */
protected List<Integer> _components;
/** The label denoting the currently selected component index. */
protected JLabel _clabel;
/** The builder model. */
protected BuilderModel _model;
}
@@ -0,0 +1,76 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast.builder;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.JPanel;
import com.samskivert.swing.VGroupLayout;
import com.threerings.cast.ComponentClass;
import static com.threerings.cast.Log.log;
/**
* The component panel displays the available components for all
* component classes and allows the user to choose a set of components
* for compositing into a character image.
*/
public class ComponentPanel extends JPanel
{
/**
* Constructs the component panel.
*/
public ComponentPanel (BuilderModel model, String cprefix)
{
setLayout(new VGroupLayout(VGroupLayout.STRETCH));
// set up a border
setBorder(BorderFactory.createEtchedBorder());
// add the component editors to the panel
addClassEditors(model, cprefix);
}
/**
* Adds editor user interface elements for each component class to
* allow the user to select the desired component.
*/
protected void addClassEditors (BuilderModel model, String cprefix)
{
List<ComponentClass> classes = model.getComponentClasses();
int size = classes.size();
for (int ii = 0; ii < size; ii++) {
ComponentClass cclass = classes.get(ii);
if (!cclass.name.startsWith(cprefix)) {
continue;
}
List<Integer> ccomps = model.getComponents(cclass);
if (ccomps.size() > 0) {
add(new ClassEditor(model, cclass, ccomps));
} else {
log.info("Not creating editor for empty class " +
"[class=" + cclass + "].");
}
}
}
}
@@ -0,0 +1,131 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast.builder;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JPanel;
import com.threerings.util.DirectionCodes;
import com.threerings.cast.CharacterDescriptor;
import com.threerings.cast.CharacterManager;
import com.threerings.cast.CharacterSprite;
import com.threerings.cast.StandardActions;
/**
* The sprite panel displays a character sprite centered in the panel
* suitable for user perusal.
*/
public class SpritePanel extends JPanel
implements DirectionCodes, BuilderModelListener
{
/**
* Constructs the sprite panel.
*/
public SpritePanel (CharacterManager charmgr, BuilderModel model)
{
// save off references
_charmgr = charmgr;
_model = model;
// listen to the builder model so that we can update the
// sprite when a new component is selected
_model.addListener(this);
}
@Override
public void paintComponent (Graphics g)
{
super.paintComponent(g);
Graphics2D gfx = (Graphics2D)g;
if (_sprite != null) {
// render the sprite
_sprite.paint(gfx);
}
}
@Override
public void doLayout ()
{
super.doLayout();
generateSprite();
centerSprite();
}
// documentation inherited
public void modelChanged (int event)
{
if (event == COMPONENT_CHANGED) {
generateSprite();
}
}
/**
* Generates a new character sprite for display to reflect the
* currently selected character components.
*/
protected void generateSprite ()
{
int components[] = _model.getSelectedComponents();
CharacterDescriptor desc = new CharacterDescriptor(components, null);
CharacterSprite sprite = _charmgr.getCharacter(desc);
setSprite(sprite);
}
/**
* Sets the sprite to be displayed.
*/
protected void setSprite (CharacterSprite sprite)
{
sprite.setActionSequence(StandardActions.STANDING);
sprite.setOrientation(WEST);
_sprite = sprite;
centerSprite();
repaint();
}
/**
* Sets the sprite's location to render it centered within the panel.
*/
protected void centerSprite ()
{
if (_sprite != null) {
Dimension d = getSize();
int shei = _sprite.getHeight();
int x = d.width / 2, y = (d.height + shei) / 2;
_sprite.setLocation(x, y);
}
}
/** The sprite displayed by the panel. */
protected CharacterSprite _sprite;
/** The character manager. */
protected CharacterManager _charmgr;
/** The builder model. */
protected BuilderModel _model;
}
@@ -0,0 +1,104 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast.bundle;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputStream;
import com.samskivert.io.StreamUtil;
import com.threerings.resource.FileResourceBundle;
import com.threerings.resource.ResourceBundle;
import static com.threerings.cast.Log.log;
/**
* Utility functions related to creating and manipulating component bundles.
*/
public class BundleUtil
{
/** The path in the metadata bundle to the serialized action table. */
public static final String ACTIONS_PATH = "actions.dat";
/** The path in the metadata bundle to the serialized action tile sets table. */
public static final String ACTION_SETS_PATH = "action_sets.dat";
/** The path in the metadata bundle to the serialized component class table. */
public static final String CLASSES_PATH = "classes.dat";
/** The path in the component bundle to the serialized component id to class/type mapping. */
public static final String COMPONENTS_PATH = "components.dat";
/** The file extension of our action tile images. */
public static final String IMAGE_EXTENSION = ".png";
/** The serialized tileset extension for our action tilesets. */
public static final String TILESET_EXTENSION = ".dat";
/**
* Attempts to load an object from the supplied resource bundle with the specified path.
*
* @param wipeOnFailure if there is an error reading the object from the bundle and this
* parameter is true, we will instruct the bundle to delete its underlying jar file before
* propagating the exception with the expectation that it will be redownloaded and repaired the
* next time the application is run.
*
* @return the unserialized object in question.
*
* @exception IOException thrown if an I/O error occurs while reading the object from the
* bundle.
*/
public static Object loadObject (ResourceBundle bundle, String path, boolean wipeOnFailure)
throws IOException, ClassNotFoundException
{
InputStream bin = null;
try {
bin = bundle.getResource(path);
if (bin == null) {
return null;
}
return new ObjectInputStream(bin).readObject();
} catch (InvalidClassException ice) {
log.warning("Aiya! Serialized object is hosed [bundle=" + bundle +
", element=" + path + ", error=" + ice.getMessage() + "].");
return null;
} catch (IOException ioe) {
log.warning("Error reading resource from bundle [bundle=" + bundle + ", path=" + path +
", wiping?=" + wipeOnFailure + "].");
if (wipeOnFailure) {
StreamUtil.close(bin);
bin = null;
if (bundle instanceof FileResourceBundle) {
((FileResourceBundle)bundle).wipeBundle(false);
}
}
throw ioe;
} finally {
StreamUtil.close(bin);
}
}
}
@@ -0,0 +1,567 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast.bundle;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.io.IOException;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.samskivert.util.IntIntMap;
import com.samskivert.util.IntMap;
import com.samskivert.util.IntMaps;
import com.samskivert.util.Tuple;
import com.threerings.util.DirectionCodes;
import com.threerings.resource.FileResourceBundle;
import com.threerings.resource.ResourceBundle;
import com.threerings.resource.ResourceManager;
import com.threerings.media.image.BufferedMirage;
import com.threerings.media.image.Colorization;
import com.threerings.media.image.ImageDataProvider;
import com.threerings.media.image.ImageManager;
import com.threerings.media.image.Mirage;
import com.threerings.media.tile.IMImageProvider;
import com.threerings.media.tile.Tile;
import com.threerings.media.tile.TileSet;
import com.threerings.media.tile.TrimmedTile;
import com.threerings.cast.ActionFrames;
import com.threerings.cast.ActionSequence;
import com.threerings.cast.CharacterComponent;
import com.threerings.cast.ComponentClass;
import com.threerings.cast.ComponentRepository;
import com.threerings.cast.FrameProvider;
import com.threerings.cast.NoSuchComponentException;
import com.threerings.cast.StandardActions;
import com.threerings.cast.TrimmedMultiFrameImage;
import static com.threerings.cast.Log.log;
/**
* A component repository implementation that obtains information from resource bundles.
*
* @see ResourceManager
*/
public class BundledComponentRepository
implements DirectionCodes, ComponentRepository
{
/**
* Constructs a repository which will obtain its resource set from the supplied resource
* manager.
*
* @param rmgr the resource manager from which to obtain our resource set.
* @param imgr the image manager that we'll use to decode and cache images.
* @param name the name of the resource set from which we will be loading our component data.
*
* @exception IOException thrown if an I/O error occurs while reading our metadata from the
* resource bundles.
*/
public BundledComponentRepository (
ResourceManager rmgr, ImageManager imgr, String name)
throws IOException
{
// keep this guy around
_imgr = imgr;
// first we obtain the resource set from whence will come our bundles
ResourceBundle[] rbundles = rmgr.getResourceSet(name);
if (rbundles == null) {
// Couldn't find a bundle with that name, so just make empty maps for safe enumerating
_actions = Maps.newHashMap();
_classes = Maps.newHashMap();
return;
}
// look for our metadata info in each of the bundles
try {
for (ResourceBundle rbundle : rbundles) {
if (_actions == null) {
@SuppressWarnings("unchecked") Map<String, ActionSequence> amap =
(Map<String, ActionSequence>)BundleUtil.loadObject(
rbundle, BundleUtil.ACTIONS_PATH, true);
_actions = amap;
}
if (_actionSets == null) {
@SuppressWarnings("unchecked") Map<String, TileSet> asets =
(Map<String, TileSet>)BundleUtil.loadObject(
rbundle, BundleUtil.ACTION_SETS_PATH, true);
_actionSets = asets;
}
if (_classes == null) {
@SuppressWarnings("unchecked") Map<String, ComponentClass> cmap =
(Map<String, ComponentClass>)BundleUtil.loadObject(
rbundle, BundleUtil.CLASSES_PATH, true);
_classes = cmap;
}
}
// now go back and load up all of the component information
for (ResourceBundle rbundle : rbundles) {
@SuppressWarnings("unchecked") IntMap<Tuple<String, String>> comps =
(IntMap<Tuple<String, String>>)BundleUtil.loadObject(
rbundle, BundleUtil.COMPONENTS_PATH, true);
if (comps == null) {
continue;
}
// create a frame provider for this bundle
FrameProvider fprov = new ResourceBundleProvider(_imgr, rbundle);
// now create char. component instances for each component in the serialized table
Iterator<Integer> iter = comps.keySet().iterator();
while (iter.hasNext()) {
int componentId = iter.next().intValue();
Tuple<String, String> info = comps.get(componentId);
createComponent(componentId, info.left, info.right, fprov);
}
}
} catch (ClassNotFoundException cnfe) {
throw (IOException) new IOException(
"Internal error unserializing metadata").initCause(cnfe);
}
// if we failed to load our classes or actions, create empty hashtables so that we can
// safely enumerate our emptiness
if (_actions == null) {
_actions = Maps.newHashMap();
}
if (_classes == null) {
_classes = Maps.newHashMap();
}
}
/**
* Configures the bundled component repository to wipe any bundles that report certain kinds of
* failure. In the event that an unpacked bundle becomes corrupt, this is useful in that it
* will force the bundle to be unpacked on the next application invocation, potentially
* remedying the problem of a corrupt unpacking.
*/
public void setWipeOnFailure (boolean wipeOnFailure)
{
_wipeOnFailure = wipeOnFailure;
}
// documentation inherited
public CharacterComponent getComponent (int componentId)
throws NoSuchComponentException
{
CharacterComponent component = _components.get(componentId);
if (component == null) {
throw new NoSuchComponentException(componentId);
}
return component;
}
// documentation inherited
public CharacterComponent getComponent (String className, String compName)
throws NoSuchComponentException
{
// look up the list for that class
ArrayList<CharacterComponent> comps = _classComps.get(className);
if (comps != null) {
// scan the list for the named component
int ccount = comps.size();
for (int ii = 0; ii < ccount; ii++) {
CharacterComponent comp = comps.get(ii);
if (comp.name.equals(compName)) {
return comp;
}
}
}
throw new NoSuchComponentException(className, compName);
}
// documentation inherited
public ComponentClass getComponentClass (String className)
{
return _classes.get(className);
}
// documentation inherited
public Iterator<ComponentClass> enumerateComponentClasses ()
{
return _classes.values().iterator();
}
// documentation inherited
public Iterator<ActionSequence> enumerateActionSequences ()
{
return _actions.values().iterator();
}
// documentation inherited
public Iterator<Integer> enumerateComponentIds (final ComponentClass compClass)
{
Predicate<Map.Entry<Integer,CharacterComponent>> pred =
new Predicate<Map.Entry<Integer,CharacterComponent>>() {
public boolean apply (Map.Entry<Integer,CharacterComponent> entry) {
return entry.getValue().componentClass.equals(compClass);
}
};
Function<Map.Entry<Integer,CharacterComponent>,Integer> func =
new Function<Map.Entry<Integer,CharacterComponent>,Integer>() {
public Integer apply (Map.Entry<Integer,CharacterComponent> entry) {
return entry.getKey();
}
};
return Iterators.transform(Iterators.filter(_components.entrySet().iterator(), pred), func);
}
/**
* Creates a component and inserts it into the component table.
*/
protected void createComponent (
int componentId, String cclass, String cname, FrameProvider fprov)
{
// look up the component class information
ComponentClass clazz = _classes.get(cclass);
if (clazz == null) {
log.warning("Non-existent component class",
"class", cclass, "name", cname, "id", componentId);
return;
}
// create the component
CharacterComponent component = new CharacterComponent(componentId, cname, clazz, fprov);
// stick it into the appropriate tables
_components.put(componentId, component);
// we have a hash of lists for mapping components by class/name
ArrayList<CharacterComponent> comps = _classComps.get(cclass);
if (comps == null) {
comps = Lists.newArrayList();
_classComps.put(cclass, comps);
}
if (!comps.contains(component)) {
comps.add(component);
} else {
log.info("Requested to register the same component twice?", "comp", component);
}
}
protected TileSetFrameImage createTileSetFrameImage (TileSet aset, ActionSequence actseq)
{
return new TileSetFrameImage(aset, actseq);
}
/**
* Instances of these provide images to our component action tilesets and frames to our
* components.
*/
protected class ResourceBundleProvider extends IMImageProvider
implements ImageDataProvider, FrameProvider
{
/**
* Constructs an instance that will obtain image data from the specified resource bundle.
*/
public ResourceBundleProvider (ImageManager imgr, ResourceBundle bundle) {
super(imgr, (String)null);
_dprov = this;
_bundle = bundle;
}
// from interface ImageDataProvider
public String getIdent () {
return "bcr:" + _bundle.getIdent();
}
// from interface ImageDataProvider
public BufferedImage loadImage (String path) throws IOException {
return _bundle.getImageResource(path, true);
}
// from interface FrameProvider
public ActionFrames getFrames (CharacterComponent component, String action, String type) {
// obtain the action sequence definition for this action
ActionSequence actseq = _actions.get(action);
if (actseq == null) {
log.warning("Missing action sequence definition",
"action", action, "component", component);
return null;
}
// determine our image path name
String imgpath = action, dimgpath = ActionSequence.DEFAULT_SEQUENCE;
if (type != null) {
imgpath += "_" + type;
dimgpath += "_" + type;
}
String root = component.componentClass.name + "/" + component.name + "/";
String cpath = root + imgpath + BundleUtil.TILESET_EXTENSION;
String dpath = root + dimgpath + BundleUtil.TILESET_EXTENSION;
// look to see if this tileset is already cached (as the custom action or the default
// action)
TileSet aset = _setcache.get(cpath);
if (aset == null) {
aset = _setcache.get(dpath);
if (aset != null) {
// save ourselves a lookup next time
_setcache.put(cpath, aset);
}
}
try {
// then try loading up a tileset customized for this action
if (aset == null) {
aset = (TileSet)BundleUtil.loadObject(_bundle, cpath, false);
}
// if that failed, try loading the default tileset
if (aset == null) {
aset = (TileSet)BundleUtil.loadObject(_bundle, dpath, false);
_setcache.put(dpath, aset);
}
// if that failed too, we're hosed
if (aset == null) {
// if this is a shadow or crop image, no need to freak out as they are optional
if (!StandardActions.CROP_TYPE.equals(type) &&
!StandardActions.SHADOW_TYPE.equals(type)) {
log.warning("Unable to locate tileset for action '" + imgpath + "' " +
component + ".");
if (_wipeOnFailure && _bundle instanceof FileResourceBundle) {
((FileResourceBundle)_bundle).wipeBundle(false);
}
}
return null;
}
aset.setImageProvider(this);
_setcache.put(cpath, aset);
return createTileSetFrameImage(aset, actseq);
} catch (Exception e) {
log.warning("Error loading tset for action '" + imgpath + "' " + component + ".",
e);
return null;
}
}
// from interface FrameProvider
public String getFramePath (CharacterComponent component, String action, String type,
Set<String> existentPaths) {
String actionPath = makePath(component, action, type);
if(!existentPaths.contains(actionPath)) {
return makePath(component, ActionSequence.DEFAULT_SEQUENCE, type);
}
return actionPath;
}
protected String makePath(CharacterComponent component, String action, String type) {
String imgpath = action;
if (type != null) {
imgpath += "_" + type;
}
String root = component.componentClass.name + "/" + component.name + "/";
return _bundle.getIdent() + root + imgpath + BundleUtil.IMAGE_EXTENSION;
}
@Override
public Mirage getTileImage (String path, Rectangle bounds, Colorization[] zations) {
// we don't need our images prepared for screen rendering
BufferedImage src = _imgr.getImage(getImageKey(path), zations);
float percentageOfDataBuffer = 1;
if (bounds != null) {
percentageOfDataBuffer =
(bounds.height * bounds.width) / (float)(src.getHeight() * src.getWidth());
src = src.getSubimage(bounds.x, bounds.y, bounds.width, bounds.height);
}
return new BufferedMirage(src, percentageOfDataBuffer);
}
/** The resource bundle from which we obtain image data. */
protected ResourceBundle _bundle;
/** Cache of tilesets loaded from our bundle. */
protected Map<String, TileSet> _setcache = Maps.newHashMap();
}
/**
* Used to provide multiframe images using data obtained from a tileset.
*/
public static class TileSetFrameImage implements ActionFrames
{
/**
* Constructs a tileset frame image with the specified tileset and for the specified
* orientation.
*/
public TileSetFrameImage (TileSet set, ActionSequence actseq) {
this(set, actseq, 0, 0);
}
/**
* Constructs a tileset frame image with the specified tileset and for the specified
* orientation, with an optional translation.
*/
public TileSetFrameImage (TileSet set, ActionSequence actseq, int dx, int dy) {
_set = set;
_actseq = actseq;
_dx = dx;
_dy = dy;
// compute these now to avoid pointless recomputation later
_ocount = actseq.orients.length;
_fcount = set.getTileCount() / _ocount;
// create our mapping from orientation to animation sequence index
for (int ii = 0; ii < _ocount; ii++) {
_orients.put(actseq.orients[ii], ii);
}
}
// documentation inherited from interface
public int getOrientationCount () {
return _ocount;
}
// documentation inherited from interface
public TrimmedMultiFrameImage getFrames (final int orient) {
return new TrimmedMultiFrameImage() {
public int getFrameCount () {
return _fcount;
}
public int getWidth (int index) {
return _set.getTile(getTileIndex(orient, index)).getWidth();
}
public int getHeight (int index) {
return _set.getTile(getTileIndex(orient, index)).getHeight();
}
public void paintFrame (Graphics2D g, int index, int x, int y) {
paintTile(g, orient, index, x, y);
}
public boolean hitTest (int index, int x, int y) {
return _set.getTile(getTileIndex(orient, index)).hitTest(x + _dx, y + _dy);
}
public void getTrimmedBounds (int index, Rectangle bounds) {
TileSetFrameImage.this.getTrimmedBounds(orient, index, bounds);
}
};
}
protected void paintTile(Graphics2D g, int orient, int index, int x, int y) {
_set.getTile(getTileIndex(orient, index)).paint(g, x + _dx, y + _dy);
}
public void getTrimmedBounds (int orient, int index, Rectangle bounds) {
Tile tile = getTile(orient, index);
if (tile instanceof TrimmedTile) {
((TrimmedTile)tile).getTrimmedBounds(bounds);
} else {
bounds.setBounds(0, 0, tile.getWidth(), tile.getHeight());
}
bounds.translate(_dx, _dy);
}
// documentation inherited from interface
public int getXOrigin (int orient, int index) {
return _actseq.origin.x;
}
// documentation inherited from interface
public int getYOrigin (int orient, int index) {
return _actseq.origin.y;
}
// documentation inherited from interface
public ActionFrames cloneColorized (Colorization[] zations) {
return new TileSetFrameImage(_set.clone(zations), _actseq);
}
// documentation inherited from interface
public ActionFrames cloneTranslated (int dx, int dy) {
return new TileSetFrameImage(_set, _actseq, dx, dy);
}
protected int getTileIndex (int orient, int index) {
return _orients.get(orient) * _fcount + index;
}
public Tile getTile (int orient, int index) {
return _set.getTile(getTileIndex(orient, index));
}
public Mirage getTileMirage (int orient, int index) {
return _set.getTileMirage(getTileIndex(orient, index));
}
/** The tileset from which we obtain our frame images. */
protected TileSet _set;
/** The action sequence for which we're providing frame images. */
protected ActionSequence _actseq;
/** A translation to apply to the images. */
protected int _dx, _dy;
/** Frame and orientation counts. */
protected int _fcount, _ocount;
/** A mapping from orientation code to animation sequence index. */
protected IntIntMap _orients = new IntIntMap();
}
/** We use the image manager to decode and cache images. */
protected ImageManager _imgr;
/** A table of action sequences. */
protected Map<String, ActionSequence> _actions;
/** A table of action sequence tilesets. */
protected Map<String, TileSet> _actionSets;
/** A table of component classes. */
protected Map<String, ComponentClass> _classes;
/** A table of component lists indexed on classname. */
protected Map<String, ArrayList<CharacterComponent>> _classComps = Maps.newHashMap();
/** The component table. */
protected IntMap<CharacterComponent> _components = IntMaps.newHashIntMap();
/** Whether or not we wipe our bundles on any failure. */
protected boolean _wipeOnFailure;
}
@@ -0,0 +1,107 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.cast.util;
import java.util.ArrayList;
import java.util.Iterator;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
import com.samskivert.util.RandomUtil;
import com.threerings.media.image.ColorPository;
import com.threerings.media.image.Colorization;
import com.threerings.cast.CharacterDescriptor;
import com.threerings.cast.ComponentClass;
import com.threerings.cast.ComponentRepository;
import static com.threerings.cast.Log.log;
/**
* Some Cast utilities that make use of our test resources.
*/
public class CastUtil
{
/**
* Returns a new character descriptor populated with a random set of components.
*/
public static CharacterDescriptor getRandomDescriptor (
ComponentRepository crepo, String gender, String[] COMP_CLASSES,
ColorPository cpos, String[] COLOR_CLASSES)
{
// get all available classes
ArrayList<ComponentClass> classes = Lists.newArrayList();
for (String element : COMP_CLASSES) {
String cname = gender + "/" + element;
ComponentClass cclass = crepo.getComponentClass(cname);
// make sure the component class exists
if (cclass == null) {
log.warning("Missing definition for component class", "class", cname);
continue;
}
// make sure there are some components in this class
Iterator<Integer> iter = crepo.enumerateComponentIds(cclass);
if (!iter.hasNext()) {
log.info("Skipping class for which we have no components", "class", cclass);
continue;
}
classes.add(cclass);
}
// select the components
int[] components = new int[classes.size()];
Colorization[][] zations = new Colorization[components.length][];
for (int ii = 0; ii < components.length; ii++) {
ComponentClass cclass = classes.get(ii);
// get the components available for this class
ArrayList<Integer> choices = Lists.newArrayList();
Iterators.addAll(choices, crepo.enumerateComponentIds(cclass));
// each of our components has up to four colorizations: two "global" skin colorizations
// and potentially a primary and secondary clothing colorization; in a real system one
// would probably keep a separate database of which character component required which
// colorizations, but here we just assume everything could have any of the four
// colorizations; it *usually* doesn't hose an image if you apply a recoloring that it
// does not support, but it can match stray colors unnecessarily
zations[ii] = new Colorization[COLOR_CLASSES.length];
for (int zz = 0; zz < COLOR_CLASSES.length; zz++) {
zations[ii][zz] = cpos.getRandomStartingColor(COLOR_CLASSES[zz]).getColorization();
}
// choose a random component
if (choices.size() > 0) {
int idx = RandomUtil.getInt(choices.size());
components[ii] = choices.get(idx).intValue();
} else {
log.info("Have no components in class", "class", cclass);
}
}
return new CharacterDescriptor(components, zations);
}
}
@@ -0,0 +1,309 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.chat;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Composite;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.AffineTransform;
import java.awt.geom.Area;
import javax.swing.Icon;
import com.samskivert.swing.Label;
import com.samskivert.swing.util.SwingUtil;
import com.threerings.media.MediaPanel;
import com.threerings.media.animation.Animation;
import com.threerings.media.image.ColorUtil;
import static com.threerings.NenyaLog.log;
/**
* Responsible for rendering a "unit" of chat on a {@link MediaPanel}.
*/
public class ChatGlyph extends Animation
{
/** Can be used by the Overlay to mark our position in the history. */
public int histIndex;
/**
* Construct a chat glyph.
*
* @param owner the subtitle overlay that ownz us.
* @param bounds the bounds of the glyph
* @param shape the shape to draw/outline.
* @param icon the Icon to draw inside the bubble, or null for none
* @param iconpos the virtual coordinate at which to draw the icon (can be null if no icon)
* @param label the Label to draw inside the bubble.
* @param labelpos the virtual coordinate at which to draw the label
*/
public ChatGlyph (SubtitleChatOverlay owner, int type, long lifetime, Rectangle bounds,
Shape shape, Icon icon, Point iconpos, Label label, Point labelpos, Color outline)
{
super(bounds);
jiggleBounds();
_owner = owner;
_shape = shape;
_type = type;
_icon = icon;
_ipos = iconpos;
_label = label;
_lpos = labelpos;
_lifetime = lifetime;
_outline = outline;
if (Color.BLACK.equals(_outline)) {
_background = Color.WHITE;
} else {
_background = ColorUtil.blend(Color.WHITE, _outline, .8f);
}
}
/**
* Sets whether or not this glyph is in dimmed mode.
*/
public void setDim (boolean dim)
{
if (_dim != dim) {
_dim = dim;
invalidate();
}
}
/**
* Render the chat glyph with no thought to dirty rectangles or
* restoring composites.
*/
public void render (Graphics2D gfx)
{
Object oalias = SwingUtil.activateAntiAliasing(gfx);
gfx.setColor(getBackground());
gfx.fill(_shape);
gfx.setColor(_outline);
gfx.draw(_shape);
SwingUtil.restoreAntiAliasing(gfx, oalias);
if (_icon != null) {
_icon.paintIcon(_owner.getTarget(), gfx, _ipos.x, _ipos.y);
}
gfx.setColor(Color.BLACK);
_label.render(gfx, _lpos.x, _lpos.y);
}
/**
* Get the internal chat type of this bubble.
*/
public int getType ()
{
return _type;
}
/**
* Returns the shape of this chat bubble.
*/
public Shape getShape ()
{
return _shape;
}
@Override
public void setLocation (int x, int y)
{
// we'll need these so that we can translate our complex shapes
int dx = x - _bounds.x, dy = y - _bounds.y;
super.setLocation(x, y);
if (dx != 0 || dy != 0) {
// update our icon position, if any
if (_ipos != null) {
_ipos.translate(dx, dy);
}
// update our label position
_lpos.translate(dx, dy);
if (_shape instanceof Area) {
((Area) _shape).transform(
AffineTransform.getTranslateInstance(dx, dy));
} else if (_shape instanceof Polygon) {
((Polygon) _shape).translate(dx, dy);
} else {
log.warning("Unable to translate shape", "glyph", this, "shape", _shape + "]!");
}
}
}
/**
* Called when the view has scrolled. The default implementation adjusts the glyph to remain
* in the same position relative to the view rather than allowing it to scroll with the view.
*/
public void viewDidScroll (int dx, int dy)
{
translate(dx, dy);
}
/**
* Attempt to translate this glyph.
*/
public void translate (int dx, int dy)
{
setLocation(_bounds.x + dx, _bounds.y + dy);
}
@Override
public void tick (long tickStamp)
{
// set up our born stamp if we've got one
if (_bornStamp == 0L) {
_bornStamp = tickStamp;
invalidate(); // make sure we're painted the first time
}
// if we're not yet ready to die, do nothing
long deathTime = _bornStamp + _lifetime;
if (tickStamp < deathTime) {
/* TEMPORARILY disabled blinking until we sort it out
if (_type == SubtitleChatOverlay.ATTENTION) {
float alphaWas = _alpha;
long val = tickStamp - _bornStamp;
if ((val < 3000) && (val % 1000 < 500)) {
_alpha = 0f;
} else {
_alpha = ALPHA;
}
if (_alpha != alphaWas) {
invalidate();
}
}
*/
return;
}
long msecs = tickStamp - deathTime;
if (msecs >= ANIM_TIME) {
_alpha = 0f;
// stick a fork in ourselves
_finished = true;
_owner.glyphExpired(this);
} else {
_alpha = ALPHA * (ANIM_TIME - msecs) / ANIM_TIME;
}
invalidate();
}
@Override
public void fastForward (long timeDelta)
{
if (_bornStamp > 0L) {
_bornStamp += timeDelta;
}
}
@Override
public void paint (Graphics2D gfx)
{
float alpha = _dim ? _alpha / 3 : _alpha;
if (alpha == 0f) {
return;
}
if (alpha != 1f) {
Composite ocomp = gfx.getComposite();
gfx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
render(gfx);
gfx.setComposite(ocomp);
} else {
render(gfx);
}
}
protected Color getBackground ()
{
return _background;
}
/**
* The damn repaint manager expects 1 more pixel than the shape gives, so we manually resize.
*/
protected void jiggleBounds ()
{
_bounds.setSize(_bounds.width + 1, _bounds.height + 1);
}
/** The subtitle chat overlay that ownz us. */
protected SubtitleChatOverlay _owner;
/** The type of chat represented by this glyph. */
protected int _type;
/** The shape of this glyph. */
protected Shape _shape;
/** The visual icon, or null for none. */
protected Icon _icon;
/** The position at which we render our icon. */
protected Point _ipos;
/** The textual label. */
protected Label _label;
/** The position at which we render our text. */
protected Point _lpos;
/** The alpha level that we'll render at when fading out. */
protected float _alpha = ALPHA;
/** Whether we're in dimmed mode. */
protected boolean _dim = false;
/** The length of the fade animation. */
protected static final long ANIM_TIME = 800L;
/** The number of milliseconds to wait before this bubble will expire
* and should begin to fade out. */
protected long _lifetime;
/** The time at which we came into being on the screen. */
protected long _bornStamp;
/** Our outline color. */
protected Color _outline;
/** Our background color. */
protected Color _background;
/** The initial alpha of all chat glyphs. */
protected static final float ALPHA = .9f;
}
@@ -0,0 +1,398 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.chat;
import java.awt.Color;
import java.awt.Font;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import com.samskivert.swing.Label;
import com.samskivert.util.Tuple;
import com.threerings.crowd.chat.data.ChatCodes;
/**
* Contains all of the routines that might (or must) be customized by a system that makes use of
* the comic chat system.
*/
public abstract class ChatLogic
{
/** The default chat decay parameter. See {@link #getDisplayDurationIndex}. */
public static final int DEFAULT_CHAT_DECAY = 1;
/** The padding in each direction around the text to the edges of a chat 'bubble'. */
public static final int PAD = 10;
/** Type mode code for default chat type (speaking). */
public static final int SPEAK = 0;
/** Type mode code for shout chat type. */
public static final int SHOUT = 1;
/** Type mode code for emote chat type. */
public static final int EMOTE = 2;
/** Type mode code for think chat type. */
public static final int THINK = 3;
/** Type place code for default place chat (cluster, scene). */
public static final int PLACE = 1 << 4;
// 2 and 3 are skipped for legacy migration reasons
/** Type code for a chat type that was used in some special context, like in a negotiation. */
public static final int SPECIALIZED = 4 << 4;
/** Our internal code for tell chat. */
public static final int TELL = 5 << 4;
/** Our internal code for tell feedback chat. */
public static final int TELLFEEDBACK = 6 << 4;
/** Our internal code for info system messges. */
public static final int INFO = 7 << 4;
/** Our internal code for feedback system messages. */
public static final int FEEDBACK = 8 << 4;
/** Our internal code for attention system messages. */
public static final int ATTENTION = 9 << 4;
/** Our internal code for any type of chat that is continued in a subtitle. */
public static final int CONTINUATION = 10 << 4;
/** Type place code for broadcast chat type. */
public static final int BROADCAST = 11 << 4;
/** Our internal code for a chat type we will ignore. */
public static final int IGNORECHAT = -1;
/**
* Returns the message bundle used to translate default messages.
*/
public abstract String getDefaultMessageBundle ();
/**
* Determines the format string and whether to use quotes based on the chat type.
*/
public Tuple<String, Boolean> decodeFormat (int type, String format)
{
boolean quotes = true;
switch (placeOf(type)) {
// derived classes may wish to change the format here based on the place
case PLACE:
switch (modeOf(type)) {
case EMOTE:
quotes = false;
break;
}
break;
}
return Tuple.newTuple(format, quotes);
}
/**
* Decodes the main chat type given the supplied localtype provided by the chat system.
*/
public int decodeType (String localtype)
{
if (ChatCodes.USER_CHAT_TYPE.equals(localtype)) {
return TELL;
} else if (ChatCodes.PLACE_CHAT_TYPE.equals(localtype)) {
return PLACE;
} else {
return 0;
}
}
/**
* Adjust the chat type based on the mode of the chat message.
*/
public int adjustTypeByMode (int mode, int type)
{
switch (mode) {
case ChatCodes.DEFAULT_MODE:
return type | SPEAK;
case ChatCodes.EMOTE_MODE:
return type | EMOTE;
case ChatCodes.THINK_MODE:
return type | THINK;
case ChatCodes.SHOUT_MODE:
return type | SHOUT;
case ChatCodes.BROADCAST_MODE:
return BROADCAST; // broadcast always looks like broadcast
default:
return type;
}
}
/**
* Get the font to use for the given bubble type.
*/
public Font getFont (int type)
{
return DEFAULT_FONT;
}
/**
* Creates a label for the specified text. Derived classes may wish to use specialized labels.
*/
public Label createLabel (String text)
{
return new Label(text);
}
/**
* Computes the chat glyph outline color from the chat message type.
*/
public Color getOutlineColor (int type)
{
switch (type) {
case BROADCAST:
return BROADCAST_COLOR;
case TELL:
return TELL_COLOR;
case TELLFEEDBACK:
return TELLFEEDBACK_COLOR;
case INFO:
return INFO_COLOR;
case FEEDBACK:
return FEEDBACK_COLOR;
case ATTENTION:
return ATTENTION_COLOR;
default:
return Color.black;
}
}
/**
* Get the appropriate shape for the specified type of chat.
*
* @param r the rectangle bounding the chat label.
* @param b the rectangle bounding the chat label and icon.
*/
public Shape getSubtitleShape (int type, Rectangle r, Rectangle b)
{
int placeType = placeOf(type);
switch (placeType) {
case SPECIALIZED:
case PLACE:
return getPlaceSubtitleShape(type, r);
case TELL: {
// lightning box!
int halfy = r.y + r.height/2;
Polygon p = new Polygon();
p.addPoint(r.x - PAD - 2, r.y);
p.addPoint(r.x - PAD / 2 - 2, halfy);
p.addPoint(r.x - PAD, halfy);
p.addPoint(r.x - PAD / 2, r.y + r.height);
p.addPoint(r.x + r.width + PAD + 2, r.y + r.height);
p.addPoint(r.x + r.width + PAD / 2 + 2, halfy);
p.addPoint(r.x + r.width + PAD, halfy);
p.addPoint(r.x + r.width + PAD / 2, r.y);
return p;
}
case TELLFEEDBACK: {
// reverse-lightning box!
int halfy = r.y + r.height/2;
Polygon p = new Polygon();
p.addPoint(r.x - PAD - 2, r.y + r.height);
p.addPoint(r.x - PAD / 2 - 2, halfy);
p.addPoint(r.x - PAD, halfy);
p.addPoint(r.x - PAD / 2, r.y);
p.addPoint(r.x + r.width + PAD + 2, r.y);
p.addPoint(r.x + r.width + PAD / 2 + 2, halfy);
p.addPoint(r.x + r.width + PAD, halfy);
p.addPoint(r.x + r.width + PAD / 2, r.y + r.height);
return p;
}
case FEEDBACK: {
// slanted box subtitle
Polygon p = new Polygon();
p.addPoint(r.x - PAD / 2, r.y);
p.addPoint(r.x + r.width + PAD, r.y);
p.addPoint(r.x + r.width + PAD / 2, r.y + r.height);
p.addPoint(r.x - PAD, r.y + r.height);
return p;
}
case BROADCAST:
case CONTINUATION:
case INFO:
case ATTENTION:
default: {
Rectangle grown = new Rectangle(r);
grown.grow(PAD, 0);
return new Area(grown);
}
}
}
/**
* Get the spacing, in pixels, between the latest subtitle of the specified type and the
* previous subtitle.
*/
public int getSubtitleSpacing (int type)
{
switch (placeOf(type)) {
// derived classes may wish to adjust subtitle spacing here based on chat type
default:
return 1;
}
}
/**
* A helper function for {@link #getSubtitleShape}.
*/
public Shape getPlaceSubtitleShape (int type, Rectangle r)
{
switch (modeOf(type)) {
default:
case SPEAK: {
// rounded rectangle subtitle
Area a = new Area(r);
a.add(new Area(new Ellipse2D.Float(r.x - PAD, r.y, PAD * 2, r.height)));
a.add(new Area(new Ellipse2D.Float(r.x + r.width - PAD, r.y, PAD * 2, r.height)));
return a;
}
case THINK: {
r.grow(PAD / 2, 0);
Area a = new Area(r);
int dia = 8;
int num = (int) Math.ceil(r.height / ((float) dia));
int leftside = r.x - dia/2;
int rightside = r.x + r.width - dia/2 - 1;
int maxh = r.height - dia;
for (int ii=0; ii < num; ii++) {
int ypos = r.y + Math.min((r.height * ii) / num, maxh);
a.add(new Area(new Ellipse2D.Float(leftside, ypos, dia, dia)));
a.add(new Area(new Ellipse2D.Float(rightside, ypos, dia, dia)));
}
return a;
}
case SHOUT: {
r.grow(PAD / 2, 0);
Area a = new Area(r);
Polygon left = new Polygon();
Polygon right = new Polygon();
int spikehei = 8;
int num = (int) Math.ceil(r.height / ((float) spikehei));
left.addPoint(r.x, r.y);
left.addPoint(r.x - PAD, r.y + spikehei / 2);
left.addPoint(r.x, r.y + spikehei);
right.addPoint(r.x + r.width , r.y);
right.addPoint(r.x + r.width + PAD, r.y + spikehei / 2);
right.addPoint(r.x + r.width, r.y + spikehei);
int ypos = 0;
int maxpos = r.y + r.height - spikehei + 1;
for (int ii=0; ii < num; ii++) {
int newpos = Math.min((r.height * ii) / num, maxpos);
left.translate(0, newpos - ypos);
right.translate(0, newpos - ypos);
a.add(new Area(left));
a.add(new Area(right));
ypos = newpos;
}
return a;
}
case EMOTE: {
// a box that curves inward on the left and right
r.grow(PAD, 0);
Area a = new Area(r);
a.subtract(new Area(new Ellipse2D.Float(r.x - PAD / 2, r.y, PAD, r.height)));
a.subtract(new Area(new Ellipse2D.Float(r.x + r.width - PAD / 2, r.y, PAD, r.height)));
return a;
}
}
}
/**
* Returns metrics on how long chat messages should be displayed.
*/
public long[] getDisplayDurations (int indexOffset)
{
return DISPLAY_DURATION_PARAMS[getDisplayDurationIndex() + indexOffset];
}
/**
* Get the current display duration parameters: 0 = fast, 1 = medium, 2 = long.
* See {@link #DISPLAY_DURATION_PARAMS}.
*/
protected int getDisplayDurationIndex ()
{
return DEFAULT_CHAT_DECAY;
}
/**
* Extracts the mode constant from the type value.
*/
protected static int modeOf (int type)
{
return type & 0xF;
}
/**
* Extract the place constant from the type value.
*/
protected static int placeOf (int type)
{
return type & ~0xF;
}
/**
* Times to display chat: { (time per character), (min time), (max time) }
*
* Groups 0/1/2 are short/medium/long for chat bubbles, and groups 1/2/3 are short/medium/long
* for subtitles.
*/
protected static final long[][] DISPLAY_DURATION_PARAMS = {
{ 125L, 10000L, 30000L }, // fastest durations
{ 200L, 15000L, 40000L }, // medium (default) durations
{ 275L, 20000L, 50000L }, // longest regular duration..
{ 350L, 25000L, 60000L } // grampatime!
};
// used to color chat bubbles
protected static final Color BROADCAST_COLOR = new Color(0x990000);
protected static final Color FEEDBACK_COLOR = new Color(0x00AA00);
protected static final Color TELL_COLOR = new Color(0x0000AA);
protected static final Color TELLFEEDBACK_COLOR = new Color(0x00AAAA);
protected static final Color INFO_COLOR = new Color(0xAAAA00);
protected static final Color ATTENTION_COLOR = new Color(0xFF5000);
/** Our default chat font. */
protected static final Font DEFAULT_FONT = new Font("SansSerif", Font.PLAIN, 12);
}
@@ -0,0 +1,204 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.chat;
import java.awt.Point;
import java.awt.Shape;
import java.util.List;
import com.threerings.util.MessageBundle;
import com.threerings.util.Name;
import com.threerings.crowd.chat.client.ChatDisplay;
import com.threerings.crowd.util.CrowdContext;
import com.threerings.media.VirtualMediaPanel;
import static com.threerings.NenyaLog.log;
/**
* An abstract class that represents a chat display that can be overlayed upon another component.
*/
public abstract class ChatOverlay
implements ChatDisplay
{
/**
* An interface for providing information about what is under the overlay.
*/
public interface InfoProvider
{
/**
* Get a list of Shape objects that we should attempt to avoid when laying out the chat.
* The ChatOverlay will not modify these shape objects. The easy thing to do would be to
* just return java.awt.Rectangle objects.
*
* @param speaker The username of the speaking player, or null.
* @param high Add to this list shapes that should never be drawn on.
*
* @param low If non-null, add to this list shapes that can be drawn on if needed.
*/
public void getAvoidables (Name speaker, List<Shape> high, List<Shape> low);
/**
* Get a point which is approximately the origin of the speaker, or null if unknown.
*/
public Point getSpeaker (Name speaker);
}
/**
* Causes the chat overlay to make itself visible or invisible.
*/
public void setVisible (boolean visible)
{
// derived classes will want to do the right thing here
}
/**
* Set the dimmed mode of the currently displaying glyphs.
*/
public void setDimmed (boolean dimmed)
{
_dimmed = dimmed;
}
/**
* Indicates that the target component was added to the widget hier. Should be called when we
* wish to start displaying chat.
*/
public void added (VirtualMediaPanel target)
{
_target = target;
}
/**
* Layout the chat overlay inside the previously configured target component. Should be called
* if our component changes size.
*/
public abstract void layout ();
/**
* Indicates that the target component was removed from the widget hier. Should be called when
* we no longer wish to paint chat.
*/
public void removed ()
{
_target = null;
}
/**
* Callback from the target that the place has changed and we are to now talk to the new info
* provider.
*/
public void newPlaceEntered (InfoProvider provider)
{
_provider = provider;
}
/**
* A callback indicating that we've left the place and should stop talking to a particular
* infoprovider.
*/
public void placeExited ()
{
_provider = null;
}
/**
* Returns the media panel on which this chat overlay is operating.
*/
public VirtualMediaPanel getTarget ()
{
return _target;
}
/**
* Should be called when a speaker departs the chat area to allow the overlay to clean up.
*/
public void speakerDeparted (Name speaker)
{
// The regular overlay doesn't care about speakers leaving.
}
/**
* Called if our containing media panel scrolled its view.
*/
public void viewDidScroll (int dx, int dy)
{
}
/**
* Construct a chat overlay.
*/
protected ChatOverlay (CrowdContext ctx, ChatLogic logic)
{
_ctx = ctx;
_logic = logic;
}
/**
* Returns true if this chat overlay is showing and should therefore update its display
* accordingly.
*/
protected boolean isShowing ()
{
return (_target != null) && _target.isShowing();
}
/**
* Translates a string using the general client message bundle.
*/
protected String xlate (String message)
{
return xlate(_logic.getDefaultMessageBundle(), message);
}
/**
* Translates a string using the specified bundle.
*/
protected String xlate (String bundle, String message)
{
if (bundle != null) {
MessageBundle msgb = _ctx.getMessageManager().getBundle(bundle);
if (msgb == null) {
log.warning("No message bundle available to translate message",
"bundle", bundle, "message", message);
} else {
message = msgb.xlate(message);
}
}
return message;
}
/** The light of our life. */
protected CrowdContext _ctx;
/** Contains all of our customizations. */
protected ChatLogic _logic;
/** The component in which we are being displayed. */
protected VirtualMediaPanel _target;
/** The source of hints to how we layout the overlay. */
protected InfoProvider _provider;
/** Whether the chat glyphs are dimmed or not. */
protected boolean _dimmed;
}
@@ -0,0 +1,937 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.chat;
import java.util.List;
import java.util.Iterator;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Polygon;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.Area;
import java.awt.geom.Ellipse2D;
import java.awt.geom.RoundRectangle2D;
import javax.swing.JScrollBar;
import com.google.common.collect.Lists;
import com.samskivert.swing.Label;
import com.samskivert.swing.util.SwingUtil;
import com.threerings.util.MessageBundle;
import com.threerings.util.Name;
import com.threerings.media.image.ColorUtil;
import com.threerings.crowd.chat.data.ChatCodes;
import com.threerings.crowd.chat.data.ChatMessage;
import com.threerings.crowd.chat.data.UserMessage;
import com.threerings.crowd.util.CrowdContext;
import static com.threerings.NenyaLog.log;
/**
* Implements comic chat in the yohoho client.
*/
public class ComicChatOverlay extends SubtitleChatOverlay
{
/**
* Construct a comic chat overlay.
*
* @param subtitleHeight the amount of vertical space to use for subtitles.
*/
public ComicChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar historyBar,
int subtitleHeight)
{
super(ctx, logic, historyBar, subtitleHeight);
}
@Override
public void newPlaceEntered (InfoProvider provider)
{
_newPlacePoint = _ctx.getChatDirector().getHistory().size();
super.newPlaceEntered(provider);
// and clear place-oriented bubbles
clearBubbles(false);
}
@Override
public void layout ()
{
clearBubbles(true); // these will get repopulated from the history
super.layout();
}
@Override
public void removed ()
{
// we do this before calling super because we want our target to
// be around for the bubble clearing
clearBubbles(true);
super.removed();
}
@Override
public void clear ()
{
super.clear();
clearBubbles(true);
}
@Override
public void viewDidScroll (int dx, int dy)
{
super.viewDidScroll(dx, dy);
viewDidScroll(_bubbles, dx, dy);
}
@Override
public void setDimmed (boolean dimmed)
{
super.setDimmed(dimmed);
updateDimmed(_bubbles);
}
@Override
public void speakerDeparted (Name speaker)
{
for (Iterator<BubbleGlyph> iter = _bubbles.iterator(); iter.hasNext();) {
BubbleGlyph rec = iter.next();
if (rec.isSpeaker(speaker)) {
_target.abortAnimation(rec);
iter.remove();
}
}
}
@Override
public void historyUpdated (int adjustment)
{
_newPlacePoint -= adjustment;
super.historyUpdated(adjustment);
}
/**
* Clear chat bubbles, either all of them or just the place-oriented ones.
*/
protected void clearBubbles (boolean all)
{
for (Iterator<BubbleGlyph> iter = _bubbles.iterator(); iter.hasNext();) {
ChatGlyph rec = iter.next();
if (all || isPlaceOrientedType(rec.getType())) {
_target.abortAnimation(rec);
iter.remove();
}
}
}
@Override
protected boolean shouldShowFromHistory (ChatMessage msg, int index)
{
// only show if the message was received since we last entered
// a new place, or if it's place-less chat.
return ((index >= _newPlacePoint) ||
(! isPlaceOrientedType(getType(msg, false))));
}
@Override
protected boolean isApprovedLocalType (String localtype)
{
if (ChatCodes.PLACE_CHAT_TYPE.equals(localtype) ||
ChatCodes.USER_CHAT_TYPE.equals(localtype)) {
return true;
}
log.debug("Ignoring non-standard system/feedback chat", "localtype", localtype);
return false;
}
/**
* Is the type of chat place-oriented.
*/
protected boolean isPlaceOrientedType (int type)
{
return (ChatLogic.placeOf(type)) == ChatLogic.PLACE;
}
@Override
protected void displayMessage (ChatMessage message, int type, Graphics2D layoutGfx)
{
// we might need to modify the textual part with translations,
// but we can't do that to the message object, since other chatdisplays also get it.
String text = message.message;
switch (ChatLogic.placeOf(type)) {
case ChatLogic.INFO:
case ChatLogic.ATTENTION:
if (createBubble(layoutGfx, type, message.timestamp, text, null, null)) {
return; // EXIT;
}
// if the bubble didn't fit (unlikely), make it a subtitle
break;
case ChatLogic.PLACE: {
UserMessage msg = (UserMessage) message;
Point speakerloc = _provider.getSpeaker(msg.speaker);
if (speakerloc == null) {
log.warning("ChatOverlay.InfoProvider doesn't know the speaker!",
"speaker", msg.speaker, "type", type);
return;
}
// emotes won't actually have tails, but we do want them to appear near the pirate
if (ChatLogic.modeOf(type) == ChatLogic.EMOTE) {
text = xlate(
MessageBundle.tcompose("m.emote_format", msg.getSpeakerDisplayName())) +
" " + text;
}
// try to add all the text as a bubble, but if it doesn't
// fit, add some of it and 'continue' the rest in a subtitle.
String leftover = text;
for (int ii = 1; ii < 7; ii++) {
String bubtext = splitNear(text, text.length() / ii);
if (createBubble(layoutGfx, type, msg.timestamp,
bubtext + ((ii > 1) ? "..." : ""), msg.speaker, speakerloc)) {
leftover = text.substring(bubtext.length());
break;
}
}
if (leftover.length() > 0 && !isHistoryMode()) {
String ltext = MessageBundle.tcompose("m.continue_format", msg.speaker);
ltext = xlate(ltext) + " \"" + leftover + "\"";
addSubtitle(createSubtitle(layoutGfx, ChatLogic.CONTINUATION,
message.timestamp, null, 0, ltext, true));
}
return; // EXIT
}
}
super.displayMessage(message, type, layoutGfx);
}
/**
* Split the text at the space nearest the specified location.
*/
protected String splitNear (String text, int pos)
{
if (pos >= text.length()) {
return text;
}
int forward = text.indexOf(' ', pos);
int backward = text.lastIndexOf(' ', pos);
int newpos = (Math.abs(pos - forward) < Math.abs(pos - backward)) ? forward : backward;
// if we couldn't find a decent place to split, just do it wherever
if (newpos == -1) {
newpos = pos;
} else {
// actually split the space onto the first part
newpos++;
}
return text.substring(0, newpos);
}
/**
* Create a chat bubble with the specified type and text.
*
* @param speakerloc if non-null, specifies that a tail should be added which points to that
* location.
* @return true if we successfully laid out the bubble
*/
protected boolean createBubble (
Graphics2D gfx, int type, long timestamp, String text, Name speaker, Point speakerloc)
{
Label label = layoutText(gfx, _logic.getFont(type), text);
label.setAlignment(Label.CENTER);
gfx.dispose();
// get the size of the new bubble
Rectangle r = getBubbleSize(type, label.getSize());
// get the user's old bubbles.
List<BubbleGlyph> oldbubs = getAndExpireBubbles(speaker);
int numold = oldbubs.size();
Rectangle placer, bigR = null;
if (numold == 0) {
placer = new Rectangle(r);
positionRectIdeally(placer, type, speakerloc);
} else {
// get a big rectangle encompassing the old and new
bigR = getRectWithOlds(r, oldbubs);
placer = new Rectangle(bigR);
positionRectIdeally(placer, type, speakerloc);
// we actually try to place midway between ideal and old
// and adjust up half the height of the new boy
placer.setLocation((placer.x + bigR.x) / 2,
(placer.y + (bigR.y - (r.height / 2))) / 2);
}
// then look for a place nearby where it will fit
// (making sure we only put it in the area above the subtitles)
Rectangle vbounds = new Rectangle(_target.getViewBounds());
vbounds.height -= _subtitleHeight;
if (!SwingUtil.positionRect(placer, vbounds, getAvoidList(speaker))) {
// we couldn't fit the bubble!
return false;
}
// now 'placer' is positioned reasonably.
if (0 == numold) {
r.setLocation(placer.x, placer.y);
} else {
int dx = placer.x - bigR.x;
int dy = placer.y - bigR.y;
for (int ii=0; ii < numold; ii++) {
BubbleGlyph bub = oldbubs.get(ii);
bub.removeTail();
Rectangle ob = bub.getBubbleBounds();
// recenter the translated bub within placer's width..
int xadjust = dx - (ob.x - bigR.x) +
(placer.width - ob.width) / 2;
bub.translate(xadjust, dy);
}
// and position 'r' in the right place relative to 'placer'
r.setLocation(placer.x + (placer.width - r.width) / 2,
placer.y + placer.height - r.height);
}
Shape shape = getBubbleShape(type, r);
Shape full = shape;
// if we have a tail, the full area should include that.
if (speakerloc != null) {
Area area = new Area(getTail(type, r, speakerloc));
area.add(new Area(shape));
full = area;
}
// finally, add the bubble
long lifetime = getChatExpire(timestamp, label.getText())-timestamp;
BubbleGlyph newbub = new BubbleGlyph(
this, type, lifetime, full, label, adjustLabel(type, r.getLocation()), shape,
speaker, _logic.getOutlineColor(type));
newbub.setDim(_dimmed);
_bubbles.add(newbub);
_target.addAnimation(newbub);
// and we need to dirty all the bubbles because they'll all be painted in slightly
// different colors
int numbubs = _bubbles.size();
for (int ii=0; ii < numbubs; ii++) {
_bubbles.get(ii).setAgeLevel(numbubs - ii - 1);
}
return true; // success!
}
/**
* Calculate the size of the chat bubble based on the dimensions of the label and the type of
* chat. It will be turned into a shape later, but we manipulate it for a while as just a
* rectangle (which are easier to move about and do intersection tests with, and besides the
* Shape interface has no way to translate).
*/
protected Rectangle getBubbleSize (int type, Dimension d)
{
switch (ChatLogic.modeOf(type)) {
case ChatLogic.SHOUT:
case ChatLogic.THINK:
case ChatLogic.EMOTE:
// extra room for these two monsters
return new Rectangle(d.width + (PAD * 4), d.height + (PAD * 4));
default:
return new Rectangle(d.width + (PAD * 2), d.height + (PAD * 2));
}
}
/**
* Position the label based on the type.
*/
protected Point adjustLabel (int type, Point labelpos)
{
switch (ChatLogic.modeOf(type)) {
case ChatLogic.SHOUT:
case ChatLogic.EMOTE:
case ChatLogic.THINK:
labelpos.translate(PAD * 2, PAD * 2);
break;
default:
labelpos.translate(PAD, PAD);
break;
}
return labelpos;
}
/**
* Position the rectangle in its ideal location given the type and speaker positon (which may
* be null).
*/
protected void positionRectIdeally (Rectangle r, int type, Point speaker)
{
if (speaker != null) {
// center it on top of the speaker (it'll be moved..)
r.setLocation(speaker.x - (r.width / 2),
speaker.y - (r.height / 2));
return;
}
// otherwise we have different areas for different types
Rectangle vbounds = _target.getViewBounds();
switch (ChatLogic.placeOf(type)) {
case ChatLogic.INFO:
case ChatLogic.ATTENTION:
// upper left
r.setLocation(vbounds.x + BUBBLE_SPACING,
vbounds.y + BUBBLE_SPACING);
return;
case ChatLogic.PLACE:
log.warning("Got to a place where I shouldn't get!");
break; // fall through
}
// put it in the center..
log.debug("Unhandled chat type in getLocation()", "type", type);
r.setLocation((vbounds.width - r.width) / 2,
(vbounds.height - r.height) / 2);
}
/**
* Get a rectangle based on the old bubbles, but with room for the new one.
*/
protected Rectangle getRectWithOlds (Rectangle r, List<BubbleGlyph> oldbubs)
{
int n = oldbubs.size();
// if no old bubs, just return the new one.
if (n == 0) {
return r;
}
// otherwise, encompass all the oldies
Rectangle bigR = null;
for (int ii=0; ii < n; ii++) {
BubbleGlyph bub = oldbubs.get(ii);
if (ii == 0) {
bigR = bub.getBubbleBounds();
} else {
bigR = bigR.union(bub.getBubbleBounds());
}
}
// and add space for the new boy
bigR.width = Math.max(bigR.width, r.width);
bigR.height += r.height;
return bigR;
}
/**
* Get the appropriate shape for the specified type of chat.
*/
protected Shape getBubbleShape (int type, Rectangle r)
{
switch (ChatLogic.placeOf(type)) {
case ChatLogic.INFO:
case ChatLogic.ATTENTION:
// boring rectangle wrapped in an Area for translation
return new Area(r);
}
switch (ChatLogic.modeOf(type)) {
case ChatLogic.SPEAK:
// a rounded rectangle balloon, put in an Area so that it's
// translatable
return new Area(new RoundRectangle2D.Float(
r.x, r.y, r.width, r.height, PAD * 4, PAD * 4));
case ChatLogic.SHOUT: {
// spikey balloon
Polygon left = new Polygon(), right = new Polygon();
Polygon top = new Polygon(), bot = new Polygon();
int x = r.x + PAD;
int y = r.y + PAD;
int wid = r.width - PAD * 2;
int hei = r.height - PAD * 2;
Area a = new Area(new Rectangle(x, y, wid, hei));
int spikebase = 10;
int cornbase = spikebase*3/4;
// configure spikes to the left and right sides
left.addPoint(x, y);
left.addPoint(x - PAD, y + spikebase/2);
left.addPoint(x, y + spikebase);
right.addPoint(x + wid, y);
right.addPoint(x + wid + PAD, y + spikebase/2);
right.addPoint(x + wid, y + spikebase);
// add the left and right side spikes
int ypos = 0;
int ahei = hei - cornbase;
int maxpos = ahei - spikebase + 1;
int numvert = (int) Math.ceil(ahei / ((float) spikebase));
for (int ii=0; ii < numvert; ii++) {
int newpos = cornbase/2 +
Math.min((ahei * ii) / numvert, maxpos);
left.translate(0, newpos - ypos);
right.translate(0, newpos - ypos);
a.add(new Area(left));
a.add(new Area(right));
ypos = newpos;
}
// configure spikes for the top and bottom
top.addPoint(x, y);
top.addPoint(x + spikebase/2, y - PAD);
top.addPoint(x + spikebase, y);
bot.addPoint(x, y + hei);
bot.addPoint(x + spikebase/2, y + hei + PAD);
bot.addPoint(x + spikebase, y + hei);
// add top and bottom spikes
int xpos = 0;
int awid = wid - cornbase;
maxpos = awid - spikebase + 1;
int numhorz = (int) Math.ceil(awid / ((float) spikebase));
for (int ii=0; ii < numhorz; ii++) {
int newpos = cornbase/2 +
Math.min((awid * ii) / numhorz, maxpos);
top.translate(newpos - xpos, 0);
bot.translate(newpos - xpos, 0);
a.add(new Area(top));
a.add(new Area(bot));
xpos = newpos;
}
// and lets also add corner spikes
Polygon corner = new Polygon();
corner.addPoint(x, y + cornbase);
corner.addPoint(x - PAD + 2, y - PAD + 2);
corner.addPoint(x + cornbase, y);
a.add(new Area(corner));
corner.reset();
corner.addPoint(x + wid - cornbase, y);
corner.addPoint(x + wid + PAD - 2, y - PAD + 2);
corner.addPoint(x + wid, y + cornbase);
a.add(new Area(corner));
corner.reset();
corner.addPoint(x + wid, y + hei - cornbase);
corner.addPoint(x + wid + PAD - 2, y + hei + PAD - 2);
corner.addPoint(x + wid - cornbase, y + hei);
a.add(new Area(corner));
corner.reset();
corner.addPoint(x + cornbase, y + hei);
corner.addPoint(x - PAD + 2, y + hei + PAD - 2);
corner.addPoint(x, y + hei - cornbase);
a.add(new Area(corner));
// grunt work!
return a;
}
case ChatLogic.EMOTE: {
// a box that curves inward on all sides
Area a = new Area(r);
a.subtract(new Area(new Ellipse2D.Float(r.x, r.y - PAD, r.width, PAD * 2)));
a.subtract(new Area(new Ellipse2D.Float(r.x, r.y + r.height - PAD, r.width, PAD * 2)));
a.subtract(new Area(new Ellipse2D.Float(r.x - PAD, r.y, PAD * 2, r.height)));
a.subtract(new Area(new Ellipse2D.Float(r.x + r.width - PAD, r.y, PAD * 2, r.height)));
return a;
}
case ChatLogic.THINK: {
// cloudy balloon!
int x = r.x + PAD;
int y = r.y + PAD;
int wid = r.width - PAD * 2;
int hei = r.height - PAD * 2;
Area a = new Area(new Rectangle(x, y, wid, hei));
// small circles on the left and right
int dia = 12;
int numvert = (int) Math.ceil(hei / ((float) dia));
int leftside = x - dia/2;
int rightside = x + wid - (dia/2) - 1;
int maxh = hei - dia;
for (int ii=0; ii < numvert; ii++) {
int ypos = y + Math.min((hei * ii) / numvert, maxh);
a.add(new Area(new Ellipse2D.Float(leftside, ypos, dia, dia)));
a.add(new Area(new Ellipse2D.Float(rightside, ypos, dia, dia)));
}
// larger ovals on the top and bottom
dia = 16;
int numhorz = (int) Math.ceil(wid / ((float) dia));
int topside = y - dia/3;
int botside = y + hei - (dia/3) - 1;
int maxw = wid - dia;
for (int ii=0; ii < numhorz; ii++) {
int xpos = x + Math.min((wid * ii) / numhorz, maxw);
a.add(new Area(new Ellipse2D.Float(xpos, topside, dia, dia*2/3)));
a.add(new Area(new Ellipse2D.Float(xpos, botside, dia, dia*2/3)));
}
return a;
}
}
// fall back to subtitle shape
return _logic.getSubtitleShape(type, r, r);
}
/**
* Create a tail to the specified rectangular area from the speaker point.
*/
protected Shape getTail (int type, Rectangle r, Point speaker)
{
// emotes don't actually have tails
if (ChatLogic.modeOf(type) == ChatLogic.EMOTE) {
return new Area(); // empty shape
}
int midx = r.x + (r.width / 2);
int midy = r.y + (r.height / 2);
// we actually want to start about SPEAKER_DISTANCE away from the
// speaker
int xx = speaker.x - midx;
int yy = speaker.y - midy;
float dist = (float) Math.sqrt(xx * xx + yy * yy);
float perc = (dist - SPEAKER_DISTANCE) / dist;
if (ChatLogic.modeOf(type) == ChatLogic.THINK) {
int steps = Math.max((int) (dist / SPEAKER_DISTANCE), 2);
float step = perc / steps;
Area a = new Area();
for (int ii=0; ii < steps; ii++, perc -= step) {
int radius = Math.min(SPEAKER_DISTANCE / 2 - 1, ii + 2);
a.add(new Area(new Ellipse2D.Float(
(int) ((1 - perc) * midx + perc * speaker.x) + perc * radius,
(int) ((1 - perc) * midy + perc * speaker.y) + perc * radius,
radius * 2, radius * 2)));
}
return a;
}
// ELSE draw a triangular tail shape
Polygon p = new Polygon();
p.addPoint((int) ((1 - perc) * midx + perc * speaker.x),
(int) ((1 - perc) * midy + perc * speaker.y));
if (Math.abs(speaker.x - midx) > Math.abs(speaker.y - midy)) {
int x;
if (midx > speaker.x) {
x = r.x + PAD;
} else {
x = r.x + r.width - PAD;
}
p.addPoint(x, midy - (TAIL_WIDTH / 2));
p.addPoint(x, midy + (TAIL_WIDTH / 2));
} else {
int y;
if (midy > speaker.y) {
y = r.y + PAD;
} else {
y = r.y + r.height - PAD;
}
p.addPoint(midx - (TAIL_WIDTH / 2), y);
p.addPoint(midx + (TAIL_WIDTH / 2), y);
}
return p;
}
/**
* Expire a bubble, if necessary, and return the old bubbles for the specified speaker.
*/
protected List<BubbleGlyph> getAndExpireBubbles (Name speaker)
{
int num = _bubbles.size();
// first, get all the old bubbles belonging to the user
List<BubbleGlyph> oldbubs = Lists.newArrayList();
if (speaker != null) {
for (int ii=0; ii < num; ii++) {
BubbleGlyph bub = _bubbles.get(ii);
if (bub.isSpeaker(speaker)) {
oldbubs.add(bub);
}
}
}
// see if we need to expire this user's oldest bubble
if (oldbubs.size() >= MAX_BUBBLES_PER_USER) {
BubbleGlyph bub = oldbubs.remove(0);
_bubbles.remove(bub);
_target.abortAnimation(bub);
// or some other old bubble
} else if (num >= MAX_BUBBLES) {
_target.abortAnimation(_bubbles.remove(0));
}
// return the speaker's old bubbles
return oldbubs;
}
@Override
protected void glyphExpired (ChatGlyph glyph)
{
super.glyphExpired(glyph);
_bubbles.remove(glyph);
}
/**
* Get a label formatted as close to the golden ratio as possible for the specified text and
* given the standard padding we use on all bubbles.
*/
protected Label layoutText (Graphics2D gfx, Font font, String text)
{
Label label = _logic.createLabel(text);
label.setFont(font);
// layout in one line
Rectangle vbounds = _target.getViewBounds();
label.setTargetWidth(vbounds.width - PAD * 2);
label.layout(gfx);
Dimension d = label.getSize();
// if the label is wide enough, try to split the text into multiple
// lines
if (d.width > MINIMUM_SPLIT_WIDTH) {
int targetheight = getGoldenLabelHeight(d);
if (targetheight > 1) {
label.setTargetHeight(targetheight * d.height);
label.layout(gfx);
}
}
return label;
}
/**
* Given the specified label dimensions, attempt to find the height that will give us the
* width/height ratio that is closest to the golden ratio.
*/
protected int getGoldenLabelHeight (Dimension d)
{
// compute the ratio of the one line (addin' the paddin')
double lastratio = ((double) d.width + (PAD * 2)) /
((double) d.height + (PAD * 2));
// now try increasing the # of lines and seeing if we get closer to the golden ratio
for (int lines=2; true; lines++) {
double ratio = ((double) (d.width / lines) + (PAD * 2)) /
((double) (d.height * lines) + (PAD * 2));
if (Math.abs(ratio - GOLDEN) < Math.abs(lastratio - GOLDEN)) {
// we're getting closer
lastratio = ratio;
} else {
// we're getting further away, the last one was the one we want
return lines - 1;
}
}
}
/**
* Return a list of rectangular areas that we should avoid while laying out a bubble for the
* specified speaker.
*/
protected List<Shape> getAvoidList (Name speaker)
{
List<Shape> avoid = Lists.newArrayList();
if (_provider == null) {
return avoid;
}
// for now we don't accept low-priority avoids
_provider.getAvoidables(speaker, avoid, null);
// add the existing chatbub non-tail areas from other speakers
for (BubbleGlyph bub : _bubbles) {
if (!bub.isSpeaker(speaker)) {
avoid.add(bub.getBubbleTerritory());
}
}
return avoid;
}
@Override
protected int getDisplayDurationOffset ()
{
return 0; // we don't do any funny hackery, unlike our super class
}
/**
* A glyph of a particlar chat bubble
*/
protected static class BubbleGlyph extends ChatGlyph
{
/**
* Construct a chat bubble glyph.
*
* @param sansTail the chat bubble shape without the tail.
*/
public BubbleGlyph (
SubtitleChatOverlay owner, int type, long lifetime, Shape shape, Label label,
Point labelpos, Shape sansTail, Name speaker, Color outline) {
super(owner, type, lifetime, shape.getBounds(), shape, null, null,
label, labelpos, outline);
_sansTail = sansTail;
_speaker = speaker;
}
public void setAgeLevel (int agelevel) {
_agelevel = agelevel;
invalidate();
}
@Override
public void viewDidScroll (int dx, int dy) {
// only system info and attention messages remain fixed, all others scroll
if ((_type == ChatLogic.INFO) || (_type == ChatLogic.ATTENTION)) {
translate(dx, dy);
}
}
@Override
protected Color getBackground () {
if (_background == Color.WHITE) {
return BACKGROUNDS[_agelevel];
} else {
return _background;
}
}
/**
* Get the screen real estate that this bubble has reserved and doesn't want to let any
* other bubbles take.
*/
public Shape getBubbleTerritory () {
Rectangle bounds = getBubbleBounds();
bounds.grow(BUBBLE_SPACING, BUBBLE_SPACING);
return bounds;
}
/**
* Get the bounds of this bubble, sans tail space.
*/
public Rectangle getBubbleBounds () {
return _sansTail.getBounds();
}
/**
* Is the specified player the speaker of this bubble?
*/
public boolean isSpeaker (Name player) {
return (_speaker != null) && _speaker.equals(player);
}
/**
* Remove the tail on this bubble, if any.
*/
public void removeTail () {
invalidate();
_shape = _sansTail;
_bounds = _shape.getBounds();
jiggleBounds();
invalidate();
}
/** The shape of this chat bubble, without the tail. */
protected Shape _sansTail;
/** The name of the speaker. */
protected Name _speaker;
/** The age level of the bubble, used to pick the background color. */
protected int _agelevel = 0;
}
/** The place in our history at which we last entered a new place. */
protected int _newPlacePoint = 0;
/** The currently displayed bubble areas. */
protected List<BubbleGlyph> _bubbles = Lists.newArrayList();
/** The minimum width of a bubble's label before we consider splitting lines. */
protected static final int MINIMUM_SPLIT_WIDTH = 90;
/** The golden ratio. */
protected static final double GOLDEN = (1.0d + Math.sqrt(5.0d)) / 2.0d;
/** The space we force between adjacent bubbles. */
protected static final int BUBBLE_SPACING = 15;
/** The distance to stay from the speaker. */
protected static final int SPEAKER_DISTANCE = 20;
/** The width of the end of the tail. */
protected static final int TAIL_WIDTH = 12;
/** The maximum number of bubbles to show. */
protected static final int MAX_BUBBLES = 8;
/** The maximum number of bubbles to show per user. */
protected static final int MAX_BUBBLES_PER_USER = 3;
/** The background colors to use when drawing bubbles. */
protected static final Color[] BACKGROUNDS = new Color[MAX_BUBBLES];
static {
Color yellowy = new Color(0xdd, 0xdd, 0x6a);
Color blackish = new Color(0xcccccc);
float steps = (MAX_BUBBLES - 1) / 2;
for (int ii=0; ii < MAX_BUBBLES / 2; ii++) {
BACKGROUNDS[ii] = ColorUtil.blend(Color.white, yellowy, (steps - ii) / steps);
}
for (int ii= MAX_BUBBLES / 2; ii < MAX_BUBBLES; ii++) {
BACKGROUNDS[ii] = ColorUtil.blend(blackish, yellowy, (ii - steps) / steps);
}
}
}
@@ -0,0 +1,836 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.chat;
import java.util.Iterator;
import java.util.List;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import javax.swing.BoundedRangeModel;
import javax.swing.Icon;
import javax.swing.JScrollBar;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import com.google.common.collect.Lists;
import com.samskivert.swing.Label;
import com.samskivert.util.Tuple;
import com.threerings.media.VirtualMediaPanel;
import com.threerings.util.MessageBundle;
import com.threerings.crowd.chat.client.HistoryList;
import com.threerings.crowd.chat.data.ChatMessage;
import com.threerings.crowd.chat.data.SystemMessage;
import com.threerings.crowd.chat.data.TellFeedbackMessage;
import com.threerings.crowd.chat.data.UserMessage;
import com.threerings.crowd.util.CrowdContext;
import static com.threerings.NenyaLog.log;
/**
* Implements subtitle chat.
*/
public class SubtitleChatOverlay extends ChatOverlay
implements ChangeListener, HistoryList.Observer
{
/**
* Construct a subtitle chat overlay.
*
* @param subtitleHeight the height of the subtitle area.
*/
public SubtitleChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar bar,
int subtitleHeight)
{
this(ctx, logic, bar, subtitleHeight, false, 8, 8);
}
/**
* Construct a comic chat overlay using all the available space for subtitling.
*/
public SubtitleChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar bar)
{
this(ctx, logic, bar, false);
}
public SubtitleChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar bar, boolean full)
{
this(ctx, logic, bar, 0, true, full ? 8 : 3, full ? 8 : 4);
}
public SubtitleChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar bar, boolean full,
boolean overrideHistory)
{
this(ctx, logic, bar, full);
_overrideHistory = overrideHistory;
}
// from interface HistoryList.Observer
public void historyUpdated (int adjustment)
{
if (adjustment != 0) {
for (int ii = 0, nn = _showingHistory.size(); ii < nn; ii++) {
ChatGlyph cg = _showingHistory.get(ii);
cg.histIndex -= adjustment;
}
// some history entries were deleted, we need to re-figure the history scrollbar action
resetHistoryOffset();
}
if (isLaidOut() && isHistoryMode()) {
int val = _historyModel.getValue();
updateHistBar(val - adjustment);
// only repaint if we need to
if ((val != _historyModel.getValue()) || (adjustment != 0) || !_histOffsetFinal) {
figureCurrentHistory();
}
}
}
// documentation inherited from interface ChangeListener
public void stateChanged (ChangeEvent e)
{
// the scrollbar has changed.
if (!_settingBar) {
figureCurrentHistory();
}
}
// documentation inherited from superinterface ChatDisplay
public void clear ()
{
clearGlyphs(_subtitles);
}
// documentation inherited from superinterface ChatDisplay
public boolean displayMessage (ChatMessage message, boolean alreadyDisplayed)
{
// nothing doing if we've not been laid out
if (!isLaidOut()) {
return false;
}
// possibly display it now
Graphics2D gfx = getTargetGraphics();
if (gfx != null) {
displayMessage(message, gfx); // display it
gfx.dispose(); // clean up
return true;
}
return false;
}
@Override
public void viewDidScroll (int dx, int dy)
{
super.viewDidScroll(dx, dy);
viewDidScroll(_subtitles, dx, dy);
viewDidScroll(_showingHistory, dx, dy);
}
@Override
public void added (VirtualMediaPanel target)
{
super.added(target);
_history.addObserver(this);
if (_overrideHistory) {
setHistoryEnabled(true);
return;
}
// derived classes may want to override this method and set up chat history mode based on
// whatever preference storage mechanism they use
}
@Override
public void layout ()
{
// sanity check
if (_target == null) {
log.warning(this + " laid out without target?", new Exception());
return;
}
Rectangle vbounds = _target.getViewBounds();
if ((vbounds.height < 1) || (vbounds.width < 1)) {
return; // fuck that!
}
clearGlyphs(_subtitles); // we'll re-populate from the history
if (_subtitlesFill) {
_subtitleHeight = vbounds.height;
}
// make a guess as to the extent of the history (how many avg sized subtitles will fit in
// the subtitle area)
_historyExtent = ((_subtitleHeight - _subtitleYSpacing) / SUBTITLE_HEIGHT_GUESS);
_scrollbar.setBlockIncrement(_historyExtent);
// show messages that were born recently enough to be shown.
long now = System.currentTimeMillis();
// find the first message to display
int histSize = _history.size();
int index = histSize - 1;
for ( ; index >= 0; index--) {
ChatMessage msg = _history.get(index);
_lastExpire = 0L;
if (now > getChatExpire(msg.timestamp, msg.message)) {
break;
}
}
// now that we've found the message that's one too old, increment the index so that it
// points to the first message we should display
index++;
_lastExpire = 0L;
// now dispatch from that point
Graphics2D gfx = getTargetGraphics();
for ( ; index < histSize; index++) {
ChatMessage msg = _history.get(index);
if (shouldShowFromHistory(msg, index)) {
displayMessage(msg, gfx);
}
}
// and clean up
gfx.dispose();
// make a note that we're laid out
_laidout = true;
// reset the history offset..
resetHistoryOffset();
// finally, if we're in history mode, we should figure that out too
if (isHistoryMode()) {
updateHistBar(histSize - 1);
figureCurrentHistory();
}
}
@Override
public void setDimmed (boolean dimmed)
{
super.setDimmed(dimmed);
updateDimmed(_subtitles);
updateDimmed(_showingHistory);
}
@Override
public void removed ()
{
// we need to do this before super so that our target is still around
clearGlyphs(_subtitles);
clearGlyphs(_showingHistory);
super.removed();
_history.removeObserver(this);
// clear out our history so that when we are once again added, we activate it and go
// through the motions of refiguring everything
setHistoryEnabled(false);
// make a note that we'll need to lay ourselves out before we do anything fun next time
_laidout = false;
}
/**
* Shared chained constructor.
*/
protected SubtitleChatOverlay (CrowdContext ctx, ChatLogic logic, JScrollBar bar, int height,
boolean fill, int xspace, int yspace)
{
super(ctx, logic);
_scrollbar = bar;
_subtitleHeight = height;
_subtitlesFill = fill;
_subtitleXSpacing = xspace;
_subtitleYSpacing = yspace;
_history = ctx.getChatDirector().getHistory();
}
/**
* Update the chat glyphs in the specified list to be set to the current dimmed setting.
*/
protected void updateDimmed (List<? extends ChatGlyph> glyphs)
{
for (ChatGlyph glyph : glyphs) {
glyph.setDim(_dimmed);
}
}
/**
* Are we currently in history mode?
*/
protected boolean isHistoryMode ()
{
return (_historyModel != null);
}
/**
* Return the current Graphics context of our target, or null if not applicable.
*/
protected Graphics2D getTargetGraphics ()
{
// this may return null even if target is not null.
return (_target == null) ? null : (Graphics2D)_target.getGraphics();
}
/**
* Configures us for display of chat history or not.
*/
protected void setHistoryEnabled (boolean historyEnabled)
{
if (historyEnabled && _historyModel == null) {
_historyModel = _scrollbar.getModel();
_historyModel.addChangeListener(this);
resetHistoryOffset();
// out with the subtitles, we'll be displaying history
clearGlyphs(_subtitles);
// "scroll" down to the latest history entry
updateHistBar(_history.size() - 1);
// refigure our history
figureCurrentHistory();
} else if (!historyEnabled && _historyModel != null) {
_historyModel.removeChangeListener(this);
_historyModel = null;
// out with the history, we'll be displaying subtitles
clearGlyphs(_showingHistory);
}
}
/**
* Helper function for informing glyphs of the scrolled view.
*/
protected void viewDidScroll (List<? extends ChatGlyph> glyphs, int dx, int dy)
{
for (ChatGlyph glyph : glyphs) {
glyph.viewDidScroll(dx, dy);
}
}
/**
* Update the history scrollbar with the specified value.
*/
protected void updateHistBar (int val)
{
// we may need to figure out the new history offset amount..
if (!_histOffsetFinal && _history.size() > _histOffset) {
Graphics2D gfx = getTargetGraphics();
if (gfx != null) {
figureHistoryOffset(gfx);
gfx.dispose();
}
}
// then figure out the new value and range
int oldval = Math.max(_histOffset, val);
int newmaxval = Math.max(0, _history.size() - 1);
int newval = (oldval >= newmaxval - 1) ? newmaxval : oldval;
// and set it, which MAY generate a change event, but we want to ignore it so we use the
// _settingBar flag
_settingBar = true;
_historyModel.setRangeProperties(newval, _historyExtent, _histOffset,
newmaxval + _historyExtent,
_historyModel.getValueIsAdjusting());
_settingBar = false;
}
/**
* Reset the history offset so that it will be recalculated next time it is needed.
*/
protected void resetHistoryOffset ()
{
_histOffsetFinal = false;
_histOffset = 0;
}
/**
* Figure out how many of the first history elements fit in our bounds such that we can set the
* bounds on the scrollbar correctly such that the scrolling to the smallest value just barely
* puts the first element onscreen.
*/
protected void figureHistoryOffset (Graphics2D gfx)
{
if (!isLaidOut()) {
return;
}
int hei = _subtitleYSpacing;
int hsize = _history.size();
for (int ii = 0; ii < hsize; ii++) {
ChatGlyph rec = getHistorySubtitle(ii, gfx);
Rectangle r = rec.getBounds();
hei += r.height;
// oop, we passed it, it was the last one
if (hei >= _subtitleHeight) {
_histOffset = Math.max(0, ii - 1);
_histOffsetFinal = true;
return;
}
hei += getHistorySubtitleSpacing(ii);
}
// basically, this means there isn't yet enough history to fill the first 'page' of the
// history scrollback, so we set the offset to the max value, but we do not set
// _histOffsetFinal to be true so that this will be recalculated
_histOffset = hsize - 1;
}
/**
* Figure out which ChatMessages in the history should currently appear in the showing history.
*/
protected void figureCurrentHistory ()
{
int first = _historyModel.getValue();
int count = 0;
Graphics2D gfx = null;
if (isLaidOut() && !_history.isEmpty()) {
gfx = getTargetGraphics();
if (gfx == null) {
log.warning("Can't figure current history, no graphics.");
return;
}
// start from the bottom..
Rectangle vbounds = _target.getViewBounds();
int ypos = vbounds.height - _subtitleYSpacing;
for (int ii = first; ii >= 0; ii--, count++) {
ChatGlyph rec = getHistorySubtitle(ii, gfx);
// see if it will fit
Rectangle r = rec.getBounds();
ypos -= r.height;
if ((count != 0) && ypos <= (vbounds.height - _subtitleHeight)) {
break; // don't add that one..
}
// position it
rec.setLocation(vbounds.x + _subtitleXSpacing, vbounds.y + ypos);
// add space for the next
ypos -= getHistorySubtitleSpacing(ii);
}
}
// finally, because we've been adding to the _showingHistory here (via getHistorySubtitle)
// and in figureHistoryOffset (possibly called prior to this method) we now need to prune
// out the ChatGlyphs that aren't actually needed and make sure the ones that are are
// positioned on the screen correctly
for (Iterator<ChatGlyph> itr = _showingHistory.iterator(); itr.hasNext(); ) {
ChatGlyph cg = itr.next();
boolean managed = (_target != null) && _target.isManaged(cg);
if (cg.histIndex <= first && cg.histIndex > (first - count)) {
// it should be showing
if (!managed) {
_target.addAnimation(cg);
}
} else {
// it shouldn't be showing
if (managed) {
_target.abortAnimation(cg);
}
itr.remove();
}
}
if (gfx != null) {
gfx.dispose();
}
}
/**
* Get the glyph for the specified history index, creating if necessary.
*/
protected ChatGlyph getHistorySubtitle (int index, Graphics2D layoutGfx)
{
// do a brute search (over a small set) for an already-created subtitle
for (int ii = 0, nn = _showingHistory.size(); ii < nn; ii++) {
ChatGlyph cg = _showingHistory.get(ii);
if (cg.histIndex == index) {
return cg;
}
}
// it looks like we have to create a new one: expensive!
ChatGlyph cg = createHistorySubtitle(index, layoutGfx);
cg.histIndex = index;
cg.setDim(_dimmed);
_showingHistory.add(cg);
return cg;
}
/**
* Creates a subtitle for display in the history panel.
*
* @param index the index of the message in the history list
*/
protected ChatGlyph createHistorySubtitle (int index, Graphics2D layoutGfx)
{
ChatMessage message = _history.get(index);
int type = getType(message, true);
return createSubtitle(message, type, layoutGfx, false);
}
/**
* Determines the amount of spacing to put after a history subtitle.
*
* @param index the index of the message in the history list
*/
protected int getHistorySubtitleSpacing (int index)
{
ChatMessage message = _history.get(index);
return _logic.getSubtitleSpacing(getType(message, true));
}
protected boolean isLaidOut ()
{
return isShowing() && _laidout;
}
/**
* We're looking through history to figure out what messages we should be showing, should we
* show the following?
*/
protected boolean shouldShowFromHistory (ChatMessage msg, int index)
{
return true; // yes by default.
}
/**
* Clears out the supplied list of chat glyphs.
*/
protected void clearGlyphs (List<ChatGlyph> glyphs)
{
if (_target != null) {
for (int ii = 0, nn = glyphs.size(); ii < nn; ii++) {
ChatGlyph rec = glyphs.get(ii);
_target.abortAnimation(rec);
}
} else if (!glyphs.isEmpty()) {
log.warning("No target to abort chat animations");
}
glyphs.clear();
}
/**
* Display the specified message now, unless we are to ignore it.
*/
protected void displayMessage (ChatMessage message, Graphics2D gfx)
{
// get the non-history message type...
int type = getType(message, false);
if (type != ChatLogic.IGNORECHAT) {
// display it now
displayMessage(message, type, gfx);
}
}
/**
* Display the message after we've decided which type it is.
*/
protected void displayMessage (ChatMessage message, int type, Graphics2D layoutGfx)
{
// if we're in history mode, this will show up in the history and we'll rebuild our
// subtitle list if and when history goes away
if (isHistoryMode()) {
return;
}
addSubtitle(createSubtitle(message, type, layoutGfx, true));
}
/**
* Add a subtitle for display now.
*/
protected void addSubtitle (ChatGlyph rec)
{
// scroll up the old subtitles
Rectangle r = rec.getBounds();
scrollUpSubtitles(-r.height - _logic.getSubtitleSpacing(rec.getType()));
// put this one in place
Rectangle vbounds = _target.getViewBounds();
rec.setLocation(vbounds.x + _subtitleXSpacing,
vbounds.y + vbounds.height - _subtitleYSpacing - r.height);
// add it to our list and to our media panel
rec.setDim(_dimmed);
_subtitles.add(rec);
_target.addAnimation(rec);
}
/**
* Create a subtitle, but don't do anything funny with it.
*/
protected ChatGlyph createSubtitle (ChatMessage message, int type, Graphics2D layoutGfx,
boolean expires)
{
// we might need to modify the textual part with translations, but we can't do that to the
// message object, since other chatdisplays also get it.
String text = message.message;
Tuple<String, Boolean> finfo = _logic.decodeFormat(type, message.getFormat());
String format = finfo.left;
boolean quotes = finfo.right;
// now format the text
if (format != null) {
if (quotes) {
text = "\"" + text + "\"";
}
text = " " + text;
text = xlate(MessageBundle.tcompose(
format, ((UserMessage) message).getSpeakerDisplayName())) + text;
}
return createSubtitle(layoutGfx, type, message.timestamp, null, 0, text, expires);
}
/**
* Create a subtitle- a line of text that goes on the bottom.
*/
protected ChatGlyph createSubtitle (Graphics2D gfx, int type, long timestamp, Icon icon,
int indent, String text, boolean expires)
{
Dimension is = new Dimension();
if (icon != null) {
is.setSize(icon.getIconWidth(), icon.getIconHeight());
}
Rectangle vbounds = _target.getViewBounds();
Label label = _logic.createLabel(text);
label.setFont(_logic.getFont(type));
int paddedIconWidth = (icon == null) ? 0 : is.width + ICON_PADDING;
label.setTargetWidth(
vbounds.width - indent - paddedIconWidth -
2 * (_subtitleXSpacing + Math.max(UIManager.getInt("ScrollBar.width"), PAD)));
label.layout(gfx);
gfx.dispose();
Dimension ls = label.getSize();
Rectangle r = new Rectangle(0, 0, ls.width + indent + paddedIconWidth,
Math.max(is.height, ls.height));
r.grow(0, 1);
Point iconpos = r.getLocation();
iconpos.translate(indent, r.height - is.height - 1);
Point labelpos = r.getLocation();
labelpos.translate(indent + paddedIconWidth, 1);
Rectangle lr = new Rectangle(r.x + indent + paddedIconWidth, r.y,
r.width - indent - paddedIconWidth, ls.height + 2);
// last a really long time if we're not supposed to expire
long lifetime = Integer.MAX_VALUE;
if (expires) {
lifetime = getChatExpire(timestamp, label.getText()) - timestamp;
}
Shape shape = _logic.getSubtitleShape(type, lr, r);
return new ChatGlyph(this, type, lifetime, r.union(shape.getBounds()), shape, icon,
iconpos, label, labelpos, _logic.getOutlineColor(type));
}
/**
* Get the expire time for the specified chat.
*/
protected long getChatExpire (long timestamp, String text)
{
long[] durations = _logic.getDisplayDurations(getDisplayDurationOffset());
// start the computation from the maximum of the timestamp or our last expire time
long start = Math.max(timestamp, _lastExpire);
// set the next expire to a time proportional to the text length
_lastExpire = start + Math.min(text.length() * durations[0], durations[2]);
// but don't let it be longer than the maximum display time
_lastExpire = Math.min(timestamp + durations[2], _lastExpire);
// and be sure to pop up the returned time so that it is above the min
return Math.max(timestamp + durations[1], _lastExpire);
}
/**
* A hack to allow subtitle chat to display longer and comic chat to display for a normal
* duration.
*/
protected int getDisplayDurationOffset ()
{
// the subtitle view adds one to bump up to the next longer display duration because we
// want subtitles to stick around longer
return 1;
}
/**
* Called by a chat glyph when it has determined that it is expired.
*/
protected void glyphExpired (ChatGlyph glyph)
{
_subtitles.remove(glyph);
}
/**
* Convert the Message class/localtype/mode into our internal type code.
*/
protected int getType (ChatMessage message, boolean history)
{
String localtype = message.localtype;
if (message instanceof TellFeedbackMessage) {
if (((TellFeedbackMessage)message).isFailure()) {
return ChatLogic.FEEDBACK;
}
return (history || isApprovedLocalType(localtype)) ?
ChatLogic.TELLFEEDBACK : ChatLogic.IGNORECHAT;
} else if (message instanceof UserMessage) {
int type = _logic.decodeType(localtype);
if (type != 0) {
// factor in the mode
return _logic.adjustTypeByMode(((UserMessage) message).mode, type);
}
// if we're showing from history, include specialized chat messages
if (history) {
return ChatLogic.SPECIALIZED;
}
// otherwise fall through and IGNORECHAT
} else if (message instanceof SystemMessage) {
if (history || isApprovedLocalType(localtype)) {
switch (((SystemMessage) message).attentionLevel) {
case SystemMessage.INFO:
return ChatLogic.INFO;
case SystemMessage.FEEDBACK:
return ChatLogic.FEEDBACK;
case SystemMessage.ATTENTION:
return ChatLogic.ATTENTION;
default:
log.warning("Unknown attention level for system message", "msg", message);
}
}
return ChatLogic.IGNORECHAT;
}
log.warning("Skipping received message of unknown type", "msg", message);
return ChatLogic.IGNORECHAT;
}
/**
* Check to see if we want to display the specified localtype.
*/
protected boolean isApprovedLocalType (String localtype)
{
return true; // we show everything, the ComicChat is a little more picky
}
/**
* Scroll all the subtitles up by the specified amount.
*/
protected void scrollUpSubtitles (int dy)
{
// dirty and move all the old glyphs
Rectangle vbounds = _target.getViewBounds();
int miny = vbounds.y + vbounds.height - _subtitleHeight;
for (Iterator<ChatGlyph> iter = _subtitles.iterator(); iter.hasNext();) {
ChatGlyph sub = iter.next();
sub.translate(0, dy);
if (sub.getBounds().y <= miny) {
iter.remove();
_target.abortAnimation(sub);
}
}
}
/** List of existing messages from our chat director. */
protected HistoryList _history;
/** If set, show history no matter what the client prefs say. */
protected boolean _overrideHistory;
/** The currently displayed subtitles O' history. */
protected List<ChatGlyph> _showingHistory = Lists.newArrayList();
/** Our history scrollbar. */
protected JScrollBar _scrollbar;
/** Tracks whether or not we've been laid out. */
protected boolean _laidout;
/** If we're in history mode, this will be non-null and will notify
* us of our historical positioning. */
protected BoundedRangeModel _historyModel = null;
/** The currently displayed subtitle areas. */
protected List<ChatGlyph> _subtitles = Lists.newArrayList();
/** The amount of vertical space to use for subtitles. */
protected int _subtitleHeight;
/** If true, subtitles should fill all available height. */
protected boolean _subtitlesFill;
/** The amount of space we want around the subtitles. */
protected int _subtitleXSpacing;
protected int _subtitleYSpacing;
/** The unbounded expire time of the last chat glyph displayed. */
protected long _lastExpire;
/** If the history offset we've figured is all figured out or needs to be refigured. */
protected boolean _histOffsetFinal = false;
/** If true, we're the ones updating the history scrollbar and change
* events should be ignored. */
boolean _settingBar = false;
/** The history offset (from 0) such that the history lines (0, _histOffset - 1) will all fit
* onscreen if the lowest scrollbar position is _histOffset. */
protected int _histOffset = 0;
/** A guess of how many history lines fit onscreen at a time. */
protected int _historyExtent;
/** A guess as to the height of a subtitle (plus spacing). */
protected static final int SUBTITLE_HEIGHT_GUESS = 16;
/** The amount of space to insert between the icon and the text. */
protected static final int ICON_PADDING = 4;
/** The padding in each direction around the text to the edges of a chat 'bubble'. */
protected static final int PAD = ChatLogic.PAD;
}
@@ -0,0 +1,248 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.geom;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.geom.Point2D;
import static com.threerings.geom.Log.log;
/**
* General geometry utilities.
*/
public class GeomUtil
{
/**
* Computes and returns the dot product of the two vectors.
*
* @param v1s the starting point of the first vector.
* @param v1e the ending point of the first vector.
* @param v2s the starting point of the second vector.
* @param v2e the ending point of the second vector.
*/
public static int dot (Point v1s, Point v1e, Point v2s, Point v2e)
{
return ((v1e.x - v1s.x) * (v2e.x - v2s.x) + (v1e.y - v1s.y) * (v2e.y - v2s.y));
}
/**
* Computes and returns the dot product of the two vectors. See {@link
* #dot(Point,Point,Point,Point)} for an explanation of the arguments
*/
public static int dot (int v1sx, int v1sy, int v1ex, int v1ey,
int v2sx, int v2sy, int v2ex, int v2ey)
{
return ((v1ex - v1sx) * (v2ex - v2sx) + (v1ey - v1sy) * (v2ey - v2sy));
}
/**
* Computes and returns the dot product of the two vectors. The vectors are assumed to start
* with the same coordinate and end with different coordinates.
*
* @param vs the starting point of both vectors.
* @param v1e the ending point of the first vector.
* @param v2e the ending point of the second vector.
*/
public static int dot (Point vs, Point v1e, Point v2e)
{
return ((v1e.x - vs.x) * (v2e.x - vs.x) + (v1e.y - vs.y) * (v2e.y - vs.y));
}
/**
* Computes and returns the dot product of the two vectors. See {@link
* #dot(Point,Point,Point)} for an explanation of the arguments
*/
public static int dot (int vsx, int vsy, int v1ex, int v1ey, int v2ex, int v2ey)
{
return ((v1ex - vsx) * (v2ex - vsx) + (v1ey - vsy) * (v2ey - vsy));
}
/**
* Computes the point nearest to the specified point <code>p3</code> on the line defined by the
* two points <code>p1</code> and <code>p2</code>. The computed point is stored into
* <code>n</code>. <em>Note:</em> <code>p1</code> and <code>p2</code> must not be coincident.
*
* @param p1 one point on the line.
* @param p2 another point on the line (not equal to <code>p1</code>).
* @param p3 the point to which we wish to be most near.
* @param n the point on the line defined by <code>p1</code> and <code>p2</code> that is
* nearest to <code>p</code>.
*
* @return the point object supplied via <code>n</code>.
*/
public static Point nearestToLine (Point p1, Point p2, Point p3, Point n)
{
// see http://astronomy.swin.edu.au/~pbourke/geometry/pointline/ for a (not very good)
// explanation of the math
int Ax = p2.x - p1.x, Ay = p2.y - p1.y;
float u = (p3.x - p1.x) * Ax + (p3.y - p1.y) * Ay;
u /= (Ax * Ax + Ay * Ay);
n.x = p1.x + Math.round(Ax * u);
n.y = p1.y + Math.round(Ay * u);
return n;
}
/**
* Calculate the intersection of two lines. Either line may be considered as a line segment,
* and the intersecting point is only considered valid if it lies upon the segment. Note that
* Point extends Point2D.
*
* @param p1 and p2 the coordinates of the first line.
* @param seg1 if the first line should be considered a segment.
* @param p3 and p4 the coordinates of the second line.
* @param seg2 if the second line should be considered a segment.
* @param result the point that will be filled in with the intersecting point.
*
* @return true if result was filled in, or false if the lines are parallel or the point of
* intersection lies outside of a segment.
*/
public static boolean lineIntersection (Point2D p1, Point2D p2, boolean seg1,
Point2D p3, Point2D p4, boolean seg2, Point2D result)
{
// see http://astronomy.swin.edu.au/~pbourke/geometry/lineline2d/
double y43 = p4.getY() - p3.getY();
double x21 = p2.getX() - p1.getX();
double x43 = p4.getX() - p3.getX();
double y21 = p2.getY() - p1.getY();
double denom = y43 * x21 - x43 * y21;
if (denom == 0) {
return false;
}
double y13 = p1.getY() - p3.getY();
double x13 = p1.getX() - p3.getX();
double ua = (x43 * y13 - y43 * x13) / denom;
if (seg1 && ((ua < 0) || (ua > 1))) {
return false;
}
if (seg2) {
double ub = (x21 * y13 - y21 * x13) / denom;
if ((ub < 0) || (ub > 1)) {
return false;
}
}
double x = p1.getX() + ua * x21;
double y = p1.getY() + ua * y21;
result.setLocation(x, y);
return true;
}
/**
* Returns less than zero if <code>p2</code> is on the left hand side of the line created by
* <code>p1</code> and <code>theta</code> and greater than zero if it is on the right hand
* side. In theory, it will return zero if the point is on the line, but due to rounding errors
* it almost always decides that it's not exactly on the line.
*
* @param p1 the point on the line whose side we're checking.
* @param theta the (logical) angle defining the line.
* @param p2 the point that lies on one side or the other of the line.
*/
public static int whichSide (Point p1, double theta, Point p2)
{
// obtain the point defining the right hand normal (N)
theta += Math.PI/2;
int x = p1.x + (int)Math.round(1000*Math.cos(theta)),
y = p1.y + (int)Math.round(1000*Math.sin(theta));
// now dot the vector from p1->p2 with the vector from p1->N, if it's positive, we're on
// the right hand side, if it's negative we're on the left hand side and if it's zero,
// we're on the line
return dot(p1.x, p1.y, p2.x, p2.y, x, y);
}
/**
* Shifts the position of the <code>tainer</code> rectangle to ensure that it contains the
* <code>tained</code> rectangle. The <code>tainer</code> rectangle must be larger than or
* equal to the size of the <code>tained</code> rectangle.
*/
public static void shiftToContain (Rectangle tainer, Rectangle tained)
{
if (tained.x < tainer.x) {
tainer.x = tained.x;
}
if (tained.y < tainer.y) {
tainer.y = tained.y;
}
if (tained.x + tained.width > tainer.x + tainer.width) {
tainer.x = tained.x - (tainer.width - tained.width);
}
if (tained.y + tained.height > tainer.y + tainer.height) {
tainer.y = tained.y - (tainer.height - tained.height);
}
}
/**
* Adds the target rectangle to the bounds of the source rectangle. If the source rectangle is
* null, a new rectangle is created that is the size of the target rectangle.
*
* @return the source rectangle.
*/
public static Rectangle grow (Rectangle source, Rectangle target)
{
if (target == null) {
log.warning("Can't grow with null rectangle [src=" + source + ", tgt=" + target + "].",
new Exception());
} else if (source == null) {
source = new Rectangle(target);
} else {
source.add(target);
}
return source;
}
/**
* Returns the rectangle containing the specified tile in the supplied larger rectangle. Tiles
* go from left to right, top to bottom.
*/
public static Rectangle getTile (
int width, int height, int tileWidth, int tileHeight, int tileIndex)
{
Rectangle bounds = new Rectangle();
getTile(width, height, tileWidth, tileHeight, tileIndex, bounds);
return bounds;
}
/**
* Fills in the bounds of the specified tile in the supplied larger rectangle. Tiles go from
* left to right, top to bottom.
*/
public static void getTile (int width, int height, int tileWidth, int tileHeight, int tileIndex,
Rectangle bounds)
{
// figure out from whence to crop the tile
int tilesPerRow = width / tileWidth;
// if we got a bogus region, return bogus tile bounds
if (tilesPerRow == 0) {
bounds.setBounds(0, 0, width, height);
} else {
int row = tileIndex / tilesPerRow;
int col = tileIndex % tilesPerRow;
// crop the tile-sized image chunk from the full image
bounds.setBounds(tileWidth*col, tileHeight*row, tileWidth, tileHeight);
}
}
}
@@ -0,0 +1,32 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.geom;
import com.samskivert.util.Logger;
/**
* Contains a reference to the log object used by this package.
*/
public class Log
{
public static Logger log = Logger.getLogger("com.threerings.geom");
}
@@ -0,0 +1,368 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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;
import static com.threerings.media.Log.log;
/**
* 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;
}
// from interface Shape
public boolean contains (double x, double y)
{
return _bounds.contains(x, y);
}
// from interface Shape
public boolean contains (Point2D p)
{
return _bounds.contains(p);
}
// from interface Shape
public boolean intersects (double x, double y, double w, double h)
{
return _bounds.intersects(x, y, w, h);
}
// from interface Shape
public boolean intersects (Rectangle2D r)
{
return _bounds.intersects(r);
}
// from interface Shape
public boolean contains (double x, double y, double w, double h)
{
return _bounds.contains(x, y, w, h);
}
// from interface Shape
public boolean contains (Rectangle2D r)
{
return _bounds.contains(r);
}
// from interface Shape
public PathIterator getPathIterator (AffineTransform at)
{
return _bounds.getPathIterator(at);
}
// from interface Shape
public PathIterator getPathIterator (AffineTransform at, double flatness)
{
return _bounds.getPathIterator(at, flatness);
}
/**
* Compares this media to the specified media by render order.
*/
public int renderCompareTo (AbstractMedia other)
{
int result = _renderOrder - other._renderOrder;
return (result != 0) ? result : naturalCompareTo(other);
}
/**
* 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<Object> 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);
}
}
@Override
public String toString ()
{
StringBuilder buf = new StringBuilder();
buf.append(StringUtil.shortClassName(this));
buf.append("[");
toString(buf);
return buf.append("]").toString();
}
/**
* Initialize the media.
*/
public 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;
}
/**
* If this media's size or location are changing, it should create a new rectangle from its old
* bounds (new Rectangle(_bounds)), then effect the bounds changes and then call this method
* with the old bounds. This method will either merge the new bounds with the old to create a
* single dirty rectangle or dirty them separately depending on which is more appropriate. It
* will also behave properly if this media is not currently managed (not being rendered) by
* NOOPing.
*
* <em>Do not</em> pass {@link #_bounds} to this method. The rectangle passed in will be
* modified and then passed on to the region manager which will modify it further.
*/
protected void invalidateAfterChange (Rectangle obounds)
{
// if we're not added we need not dirty
if (_mgr == null) {
return;
}
// if our new bounds intersect our old bounds, grow a single dirty
// rectangle to incorporate them both
if (_bounds.intersects(obounds)) {
obounds.add(_bounds);
} else {
// otherwise invalidate our new bounds separately
_mgr.getRegionManager().invalidateRegion(_bounds);
}
// finally invalidate the original/merged bounds
_mgr.getRegionManager().addDirtyRegion(obounds);
}
/**
* 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 = ObserverList.newFastUnsafe();
}
_observers.add(obs);
}
/**
* Remove the specified observer from this media element.
*/
protected void removeObserver (Object obs)
{
if (_observers != null) {
_observers.remove(obs);
}
}
/**
* "Naturally" compares this media with the specified other media (which by definition will
* have the same render order value). The default behavior, for legacy reasons, is to compare
* using {@link Object#hashCode} which is not consistent across VM invocations.
*/
protected int naturalCompareTo (AbstractMedia other)
{
return hashCode() - other.hashCode();
}
/**
* 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<Object> _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,315 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.util.Comparator;
import java.util.List;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Shape;
import com.google.common.collect.Lists;
import com.samskivert.util.ObserverList;
import com.samskivert.util.SortableArrayList;
import com.samskivert.util.Tuple;
import com.samskivert.util.ObserverList.ObserverOp;
import static com.threerings.media.Log.log;
/**
* Manages, ticks, and paints {@link AbstractMedia}.
*/
public abstract class AbstractMediaManager
implements MediaConstants
{
/**
* Provides this media manager with its host and region manager.
*/
public void init (MediaHost host, RegionManager remgr)
{
_host = host;
_remgr = remgr;
}
/**
* Returns region manager in use by this manager.
*/
public RegionManager getRegionManager ()
{
return _remgr;
}
/**
* Creates a graphics that can be used to compute metrics or whatever else a media might need.
* The caller should call {@link Graphics#dispose} on the returned object when it is done
* with it.
*/
public Graphics2D createGraphics ()
{
return _host.createGraphics();
}
/**
* 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 = _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);
}
}
}
/**
* 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.", new Exception());
}
for (int ii = 0, nn = _media.size(); ii < nn; ii++) {
_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++) {
_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.", new Exception());
}
_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(_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 + "].",
new Exception());
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 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.", new Exception());
}
for (int ii = _media.size() - 1; ii >= 0; ii--) {
_media.remove(ii).shutdown();
}
}
/**
* Queues the notification for dispatching after we've ticked all the media.
*/
public void queueNotification (ObserverList<Object> observers, ObserverOp<Object> event)
{
_notify.add(new Tuple<ObserverList<Object>,ObserverOp<Object>>(observers, event));
}
/** Type safety jockeying. */
protected abstract SortableArrayList<? extends AbstractMedia> createMediaList ();
/**
* Dispatches all queued media notifications.
*/
protected void dispatchNotifications ()
{
for (int ii = 0, nn = _notify.size(); ii < nn; ii++) {
Tuple<ObserverList<Object>,ObserverOp<Object>> tuple = _notify.get(ii);
tuple.left.apply(tuple.right);
}
_notify.clear();
}
/** Used to sort media by render order. */
protected static final Comparator<AbstractMedia> RENDER_ORDER =
new Comparator<AbstractMedia>() {
public int compare (AbstractMedia am1, AbstractMedia am2) {
return am1.renderCompareTo(am2);
}
};
/** The media host we're working with. */
protected MediaHost _host;
/** The region manager. */
protected RegionManager _remgr;
/** List of observers to notify at the end of the tick. */
protected List<Tuple<ObserverList<Object>, ObserverOp<Object>>> _notify = Lists.newArrayList();
/** Our render-order sorted list of media. */
@SuppressWarnings("unchecked") protected SortableArrayList<AbstractMedia> _media =
(SortableArrayList<AbstractMedia>)createMediaList();
/** The position in our media list that we're ticking (while 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 0. */
protected long _tickStamp;
}
@@ -0,0 +1,456 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.util.Iterator;
import java.util.Map;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
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 com.google.common.collect.Maps;
import com.samskivert.util.ListUtil;
import com.samskivert.util.RunAnywhere;
import com.samskivert.util.StringUtil;
import static com.threerings.media.Log.log;
/**
* 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;
}
@Override
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 its 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);
}
}
@Override
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 = _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;
}
drect = new Rectangle(x, y, width, height);
if (DEBUG) {
log.info("Dirtying component", "comp", toString(comp), "drect", drect);
}
// 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, drect);
}
/**
* 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;
}
@Override
public synchronized Rectangle getDirtyRegion (JComponent comp)
{
Rectangle drect = _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);
}
@Override
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
Map<JComponent,Rectangle> 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<Map.Entry<JComponent,Rectangle>> iter = _spare.entrySet().iterator();
PRUNE:
while (iter.hasNext()) {
Map.Entry<JComponent,Rectangle> entry = iter.next();
JComponent comp = entry.getKey();
Rectangle drect = 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 = _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()) {
Map.Entry<JComponent,Rectangle> entry = iter.next();
JComponent comp = entry.getKey();
Rectangle drect = 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);
}
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, drect);
} 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 Map<JComponent, Rectangle> _dirty = Maps.newHashMap();
/** A spare hashmap that we swap in while repainting dirty components in the old hashmap. */
protected Map<JComponent, Rectangle> _spare = Maps.newHashMap();
/** 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,156 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Rectangle;
import java.awt.image.VolatileImage;
import static com.threerings.media.Log.log;
/**
* A {@link FrameManager} extension that uses a volatile off-screen image to do its rendering.
*/
public class BackFrameManager extends FrameManager
{
@Override
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 = _backimg.createGraphics();
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) {
Component comp = (_root instanceof Component) ? (Component)_root : _window;
_fgfx = (Graphics2D)(comp.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());
}
@Override
protected Graphics2D createGraphics ()
{
return _backimg.createGraphics();
}
@Override
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,106 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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;
import static com.threerings.media.Log.log;
/**
* A {@link FrameManager} extension that uses a flip-buffer (via {@link BufferStrategy} to do its
* rendering.
*/
public class FlipFrameManager extends FrameManager
{
@Override
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());
}
@Override
protected Graphics2D createGraphics ()
{
return (Graphics2D)_bufstrat.getDrawGraphics();
}
@Override
protected void restoreFromBack (Rectangle dirty)
{
// nothing doing
}
/** The buffer strategy used to do our rendering. */
protected BufferStrategy _bufstrat;
}
@@ -0,0 +1,129 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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;
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,799 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.EventQueue;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Window;
import javax.swing.JLayeredPane;
import javax.swing.JRootPane;
import javax.swing.RepaintManager;
import com.samskivert.util.ListUtil;
import com.samskivert.util.RunAnywhere;
import com.samskivert.util.StringUtil;
import com.samskivert.swing.RuntimeAdjust;
import com.threerings.util.unsafe.Unsafe;
import com.threerings.media.timer.CalibratingTimer;
import com.threerings.media.timer.MediaTimer;
import com.threerings.media.timer.MillisTimer;
import com.threerings.media.util.TrailingAverage;
import static com.threerings.media.Log.log;
/**
* 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
{
}
/**
* Provides a bridge between either {@link ManagedJFrame} or {@link ManagedJApplet} and the
* frame manager.
*/
public static interface ManagedRoot
{
/** Configures the root with a reference to its frame manager. */
public void init (FrameManager fmgr);
/** Returns the window at the root of the UI hierarchy. */
public Window getWindow ();
/** Returns the top-level Swing pane. */
public JRootPane getRootPane();
}
/**
* Creates a frame manager that will try to use a high resolution timer for timing but will
* fall back to {@link MillisTimer}, which is available on every platform, but returns
* inaccurate time stamps on many platforms.
*
* @see #newInstance(ManagedRoot, MediaTimer)
*/
public static FrameManager newInstance (ManagedRoot root)
{
return newInstance(root, createTimer());
}
/**
* Attempts to create a high resolution timer, but if that isn't possible, uses a
* System.currentTimeMillis based timer.
*/
public static MediaTimer createTimer ()
{
MediaTimer timer = null;
for (String timerClass : PERF_TIMERS) {
try {
timer = (MediaTimer)Class.forName(timerClass).newInstance();
break;
} catch (Throwable t) {
// try the next one
}
}
if (timer == null) {
log.info("Can't use high performance timer, reverting to " +
"System.currentTimeMillis() based timer.");
timer = new MillisTimer();
}
return timer;
}
/**
* Constructs a frame manager that will do its rendering to the supplied root and use the
* supplied media timer for timing information.
*/
public static FrameManager newInstance (ManagedRoot root, MediaTimer timer)
{
FrameManager fmgr = (root instanceof ManagedJFrame && _useFlip.getValue()) ?
new FlipFrameManager() : new BackFrameManager();
fmgr.init(root, timer);
return fmgr;
}
/**
* 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 allotted 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();
}
/**
* Returns the highest drift ratio our timer has seen. 1.0 means no drift (and is what we
* return if we're not using a CalibratingTimer)
*/
public float getMaxTimerDriftRatio ()
{
if (_timer instanceof CalibratingTimer) {
return ((CalibratingTimer)_timer).getMaxDriftRatio();
} else {
return 1.0F;
}
}
/**
* Clears out the maximum drift our timer remembers seeing.
*/
public void clearMaxTimerDriftRatio ()
{
if (_timer instanceof CalibratingTimer) {
((CalibratingTimer)_timer).clearMaxDriftRatio();
}
}
/**
* Returns an overlay that can be used to render sprites and animations on top of the entire
* frame. This is lazily created the first time this method is called and the overlay will
* remain a frame participant until {@link #clearMediaOverlay} is called. Be sure to
* coordinate access to the overlay in your application as there is only one overlay in
* existence at any time, and attempts to use an overlay after it has been cleared will fail.
*/
public MediaOverlay getMediaOverlay ()
{
if (_overlay == null) {
_overlay = new MediaOverlay(this);
}
return _overlay;
}
/**
* Returns the managed root on which this frame manager is operating.
*/
public ManagedRoot getManagedRoot ()
{
return _root;
}
/**
* Clears out any media overlay that is in use.
*/
public void clearMediaOverlay ()
{
if (_overlay != null) {
_overlay = null;
}
}
/**
* 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;
}
/**
* 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;
}
/**
* Initializes this frame manager and prepares it for operation.
*/
protected void init (ManagedRoot root, MediaTimer timer)
{
_window = root.getWindow();
_root = root;
_root.init(this);
_timer = timer;
// set up our custom repaint manager
_repainter = new ActiveRepaintManager(
(_root instanceof Component) ? (Component)_root : _window);
RepaintManager.setCurrentManager(_repainter);
// turn off double buffering for the whole business because we handle repaints
_repainter.setDoubleBufferingEnabled(false);
}
/**
* Called to perform the frame processing and rendering.
*/
protected void tick (long tickStamp)
{
long start = 0L, paint = 0L;
if (_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 (_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 {
_repainter.validateComponents();
} catch (Throwable t) {
log.warning("Failure validating components.", t);
}
// tick all of our frame participants
for (Object participant : _participants) {
FrameParticipant part = (FrameParticipant)participant;
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), t);
}
}
// if we have an overlay, tick that too
if (_overlay != null) {
_overlay.tick(tickStamp);
}
}
/**
* 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);
/**
* Returns a graphics context with which to layout its media objects. The returned context must
* be disposed when layout is complete and must not be retained across frame ticks. Used by the
* {@link MediaOverlay}.
*/
protected abstract Graphics2D createGraphics ();
/**
* 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 (Object participant : _participants) {
FrameParticipant part = (FrameParticipant)participant;
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 + "].", t);
}
// render any components in our layered pane that are not in the default layer
_clipped[0] = false;
renderLayers(gfx, pcomp, _tbounds, _clipped, _tbounds);
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");
}
}
}
// if we have a media overlay, give that a chance to propagate its dirty regions to the
// active repaint manager for areas it dirtied during this tick
if (_overlay != null) {
_overlay.propagateDirtyRegions(_repainter, _root.getRootPane());
}
// repaint any widgets that have declared they need to be repainted since the last tick
boolean pcomp = _repainter.paintComponents(gfx, this);
// if we have a media overlay, give it a chance to paint on top of everything
if (_overlay != null) {
pcomp |= _overlay.paint(gfx);
}
// 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, Rectangle dirty)
{
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);
}
// if we have a MediaOverlay, let it know that any sprites in this region need to be
// repainted as the components beneath them have just been redrawn
if (_overlay != null) {
_overlay.addDirtyRegion(dirty);
}
}
/**
* 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
Rectangle compBounds = new Rectangle(0, 0, comp.getWidth(), comp.getHeight());
getRoot(comp, compBounds);
if (!compBounds.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(compBounds.x, compBounds.y);
try {
comp.paint(g);
} catch (Exception e) {
log.warning("Component choked while rendering.", e);
}
g.translate(-compBounds.x, -compBounds.y);
}
}
/** Used to effect periodic calls to {@link FrameManager#tick}. */
protected class Ticker extends Thread
{
public Ticker () {
super("FrameManagerTicker");
}
@Override
public void run () {
log.info("Frame manager ticker running", "sleepGran", _sleepGranularity.getValue());
while (_running) {
long start = 0L;
if (_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 FrameManager#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 ManagedRoot _root;
/** Used to obtain timing measurements. */
protected MediaTimer _timer;
/** Our custom repaint manager. */
protected ActiveRepaintManager _repainter;
/** If active, an overlay that will be rendering sprites and animations on top of the frame. */
protected MediaOverlay _overlay;
/** 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 entities 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);
/** 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);
/** The name of the high-performance timer class we attempt to load. */
protected static final String[] PERF_TIMERS = {
"com.threerings.media.timer.PerfTimer",
"com.threerings.media.timer.NanoTimer",
};
}
@@ -0,0 +1,64 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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 participant
* does not wish to be actively rendered, it can safely return null.
*/
public Component getComponent ();
}
@@ -0,0 +1,189 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.Point;
import java.awt.Rectangle;
import java.awt.Shape;
import javax.swing.JComponent;
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;
}
@Override
public void changeComplete (float complete)
{
super.changeComplete(complete);
setSandTrickleY();
}
@Override
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();
}
}
@Override
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,182 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.util.Map;
import java.util.Properties;
import java.io.IOException;
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;
import static com.threerings.media.Log.log;
/**
* 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 = _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 Map<String, TileSet> _icons = new LRUHashMap<String, TileSet>(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,33 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.Logger;
/**
* Contains a reference to the log object used by the media services package.
*/
public class Log
{
/** We dispatch our log messages through this logger. */
public static Logger log = Logger.getLogger("com.threerings.media");
}
@@ -0,0 +1,60 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.Window;
import javax.swing.JApplet;
/**
* When using the {@link FrameManager} in an Applet, one must use this top-level class.
*/
public class ManagedJApplet extends JApplet
implements FrameManager.ManagedRoot
{
// from interface FrameManager.ManagedRoot
public void init (FrameManager fmgr)
{
_fmgr = fmgr;
}
// from interface FrameManager.ManagedRoot
public Window getWindow ()
{
Component parent = getParent();
while (!(parent instanceof Window) && parent != null) {
parent = parent.getParent();
}
return (Window)parent;
}
/**
* Returns the frame manager managing this frame.
*/
public FrameManager getFrameManager ()
{
return _fmgr;
}
protected FrameManager _fmgr;
}
@@ -0,0 +1,116 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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 java.awt.Window;
import javax.swing.JFrame;
/**
* When using the {@link FrameManager}, one must use this top-level frame class.
*/
public class ManagedJFrame extends JFrame
implements FrameManager.ManagedRoot
{
/**
* 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);
}
// from interface FrameManager.ManagedRoot
public void init (FrameManager fmgr)
{
_fmgr = fmgr;
}
// from interface FrameManager.ManagedRoot
public Window getWindow ()
{
return this;
}
/**
* Returns the frame manager managing this frame.
*/
public FrameManager getFrameManager ()
{
return _fmgr;
}
/**
* We catch paint requests and forward them on to the repaint infrastructure.
*/
@Override
public void paint (Graphics g)
{
update(g);
}
/**
* We catch update requests and forward them on to the repaint infrastructure.
*/
@Override
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);
}
}
protected FrameManager _fmgr;
}
@@ -0,0 +1,37 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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,38 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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;
/**
* Provides the glue between {@link AbstractMediaManager} and {@link MediaPanel} and {@link
* MediaOverlay}.
*/
public interface MediaHost
{
/**
* Creates a graphics context for the component underlying this media host. This method can
* return null if the component is not currently displayable. The graphics created must be
* disposed by the caller.
*/
public Graphics2D createGraphics ();
}
@@ -0,0 +1,216 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.util.List;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import javax.swing.JRootPane;
import com.threerings.media.animation.Animation;
import com.threerings.media.animation.AnimationManager;
import com.threerings.media.sprite.Sprite;
import com.threerings.media.sprite.SpriteManager;
/**
* Provides an overlaid media "canvas" that allows for the rendering of sprites and animations on
* top of everything else that takes place the view managed by a {@link FrameManager}. The media
* overlay coordinates through the {@link ActiveRepaintManager} to repaint areas of the screen that
* it has left dirty.
*/
public class MediaOverlay
implements MediaHost
{
/**
* Returns a reference to the animation manager used by this media panel.
*/
public AnimationManager getAnimationManager ()
{
return _metamgr.getAnimationManager();
}
/**
* Returns a reference to the sprite manager used by this media overlay.
*/
public SpriteManager getSpriteManager ()
{
return _metamgr.getSpriteManager();
}
/**
* Adds a sprite to this overlay.
*/
public void addSprite (Sprite sprite)
{
_metamgr.addSprite(sprite);
}
/**
* @return true if the sprite is already added to this overlay.
*/
public boolean isManaged (Sprite sprite)
{
return _metamgr.isManaged(sprite);
}
/**
* Removes a sprite from this overlay.
*/
public void removeSprite (Sprite sprite)
{
_metamgr.removeSprite(sprite);
}
/**
* Removes all sprites from this overlay.
*/
public void clearSprites ()
{
_metamgr.clearSprites();
}
/**
* Adds an animation to this overlay. Animations are automatically removed when they finish.
*/
public void addAnimation (Animation anim)
{
_metamgr.addAnimation(anim);
}
/**
* @return true if the animation is already added to this overlay.
*/
public boolean isManaged (Animation anim)
{
return _metamgr.isManaged(anim);
}
/**
* Aborts a currently running animation and removes it from this overlay. Animations are
* normally automatically removed when they finish.
*/
public void abortAnimation (Animation anim)
{
_metamgr.abortAnimation(anim);
}
/**
* Removes all animations from this overlay.
*/
public void clearAnimations ()
{
_metamgr.clearAnimations();
}
/**
* Adds a dirty region to this overlay.
*/
public void addDirtyRegion (Rectangle rect)
{
// Only add dirty regions where rect intersects our actual media as the set region will
// propagate out to the repaint manager.
for (AbstractMedia media : _metamgr) {
Rectangle intersection = media.getBounds().intersection(rect);
if (!intersection.isEmpty()) {
_metamgr.getRegionManager().addDirtyRegion(intersection);
}
}
}
/**
* Called by the {@link FrameManager} to propagate our dirty regions to the active repaint
* manager so that it can repaint the underlying components just prior to our painting our
* media. This will be followed by a call to {@link #paint} after the components have been
* repainted.
*/
public void propagateDirtyRegions (ActiveRepaintManager repmgr, JRootPane root)
{
if (_metamgr.needsPaint()) {
// tell the repaint manager about our raw (unmerged) dirty regions so that it can dirty
// only components that are actually impacted
List<Rectangle> dlist = _metamgr.getRegionManager().peekDirtyRegions();
for (int ii = 0, ll = dlist.size(); ii < ll; ii++) {
Rectangle dirty = dlist.get(ii);
repmgr.addDirtyRegion(root, dirty.x - root.getX(), dirty.y - root.getY(),
dirty.width, dirty.height);
}
}
}
/**
* Called by the {@link FrameManager} after everything is done painting, allowing us to paint
* gloriously overtop of everything in the frame.
*
* @return true if we painted something, false otherwise.
*/
public boolean paint (Graphics2D gfx)
{
if (!_metamgr.getRegionManager().haveDirtyRegions()) {
return false;
}
Rectangle[] dirty = _metamgr.getRegionManager().getDirtyRegions();
for (Rectangle element : dirty) {
gfx.setClip(element);
_metamgr.paintMedia(gfx, MediaConstants.BACK, element);
_metamgr.paintMedia(gfx, MediaConstants.FRONT, element);
}
return true;
}
// from interface MediaHost
public Graphics2D createGraphics ()
{
return _metamgr.getFrameManager().createGraphics();
}
/**
* Called by the frame manager on every tick.
*/
public void tick (long tickStamp)
{
if (!_metamgr.isPaused()) {
// tick our meta manager which will tick our sprites and animations
_metamgr.tick(tickStamp);
}
}
/**
* Creates a media overlay. Only the {@link FrameManager} will construct an instance.
*/
protected MediaOverlay (FrameManager fmgr)
{
_framemgr = fmgr;
_metamgr = new MetaMediaManager(fmgr, this);
}
/** The frame manager with whom we cooperate. */
protected FrameManager _framemgr;
/** Handles the heavy lifting involving media. */
protected MetaMediaManager _metamgr;
/** A temporary list of dirty rectangles maintained during the painting process. */
protected Rectangle[] _dirty;
}
@@ -0,0 +1,681 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.util.ArrayList;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
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 com.google.common.collect.Lists;
import com.samskivert.swing.Controller;
import com.samskivert.swing.event.AncestorAdapter;
import com.samskivert.swing.event.CommandEvent;
import com.threerings.media.animation.Animation;
import com.threerings.media.animation.AnimationManager;
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;
import com.threerings.media.timer.MediaTimer;
import static com.threerings.media.Log.log;
/**
* 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, MediaHost
{
public interface Obscurer
{
/**
* Returns the region obscured by the obscurer, in screen coords.
*/
public Rectangle getObscured (boolean changedOnly);
}
/**
* Constructs a media panel.
*/
public MediaPanel (FrameManager framemgr)
{
setOpaque(true); // our repaints shouldn't cause other jcomponents to
// create our meta manager
_metamgr = new MetaMediaManager(framemgr, this);
_animmgr = _metamgr.getAnimationManager();
_spritemgr = _metamgr.getSpriteManager();
_remgr = _metamgr.getRegionManager();
// participate in the frame when we're visible
addAncestorListener(new AncestorAdapter() {
@Override
public void ancestorAdded (AncestorEvent event) {
_metamgr.getFrameManager().registerFrameParticipant(MediaPanel.this);
}
@Override
public void ancestorRemoved (AncestorEvent event) {
_metamgr.getFrameManager().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 _metamgr.getAnimationManager();
}
/**
* Returns a reference to the sprite manager used by this media panel.
*/
public SpriteManager getSpriteManager ()
{
return _metamgr.getSpriteManager();
}
/**
* Returns a reference to the region manager used by this media panel.
*/
public RegionManager getRegionManager ()
{
return _metamgr.getRegionManager();
}
/**
* 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)
{
_metamgr.setPaused(paused);
}
/**
* 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 _metamgr.getTimeStamp();
}
/**
* Adds a sprite to this panel.
*/
public void addSprite (Sprite sprite)
{
_metamgr.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 _metamgr.isManaged(sprite);
}
/**
* Removes a sprite from this panel.
*/
public void removeSprite (Sprite sprite)
{
_metamgr.removeSprite(sprite);
if (((sprite instanceof ActionSprite) ||
(sprite instanceof HoverSprite)) && (--_actionSpriteCount == 0)) {
removeMouseListener(_actionHandler);
removeMouseMotionListener(_actionHandler);
}
}
/**
* Removes all sprites from this panel.
*/
public void clearSprites ()
{
_metamgr.clearSprites();
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)
{
_metamgr.addAnimation(anim);
}
/**
* @return true if the animation is already added to this panel.
*/
public boolean isManaged (Animation anim)
{
return _metamgr.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)
{
_metamgr.abortAnimation(anim);
}
/**
* Removes all animations from this panel.
*/
public void clearAnimations ()
{
_metamgr.clearAnimations();
}
// from interface MediaHost
public Graphics2D createGraphics ()
{
return (Graphics2D)getGraphics();
}
// from interface FrameParticipant
public void tick (long tickStamp)
{
if (_metamgr.isPaused()) {
return;
}
// let derived classes do their business
willTick(tickStamp);
// tick our meta manager which will tick our sprites and animations
_metamgr.tick(tickStamp);
// let derived classes do their business
didTick(tickStamp);
// make a note that the next paint will correspond to a call to tick()
_tickPaintPending = true;
}
// from interface FrameParticipant
public boolean needsPaint ()
{
boolean needsPaint = _metamgr.needsPaint();
// if we have no dirty regions, clear our pending tick indicator because we're not going to
// get painted
if (!needsPaint) {
_tickPaintPending = false;
}
return needsPaint;
}
// from interface FrameParticipant
public Component getComponent ()
{
return this;
}
@Override
public void setOpaque (boolean opaque)
{
if (!opaque) {
log.warning("Media panels shouldn't be setOpaque(false).", new Exception());
}
super.setOpaque(true);
}
@Override
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));
}
}
@Override
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 mark the clipping rectangle 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;
}
addObscurerDirtyRegions(true);
// if we have no invalid rects, there's no need to repaint
if (!_metamgr.getRegionManager().haveDirtyRegions()) {
return;
}
// get our dirty rectangles and delegate the main painting to a method that can be more
// easily overridden
Rectangle[] dirty = _metamgr.getRegionManager().getDirtyRegions();
_metamgr.noteDirty(dirty.length);
try {
paint(gfx, dirty);
} catch (Throwable t) {
log.warning(this + " choked in paint(" + dirty + ").", t);
}
// render our performance debugging if it's enabled
_metamgr.paintPerf(gfx);
}
/**
* Adds an element that could be obscuring the panel and thus requires extra redrawing.
*/
public void addObscurer (Obscurer obscurer) {
if (_obscurerList == null) {
_obscurerList = Lists.newArrayList();
}
_obscurerList.add(obscurer);
}
/**
* Removes an obscuring element.
*/
public void removeObscurer (Obscurer obscurer) {
if (_obscurerList != null) {
_obscurerList.remove(obscurer);
}
}
/**
* Add dirty regions for all our obscurers.
*/
protected void addObscurerDirtyRegions (boolean changedOnly)
{
if (_obscurerList != null) {
for (Obscurer obscurer : _obscurerList) {
Rectangle obscured = obscurer.getObscured(changedOnly);
if (obscured != null) {
Point pt = new Point(obscured.x, obscured.y);
SwingUtilities.convertPointFromScreen(pt, this);
addObscurerDirtyRegion(
new Rectangle(pt.x, pt.y, obscured.width, obscured.height));
}
}
}
}
/**
* Adds the particular region as dirty.
*/
protected void addObscurerDirtyRegion (Rectangle region)
{
dirtyScreenRect(region);
}
/**
* 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)
{
}
/**
* 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?!", new Exception());
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)
{
_metamgr.getRegionManager().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)
{
_metamgr.paintMedia(gfx, layer, dirty);
}
/**
* Creates the mouse listener that will handle action sprites and their variants.
*/
protected ActionSpriteHandler createActionSpriteHandler ()
{
return new ActionSpriteHandler();
}
/** Handles ActionSprite/HoverSprite/ArmingSprite manipulation. */
protected class ActionSpriteHandler extends MouseInputAdapter
{
@Override
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);
}
}
@Override
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);
}
@Override
public void mouseDragged (MouseEvent me) {
if (_activeSprite instanceof ArmingSprite) {
((ArmingSprite) _activeSprite).setArmed(
_activeSprite.hitTest(me.getX(), me.getY()));
}
}
@Override
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<Sprite> list = Lists.newArrayList();
getSpriteManager().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;
}
/** Handles most of our heavy lifting. */
protected MetaMediaManager _metamgr;
/** Legacy reference to avoid breaking children in the wild. */
protected AnimationManager _animmgr;
/** Legacy reference to avoid breaking children in the wild. */
protected SpriteManager _spritemgr;
/** Legacy reference to avoid breaking children in the wild. */
protected RegionManager _remgr;
/** Used to correlate tick()s with paint()s. */
protected boolean _tickPaintPending = false;
/** The action sprite handler, or null for none. */
protected ActionSpriteHandler _actionHandler;
/** The number of action/hover sprites being managed. */
protected int _actionSpriteCount;
/** Anyone registered as someone who might obscure the media panel (and thus require extra
* redrawing. */
protected ArrayList<Obscurer> _obscurerList;
}
@@ -0,0 +1,34 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.PrefsConfig;
/**
* 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 PrefsConfig config = new PrefsConfig("rsrc/config/media");
}
@@ -0,0 +1,380 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.util.Arrays;
import java.util.Iterator;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import com.google.common.collect.Iterators;
import com.samskivert.util.IntListUtil;
import com.samskivert.util.StringUtil;
import com.samskivert.swing.Label;
import com.threerings.media.animation.Animation;
import com.threerings.media.animation.AnimationManager;
import com.threerings.media.sprite.Sprite;
import com.threerings.media.sprite.SpriteManager;
import com.threerings.media.timer.MediaTimer;
import static com.threerings.media.Log.log;
/**
* Coordinates interaction between a sprite and animation manager and the media host that hosts and
* renders them. This class is a little fiddly because {@link MediaPanel} has been around a long
* time and is thoroughly out in the wild and now that we need to abstract out a bunch of its
* functionality, we're constrained by all the extant usages and derivations.
*/
public class MetaMediaManager
implements MediaConstants, Iterable<AbstractMedia>
{
public MetaMediaManager (FrameManager framemgr, MediaHost host)
{
// keep these for later
_framemgr = framemgr;
_host = host;
// initialize our managers
_animmgr.init(host, _remgr);
_spritemgr.init(host, _remgr);
}
/**
* Returns the frame manager with which we are coordinating.
*/
public FrameManager getFrameManager ()
{
return _framemgr;
}
/**
* 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;
}
/**
* Returns the region manager used to coordinate our dirty regions.
*/
public RegionManager getRegionManager ()
{
return _remgr;
}
/**
* Returns true if we are paused, false if we are running normally.
*/
public boolean isPaused ()
{
return _paused;
}
/**
* 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;
}
_paused = paused;
if (_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 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();
}
/**
* Adds a sprite to this panel.
*/
public void addSprite (Sprite sprite)
{
_spritemgr.addSprite(sprite);
}
/**
* @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);
}
/**
* Removes all sprites from this panel.
*/
public void clearSprites ()
{
_spritemgr.clearMedia();
}
/**
* 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();
}
/**
* Called by the host to coordinate dirty region tracking. This should be supplied with the
* number of dirty regions being painted on this tick and called just before painting them.
*/
public void noteDirty (int regions)
{
_dirty[_tick] = regions;
}
/**
* Our media front end should implement {@link FrameParticipant} and call this method in their
* {@link FrameParticipant#tick} method. They must also first check {@link #isPaused} and not
* call this method if we are paused. As they will probably want to have willTick() and
* didTick() calldown methods, we cannot handle pausedness for them.
*/
public void tick (long tickStamp)
{
// now tick our animations and sprites
_animmgr.tick(tickStamp);
_spritemgr.tick(tickStamp);
// if performance debugging is enabled,
if (FrameManager._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 = _host.createGraphics();
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);
_remgr.addDirtyRegion(new Rectangle(_perfRect));
}
}
} else {
_perfRect = null;
}
}
/**
* Our media front end should implement {@link FrameParticipant} and call this method in their
* {@link FrameParticipant#needsPaint} method.
*/
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;
}
// 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();
// if we have dirty regions, we need painting
return _remgr.haveDirtyRegions();
}
/**
* Renders the sprites and animations that intersect the supplied dirty region in the specified
* layer.
*/
public void paintMedia (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);
}
}
/**
* Renders our performance debugging information if enabled.
*/
public void paintPerf (Graphics2D gfx)
{
if (_perfRect != null && FrameManager._perfDebug.getValue()) {
gfx.setClip(null);
_perfLabel.render(gfx, _perfRect.x, _perfRect.y);
}
}
/**
* If our host supports scrolling around in a virtual view, it should call this method when the
* view origin changes.
*/
public void viewLocationDidChange (int dx, int dy)
{
if (_perfRect != null) {
Rectangle sdirty = new Rectangle(_perfRect);
sdirty.translate(-dx, -dy);
_remgr.addDirtyRegion(sdirty);
}
// let our sprites and animations know what's up
_animmgr.viewLocationDidChange(dx, dy);
_spritemgr.viewLocationDidChange(dx, dy);
}
public Iterator<AbstractMedia> iterator ()
{
return Iterators.concat(_spritemgr.enumerateSprites(), _animmgr.iterator());
}
/** The frame manager with whom we register. */
protected FrameManager _framemgr;
/** Our media host, so gracious and accomodating. */
protected MediaHost _host;
/** Used to accumulate and merge dirty regions on each tick. */
protected RegionManager _remgr = new RegionManager();
/** The animation manager in use by this panel. */
protected AnimationManager _animmgr = new AnimationManager();
/** The sprite manager in use by this panel. */
protected SpriteManager _spritemgr = new SpriteManager();
/** 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;
// used to render performance metrics
protected String _perfStatus = "";
protected Label _perfLabel;
protected Rectangle _perfRect;
}
@@ -0,0 +1,161 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.util.List;
import java.awt.EventQueue;
import java.awt.Rectangle;
import com.google.common.collect.Lists;
import static com.threerings.media.Log.log;
/**
* 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, new Exception());
}
// sanity check
if (rect == null) {
log.warning("Attempt to dirty a null rect!?", new Exception());
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", rect, new Exception());
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), new Exception());
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);
}
/**
* Returns our unmerged list of dirty regions. <em>Do not</em> modify the returned list. It's
* just for peeking. Unlike {@link #getDirtyRegions}, this does not clear out the list of dirty
* regions and prepare for the next frame.
*/
public List<Rectangle> peekDirtyRegions ()
{
return _dirty;
}
/**
* Merges all outstanding dirty regions into a single list of rectangles and returns that to
* the caller. Internally, the list of accumulated dirty regions is cleared out and prepared
* for the next frame.
*/
public Rectangle[] getDirtyRegions ()
{
List<Rectangle> merged = Lists.newArrayList();
for (int ii = _dirty.size() - 1; ii >= 0; ii--) {
// pop the next rectangle from the dirty list
Rectangle mr = _dirty.remove(ii);
// merge in any overlapping rectangles
for (int jj = ii - 1; jj >= 0; jj--) {
Rectangle r = _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);
}
return merged.toArray(new Rectangle[merged.size()]);
}
/** A list of dirty rectangles. */
protected List<Rectangle> _dirty = Lists.newArrayList();
}
@@ -0,0 +1,91 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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);
}
}
@Override
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;
}
@Override
protected JViewport createViewport ()
{
JViewport vp = new JViewport() {
@Override
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,524 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.HierarchyEvent;
import java.awt.event.HierarchyListener;
import javax.swing.JComponent;
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<TimerView> 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<TimerView> 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;
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<TimerView> _finisher;
/** A listener to be notified when the warning time occurs. */
protected ResultListener<TimerView> _warner;
/** The default update date. */
protected static final long DEFAULT_RATE = 100L;
}
@@ -0,0 +1,36 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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;
/**
* 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,458 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.util.ArrayList;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import javax.swing.SwingUtilities;
import com.google.common.collect.Lists;
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;
import static com.threerings.media.Log.log;
/**
* 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 MediaPanel#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.
*/
@Override
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.
*/
@Override
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.
*/
@Override
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.
*/
@Override
protected void processMouseWheelEvent (MouseWheelEvent event)
{
event.translatePoint(_vbounds.x, _vbounds.y);
super.processMouseWheelEvent(event);
}
@Override
protected void dirtyScreenRect (Rectangle rect)
{
// translate the screen rect into happy coordinates
rect.translate(_vbounds.x, _vbounds.y);
_metamgr.getRegionManager().addDirtyRegion(rect);
}
@Override
public void doLayout ()
{
super.doLayout();
// we need to obtain our absolute screen coordinates to work
// around the Windows copyArea() bug
findRootBounds();
}
@Override
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();
}
@Override
protected void addObscurerDirtyRegion (Rectangle region)
{
// Adjust for any scrolling we're currently doing.
super.addObscurerDirtyRegion(
new Rectangle(region.x - _dx, region.y - _dy, region.width, region.height));
}
/**
* 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);
}
@Override
protected void didTick (long tickStamp)
{
super.didTick(tickStamp);
adjustBoundsCenter();
}
protected void adjustBoundsCenter ()
{
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 + "].");
_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);
_metamgr.getRegionManager().invalidateRegion(_nx, _ny + height - dy, width, dy);
} else if (dy < 0) {
sy -= dy;
_metamgr.getRegionManager().invalidateRegion(_nx, _ny, width, -dy);
}
if (dx > 0) {
_metamgr.getRegionManager().invalidateRegion(_nx + width - dx, sy, dx, shei);
} else if (dx < 0) {
_metamgr.getRegionManager().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;
addObscurerDirtyRegions(false);
// 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)
{
// inform our view trackers
for (int ii = 0, ll = _trackers.size(); ii < ll; ii++) {
_trackers.get(ii).viewLocationDidChange(dx, dy);
}
// pass the word on to our sprite/anim managers via the meta manager
_metamgr.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);
}
@Override
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);
}
@Override
protected void constrainToBounds (Rectangle dirty)
{
SwingUtilities.computeIntersection(
_vbounds.x, _vbounds.y, getWidth(), getHeight(), dirty);
}
@Override
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<ViewTracker> _trackers = Lists.newArrayList();
}
@@ -0,0 +1,108 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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,155 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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;
}
@Override
public void setLocation (int x, int y)
{
if (_bounds.x == x && _bounds.y == y) {
return; // no-op
}
// make a note of our current bounds
Rectangle obounds = new Rectangle(_bounds);
// move ourselves
super.setLocation(x, y);
// this method will invalidate our old and new bounds efficiently
invalidateAfterChange(obounds);
}
@Override
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<Object>
{
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<Object>
{
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$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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,71 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.util.ArrayList;
import java.awt.Rectangle;
import com.google.common.collect.Lists;
import com.samskivert.swing.util.SwingUtil;
import static com.threerings.media.Log.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());
@SuppressWarnings("unchecked") ArrayList<Animation> avoidables =
(ArrayList<Animation>) _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<Animation> _avoidAnims = Lists.newArrayList();
/** Automatically removes avoid animations when they're done. */
protected AnimationAdapter _avoidAnimObs = new AnimationAdapter() {
@Override
public void animationCompleted (Animation anim, long when) {
if (!_avoidAnims.remove(anim)) {
log.warning("Couldn't remove avoid animation?! " + anim + ".");
}
}
};
}
@@ -0,0 +1,244 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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 int 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;
}
return _sequence[_curIdx];
}
// 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<Object>
{
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,87 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.util.Iterator;
import com.samskivert.util.SortableArrayList;
import com.threerings.media.AbstractMedia;
import com.threerings.media.AbstractMediaManager;
/**
* 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
implements Iterable<Animation>
{
/**
* 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);
}
public Iterator<Animation> iterator ()
{
return _anims.iterator();
}
@Override
protected void tickAllMedia (long tickStamp)
{
super.tickAllMedia(tickStamp);
for (int ii = _anims.size() - 1; ii >= 0; ii--) {
Animation anim = _anims.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 + ".");
}
}
@Override
protected SortableArrayList<? extends AbstractMedia> createMediaList ()
{
return (_anims = new SortableArrayList<Animation>());
}
protected SortableArrayList<Animation> _anims;
}
@@ -0,0 +1,39 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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,294 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.util.ArrayList;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import com.google.common.collect.Lists;
import com.samskivert.util.StringUtil;
import static com.threerings.media.Log.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 in 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 = _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 = _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);
}
/**
* Convenience wrapper to add an animation to run at the same time as the previous one.
*/
public void addAnimation (Animation anim)
{
addAnimation(anim, 0, null);
}
/**
* Convenience wrapper to add an animation to run after the previous one.
*/
public void appendAnimation (Animation anim)
{
addAnimation(anim, -1, null);
}
/**
* Clears out the animations being managed by this sequencer.
*/
public void clear ()
{
_queued.clear();
_lastStamp = 0;
}
@Override
public void tick (long tickStamp)
{
if (_lastStamp == 0) {
_lastStamp = tickStamp;
}
// add all animations whose time has come
while (_queued.size() > 0) {
AnimRecord arec = _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) + ".");
// }
}
@Override
public void paint (Graphics2D gfx)
{
// don't care
}
@Override
public void fastForward (long timeDelta)
{
_lastStamp += timeDelta;
}
@Override
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.warning(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);
}
@Override
public void animationCompleted (Animation anim, long when) {
fireCompletion(when);
}
@Override
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<AnimRecord> _queued = Lists.newArrayList();
/** Animations that are currently running. */
protected ArrayList<AnimRecord> _running = Lists.newArrayList();
/** 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,90 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.MediaPanel;
import com.threerings.media.sprite.Sprite;
/**
* A Sprite that wraps an animation so that a sequence of frames can be easily moved around the
* screen. Animations embedded here should not be added directly to a media panel as this sprite
* will manage them. If the enclosed animation completes, we remove the sprite from the media
* panel, since the animation would normally do that itself.
*/
public class AnimationSprite extends Sprite
{
public AnimationSprite (Animation anim, MediaPanel owner)
{
super();
_anim = anim;
_owner = owner;
}
@Override
public void init ()
{
_anim.init(_mgr);
}
@Override
public void tick (long tickStamp)
{
super.tick(tickStamp);
_anim.tick(tickStamp);
if (_anim.isFinished()) {
_anim.willFinish(tickStamp);
_owner.removeSprite(AnimationSprite.this);
_anim.didFinish(tickStamp);
} else {
_bounds = (Rectangle)_anim.getBounds().clone();
}
}
@Override
public void setLocation (int x, int y)
{
_anim.setLocation(x - _oxoff, y - _oyoff);
super.setLocation(x, y);
}
@Override
public void willStart (long tickStamp)
{
super.willStart(tickStamp);
_anim.willStart(tickStamp);
}
@Override
public void paint (Graphics2D gfx)
{
// Nothing to paint for ourselves.
_anim.paint(gfx);
}
protected Animation _anim;
protected MediaPanel _owner;
}
@@ -0,0 +1,95 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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,66 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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;
}
@Override
public void tick (long timestamp)
{
if (_start == 0) {
// initialize our starting time
_start = timestamp;
}
// check whether we're done
_finished = (timestamp - _start >= _duration);
}
@Override
public void fastForward (long timeDelta)
{
if (_start > 0) {
_start += timeDelta;
}
}
@Override
public void paint (Graphics2D gfx)
{
// nothing doing
}
protected long _duration, _start;
}
@@ -0,0 +1,102 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.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);
}
@Override
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();
}
@Override
public void fastForward (long timeDelta)
{
_tfunc.fastForward(timeDelta);
}
@Override
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,119 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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, image.getWidth() + rx, image.getHeight() + ry));
// save things off
_image = image;
_sx = sx;
_sy = sy;
_rx = rx;
_ry = ry;
// calculate animation ending time
_duration = duration;
}
@Override
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.getBoolean() ? -1 : 1);
_y = _sy + dy * (RandomUtil.getBoolean() ? -1 : 1);
}
@Override
public void fastForward (long timeDelta)
{
_end += timeDelta;
}
@Override
public void paint (Graphics2D gfx)
{
_image.paint(gfx, _x, _y);
}
@Override
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,328 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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;
@Override
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;
}
@Override
public void fastForward (long timeDelta)
{
if (_start > 0) {
_start += timeDelta;
_end += timeDelta;
}
}
@Override
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 traveled
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();
}
@Override
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);
}
}
@Override
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,80 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.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);
}
@Override
public void tick (long timestamp)
{
if (_effect.tick(timestamp)) {
_finished = _effect.finished();
invalidate();
}
}
@Override
public void paint (Graphics2D gfx)
{
_effect.beforePaint(gfx);
paintAnimation(gfx);
_effect.afterPaint(gfx);
}
@Override
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,50 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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;
}
@Override
protected void paintAnimation (Graphics2D gfx)
{
_image.paint(gfx, _bounds.x, _bounds.y);
}
protected Mirage _image;
}
@@ -0,0 +1,96 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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;
/**
* 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;
}
@Override
protected void init ()
{
super.init();
// if our label is not yet laid out, do the deed
if (!_label.isLaidOut()) {
Graphics2D gfx = _mgr.createGraphics();
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;
}
@Override
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,244 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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;
}
@Override
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;
}
@Override
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);
}
@Override
public void fastForward (long timeDelta)
{
if (_start > 0) {
_start += timeDelta;
}
}
@Override
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);
}
@Override
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,207 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.Image;
import java.awt.Rectangle;
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.
*
* @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 (Sprite sprite, Color color, int upmillis, int downmillis,
boolean fadeIn)
{
this(null, sprite, color, upmillis, downmillis, fadeIn);
}
/**
* 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(new Rectangle(sprite.getBounds()));
_spmgr = spmgr;
_sprite = sprite;
_color = color;
_upmillis = upmillis;
_downmillis = downmillis;
_fadeIn = fadeIn;
}
@Override
public void tick (long timestamp)
{
if (timestamp - _lastUpdate < _millisBetweenUpdates) {
return;
}
_lastUpdate = timestamp;
int alpha;
if (_upfunc != null) {
if ((alpha = _upfunc.getValue(timestamp)) >= _maxAlpha) {
_upfunc = null;
}
} else if (_downfunc != null) {
if ((alpha = _downfunc.getValue(timestamp)) <= _minAlpha) {
_downfunc = null;
}
} else {
_finished = true;
return;
}
// if the sprite is moved or changed size while we're gleaming it, track those changes
if (!_bounds.equals(_sprite.getBounds())) {
Rectangle obounds = new Rectangle(_bounds);
_bounds.setBounds(_sprite.getBounds());
invalidateAfterChange(obounds);
}
if (_alpha != alpha) {
_alpha = alpha;
invalidate();
}
}
@Override
public void fastForward (long timeDelta)
{
if (_upfunc != null) {
_upfunc.fastForward(timeDelta);
} else if (_downfunc != null) {
_downfunc.fastForward(timeDelta);
}
}
@Override
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);
}
@Override
protected void willStart (long tickStamp)
{
_upfunc = new LinearTimeFunction(_minAlpha, _maxAlpha, _upmillis);
_downfunc = new LinearTimeFunction(_maxAlpha, _minAlpha, _downmillis);
super.willStart(tickStamp);
// remove the sprite we're fiddling with from the manager; we'll
// add it back when we're done
if (_spmgr != null && _spmgr.isManaged(_sprite)) {
_spmgr.removeSprite(_sprite);
}
}
@Override
protected void shutdown ()
{
super.shutdown();
if (_spmgr != null && !_spmgr.isManaged(_sprite)) {
_spmgr.addSprite(_sprite);
}
}
protected SpriteManager _spmgr;
protected Sprite _sprite;
protected Color _color;
protected Image _offimg;
protected boolean _fadeIn;
protected TimeFunction _upfunc;
protected TimeFunction _downfunc;
protected int _upmillis;
protected int _downmillis;
protected int _maxAlpha = 750;
protected int _minAlpha;
protected int _alpha;
protected long _lastUpdate;
protected int _millisBetweenUpdates;
}
@@ -0,0 +1,134 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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();
}
@Override
public Rectangle getBounds ()
{
// fill in the bounds with our current animation frame's bounds
return _bounds;
}
@Override
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
setFrameIndex(_seeker.init(_frames));
if (_seeker instanceof AnimationFrameSequencer) {
((AnimationFrameSequencer) _seeker).setAnimation(this);
}
}
@Override
public void tick (long tickStamp)
{
int fidx = _seeker.tick(tickStamp);
if (fidx == -1) {
_finished = true;
} else if (fidx != _fidx) {
// make a note of our current bounds
Rectangle obounds = new Rectangle(_bounds);
// update our frame index and bounds
setFrameIndex(fidx);
// invalidate our old and new bounds
invalidateAfterChange(obounds);
}
}
/**
* 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);
}
@Override
public void paint (Graphics2D gfx)
{
_frames.paintFrame(gfx, _fidx, _bounds.x, _bounds.y);
}
@Override
public void fastForward (long timeDelta)
{
_seeker.fastForward(timeDelta);
}
protected MultiFrameImage _frames;
protected FrameSequencer _seeker;
protected int _fidx;
}
@@ -0,0 +1,134 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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;
}
@Override
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();
}
@Override
public void fastForward (long timeDelta)
{
_end += timeDelta;
}
@Override
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,188 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import com.threerings.media.image.Mirage;
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;
// 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 point.
*/
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);
}
@Override
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);
}
@Override
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;
}
}
@Override
public void fastForward (long timeDelta)
{
_scaleFunc.fastForward(timeDelta);
}
@Override
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 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,34 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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,254 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.image.Mirage;
/**
* 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.getBoolean() ? -1 : 1;
}
@Override
protected void willStart (long stamp)
{
super.willStart(stamp);
_start = stamp;
}
@Override
public void fastForward (long timeDelta)
{
_start += timeDelta;
}
@Override
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();
}
@Override
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);
}
@Override
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,98 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.PathObserver;
import com.threerings.media.sprite.Sprite;
import com.threerings.media.sprite.SpriteManager;
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);
}
@Override
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;
}
}
@Override
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;
}
@Override
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,72 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.PathObserver;
import com.threerings.media.sprite.Sprite;
import com.threerings.media.util.Path;
/**
* Wraps the dirty work of moving a sprite along a {@link Path} in an Animation.
*/
public class SpritePathAnimation extends Animation
implements PathObserver
{
public SpritePathAnimation (Sprite sprite, Path path)
{
super(new Rectangle(0, 0, 0, 0)); // We don't render ourselves
_sprite = sprite;
_path = path;
}
@Override
public void paint (Graphics2D gfx) { }
@Override
public void tick (long tickStamp) { }
public void pathCancelled (Sprite sprite, Path path)
{
_finished = true;
sprite.removeSpriteObserver(this);
}
public void pathCompleted (Sprite sprite, Path path, long when)
{
_finished = true;
sprite.removeSpriteObserver(this);
}
@Override
protected void willStart (long tickStamp)
{
super.willStart(tickStamp);
_sprite.addSpriteObserver(this);
_sprite.move(_path);
}
final protected Sprite _sprite;
final protected Path _path;
}
@@ -0,0 +1,124 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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 BaseImageManager.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,80 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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 static com.threerings.media.Log.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();
}
@Override
protected int getTransparency ()
{
return _source.getColorModel().getTransparency();
}
@Override
protected void refreshVolatileImage ()
{
Graphics gfx = _image.getGraphics();
try {
gfx.drawImage(_source, -_bounds.x, -_bounds.y, null);
} catch (Exception e) {
log.warning("Failure refreshing mirage " + this + ".", e);
} finally {
gfx.dispose();
}
}
@Override
protected void toString (StringBuilder buf)
{
super.toString(buf);
buf.append(", src=").append(_source);
}
protected BufferedImage _source;
}
@@ -0,0 +1,77 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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,88 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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)
{
this(image, 1);
}
/**
* @param percentageOfDataBuffer - the percentage of image's data buffer used by image for use
* in getEstimatedMemory. ie if this image is a subimage of another image, and they share a
* data buffer, this is the percentage of the size of this image compared to the source.
*/
public BufferedMirage (BufferedImage image, float percentageOfDataBuffer)
{
_image = image;
_percentageOfDataBuffer = percentageOfDataBuffer;
}
// 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 (long)(ImageUtil.getEstimatedMemoryUsage(_image.getRaster()) *
_percentageOfDataBuffer);
}
// documentation inherited from interface
public BufferedImage getSnapshot ()
{
return _image;
}
protected float _percentageOfDataBuffer;
protected BufferedImage _image;
}
@@ -0,0 +1,107 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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 static com.threerings.media.Log.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();
}
@Override
protected int getTransparency ()
{
BufferedImage source = _imgr.getImage(_source, _zations);
return (source == null) ? Transparency.OPAQUE :
source.getColorModel().getTransparency();
}
@Override
protected void refreshVolatileImage ()
{
Graphics gfx = null;
try {
BufferedImage source = _imgr.getImage(_source, _zations);
if (source != null) {
gfx = _image.getGraphics();
// We grab a subimage before drawing because otherwise bufferedimage does all its
// computations across the entire image, including those parts outside the bounds.
BufferedImage subImg =
source.getSubimage(_bounds.x, _bounds.y, _bounds.width, _bounds.height);
gfx.drawImage(subImg, 0, 0, null);
}
} catch (Exception e) {
log.warning("Failure refreshing mirage " + this + ".", e);
} finally {
if (gfx != null) {
gfx.dispose();
}
}
}
@Override
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,129 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.Rectangle;
import java.awt.image.BufferedImage;
import com.samskivert.swing.RuntimeAdjust;
import com.threerings.resource.ResourceManager;
import com.threerings.media.MediaPrefs;
/**
* Provides a single point of access for image retrieval and caching - just like the ImageManager
* but adds a tie in to the RuntimeAdjust system to control caching and mirage creation.
*/
public class ClientImageManager extends ImageManager
{
/**
* Sets the size of the image cache. This must be called before the ImageManager is created.
*/
public static void setCacheSize (int cacheKilobytes)
{
_runCacheSize = cacheKilobytes;
}
/**
* Sets if images should be recreated in the graphics context's preferred format before
* rendering. This must be called before the ImageManager is created.
*/
public static void setPrepareImages (boolean prepareImages)
{
_runPrepareImages = prepareImages;
}
public ClientImageManager (ResourceManager rmgr, OptimalImageCreator icreator)
{
super(rmgr, icreator);
}
public ClientImageManager (ResourceManager rmgr, Component context)
{
super(rmgr, context);
}
@Override
public int getCacheSize ()
{
return _runCacheSize;
}
@Override
public Mirage getMirage (ImageKey key, Rectangle bounds, Colorization[] zations)
{
// We need to do something more complicated than the BaseImageManager because our
// runtime adjustments may affect how we create our mirages.
BufferedImage src = null;
float percentageOfDataBuffer = 1;
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 (!_runPrepareImages) {
src = getImage(key, zations);
percentageOfDataBuffer =
(bounds.width * bounds.height)/(float)(src.getHeight() * src.getWidth());
src = src.getSubimage(bounds.x, bounds.y, bounds.width, bounds.height);
}
if (_runBlank.getValue()) {
return new BlankMirage(bounds.width, bounds.height);
} else if (_runPrepareImages) {
return new CachedVolatileMirage(this, key, bounds, zations);
} else {
return new BufferedMirage(src, percentageOfDataBuffer);
}
}
/** 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, DEFAULT_CACHE_SIZE);
/**
* Cache size to be used in this run. Adjusted by setCacheSize without affecting the stored
* value.
*/
protected static int _runCacheSize = _cacheSize.getValue();
/** 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);
/**
* If images should be prepared for the graphics context in this run.
*/
protected static boolean _runPrepareImages = _prepareImages.getValue();
/** 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,528 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Random;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.text.ParseException;
import java.awt.Color;
import com.google.common.collect.Lists;
import com.samskivert.util.HashIntMap;
import com.samskivert.util.RandomUtil;
import com.samskivert.util.StringUtil;
import com.threerings.util.CompiledConfig;
import com.threerings.resource.ResourceManager;
import static com.threerings.media.Log.log;
/**
* 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, Comparable<ClassRecord>
{
/** 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<ColorRecord> colors = new HashIntMap<ColorRecord>();
/** Used when parsing the color definitions. */
public void addColor (ColorRecord record) {
// validate the color id
if (record.colorId > 127) {
log.warning("Refusing to add color record; colorId > 127",
"class", this, "record", record);
} else if (colors.containsKey(record.colorId)) {
log.warning("Refusing to add duplicate colorId",
"class", this, "record", record, "existing", colors.get(record.colorId));
} else {
record.cclass = this;
colors.put(record.colorId, record);
}
}
/**
* Translates 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
for (ColorRecord color : colors.values()) {
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 () {
return randomStartingColor(RandomUtil.rand);
}
/** Returns a random starting id from the entries in this class. */
public ColorRecord randomStartingColor (Random rand) {
// figure out our starter ids if we haven't already
if (_starters == null) {
ArrayList<ColorRecord> list = Lists.newArrayList();
for (ColorRecord color : colors.values()) {
if (color.starter) {
list.add(color);
}
}
_starters = list.toArray(new ColorRecord[list.size()]);
}
// sanity check
if (_starters.length < 1) {
log.warning("Requested random starting color from colorless component class",
"class", this);
return null;
}
// return a random entry from the array
return _starters[RandomUtil.getInt(_starters.length, rand)];
}
/**
* Get the default ColorRecord defined for this color class, or null if none.
*/
public ColorRecord getDefault () {
return colors.get(defaultId);
}
// from interface Comparable<ClassRecord>
public int compareTo (ClassRecord other) {
return name.compareTo(other.name);
}
@Override
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, Comparable<ColorRecord>
{
/** 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);
}
// from interface Comparable<ColorRecord>
public int compareTo (ColorRecord other) {
return name.compareTo(other.name);
}
@Override
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<ClassRecord> enumerateClasses ()
{
return _classes.values().iterator();
}
public Collection<ClassRecord> getClasses ()
{
return _classes.values();
}
/**
* 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<ColorRecord> iter = record.colors.values().iterator();
for (int ii = 0; iter.hasNext(); ii++) {
crecs[ii] = 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<ColorRecord> crecs = record.colors.values().iterator();
for (int ii = 0; crecs.hasNext(); ii++) {
cids[ii] = 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 colorPrint)
{
ColorRecord color = getColorRecord(colorPrint >> 8, colorPrint & 0xFF);
return (color == null) ? false : color.starter;
}
/**
* 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)
{
return getRandomStartingColor(className, RandomUtil.rand);
}
/**
* Returns a random starting color from the specified color class.
*/
public ColorRecord getRandomStartingColor (String className, Random rand)
{
// make sure the class exists
ClassRecord record = getClassRecord(className);
return (record == null) ? null : record.randomStartingColor(rand);
}
/**
* 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 = crec.colors.get(colorId);
if (color != null) {
return color.getColorization();
}
}
return null;
}
/**
* Looks up a colorization by class and color names.
*/
public Colorization getColorization (String className, String colorName)
{
ClassRecord crec = getClassRecord(className);
if (crec != null) {
int colorId = 0;
try {
colorId = crec.getColorId(colorName);
} catch (ParseException pe) {
log.info("Error getting colorization by name", "error", pe);
return null;
}
ColorRecord color = 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<ClassRecord> iter = _classes.values().iterator();
while (iter.hasNext()) {
ClassRecord crec = iter.next();
if (crec.name.equals(className)) {
return crec;
}
}
log.warning("No such color class", "class", className, new Exception());
return null;
}
/**
* Looks up the requested color record.
*/
public ColorRecord getColorRecord (int classId, int colorId)
{
ClassRecord record = getClassRecord(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, new Exception());
}
return null;
}
return record.colors.get(colorId);
}
/**
* Looks up the requested color record by class & color names.
*/
public ColorRecord getColorRecord (String className, String colorName)
{
ClassRecord record = getClassRecord(className);
if (record == null) {
log.warning("Requested unknown color class",
"className", className, "colorName", colorName, new Exception());
return null;
}
int colorId = 0;
try {
colorId = record.getColorId(colorName);
} catch (ParseException pe) {
log.info("Error getting color record by name", "error", pe);
return null;
}
return record.colors.get(colorId);
}
/**
* Looks up the requested color class record.
*/
public ClassRecord getClassRecord (int classId)
{
return _classes.get(classId);
}
/**
* 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 ColorPository 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 ColorPository 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", 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<ClassRecord> _classes = new HashIntMap<ClassRecord>();
/** 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$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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,183 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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. Note that this is inclusive. */
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 offsets 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;
}
@Override
public int hashCode ()
{
return colorizationId ^ rootColor.hashCode();
}
@Override
public boolean equals (Object other)
{
if (other instanceof Colorization) {
return ((Colorization)other).colorizationId == colorizationId;
} else {
return false;
}
}
@Override
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 ii = 0; ii < hsv.length; ii++) {
// fhsv[i] = (int)(hsv[i]*Integer.MAX_VALUE);
fhsv[ii] = (int)(hsv[ii]*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,112 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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;
public class CompositeMirage implements Mirage
{
public CompositeMirage (Mirage... mirages)
{
_mirages = mirages;
}
// 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,48 @@
//
// $Id$
//
// Nenya library - tools for developing networked games
// Copyright (C) 2002-2012 Three Rings Design, Inc., All Rights Reserved
// http://code.google.com/p/nenya/
//
// 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;
}

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