Split Nenya into proper Maven submodules (same treatment Narya got).
This commit is contained in:
@@ -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
Reference in New Issue
Block a user