diff --git a/src/java/com/threerings/media/FrameManager.java b/src/java/com/threerings/media/FrameManager.java new file mode 100644 index 000000000..ff126799c --- /dev/null +++ b/src/java/com/threerings/media/FrameManager.java @@ -0,0 +1,482 @@ +// +// $Id: FrameManager.java,v 1.1 2002/04/23 01:16:27 mdb Exp $ + +package com.threerings.media; + +import java.applet.Applet; + +import java.awt.Component; +import java.awt.Frame; +import java.awt.Graphics; +import java.awt.GraphicsConfiguration; +import java.awt.GraphicsDevice; +import java.awt.Image; +import java.awt.Insets; +import java.awt.Rectangle; +import java.awt.Window; + +import java.awt.image.BufferStrategy; +import java.awt.image.VolatileImage; + +import java.awt.EventQueue; + +import javax.swing.JComponent; +import javax.swing.RepaintManager; + +import java.util.ArrayList; + +import com.samskivert.util.Interval; +import com.samskivert.util.IntervalManager; +import com.samskivert.util.StringUtil; + +import com.threerings.media.util.PerformanceMonitor; +import com.threerings.media.util.PerformanceObserver; + +/** + * 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. + * + *
The frame manager goes through a simple two part procedure every + * frame: + * + *
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 must not 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. + */ +public class FrameManager + implements PerformanceObserver +{ + /** + * Constructs a frame manager that will do its rendering to the + * supplied frame. It is likely that the caller will want to have put + * the frame into full-screen exclusive mode prior to providing it to + * the frame manager so that the frame manager can take advantage of + * optimizations available in that mode. + * + * @see GraphicsDevice#setFullScreenWindow + */ + public FrameManager (Frame frame) + { + _frame = frame; + _frame.setIgnoreRepaint(true); + + // set up our custom repaint manager + _remgr = new FrameRepaintManager(); + RepaintManager.setCurrentManager(_remgr); + + // turn off double buffering for the whole business because we + // handle repaints + _remgr.setDoubleBufferingEnabled(false); + + // register with the performance monitor + PerformanceMonitor.register(this, "frame-rate", 1000l); + } + + /** + * Instructs the frame manager to target the specified number of + * frames per second. If the computation and rendering for a frame are + * completed with time to spare, the frame manager will wait until the + * proper time to begin processing for the next frame. If a frame + * takes longer than its alotted time, the frame manager will + * immediately begin processing on the next frame. + */ + public void setTargetFrameRate (int fps) + { + // compute the number of milliseconds per frame + _millisPerFrame = 1000/fps; + } + + /** + * Registers a frame participant. The participant will be given the + * opportunity to do processing and rendering on each frame. + */ + public void registerFrameParticipant (FrameParticipant participant) + { + _participants.add(participant); + } + + /** + * Removes a frame participant. + */ + public void removeFrameParticipant (FrameParticipant participant) + { + _participants.remove(participant); + } + + /** + * Starts up the per-frame tick + */ + public void start () + { + if (_ticker == null) { + // create ticker for queueing up tick requests on AWT thread + _ticker = new Ticker(); + // and start it up + _ticker.start(); + // and kick off our first frame + _ticker.tickIn(_millisPerFrame, System.currentTimeMillis()); + } + } + + /** + * Stops the per-frame tick. + */ + public synchronized void stop () + { + if (_ticker != null) { + _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); + } + + /** + * Called to perform the frame processing and rendering. + */ + protected void tick () + { + // if our frame is not showing (or is impossibly sized), don't try + // rendering anything + if (_frame.isShowing() && + _frame.getWidth() > 0 && _frame.getHeight() > 0) { + long tickStamp = System.currentTimeMillis(); + // tick our participants + tickParticipants(tickStamp); + // repaint our participants + paintParticipants(tickStamp); + } + + // now determine how many milliseconds we have left before we need + // to start the next frame (if any) + long end = System.currentTimeMillis(); + long duration = end - _frameStart; + long remaining = _millisPerFrame - duration; + + // note that we've done a frame + PerformanceMonitor.tick(this, "frame-rate"); + + // if we have no time remaining, queue up another tick immediately + if (remaining <= 0) { + // make a note that we're starting our next frame now + _frameStart = end; + EventQueue.invokeLater(_callTick); + + } else { + // otherwise queue one up in the requisite number of millis + _ticker.tickIn(remaining, end); + } + } + + /** + * Called once per frame to invoke {@link FrameParticipant#tick} on + * all of our frame participants. + */ + protected void tickParticipants (long tickStamp) + { + // tick all of our frame participants + int pcount = _participants.size(); + for (int ii = 0; ii < pcount; ii++) { + FrameParticipant part = (FrameParticipant) + _participants.get(ii); + try { + part.tick(tickStamp); + } catch (Throwable t) { + Log.warning("Frame participant choked during tick " + + "[part=" + + StringUtil.safeToString(part) + "]."); + Log.logStackTrace(t); + } + } + + // validate any invalid components + try { + _remgr.validateComponents(); + } catch (Throwable t) { + Log.warning("Failure validating components."); + Log.logStackTrace(t); + } + } + + /** + * Called once per frame to invoke {@link FrameParticipant#paint} on + * all of our frame participants. + */ + protected void paintParticipants (long tickStamp) + { + // create our buffer strategy if we don't already have one + if (_bufstrat == null) { + _frame.createBufferStrategy(2); + _bufstrat = _frame.getBufferStrategy(); + } + + // create our off-screen buffer if necessary + GraphicsConfiguration gc = _frame.getGraphicsConfiguration(); + if (_backimg == null) { + createBackBuffer(gc); + } + + // render into our back buffer + do { + // 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); + } + + Rectangle bounds = new Rectangle(); + Graphics g = null, fg = null; + Insets fi = _frame.getInsets(); + try { + g = _backimg.getGraphics(); + fg = _frame.getGraphics(); + + // 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."); + } + + // repaint any widgets that have declared there need to be + // repainted since the last tick + _remgr.paintComponents(g); + + // paint our frame participants (which want to be handled + // specially) + int pcount = _participants.size(); + for (int ii = 0; ii < pcount; ii++) { + FrameParticipant part = (FrameParticipant) + _participants.get(ii); + Component pcomp = part.getComponent(); + if (pcomp == null) { + continue; + } + + // get the bounds of this component + pcomp.getBounds(bounds); + + // 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, bounds) == null) { + continue; + } + + try { + // render this participant +// Log.info("Rendering [comp=" + pcomp.getClass().getName() + +// ", bounds=" + StringUtil.toString(bounds) + "]."); + g.setClip(bounds); + g.translate(bounds.x, bounds.y); + pcomp.paint(g); + g.translate(-bounds.x, -bounds.y); + +// // copy the off-screen buffer on-screen +// fg.setClip(bounds); +// fg.drawImage(_backimg, fi.left, fi.top, null); + + } catch (Throwable t) { + String ptos = StringUtil.safeToString(part); + Log.warning("Frame participant choked during paint " + + "[part=" + ptos + "]."); + Log.logStackTrace(t); + } + } + + // Log.info("insets: " + fi + ", fb: " + _frame.getBounds()); + // _frame.paint(g); + fg.drawImage(_backimg, 0, 0, null); + + } finally { + if (g != null) { + g.dispose(); + } + if (fg != null) { + fg.dispose(); + } + } + + } while (_backimg.contentsLost()); + +// Graphics g = null; +// try { +// g = _bufstrat.getDrawGraphics(); +// _frame.paint(g); +// _bufstrat.show(); + +// } catch (Throwable t) { +// Log.warning("Frame rendering choked."); +// Log.logStackTrace(t); + +// } finally { +// if (g != null) { +// g.dispose(); +// } +// } + } + + // documentation inherited + public void checkpoint (String name, int ticks) + { +// Log.info("Frames in last second: " + ticks); + } + + /** + * Used to queue up frame ticks on the AWT thread at some point in the + * future. + */ + protected class Ticker extends Thread + { + /** + * Tells the ticker to queue up a frame in the requisite number of + * milliseconds. + */ + public synchronized void tickIn (long millis, long now) + { + _sleepfor = millis; + _now = now; + this.notify(); + } + + public void run () + { + synchronized (this) { + while (_sleepfor != -1) { + try { + if (_sleepfor == 0) { + this.wait(); + } + if (_sleepfor > 0) { + Thread.sleep(_sleepfor); + + // make a note of our frame start time + _frameStart = System.currentTimeMillis(); +// long error =_frameStart - (_sleepfor + _now); +// if (Math.abs(error) > 3) { +// Log.warning("Funny business: " + error); +// } + + // queue up our ticker on the AWT thread + EventQueue.invokeLater(_callTick); + _sleepfor = 0; + } + + } catch (InterruptedException ie) { + Log.warning("Girl interrupted!"); + } + } + } + } + + protected long _sleepfor = 0l; + protected long _now = 0l; + } + + /** + * Creates the off-screen buffer used to perform double buffered + * rendering of the animated panel. + */ + protected void createBackBuffer (GraphicsConfiguration gc) + { + _backimg = gc.createCompatibleVolatileImage( + _frame.getWidth(), _frame.getHeight()); + } + + /** + * 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.getPeer() == null) { + return null; + } + if (c instanceof Window || c instanceof Applet) { + return c; + } + rect.x += c.getX(); + rect.y += c.getY(); + } + return null; + } + + /** The frame into which we do our rendering. */ + protected Frame _frame; + + /** Our custom repaint manager. */ + protected FrameRepaintManager _remgr; + + /** The buffer strategy used to do our rendering. */ + protected BufferStrategy _bufstrat; + + /** The image used to render off-screen. */ + protected VolatileImage _backimg; + + /** The number of milliseconds per frame (33 by default, which gives + * an fps of 30). */ + protected long _millisPerFrame = 33; + + /** The time at which we started the most recent "frame". */ + protected long _frameStart; + + /** Used to queue up a tick. */ + protected Ticker _ticker; + + /** Used to queue up a call to {@link #tick} on the AWT thread. */ + protected Runnable _callTick = new Runnable () { + public void run () { + tick(); + } + }; + + /** The entites that are ticked each frame. */ + protected ArrayList _participants = new ArrayList(); +} diff --git a/src/java/com/threerings/media/FrameParticipant.java b/src/java/com/threerings/media/FrameParticipant.java new file mode 100644 index 000000000..18d058683 --- /dev/null +++ b/src/java/com/threerings/media/FrameParticipant.java @@ -0,0 +1,34 @@ +// +// $Id: FrameParticipant.java,v 1.1 2002/04/23 01:16:27 mdb Exp $ + +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); + + /** + * 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 properly + * configured graphics object. If a particpant does not wish to be + * actively rendered, it can safely return null. + */ + public Component getComponent (); +} diff --git a/src/java/com/threerings/media/FrameRepaintManager.java b/src/java/com/threerings/media/FrameRepaintManager.java new file mode 100644 index 000000000..d3ea8964f --- /dev/null +++ b/src/java/com/threerings/media/FrameRepaintManager.java @@ -0,0 +1,232 @@ +// +// $Id: FrameRepaintManager.java,v 1.1 2002/04/23 01:16:27 mdb Exp $ + +package com.threerings.media; + +import java.applet.Applet; + +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Rectangle; +import java.awt.Window; + +import javax.swing.CellRendererPane; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.RepaintManager; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map.Entry; + +import com.samskivert.util.ListUtil; +import com.samskivert.util.StringUtil; + +/** + * Used to get Swing's repainting to jive with our active rendering + * strategy. + * + * @see FrameManager + */ +public class FrameRepaintManager extends RepaintManager +{ + // documentation inherited + public synchronized void addInvalidComponent (JComponent comp) + { + Component vroot = null; + + // 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.getPeer() == null || c instanceof CellRendererPane) { + return; + } + + // skip non-Swing components + if (!(c instanceof JComponent)) { + continue; + } + + // if we find our validate root, we can stop looking + if (((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) { +// Log.info("Skipping vrootless component: " + comp); + return; + } + + // make sure that the component is actually in a window or applet + // that is showing + if (getRoot(vroot) == null) { +// Log.info("Skipping rootless component: " + comp + "/" + vroot); + return; + } + + // add the invalid component to our list and we'll validate it on + // the next frame + if (!ListUtil.contains(_invalid, vroot)) { + _invalid = ListUtil.add(_invalid, vroot); + } + } + + // documentation inherited + public synchronized void addDirtyRegion ( + JComponent comp, int x, int y, int width, int height) + { + // ignore invalid requests + if ((width <= 0) || (height <= 0) || (comp == null) || + (comp.getWidth() <= 0) || (comp.getHeight() <= 0)) { +// Log.info("Skipping bogus region " + comp.getClass().getName() + +// ", x=" + x + ", y=" + y + ", width=" + width + ", height=" + height + "."); + return; + } + + // if this component is already dirty, simply expand their + // existing dirty rectangle + Rectangle drect = (Rectangle)_dirty.get(comp); + if (drect != null) { + drect.add(x, y); + drect.add(x+width, y+height); + return; + } + + // make sure this component has a valid root + if (getRoot(comp) == null) { +// Log.info("Skipping rootless repaint " + comp + "."); + return; + } + + // if we made it this far, we can queue up a dirty region for this + // component to be repainted on the next tick + _dirty.put(comp, new Rectangle(x, y, width, height)); + } + + /** + * Returns the root component for the supplied component or null if it + * is not part of a rooted hierarchy or if any parent along the way is + * found to be hidden or without a peer. + */ + protected Component getRoot (Component comp) + { + for (Component c = comp; c != null; c = c.getParent()) { + if (!c.isVisible() || c.getPeer() == null) { + return null; + } + if (c instanceof Window || c instanceof Applet) { + return c; + } + } + return null; + } + + // documentation inherited + public synchronized Rectangle getDirtyRegion (JComponent comp) + { + Rectangle drect = (Rectangle)_dirty.get(comp); + // copy the rectangle if we found one, otherwise create an empty + // rectangle because we don't want them leaving empty handed + return (drect == null) ? + new Rectangle(0, 0, 0, 0) : new Rectangle(drect); + } + + // documentation inherited + public synchronized void markCompletelyClean (JComponent comp) + { + _dirty.remove(comp); + } + + /** + * Validates the invalid components that have been queued up since the + * last frame tick. + */ + public void validateComponents () + { + // swap out our invalid array + Object[] invalid = null; + synchronized (this) { + invalid = _invalid; + _invalid = null; + } + + // if there's nothing to validate, we're home free + if (invalid == null) { + return; + } + + // validate everything therein + int icount = invalid.length; + for (int ii = 0; ii < icount; ii++) { + if (invalid[ii] != null) { +// Log.info("Validating " + invalid[ii]); + ((Component)invalid[ii]).validate(); + } + } + } + + /** + * Paints the components that have become dirty since the last tick. + */ + public void paintComponents (Graphics g) + { + // swap out our hashmap + synchronized (this) { + HashMap tmap = _spare; + _spare = _dirty; + _dirty = tmap; + } + + // now paint each of the dirty components, by setting the clipping + // rectangle appropriately and calling paint() on the associated + // root component + Iterator iter = _spare.entrySet().iterator(); + while (iter.hasNext()) { + Entry entry = (Entry)iter.next(); + JComponent comp = (JComponent)entry.getKey(); + Rectangle drect = (Rectangle)entry.getValue(); + Rectangle orect = (Rectangle)drect.clone(); + Component root = FrameManager.getRoot(comp, drect); + + Rectangle cbounds = + new Rectangle(0, 0, comp.getWidth(), comp.getHeight()); + Rectangle obounds = comp.getBounds(); + FrameManager.getRoot(comp, cbounds); + + Rectangle clip = drect.intersection(cbounds); + + if (root != null) { +// if (!(comp instanceof JButton)) { +// Log.info("Repainting [comp=" + comp.getClass().getName() + +// StringUtil.toString(obounds) + +// StringUtil.toString(cbounds) + +// ", root=" + root.getClass().getName() + +// ", clip=" + StringUtil.toString(clip) + +// ", drect=" + StringUtil.toString(drect) + +// ", orect=" + StringUtil.toString(orect) + "]."); +// } + g.setClip(clip); + root.paint(g); + } + } + + // clear out the mapping of dirty components + _spare.clear(); + } + + /** A list of invalid components. */ + protected Object[] _invalid; + + /** A mapping of invalid rectangles for each widget that is dirty. */ + protected HashMap _dirty = new HashMap(); + + /** A spare hashmap that we swap in while repainting dirty components + * in the old hashmap. */ + protected HashMap _spare = new HashMap(); +} diff --git a/src/java/com/threerings/media/ManagedJFrame.java b/src/java/com/threerings/media/ManagedJFrame.java new file mode 100644 index 000000000..cd0c7c850 --- /dev/null +++ b/src/java/com/threerings/media/ManagedJFrame.java @@ -0,0 +1,47 @@ +// +// $Id: ManagedJFrame.java,v 1.1 2002/04/23 01:16:27 mdb Exp $ + +package com.threerings.media; + +import java.awt.Graphics; +import java.awt.Rectangle; + +import javax.swing.JFrame; +import javax.swing.RepaintManager; + +import com.threerings.media.Log; + +/** + * When using the {@link FrameManager}, one must use this top-level frame + * class (or the {@link ManagedFrame} class if one is not using Swing. + */ +public class ManagedJFrame extends JFrame +{ + /** + * Constructs a managed frame with no title. + */ + public ManagedJFrame () + { + } + + /** + * Constructs a managed frame with the specified title. + */ + public ManagedJFrame (String title) + { + super(title); + } + + /** + * We catch update requests and forward them on to the repaint + * infrastructure. + */ + public void update (Graphics g) + { + Rectangle clip = g.getClip().getBounds(); + if (clip != null) { + RepaintManager.currentManager(this).addDirtyRegion( + getRootPane(), clip.x, clip.y, clip.width, clip.height); + } + } +} diff --git a/src/java/com/threerings/media/MediaConstants.java b/src/java/com/threerings/media/MediaConstants.java new file mode 100644 index 000000000..1b4050ed9 --- /dev/null +++ b/src/java/com/threerings/media/MediaConstants.java @@ -0,0 +1,19 @@ +// +// $Id: MediaConstants.java,v 1.1 2002/04/23 01:16:27 mdb Exp $ + +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; +} diff --git a/src/java/com/threerings/media/MediaPanel.java b/src/java/com/threerings/media/MediaPanel.java new file mode 100644 index 000000000..c64d7d8b4 --- /dev/null +++ b/src/java/com/threerings/media/MediaPanel.java @@ -0,0 +1,486 @@ +// +// $Id: MediaPanel.java,v 1.1 2002/04/23 01:16:27 mdb Exp $ + +package com.threerings.media; + +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Graphics; +import java.awt.Rectangle; +import java.awt.Shape; + +import javax.swing.JComponent; +import javax.swing.RepaintManager; +import javax.swing.event.AncestorEvent; + +import java.util.ArrayList; +import java.util.List; + +import com.samskivert.swing.event.AncestorAdapter; +import com.samskivert.util.StringUtil; + +import com.threerings.media.FrameManager; +import com.threerings.media.Log; + +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 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}). + * + *
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. + * + *
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}). + * + *
The animated panel automatically registers with the {@link + * FrameManager} to participate in the frame tick. It only does so while + * it is a visible part of the UI hierarchy, so animations and sprites are + * paused while the animated panel is hidden. + */ +public class MediaPanel extends JComponent + implements FrameParticipant, MediaConstants +{ + /** + * Constructs an animated panel. + */ + public MediaPanel (FrameManager framemgr) + { + // keep this for later + _framemgr = framemgr; + + // create our region manager + _remgr = new RegionManager(); + + // create our animation and sprite managers + _animmgr = new AnimationManager(_remgr); + _spritemgr = new SpriteManager(_remgr); + + // we don't want to hear about repaints + setIgnoreRepaint(true); + + // participate in the frame when we're visible + addAncestorListener(new AncestorAdapter() { + public void ancestorAdded (AncestorEvent event) { + _framemgr.registerFrameParticipant(MediaPanel.this); + } + public void ancestorRemoved (AncestorEvent event) { + _framemgr.removeFrameParticipant(MediaPanel.this); + } + }); + } + + /** + * Instructs the view to scroll by the specified number of pixels + * (which can be negative if it should scroll in the negative x or y + * direction) in the specified number of milliseconds. While the view + * is scrolling, derived classes can hear about the scrolled + * increments by overriding {@link #viewWillScroll} and they can find + * out when scrolling is complete by overriding {@link + * #viewFinishedScrolling}. + * + * @param dx the number of pixels in the x direction to scroll. + * @param dy the number of pixels in the y direction to scroll. + * @param millis the number of milliseconds in which to do it. The + * scrolling is calculated such that we will "drop frames" in order to + * scroll the necessary distance by the requested time. + */ + public void setScrolling (int dx, int dy, long millis) + { + // if dx and dy are zero, we've got nothing to do + if (dx == 0 && dy == 0) { + return; + } + + // set our scrolling parameters + _scrollx = dx; + _scrolly = dy; + _last = System.currentTimeMillis(); + _ttime = _last + millis; + +// // figure out the lesser (but non-zero) of the two scroll deltas +// int absx = Math.abs(dx), absy = Math.abs(dy); +// int mindist = absx; +// if (absx == 0) { +// mindist = absy; +// } else if (absy == 0) { +// mindist = absx; +// } else { +// mindist = Math.min(absx, absy); +// } + +// // let the animation manager know how "fast" we'll be scrolling so +// // that it can determine its frame rate +// int mspp = (int)(millis/mindist); +// _animmgr.setScrolling(mspp); + +// Log.info("Scrolling [dx=" + dx + ", dy=" + dy + +// ", millis=" + millis + "ms, mspp=" + mspp + "]."); + } + + /** + * Returns a reference to the region manager that is used to + * accumulate dirty regions each frame. + */ + public RegionManager getRegionManager () + { + return _remgr; + } + + /** + * Adds a sprite to this panel. + */ + public void addSprite (Sprite sprite) + { + _spritemgr.addSprite(sprite); + } + + /** + * Removes a sprite from this panel. + */ + public void removeSprite (Sprite sprite) + { + _spritemgr.removeSprite(sprite); + } + + /** + * Adds an animation to this panel. + */ + public void addAnimation (Animation anim) + { + _animmgr.registerAnimation(anim); + } + + /** + * Removes an animation from this panel. + */ + public void removeAnimation (Animation anim) + { + _animmgr.unregisterAnimation(anim); + } + + // documentation inherited + public void doLayout () + { + super.doLayout(); + + // figure out our viewport offsets + Dimension size = getSize(), vsize = getViewSize(); + _tx = (vsize.width - size.width)/2; + _ty = (vsize.height - size.height)/2; + +// Log.info("Size: " + size + ", vsize: " + vsize + +// ", tx: " + _ty + ", ty: " + _ty + "."); + } + + // documentation inherited + public void invalidate () + { + super.invalidate(); + // invalidate our bounds with the region manager + _remgr.invalidateRegion(getBounds()); + } + + // documentation inherited from interface + public void tick (long tickStamp) + { + int width = getWidth(), height = getHeight(); + + // if scrolling is enabled, determine the scrolling delta to be + // used and do the business + _dx = 0; + _dy = 0; + if (_ttime != 0) { + // if we've blown past our allotted time, we want to scroll + // the rest of the way + if (tickStamp > _ttime) { + _dx = _scrollx; + _dy = _scrolly; + +// Log.info("Scrolling rest [dx=" + dx + ", dy=" + dy + "]."); + + } else { + // otherwise figure out how many milliseconds have gone by + // since we last scrolled and scroll the requisite amount + float dt = (float)(tickStamp - _last); + float rt = (float)(_ttime - _last); + + // our delta is the remaining distance multiplied by the + // time delta divided by the remaining time + _dx = Math.round((float)(_scrollx * dt) / rt); + _dy = Math.round((float)(_scrolly * dt) / rt); + +// Log.info("Scrolling delta [dt=" + dt + ", rt=" + rt + +// ", dx=" + dx + ", dy=" + dy + "]."); + } + + // and add invalid rectangles for the exposed areas + if (_dx > 0) { + _remgr.invalidateRegion(width - _dx + _tx, _ty, _dx, height); + } else if (_dx < 0) { + _remgr.invalidateRegion(_tx, _ty, -_dx, height); + } + if (_dy > 0) { + _remgr.invalidateRegion(_tx, height - _dy + _ty, width, _dy); + } else if (_dy < 0) { + _remgr.invalidateRegion(_tx, _ty, width, -_dy); + } + + // make sure we're actually scrolling before telling people + // about it + if (_dx != 0 || _dy != 0) { + // if we are working with a sprite manager, let it know + // that we're about to scroll out from under its sprites + // and allow it to provide us with more dirty rects + if (_spritemgr != null) { + _spritemgr.viewWillScroll(_dx, _dy); + } + + // let our derived classes do whatever they need to do to + // prepare to be scrolled + viewWillScroll(_dx, _dy, tickStamp); + + // keep track of the last time we scrolled + _last = tickStamp; + + // subtract our scrolled deltas from the distance remaining + _scrollx -= _dx; + _scrolly -= _dy; + + // if we've reached our desired position, finish the job + if (_scrollx == 0 && _scrolly == 0) { + _ttime = 0; + viewFinishedScrolling(); + } + } + } + + // now tick our animations and sprites + _animmgr.tick(tickStamp); + _spritemgr.tick(tickStamp); + + // make a note that the next paint will correspond to a call to + // tick() + _tickPaintPending = true; + } + + /** + * We want to be painted every tick. + */ + public Component getComponent () + { + return this; + } + + // documentation inherited + public void repaint (long tm, int x, int y, int width, int height) + { + _remgr.invalidateRegion(x, y, width, height); + } + + // documentation inherited + public void paint (Graphics g) + { + Graphics2D gfx = (Graphics2D)g; + + // no use in painting if we're not showing or if we've not yet + // been validated + if (!isValid() || !isShowing()) { + return; + } + + // if this isn't a tick paint, then we need to grab the clipping + // rectangle and mark it as dirty + if (!_tickPaintPending) { + Shape clip = g.getClip(); + _remgr.invalidateRegion((clip == null) ? + getBounds() : clip.getBounds()); + + } else { + _tickPaintPending = false; + } + + // if we didn't scroll and have no invalid rects, there's no need + // to repaint anything + if (!_remgr.haveDirtyRegions() && _dx == 0 && _dy == 0) { + return; + } + + // we may need to do some scrolling + if (_dx != 0 || _dy != 0) { + gfx.copyArea(0, 0, getWidth(), getHeight(), -_dx, -_dy); + } + + // get our dirty rectangles + Rectangle[] dirty = _remgr.getDirtyRegions(); + int dcount = dirty.length; + + // translate into happy space + gfx.translate(-_tx, -_ty); + for (int ii = 0; ii < dcount; ii++) { + dirty[ii].translate(_tx, _ty); + } + + // paint the behind the scenes stuff + paintBehind(gfx, dirty); + + // paint back sprites and animations + for (int ii = 0; ii < dcount; ii++) { + paintBits(gfx, AnimationManager.BACK, dirty[ii]); + } + + // paint the between the scenes stuff + paintBetween(gfx, dirty); + + // paint front sprites and animations + for (int ii = 0; ii < dcount; ii++) { + paintBits(gfx, AnimationManager.FRONT, dirty[ii]); + } + + // paint anything in front + paintInFront(gfx, dirty); + + // translate back out of happy space + gfx.translate(_tx, _ty); + } + + /** + * Paints behind all sprites and animations. The supplied list of + * invalid rectangles should be redrawn in the supplied graphics + * context. The rectangles will be in the view coordinate system + * (which may differ from screen coordinates, see {@link + * #getViewSize}. Sub-classes should override this method to do the + * actual rendering for their display. + */ + protected void paintBehind (Graphics2D gfx, Rectangle[] dirtyRects) + { + } + + /** + * Paints between the front and back layer of sprites and animations. + * The supplied list of invalid rectangles should be redrawn in the + * supplied graphics context. The rectangles will be in the view + * coordinate system (which may differ from screen coordinates, see + * {@link #getViewSize}. Sub-classes should override this method to do + * the actual rendering for their display. + */ + protected void paintBetween (Graphics2D gfx, Rectangle[] dirtyRects) + { + } + + /** + * Paints in front of all sprites and animations. The supplied list of + * invalid rectangles should be redrawn in the supplied graphics + * context. The rectangles will be in the view coordinate system + * (which may differ from screen coordinates, see {@link + * #getViewSize}. Sub-classes should override this method to do the + * actual rendering for their display. + */ + protected void paintInFront (Graphics2D gfx, Rectangle[] dirtyRects) + { + } + + /** + * Renders the sprites and animations that intersect the supplied + * clipping 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). This method also takes care of setting the clipping + * region in the graphics object which an overridden method should + * also do to preserve performance. + */ + protected void paintBits (Graphics2D gfx, int layer, Rectangle clip) + { + Shape oclip = gfx.getClip(); + gfx.setClip(clip); + _animmgr.renderAnimations(gfx, layer, clip); + _spritemgr.renderSprites(gfx, layer, clip); + gfx.setClip(oclip); + } + + /** + * This is called, when the view is scrolling, during the tick + * processing phase. The animated panel will take care of scrolling + * the contents of the offscreen buffer when the time comes to render, + * but the derived class will need to do whatever is necessary to + * prepare to repaint the exposed regions as well as update its own + * internal state accordingly. + * + * @param dx the distance (in pixels) that the view will scroll in the + * x direction. + * @param dy the distance (in pixels) that the view will scroll in the + * y direction. + * @param now the current time, provided because we have it and + * scrolling views are likely to want to use it in calculating stuff. + */ + protected void viewWillScroll (int dx, int dy, long tickStamp) + { + // nothing to do here + } + + /** + * Called during the same frame that we scrolled into the final + * desired position. This method is called after {@link + * #viewWillScroll} is called with the final scrolling deltas. + */ + protected void viewFinishedScrolling () + { +// Log.info("viewFinishedScrolling"); + } + + /** + * Derived classes that wish to operate in a coordinate system based + * on a view size that is larger or smaller than the viewport size + * (the actual dimensions of the animated panel) can override this + * method and return the desired size of the view. The animated panel + * will take this size into account and translate into the view + * coordinate system before calling {@link #render}. + */ + protected Dimension getViewSize () + { + return getSize(); + } + + /** The frame manager with whom we register. */ + protected FrameManager _framemgr; + + /** The animation manager in use by this panel. */ + protected AnimationManager _animmgr; + + /** The sprite manager in use by this panel. */ + protected SpriteManager _spritemgr; + + /** Used to accumulate and merge dirty regions on each tick. */ + protected RegionManager _remgr; + + /** Our viewport offsets. */ + protected int _tx, _ty; + + /** How many pixels we have left to scroll. */ + protected int _scrollx, _scrolly; + + /** Our scroll offsets for this frame tick. */ + protected int _dx, _dy; + + /** The time at which we expect to stop scrolling. */ + protected long _ttime; + + /** The last time we were scrolled. */ + protected long _last; + + /** Used to correlate tick()s with paint()s. */ + protected boolean _tickPaintPending = false; +} diff --git a/src/java/com/threerings/media/RegionManager.java b/src/java/com/threerings/media/RegionManager.java new file mode 100644 index 000000000..5ba04728a --- /dev/null +++ b/src/java/com/threerings/media/RegionManager.java @@ -0,0 +1,92 @@ +// +// $Id: RegionManager.java,v 1.1 2002/04/23 01:16:27 mdb Exp $ + +package com.threerings.media; + +import java.awt.Rectangle; + +import java.util.ArrayList; +import java.util.List; + +/** + * 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) + { + _dirty.add(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) + { + _dirty.add((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) + { + _dirty.add(rect); + } + + /** + * Returns true if dirty regions have been accumulated since the last + * call to {@link #getDirtyRects}. + */ + public boolean haveDirtyRegions () + { + return (_dirty.size() > 0); + } + + /** + * Merges all outstanding dirty regions into a single list of + * rectangles and returns that to the caller. Interally, the list of + * accumulated dirty regions is cleared out and prepared for the next + * frame. + */ + public Rectangle[] getDirtyRegions () + { + ArrayList merged = new ArrayList(); + + while (_dirty.size() > 0) { + // pop the next rectangle from the dirty list + Rectangle mr = (Rectangle)_dirty.remove(0); + + // merge in any overlapping rectangles + for (int ii = 0; ii < _dirty.size(); ii++) { + Rectangle r = (Rectangle)_dirty.get(ii); + if (mr.intersects(r)) { + // remove the overlapping rectangle from the list + _dirty.remove(ii--); + // grow the merged dirty rectangle + mr.add(r); + } + } + + // add the merged rectangle to the list + merged.add(mr); + } + + Rectangle[] rects = new Rectangle[merged.size()]; + merged.toArray(rects); + return rects; + } + + /** A list of dirty rectangles. */ + protected ArrayList _dirty = new ArrayList(); +} diff --git a/src/java/com/threerings/media/SafeScrollPane.java b/src/java/com/threerings/media/SafeScrollPane.java new file mode 100644 index 000000000..1b03abf08 --- /dev/null +++ b/src/java/com/threerings/media/SafeScrollPane.java @@ -0,0 +1,27 @@ +// +// $Id: SafeScrollPane.java,v 1.1 2002/04/23 01:16:27 mdb Exp $ + +package com.threerings.media; + +import java.awt.Component; + +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 (Component view) + { + super(view); + } + + protected JViewport createViewport () + { + JViewport vp = new JViewport(); + vp.setScrollMode(JViewport.SIMPLE_SCROLL_MODE); + return vp; + } +} diff --git a/src/java/com/threerings/media/animation/AnimatedPanel.java b/src/java/com/threerings/media/animation/AnimatedPanel.java deleted file mode 100644 index 36eec44b9..000000000 --- a/src/java/com/threerings/media/animation/AnimatedPanel.java +++ /dev/null @@ -1,506 +0,0 @@ -// -// $Id: AnimatedPanel.java,v 1.21 2002/04/18 22:38:24 mdb Exp $ - -package com.threerings.media.animation; - -import java.awt.AWTException; -import java.awt.BufferCapabilities; -import java.awt.Component; -import java.awt.Dimension; -import java.awt.Graphics2D; -import java.awt.Graphics; -import java.awt.GraphicsConfiguration; -import java.awt.ImageCapabilities; -import java.awt.Rectangle; - -import java.awt.image.BufferStrategy; -import java.awt.image.VolatileImage; - -import javax.swing.event.AncestorEvent; -import javax.swing.JComponent; - -import java.util.ArrayList; -import java.util.List; - -import com.samskivert.swing.event.AncestorAdapter; -import com.samskivert.util.Histogram; -import com.samskivert.util.StringUtil; - -import com.threerings.media.Log; -import com.threerings.media.sprite.SpriteManager; - -/** - * The animated panel provides a useful extensible implementation of a - * {@link JComponent} that implements the {@link AnimatedView} interface. - * It takes care of automatically creating an animation manager and a - * sprite manager to manage the animations that take place within this - * panel. If a sprite manager is not needed, {@link #needsSpriteManager} - * can be overridden to prevent it from being created. - * - *
Users of the animated panel must be sure to call {@link #stop} when - * their panel is no longer being displayed and {@link #start} to start it - * back up again when it is once again being displayed. These methods - * deactivate and reactivate the underlying animation manager. - * - *
Sub-classes should override {@link #render} to draw their
- * panel-specific contents.
- */
-public class AnimatedPanel extends JComponent
- implements AnimatedView
-{
- /**
- * Constructs an animated panel.
- */
- public AnimatedPanel ()
- {
- // create our animation manager
- _animmgr = new AnimationManager(this);
-
- // create a sprite manager if we haven't been requested not to
- if (needsSpriteManager()) {
- _spritemgr = new SpriteManager();
- _animmgr.setSpriteManager(_spritemgr);
- }
-
- // turn off double buffering because we handle our own rendering;
- // it's not enough to turn it off for this component, we need to
- // climb all the way up the chain, otherwise one of our parents
- // might be painting and decide to do its own double buffering
- addAncestorListener(new AncestorAdapter() {
- public void ancestorAdded (AncestorEvent event) {
- Component c = AnimatedPanel.this;
- while (c != null) {
- if (c instanceof JComponent) {
- ((JComponent)c).setDoubleBuffered(false);
- }
- c = c.getParent();
- }
- setOpaque(true);
- }
- });
- }
-
- /**
- * Derived classes can override this method to prevent the automatic
- * creation of a sprite manager to work with the animation manager
- * that is managing this panel.
- */
- protected boolean needsSpriteManager ()
- {
- return true;
- }
-
- /**
- * Starts up the animation manager associated with this panel.
- */
- public void start ()
- {
- _animmgr.start();
- }
-
- /**
- * Shuts down the animation manager associated with this panel.
- */
- public void stop ()
- {
- _animmgr.stop();
- }
-
- /**
- * Instructs the view to scroll by the specified number of pixels
- * (which can be negative if it should scroll in the negative x or y
- * direction) in the specified number of milliseconds. While the view
- * is scrolling, derived classes can hear about the scrolled
- * increments by overriding {@link #viewWillScroll} and they can find
- * out when scrolling is complete by overriding {@link
- * #viewFinishedScrolling}.
- *
- * @param dx the number of pixels in the x direction to scroll.
- * @param dy the number of pixels in the y direction to scroll.
- * @param millis the number of milliseconds in which to do it. The
- * scrolling is calculated such that we will "drop frames" in order to
- * scroll the necessary distance by the requested time.
- */
- public void setScrolling (int dx, int dy, long millis)
- {
- // if dx and dy are zero, we've got nothing to do
- if (dx == 0 && dy == 0) {
- return;
- }
-
- // set our scrolling parameters
- _scrollx = dx;
- _scrolly = dy;
- _last = System.currentTimeMillis();
- _ttime = _last + millis;
-
- // figure out the lesser (but non-zero) of the two scroll deltas
- int absx = Math.abs(dx), absy = Math.abs(dy);
- int mindist = absx;
- if (absx == 0) {
- mindist = absy;
- } else if (absy == 0) {
- mindist = absx;
- } else {
- mindist = Math.min(absx, absy);
- }
-
- // let the animation manager know how "fast" we'll be scrolling so
- // that it can determine its frame rate
- int mspp = (int)(millis/mindist);
- _animmgr.setScrolling(mspp);
-
-// Log.info("Scrolling [dx=" + dx + ", dy=" + dy +
-// ", millis=" + millis + "ms, mspp=" + mspp + "].");
- }
-
- // documentation inherited
- public void paint (Graphics g)
- {
- // convert the clipping rectangle into the proper coordinates and
- // call into our rendering system
- Rectangle r = g.getClipBounds();
- r.translate(_tx, _ty);
- _paintList.add(r);
- // we have to tell paintImmediately() to use the graphics object
- // that we were passed, otherwise Swing will fuck everything to
- // the high heavens
- paintImmediately(g, _paintList);
- _paintList.clear();
- }
-
- // documentation inherited
- public void paintImmediately (int x, int y, int w, int h)
- {
- paintImmediately(new Rectangle(x, y, w, h));
- }
-
- // documentation inherited
- public void paintImmediately (Rectangle r)
- {
- // convert the clipping rectangle into the proper coordinates and
- // call into our rendering system
- r.translate(_tx, _ty);
- _paintList.add(r);
- paintImmediately(_paintList);
- _paintList.clear();
- }
-
- // documentation inherited
- public void doLayout ()
- {
- super.doLayout();
-
- // if we change size, clear out our old back buffer
- _backimg = null;
-
- // figure out our viewport offsets
- Dimension size = getSize(), vsize = getViewSize();
- _tx = (vsize.width - size.width)/2;
- _ty = (vsize.height - size.height)/2;
-
-// Log.info("Size: " + size + ", vsize: " + vsize +
-// ", tx: " + _ty + ", ty: " + _ty + ".");
- }
-
- // documentation inherited
- public void invalidateRect (Rectangle invalidRect)
- {
- // pass it on to the animation manager
- _animmgr.addDirtyRect(invalidRect);
- }
-
- // documentation inherited
- public void paintImmediately (List invalidRects)
- {
- paintImmediately(null, invalidRects);
- }
-
- /**
- * Renders this animated panel. If the supplied graphics reference is
- * non-null, we will render to that instance, otherwise a graphics
- * object will be obtained via a call to {@link #getGraphics}. This
- * method, of course, must be called from the AWT thread or mayhem
- * will ensue.
- */
- protected void paintImmediately (Graphics gfx, List invalidRects)
- {
- // no use in painting if we're not showing or if we've not yet
- // been validated
- if (!isValid() || !isShowing()) {
- return;
- }
-
- // track how long it was since we were last painted
- long now = System.currentTimeMillis();
-// if (_paint != 0) {
-// int delta = (int)(now-_paint);
-// _histo.addValue(delta);
-
-// // dump the histogram every ten seconds
-// if (_paint % (10*1000) < 50) {
-// Log.info("Render histogram.");
-// Log.info(StringUtil.toMatrixString(_histo.getBuckets(), 10, 3));
-// }
-// }
-
- int width = getWidth(), height = getHeight();
-
- // if scrolling is enabled, determine the scrolling delta to be
- // used and do the business
- int dx = 0, dy = 0;
- if (_ttime != 0) {
- // if we've blown past our allotted time, we want to scroll
- // the rest of the way
- if (now > _ttime) {
- dx = _scrollx;
- dy = _scrolly;
-
-// Log.info("Scrolling rest [dx=" + dx + ", dy=" + dy + "].");
-
- } else {
- // otherwise figure out how many milliseconds have gone by
- // since we last scrolled and scroll the requisite amount
- float dt = (float)(now - _last);
- float rt = (float)(_ttime - _last);
-
- // our delta is the remaining distance multiplied by the
- // time delta divided by the remaining time
- dx = Math.round((float)(_scrollx * dt) / rt);
- dy = Math.round((float)(_scrolly * dt) / rt);
-
-// Log.info("Scrolling delta [dt=" + dt + ", rt=" + rt +
-// ", dx=" + dx + ", dy=" + dy + "].");
- }
-
- // and add invalid rectangles for the exposed areas
- if (dx > 0) {
- Rectangle dirty = new Rectangle(width - dx, 0, dx, height);
- dirty.translate(_tx, _ty);
- invalidRects.add(dirty);
- } else if (dx < 0) {
- Rectangle dirty = new Rectangle(0, 0, -dx, height);
- dirty.translate(_tx, _ty);
- invalidRects.add(dirty);
- }
- if (dy > 0) {
- Rectangle dirty = new Rectangle(0, height - dy, width, dy);
- dirty.translate(_tx, _ty);
- invalidRects.add(dirty);
- } else if (dy < 0) {
- Rectangle dirty = new Rectangle(0, 0, width, -dy);
- dirty.translate(_tx, _ty);
- invalidRects.add(dirty);
- }
-
- // make sure we're actually scrolling before telling people
- // about it
- if (dx != 0 || dy != 0) {
- // if we are working with a sprite manager, let it know
- // that we're about to scroll out from under its sprites
- // and allow it to provide us with more dirty rects
- if (_spritemgr != null) {
- _spritemgr.viewWillScroll(dx, dy, invalidRects);
- }
-
- // let our derived classes do whatever they need to do to
- // prepare to be scrolled
- viewWillScroll(dx, dy, now, invalidRects);
-
- // keep track of the last time we scrolled
- _last = now;
-
- // subtract our scrolled deltas from the distance remaining
- _scrollx -= dx;
- _scrolly -= dy;
-
- // if we've reached our desired position, finish the job
- if (_scrollx == 0 && _scrolly == 0) {
- _ttime = 0;
- viewFinishedScrolling();
- }
- }
- }
-
- // if we didn't scroll and have no invalid rects, there's no need
- // to repaint anything
- if (invalidRects.size() == 0 && dx == 0 && dy == 0) {
- return;
- }
-
- // create our off-screen buffer if necessary
- GraphicsConfiguration gc = getGraphicsConfiguration();
- if (_backimg == null) {
- createBackBuffer(gc);
- }
-
- // render into our back buffer
- do {
- // 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);
- }
-
- Graphics g = null;
- try {
- g = _backimg.getGraphics();
-
- // 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) {
- invalidRects.clear();
- invalidRects.add(new Rectangle(_tx, _ty, width, height));
- Log.info("Lost back buffer, redrawing.");
-
- } else if (dx != 0 || dy != 0) {
- // if it was OK, we may need to do some scrolling
- g.copyArea(0, 0, width, height, -dx, -dy);
- }
-
- // translate into happy space
- g.translate(-_tx, -_ty);
-
- // now do our actual rendering
- render((Graphics2D)g, invalidRects);
-
- // translate back out of happy space
- g.translate(_tx, _ty);
-
- } finally {
- g.dispose();
- }
-
- // draw the back buffer to the screen
- try {
- if (gfx == null) {
- g = getGraphics();
- } else {
- g = gfx;
- }
-
- // if we're scrolling, we've got to copy the whole image
- // to the screen, otherwise we can just copy the dirty
- // regions
- if (dx != 0 || dy != 0) {
- g.drawImage(_backimg, 0, 0, null);
-
- } else {
- // iterate through the invalid rectangles, copying
- // those areas from the back buffer to the display
- int isize = invalidRects.size();
- for (int i = 0; i < isize; i++) {
- Rectangle rect = (Rectangle)invalidRects.get(i);
- // we have to translate out of view coordinates
- // before doing the actual copy
- rect.translate(-_tx, -_ty);
- g.setClip(rect);
- g.drawImage(_backimg, 0, 0, null);
- }
- }
-
- } finally {
- if (g != null) {
- g.dispose();
- }
- }
-
- } while (_backimg.contentsLost());
- }
-
- /**
- * This is called, when the view is scrolling, just before a call to
- * {@link #render}. The animated panel will take care of scrolling the
- * contents of the offscreen buffer, but the derived class will need
- * to do whatever is necessary to prepare to repaint the exposed
- * regions as well as update it's own internal state accordingly.
- *
- * @param dx the distance (in pixels) that the view will scroll in the
- * x direction.
- * @param dy the distance (in pixels) that the view will scroll in the
- * y direction.
- * @param now the current time, provided because we have it and
- * scrolling views are likely to want to use it in calculating stuff.
- * @param invalidRects the list of invalid rectangles which will be
- * redrawn; rectangles can be added to this list if necessary.
- */
- protected void viewWillScroll (int dx, int dy, long now, List invalidRects)
- {
- // nothing to do here
- }
-
- /**
- * Called during the same frame that we scrolled into the final
- * desired position. This method is called after {@link
- * #viewWillScroll} is called with the final scrolling deltas.
- */
- protected void viewFinishedScrolling ()
- {
-// Log.info("viewFinishedScrolling");
- }
-
- /**
- * Derived classes that wish to operate in a coordinate system based
- * on a view size that is larger or smaller than the viewport size
- * (the actual dimensions of the animated panel) can override this
- * method and return the desired size of the view. The animated panel
- * will take this size into account and translate into the view
- * coordinate system before calling {@link #render}.
- */
- protected Dimension getViewSize ()
- {
- return getSize();
- }
-
- /**
- * Requests that the supplied list of invalid rectangles be redrawn in
- * the supplied graphics context. The rectangles will be in the view
- * coordinate system (which may differ from screen coordinates, see
- * {@link #getViewSize}. Sub-classes should override this method to do
- * the actual rendering for their display.
- */
- protected void render (Graphics2D gfx, List invalidRects)
- {
- // nothing for now
- }
-
- /**
- * Creates the off-screen buffer used to perform double buffered
- * rendering of the animated panel.
- */
- protected void createBackBuffer (GraphicsConfiguration gc)
- {
- _backimg = gc.createCompatibleVolatileImage(getWidth(), getHeight());
- }
-
- /** The animation manager we use in this panel. */
- protected AnimationManager _animmgr;
-
- /** The sprite manager in use by this panel. */
- protected SpriteManager _spritemgr;
-
- /** The image used to render off-screen. */
- protected VolatileImage _backimg;
-
- /** Our viewport offsets. */
- protected int _tx, _ty;
-
- /** The time at which we expect to stop scrolling. */
- protected long _ttime;
-
- /** Used to determine how many pixels we have left to scroll. */
- protected int _scrollx, _scrolly;
-
- /** The last time we were rendered. */
- protected long _last;
-
- /** A histogram for tracking how frequently we're rendered. */
- protected Histogram _histo = new Histogram(0, 1, 100);
-
- /** Used to pass dirty regions supplied by the AWT to the rendering
- * system. */
- protected ArrayList _paintList = new ArrayList();
-}
diff --git a/src/java/com/threerings/media/animation/AnimatedView.java b/src/java/com/threerings/media/animation/AnimatedView.java
deleted file mode 100644
index 6d6d3532c..000000000
--- a/src/java/com/threerings/media/animation/AnimatedView.java
+++ /dev/null
@@ -1,35 +0,0 @@
-//
-// $Id: AnimatedView.java,v 1.4 2002/02/21 06:01:29 mdb Exp $
-
-package com.threerings.media.animation;
-
-import java.awt.Rectangle;
-import java.util.List;
-
-/**
- * A view that wishes to interact with the animation manager needs to
- * implement this interface to give the animation manager a means by which
- * to communicate the regions of the view that need to be repainted
- * because of the process of animating on top of the view.
- */
-public interface AnimatedView
-{
- /**
- * Requests that the specified rectangle (in view coordinates, which
- * need not account for scrolling offsets or viewport offsets) be
- * rendered invalid. The animated view should massage the location of
- * the invalid rectangle and pass it on to the animation manager.
- */
- public void invalidateRect (Rectangle invalidRect);
-
- /**
- * Requests that the animated view paint itself immediately (that it
- * complete the painting process before returning from this function).
- * This will only be called on the AWT thread and when it is safe to
- * paint.
- *
- * @param invalidRects the list of rectangles that have been
- * invalidated since the last call to this method.
- */
- public void paintImmediately (List invalidRects);
-}
diff --git a/src/java/com/threerings/media/animation/Animation.java b/src/java/com/threerings/media/animation/Animation.java
index 8bc7ec251..c40e58929 100644
--- a/src/java/com/threerings/media/animation/Animation.java
+++ b/src/java/com/threerings/media/animation/Animation.java
@@ -1,5 +1,5 @@
//
-// $Id: Animation.java,v 1.4 2002/03/16 03:11:23 shaper Exp $
+// $Id: Animation.java,v 1.5 2002/04/23 01:16:28 mdb Exp $
package com.threerings.media.animation;
@@ -8,12 +8,14 @@ import java.awt.Rectangle;
import java.util.ArrayList;
+import com.samskivert.util.StringUtil;
+
import com.threerings.media.Log;
/**
* The animation class is an abstract class that should be extended to
- * provide animation functionality for use with the {@link
- * AnimationManager} and an {@link AnimatedView}.
+ * provide animation functionality. It is generally used in conjunction
+ * with an {@link AnimationManager}.
*/
public abstract class Animation
{
@@ -47,43 +49,49 @@ public abstract class Animation
}
/**
- * Called by the {@link AnimationManager} to allow the animation to
- * render itself to the given graphics context.
+ * Returns a rectangle containing all pixels rendered by this
+ * animation.
*/
- public abstract void paint (Graphics2D gfx);
+ public Rectangle getBounds ()
+ {
+ return _bounds;
+ }
/**
* Called periodically by the {@link AnimationManager} to give the
* animation a chance to do its thing.
+ *
+ * @param tickStamp the system time for this tick.
*/
- public abstract void tick (long timestamp);
+ public abstract void tick (long tickStamp);
/**
- * Invalidates the animation's bounds for later re-rendering by the
- * {@link AnimationManager}.
+ * Called by the {@link AnimationManager} to request that the
+ * animation render itself with the given graphics context. The
+ * animation 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
+ * animation's bounds intersect the clipping region.
*/
- public void invalidate ()
- {
- _animmgr.addDirtyRect(new Rectangle(_bounds));
- }
+ public abstract void paint (Graphics2D gfx);
/**
- * Invalidates the specified rectangle for later re-rendering by the
- * {@link AnimationManager}.
- */
- public void invalidate (int x, int y, int width, int height)
- {
- _animmgr.addDirtyRect(new Rectangle(x, y, width, height));
- }
-
- /**
- * Returns whether the animation has finished all of its business.
+ * Returns true if the animation has finished all of its business,
+ * false if not.
*/
public boolean isFinished ()
{
return _finished;
}
+ /**
+ * Invalidates the bounds of this animation.
+ */
+ public void invalidate ()
+ {
+ _animmgr.getRegionManager().invalidateRegion(_bounds);
+ }
+
/**
* Called when the animation is finished and the animation manager has
* removed it from service.
@@ -115,8 +123,8 @@ public abstract class Animation
}
/**
- * Notifies any animation observers that the given animation is
- * finished.
+ * Notifies any animation observers that the given animation event has
+ * occurred.
*/
public void notifyObservers (AnimationEvent e)
{
@@ -137,6 +145,15 @@ public abstract class Animation
return buf.append("]").toString();
}
+ /**
+ * Called automatically when an animation is added to an animation
+ * manager for management.
+ */
+ protected void setAnimationManager (AnimationManager animmgr)
+ {
+ _animmgr = animmgr;
+ }
+
/**
* This should be overridden by derived classes (which should be sure
* to call super.toString()) to append the derived class
@@ -144,20 +161,11 @@ public abstract class Animation
*/
protected void toString (StringBuffer buf)
{
- buf.append("bounds=").append(_bounds);
+ buf.append("bounds=").append(StringUtil.toString(_bounds));
}
- /**
- * Called by the animation manager when an animation is added to said
- * manager for management.
- *
- * @param animmgr the animation manager.
- * @param view the animated view.
- */
- protected void setAnimationManager (AnimationManager animmgr)
- {
- _animmgr = animmgr;
- }
+ /** Our animation manager. */
+ protected AnimationManager _animmgr;
/** Whether the animation is finished. */
protected boolean _finished = false;
@@ -170,7 +178,4 @@ public abstract class Animation
/** The list of animation observers. */
protected ArrayList _observers;
-
- /** The animation manager. */
- protected AnimationManager _animmgr;
}
diff --git a/src/java/com/threerings/media/animation/AnimationManager.java b/src/java/com/threerings/media/animation/AnimationManager.java
index 8736c33e1..ed4884909 100644
--- a/src/java/com/threerings/media/animation/AnimationManager.java
+++ b/src/java/com/threerings/media/animation/AnimationManager.java
@@ -1,127 +1,32 @@
//
-// $Id: AnimationManager.java,v 1.8 2002/03/16 03:11:23 shaper Exp $
+// $Id: AnimationManager.java,v 1.9 2002/04/23 01:16:28 mdb Exp $
package com.threerings.media.animation;
import java.awt.Graphics2D;
-import java.awt.Rectangle;
+import java.awt.Shape;
import java.util.ArrayList;
-import java.util.List;
-
-import javax.swing.JComponent;
-import javax.swing.SwingUtilities;
-import javax.swing.event.AncestorEvent;
-
-import com.samskivert.swing.event.AncestorAdapter;
-import com.samskivert.util.Interval;
-import com.samskivert.util.IntervalManager;
import com.threerings.media.Log;
-import com.threerings.media.sprite.SpriteManager;
-import com.threerings.media.util.PerformanceMonitor;
-import com.threerings.media.util.PerformanceObserver;
+import com.threerings.media.MediaConstants;
+import com.threerings.media.RegionManager;
/**
- * The animation manager handles the regular refreshing of the scene view
- * to allow for animation. It also may someday manage special scene-wide
- * animations, such as rain, fog, or earthquakes.
+ * 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
- implements Interval, PerformanceObserver
+ implements MediaConstants
{
- /** Constant for the front layer of animations. */
- public static final int FRONT = 0;
-
- /** Constant for the back layer of animations. */
- public static final int BACK = 1;
-
- /** Constant for all layers of animations. */
- public static final int ALL = 2;
-
/**
- * Construct and initialize the animation manager with a sprite
- * manager and the view in which the animations will take place. The
- * animation manager will automatically start itself up, but must be
- * explicitly shutdown when the animated view is no longer in
- * operation via a call to {@link #stop}.
+ * Construct and initialize the animation manager which readies itself
+ * to manage animations.
*/
- public AnimationManager (SpriteManager spritemgr, AnimatedView view)
+ public AnimationManager (RegionManager remgr)
{
- // save off references to the objects we care about
- _spritemgr = spritemgr;
- _view = view;
-
- // register to monitor the refresh action
- PerformanceMonitor.register(this, "refresh", 1000);
-
- // start ourselves up
- start();
- }
-
- /**
- * Constructs and initializes an animation manager. If sprites are to
- * be used with this animation manager, the other constructor should
- * be used or the sprite manager should be set shortly after
- * construction via {@link #setSpriteManager}. The animation manager
- * will automatically start itself up, but must be explicitly shutdown
- * when the animated view is no longer in operation via a call to
- * {@link #stop}.
- */
- public AnimationManager (AnimatedView view)
- {
- this(null, view);
- }
-
- /**
- * Sets the sprite manager with which this animation manager should
- * coordinate.
- */
- public void setSpriteManager (SpriteManager spritemgr)
- {
- _spritemgr = spritemgr;
- }
-
- /**
- * Starts the animation manager to doing its business.
- */
- public synchronized void start ()
- {
- if (_ticker == null) {
- // create ticker for queueing up tick requests on AWT thread
- _ticker = new Runnable() {
- public void run () {
- tick();
- }
- };
- // register the refresh interval
- _iid = IntervalManager.register(this, _refreshInterval, null, true);
- }
- }
-
- /**
- * Instructs the animation manager to stop doing its business.
- */
- public synchronized void stop ()
- {
- if (_ticker != null) {
- _ticker = null;
- // un-register the refresh interval since we're now hidden
- IntervalManager.remove(_iid);
- _iid = -1;
- }
- }
-
- /**
- * Adds a rectangle to the dirty rectangle list. Note that the
- * rectangle may be destructively modified by the animation manager at
- * some later date.
- *
- * @param rect the rectangle to add.
- */
- public void addDirtyRect (Rectangle rect)
- {
- _dirty.add(rect);
+ _remgr = remgr;
}
/**
@@ -138,12 +43,13 @@ public class AnimationManager
anim.setAnimationManager(this);
_anims.add(anim);
- // Log.info("Registered animation [anim=" + anim + "].");
}
/**
* Un-registers the given {@link Animation} from the animation
- * manager.
+ * 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)
{
@@ -151,211 +57,37 @@ public class AnimationManager
if (!_anims.remove(anim)) {
Log.warning("Attempt to un-register animation that isn't " +
"registered [anim=" + anim + "].");
+ return;
}
- // dirty the animation bounds
- anim.invalidate();
- // Log.info("Un-registered animation [anim=" + anim + "].");
+ // invalidate its bounds
+ _remgr.invalidateRegion(anim.getBounds());
}
/**
- * Renders all registered animations in the given layer 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.
+ * Provides access to the region manager that the animation manager is
+ * using to collect invalid regions every frame. This should generally
+ * only be used by animations that want to invalidate themselves.
*/
- public void renderAnimations (Graphics2D gfx, int layer)
+ public RegionManager getRegionManager ()
{
+ return _remgr;
+ }
+
+ /**
+ * Handles updating animations and generating associated events.
+ *
+ * @param tickStamp the system clock at the time of the tick.
+ */
+ public void tick (long tickStamp)
+ {
+ // tick all of our animations
int size = _anims.size();
for (int ii = 0; ii < size; ii++) {
- Animation anim = (Animation)_anims.get(ii);
- int order = anim.getRenderOrder();
- if ((layer == ALL) ||
- (layer == FRONT && order >= 0) ||
- (layer == BACK && order < 0)) {
- anim.paint(gfx);
- }
+ ((Animation)_anims.get(ii)).tick(tickStamp);
}
- }
-
- /**
- * Lets the animation manager know that the animated view is scrolling
- * at the specified rate (in milliseconds per pixel) so that it can
- * adjust its rendering loop interval to coincide with the scrolling
- * speed, and cause the animated view to be rendered every time
- * through the loop regardless of whether it has dirty regions (so
- * that it can scroll).
- */
- public void setScrolling (int mspp)
- {
- // sanity check
- if (mspp < 0) {
- String errmsg ="Negative scroll velocity illegal " +
- "[mspp=" + mspp + "]";
- throw new IllegalArgumentException(errmsg);
- }
-
- // make a note of our scrolling velocity
- _scrollvel = mspp;
-
- // we want to adjust our refresh interval at which we tick to
- // coincide with an even number of scrolled pixels
- long upperTarget = 1000 / MIN_FRAME_RATE;
- long lowerTarget = 1000 / MAX_FRAME_RATE;
-
- // start out assuming that we can refresh for every pixel
- _refreshInterval = _scrollvel;
-
- // if they've disabled scrolling, go back to the default refresh
- // interval
- if (_scrollvel == 0) {
- _refreshInterval = DEFAULT_REFRESH_INTERVAL;
-
- // if the interval is too quick, bump it up a bit
- } else if (_refreshInterval < lowerTarget) {
- // keep adding a pixel at a time until we're above our minimum
- // refresh interval
- while (_refreshInterval < lowerTarget) {
- _refreshInterval += mspp;
- }
-
- // if it's too slow, we'll want to refresh at some even
- // division of the desired rate
- } else if (_refreshInterval > upperTarget) {
- // try dividing the desired rate by larger and larger values
- // until we're under the upper target
- for (int i = 2; i < 100 && _refreshInterval > upperTarget; i++) {
- _refreshInterval = (mspp / i);
- }
-
- // if the desired velocity is more than one hundred times
- // slower than our desired framerate, then they're not going
- // to notice if things aren't perfectly in sync, so fuck 'em
- if (_refreshInterval > upperTarget) {
- _refreshInterval = DEFAULT_REFRESH_INTERVAL;
- }
- }
-
-// Log.info("Set scrolling velocity [velocity=" + _scrollvel +
-// ", refresh=" + _refreshInterval + "].");
-
- // now stop and start ourselves to reregister our interval
- stop();
- start();
- }
-
- /**
- * Called by our interval when we'd like to begin a tick. Returns
- * whether we're already ticking, and notes that we've requested
- * another tick.
- */
- protected synchronized boolean requestTick ()
- {
- return !(_ticking++ > 0);
- }
-
- /**
- * Called by the tick task when it's finished with a tick.
- * Returns whether a new tick task should be created immediately.
- */
- protected synchronized boolean finishedTick ()
- {
- if (--_ticking > 0) {
- _ticking = 1;
- return true;
- }
-
- return false;
- }
-
- /**
- * The IntervalManager calls this method as often as
- * we've requested to obtain our desired frame rate. Since we'd
- * like to avoid messy thread-synchronization between the AWT
- * thread and other threads, here we just add the tick task to the
- * AWT thread for later execution.
- */
- public void intervalExpired (int id, Object arg)
- {
- if (requestTick()) {
- // throw the tick task on the AWT thread task queue
- queueTick();
- }
- }
-
- /**
- * The tick method handles updating sprites and
- * animations, and repainting the target display.
- */
- protected void tick ()
- {
- synchronized (this) {
- // see if we were shutdown since we were last queued up
- if (_ticker == null) {
- return;
- }
- }
-
- // every tick should have a timestamp associated with it
- long now = System.currentTimeMillis();
-
- // call tick on all sprites
- if (_spritemgr != null) {
- _spritemgr.tick(now, _dirty);
- }
-
- // call tick on all animations
- tickAnimations(now);
-
- // perform a single pass merging overlapping rectangles. note
- // that this will also clear out the contents of our internal
- // dirty rectangle list.
- List rects = mergeDirtyRects(_dirty);
-
- // invalidate screen-rects dirtied by sprites and/or animations
- if (rects.size() > 0 || _scrollvel > 0) {
- // pass the dirty-rects on to the animated view and repaint
- _view.paintImmediately(rects);
- }
// remove any finished animations
- removeFinishedAnimations();
-
- // update refresh-rate information
- // PerformanceMonitor.tick(AnimationManager.this, "refresh");
-
- if (finishedTick()) {
- // finishedTick returning true means there's been a
- // request for at least one more tick since we started
- // this tick, so we want to queue up another tick
- // immediately
- // Log.info("Queueing immediate tick.");
- queueTick();
- }
- }
-
- /**
- * Calls tick on all animations currently registered with the
- * animation manager.
- */
- protected void tickAnimations (long timestamp)
- {
- int size = _anims.size();
- for (int ii = 0; ii < size; ii++) {
- ((Animation)_anims.get(ii)).tick(timestamp);
- }
- }
-
- /**
- * Removes any finished animations from the list of animations and
- * notifies their respective animation observers, if any.
- */
- protected void removeFinishedAnimations ()
- {
- int size = _anims.size();
for (int ii = size - 1; ii >= 0; ii--) {
Animation anim = (Animation)_anims.get(ii);
if (anim.isFinished()) {
@@ -371,93 +103,32 @@ public class AnimationManager
}
/**
- * Queues up a tick on the AWT event handler thread, iff we are still
- * operating.
- */
- protected synchronized void queueTick ()
- {
- if (_ticker != null) {
- SwingUtilities.invokeLater(_ticker);
- }
- }
-
- /**
- * Returns a new list of dirty rectangles representing the given list
- * with any intersecting rectangles merged. The given list is
- * destructively modified and cleared of all contents.
+ * Renders all registered animations in the given layer that intersect
+ * the supplied clipping rectangle to the given graphics context.
*
- * @return the list of merged dirty rects.
+ * @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.
*/
- protected List mergeDirtyRects (List rects)
+ public void renderAnimations (Graphics2D gfx, int layer, Shape clip)
{
- ArrayList merged = new ArrayList();
-
- while (rects.size() > 0) {
- // pop the next rectangle from the dirty list
- Rectangle mr = (Rectangle)rects.remove(0);
-
- // merge in any overlapping rectangles
- for (int ii = 0; ii < rects.size(); ii++) {
- Rectangle r = (Rectangle)rects.get(ii);
- if (mr.intersects(r)) {
- // remove the overlapping rectangle from the list
- rects.remove(ii--);
- // grow the merged dirty rectangle
- mr.add(r);
- }
+ int size = _anims.size();
+ for (int ii = 0; ii < size; ii++) {
+ Animation anim = (Animation)_anims.get(ii);
+ int order = anim.getRenderOrder();
+ if (((layer == ALL) ||
+ (layer == FRONT && order >= 0) ||
+ (layer == BACK && order < 0)) &&
+ clip.intersects(anim.getBounds())) {
+ anim.paint(gfx);
}
-
- // add the merged rectangle to the list
- merged.add(mr);
}
-
- return merged;
}
- // documentation inherited
- public void checkpoint (String name, int ticks)
- {
- Log.info(name + " [ticks=" + ticks + "].");
- }
+ /** Used to accumulate dirty regions. */
+ protected RegionManager _remgr;
/** The list of animations. */
protected ArrayList _anims = new ArrayList();
-
- /** The dirty rectangles. */
- protected ArrayList _dirty = new ArrayList();
-
- /** The ticker runnable that we put on the AWT thread periodically. */
- protected Runnable _ticker;
-
- /** The number of milliseconds in between our refreshes. */
- protected long _refreshInterval = DEFAULT_REFRESH_INTERVAL;
-
- /** The number of outstanding tick requests. */
- protected int _ticking = 0;
-
- /** The refresh interval id. */
- protected int _iid = -1;
-
- /** The sprite manager. */
- protected SpriteManager _spritemgr;
-
- /** The view on which we are animating. */
- protected AnimatedView _view;
-
- /** The velocity at which we are scrolling (in milliseconds per pixel)
- * or zero if we're not scrolling. */
- protected int _scrollvel;
-
- /** The default number of refresh operations per second. */
- protected static final int DEFAULT_FRAME_RATE = 30;
-
- /** The minimum frame rate we'll adjust to when scrolling. */
- protected static final int MIN_FRAME_RATE = 20;
-
- /** The maximum frame rate we'll adjust to when scrolling. */
- protected static final int MAX_FRAME_RATE = 40;
-
- /** The milliseconds to sleep to obtain desired frame rate. */
- protected static final long DEFAULT_REFRESH_INTERVAL =
- 1000 / DEFAULT_FRAME_RATE;
}
diff --git a/src/java/com/threerings/media/animation/RainAnimation.java b/src/java/com/threerings/media/animation/RainAnimation.java
index fb7baae80..a36b6b506 100644
--- a/src/java/com/threerings/media/animation/RainAnimation.java
+++ b/src/java/com/threerings/media/animation/RainAnimation.java
@@ -1,5 +1,5 @@
//
-// $Id: RainAnimation.java,v 1.2 2002/04/15 18:18:20 mdb Exp $
+// $Id: RainAnimation.java,v 1.3 2002/04/23 01:16:28 mdb Exp $
package com.threerings.media.animation;
@@ -62,7 +62,6 @@ public class RainAnimation extends Animation
public void tick (long timestamp)
{
_finished = (timestamp >= _end);
- _animmgr.addDirtyRect(new Rectangle(_bounds));
// calculate the latest raindrop locations
for (int ii = 0; ii < _count; ii++) {
@@ -70,6 +69,8 @@ public class RainAnimation extends Animation
int y = RandomUtil.getInt(_bounds.height);
_drops[ii] = (x << 16 | y);
}
+
+ invalidate();
}
// documentation inherited
diff --git a/src/java/com/threerings/media/sprite/ImageSprite.java b/src/java/com/threerings/media/sprite/ImageSprite.java
index 0ee50d24b..d27f5fdf4 100644
--- a/src/java/com/threerings/media/sprite/ImageSprite.java
+++ b/src/java/com/threerings/media/sprite/ImageSprite.java
@@ -1,5 +1,5 @@
//
-// $Id: ImageSprite.java,v 1.3 2002/04/17 15:52:49 mdb Exp $
+// $Id: ImageSprite.java,v 1.4 2002/04/23 01:16:28 mdb Exp $
package com.threerings.media.sprite;
@@ -175,8 +175,10 @@ public class ImageSprite extends Sprite
updateRenderOffset();
updateRenderOrigin();
- // now invalidate the dirtied region
- invalidate(dirty);
+ // give the dirty rectangle to the region manager
+ if (_spritemgr != null) {
+ _spritemgr.getRegionManager().addDirtyRegion(dirty);
+ }
}
/**
@@ -252,14 +254,6 @@ public class ImageSprite extends Sprite
}
}
- // documentation inherited
- protected void invalidate (Rectangle r)
- {
- if (_frame != null) {
- super.invalidate(r);
- }
- }
-
// documentation inherited
protected void toString (StringBuffer buf)
{
diff --git a/src/java/com/threerings/media/sprite/Sprite.java b/src/java/com/threerings/media/sprite/Sprite.java
index 754751b84..c7f5bfe41 100644
--- a/src/java/com/threerings/media/sprite/Sprite.java
+++ b/src/java/com/threerings/media/sprite/Sprite.java
@@ -1,15 +1,13 @@
//
-// $Id: Sprite.java,v 1.41 2002/04/16 02:28:50 mdb Exp $
+// $Id: Sprite.java,v 1.42 2002/04/23 01:16:28 mdb Exp $
package com.threerings.media.sprite;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Shape;
-import java.awt.image.BufferedImage;
import java.util.ArrayList;
-import java.util.Iterator;
import com.threerings.util.DirectionCodes;
@@ -17,7 +15,7 @@ import com.threerings.media.Log;
/**
* The sprite class represents a single moveable object in an animated
- * view. A sprite has a position and orientation within the view, and can
+ * view. A sprite has a position and orientation within the view, and can
* be moved along a path.
*/
public abstract class Sprite
@@ -182,8 +180,8 @@ public abstract class Sprite
*/
public void setLocation (int x, int y)
{
- // create a starting dirty rectangle with our current position
- Rectangle dirty = new Rectangle(_bounds);
+ // dirty our current bounds
+ invalidate();
// move ourselves
_x = x;
@@ -193,19 +191,8 @@ public abstract class Sprite
// size of our current bounds
updateRenderOrigin();
- if (dirty.intersects(_bounds)) {
- // grow the dirty rectangle to reflect our new location
- dirty.add(_bounds);
-
- } else {
- // dirty the our new bounds rectangle separately from the old
- // to avoid potentially creating a large dirty rectangle if
- // the sprite warps from place to place
- invalidate(null);
- }
-
- // invalidate the potentially-grown starting dirty rectangle
- invalidate(dirty);
+ // dirty our new bounds
+ invalidate();
}
/**
@@ -327,25 +314,9 @@ public abstract class Sprite
* Invalidate the sprite's bounding rectangle for later repainting.
*/
public void invalidate ()
- {
- invalidate(null);
- }
-
- /**
- * Invalidate the given display rectangle for later repainting.
- * Passing null will simply invalidate the sprite's
- * entire rendered bounds. Note that the given rectangle may be
- * destructively modified at some later time, e.g., by {@link
- * com.threerings.media.animation.AnimationManager#mergeDirtyRects}.
- */
- protected void invalidate (Rectangle r)
{
if (_spritemgr != null) {
- _spritemgr.addDirtyRect((r != null) ? r : new Rectangle(_bounds));
-
- } else {
- // Log.warning("Was invalidated but have no sprite manager " +
- // this + ".");
+ _spritemgr.getRegionManager().invalidateRegion(_bounds);
}
}
@@ -357,11 +328,11 @@ public abstract class Sprite
* the timestamp information to compute elapsed progress if it wishes
* to handle heavy loads gracefully.
*/
- public void tick (long timestamp)
+ public void tick (long tickStamp)
{
// if we've a path, move the sprite along toward its destination
if (_path != null) {
- _path.updatePosition(this, timestamp);
+ _path.updatePosition(this, tickStamp);
}
}
@@ -411,13 +382,12 @@ public abstract class Sprite
/**
* Called by the sprite manager to let us know that the view we occupy
- * is scrolling by the specified amount.
- *
- * @return a dirty rectangle that will be added to the list of
- * rectangles dirtied by the scrolling or null if the sprite scrolled
- * along with the view and didn't create any dirty region.
+ * is scrolling by the specified amount. A sprite anchored to the view
+ * will simply update its coordinates; a fixed sprite will remain in
+ * the same position but invalidate its bounds and its scrolled
+ * bounds.
*/
- protected Rectangle viewWillScroll (int dx, int dy)
+ protected void viewWillScroll (int dx, int dy)
{
if (_scrollsWithView) {
// update our coordinates
@@ -431,12 +401,9 @@ public abstract class Sprite
_path.viewWillScroll(dx, dy);
}
- return null;
-
} else {
- // if we're not scrolling, then we need to dirty our bounds
- // and the part of our bounds that were just scrolled out from
- // under us
+ // if we're not scrolling, we need to dirty our bounds and the
+ // part that was just scrolled out from under us
Rectangle dirty = new Rectangle(_bounds);
// expand the rectangle to contain the scrolled regions
@@ -449,7 +416,8 @@ public abstract class Sprite
}
dirty.add(ex, ey);
- return dirty;
+ // give the dirty rectangle to the region manager
+ _spritemgr.getRegionManager().addDirtyRegion(dirty);
}
}
diff --git a/src/java/com/threerings/media/sprite/SpriteManager.java b/src/java/com/threerings/media/sprite/SpriteManager.java
index efa63d320..5c0601834 100644
--- a/src/java/com/threerings/media/sprite/SpriteManager.java
+++ b/src/java/com/threerings/media/sprite/SpriteManager.java
@@ -1,5 +1,5 @@
//
-// $Id: SpriteManager.java,v 1.28 2002/04/15 23:10:24 mdb Exp $
+// $Id: SpriteManager.java,v 1.29 2002/04/23 01:16:28 mdb Exp $
package com.threerings.media.sprite;
@@ -19,29 +19,23 @@ import com.samskivert.util.SortableArrayList;
import com.samskivert.util.Tuple;
import com.threerings.media.Log;
+import com.threerings.media.MediaConstants;
+import com.threerings.media.RegionManager;
/**
* The sprite manager manages the sprites running about in the game.
*/
public class SpriteManager
+ implements MediaConstants
{
- /** Constant for the front layer of sprites. */
- public static final int FRONT = 0;
-
- /** Constant for the back layer of sprites. */
- public static final int BACK = 1;
-
- /** Constant for all layers of sprites. */
- public static final int ALL = 2;
-
/**
- * Construct and initialize the SpriteManager object.
+ * Construct and initialize the sprite manager.
*/
- public SpriteManager ()
+ public SpriteManager (RegionManager remgr)
{
_sprites = new SortableArrayList();
_notify = new ArrayList();
- _dirty = new ArrayList();
+ _remgr = remgr;
}
/**
@@ -49,33 +43,18 @@ public class SpriteManager
* the specified offsets. It can update the positions of its sprites
* if they are tracking the scrolled view, or generate dirty regions
* for the sprites that remain in place (meaning they move relative to
- * the scrolling view). Regions invalidated by the scrolled sprites
- * should be appended to the supplied invalid rectangles list.
+ * the scrolling view).
*/
- public void viewWillScroll (int dx, int dy, List invalidRects)
+ public void viewWillScroll (int dx, int dy)
{
// let the sprites know that the view is scrolling
int size = _sprites.size();
for (int i = 0; i < size; i++) {
Sprite sprite = (Sprite)_sprites.get(i);
- Rectangle dirty = sprite.viewWillScroll(dx, dy);
- if (dirty != null) {
- invalidRects.add(dirty);
- }
+ sprite.viewWillScroll(dx, dy);
}
}
- /**
- * Add a rectangle to the dirty rectangle list.
- *
- * @param rect the rectangle to add.
- */
- public void addDirtyRect (Rectangle rect)
- {
- // translate the rectangle according to our viewport offset
- _dirty.add(rect);
- }
-
/**
* When an animated view processes its dirty rectangles, it may
* require an expansion of the dirty region which may in turn
@@ -153,20 +132,29 @@ public class SpriteManager
}
/**
- * Render the sprites residing within the given shape and layer to the
- * given graphics context.
+ * Provides access to the region manager that the sprite manager is
+ * using to collect invalid regions every frame. This should generally
+ * only be used by sprites that want to invalidate themselves.
+ */
+ public RegionManager getRegionManager ()
+ {
+ return _remgr;
+ }
+
+ /**
+ * Render to the given graphics context the sprites intersecting the
+ * given shape and residing in the specified layer.
*
- * @param gfx the graphics context.
- * @param bounds the bounding shape.
* @param layer the layer to render; one of {@link #FRONT}, {@link
* #BACK}, or {@link #ALL}. The front layer contains all sprites with
* a positive render order; the back layer contains all sprites with a
* negative render order; all, both.
+ * @param bounds the bounding shape.
*/
- public void renderSprites (Graphics2D gfx, Shape bounds, int layer)
+ public void renderSprites (Graphics2D gfx, int layer, Shape bounds)
{
- // TODO: optimize to store sprites based on quadrants they're
- // in (or somesuch), and sorted, so that we can more quickly
+ // TODO: optimize to store sprites based on quadrants they're in
+ // (or somesuch), and sorted, so that we can more quickly
// determine which sprites to draw.
int size = _sprites.size();
@@ -197,14 +185,14 @@ public class SpriteManager
}
/**
- * Called periodically by the tick tasks put on the AWT event queue by
- * the {@link com.threerings.media.animation.AnimationManager}.
- * Handles moving about of sprites and reporting of sprite collisions.
+ * Must be called every frame so that the sprites can be properly
+ * updated. Normally a sprite manager is used in conjunction with an
+ * animated panel which case this is called automatically.
*/
- public void tick (long timestamp, List rects)
+ public void tick (long tickStamp)
{
// tick all sprites
- tickSprites(timestamp);
+ tickSprites(tickStamp);
// re-sort the sprite list to account for potential new positions
_sprites.sort(SPRITE_COMP);
@@ -219,26 +207,19 @@ public class SpriteManager
// observers may take, such as sprite removal, won't screw us
// up elsewhere.
handleSpriteEvents();
-
- // add all generated dirty rectangles to the passed-in dirty
- // rectangle list
- CollectionUtil.addAll(rects, _dirty.iterator());
-
- // clear out our internal dirty rectangle list
- _dirty.clear();
}
/**
- * Call tick() on all sprite objects to give them a
+ * Call {@link Sprite#tick} on all sprite objects to give them a
* chance to move themselves about, change their display image,
- * and so forth.
+ * generate dirty regions and so forth.
*/
- protected void tickSprites (long timestamp)
+ protected void tickSprites (long tickStamp)
{
int size = _sprites.size();
for (int ii = 0; ii < size; ii++) {
Sprite sprite = (Sprite)_sprites.get(ii);
- sprite.tick(timestamp);
+ sprite.tick(tickStamp);
}
}
@@ -302,8 +283,8 @@ public class SpriteManager
}
/**
- * Notify all sprite observers of any sprite events that took
- * place during our most recent tick().
+ * Notify all sprite observers of any sprite events that took place
+ * during our most recent tick().
*/
protected void handleSpriteEvents ()
{
@@ -336,18 +317,19 @@ public class SpriteManager
_notify.add(new Tuple(observers, event));
}
- /** The comparator used to sort sprites by horizontal position. */
- protected static final Comparator SPRITE_COMP = new SpriteComparator();
-
/** The sprite objects we're managing. */
protected SortableArrayList _sprites;
- /** The dirty rectangles created by sprites. */
- protected ArrayList _dirty;
-
/** The list of pending sprite notifications. */
protected ArrayList _notify;
+ /** Used to accumulate dirty regions. */
+ protected RegionManager _remgr;
+
+ /** The comparator used to sort sprites by horizontal position. */
+ protected static final Comparator SPRITE_COMP = new SpriteComparator();
+
+ /** Used to sort sprites. */
protected static class SpriteComparator implements Comparator
{
public int compare (Object o1, Object o2)
diff --git a/src/java/com/threerings/media/util/Path.java b/src/java/com/threerings/media/util/Path.java
index c7597b22a..a25c40ab6 100644
--- a/src/java/com/threerings/media/util/Path.java
+++ b/src/java/com/threerings/media/util/Path.java
@@ -1,5 +1,5 @@
//
-// $Id: Path.java,v 1.3 2002/04/15 23:09:10 mdb Exp $
+// $Id: Path.java,v 1.4 2002/04/23 01:16:28 mdb Exp $
package com.threerings.media.sprite;
@@ -24,7 +24,7 @@ public interface Path
* Called once to let the path prepare itself for the process of
* animating the supplied sprite.
*/
- public void init (Sprite sprite, long timestamp);
+ public void init (Sprite sprite, long tickStamp);
/**
* Called to request that this path update the position of the
@@ -33,10 +33,13 @@ public interface Path
* of the sprite along the path based on the time elapsed since the
* sprite began down the path.
*
+ * @param sprite the sprite whose position should be updated.
+ * @param tickStamp the timestamp associated with this frame.
+ *
* @return true if the sprite's position was updated, false if the
* path determined that the sprite should not move at this time.
*/
- public boolean updatePosition (Sprite sprite, long timestamp);
+ public boolean updatePosition (Sprite sprite, long tickStamp);
/**
* Sets the velocity of this sprite in pixels per millisecond. The
diff --git a/src/java/com/threerings/media/util/PerformanceMonitor.java b/src/java/com/threerings/media/util/PerformanceMonitor.java
index ec2bc4a98..6db76149c 100644
--- a/src/java/com/threerings/media/util/PerformanceMonitor.java
+++ b/src/java/com/threerings/media/util/PerformanceMonitor.java
@@ -1,5 +1,5 @@
//
-// $Id: PerformanceMonitor.java,v 1.3 2001/10/25 22:08:29 mdb Exp $
+// $Id: PerformanceMonitor.java,v 1.4 2002/04/23 01:16:28 mdb Exp $
package com.threerings.media.util;
@@ -135,9 +135,10 @@ class PerformanceAction
_numTicks++;
long now = System.currentTimeMillis();
- if ((now - _lastDelta) >= _delta) {
+ long passed = now - _lastDelta;
+ if (passed >= _delta) {
// update the last checkpoint time
- _lastDelta = now;
+ _lastDelta = now + (passed - _delta);
// notify our observer of the checkpoint
_obs.checkpoint(_name, _numTicks);