From cc6a9d00ef09f9f8b275b3c5a353fceea3eb4b3e Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Tue, 23 Apr 2002 01:16:28 +0000 Subject: [PATCH] Major low-level rendering rethink. There will be much follow-on cleanup. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@1286 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- .../com/threerings/media/FrameManager.java | 482 +++++++++++++++++ .../threerings/media/FrameParticipant.java | 34 ++ .../threerings/media/FrameRepaintManager.java | 232 ++++++++ .../com/threerings/media/ManagedJFrame.java | 47 ++ .../com/threerings/media/MediaConstants.java | 19 + src/java/com/threerings/media/MediaPanel.java | 486 +++++++++++++++++ .../com/threerings/media/RegionManager.java | 92 ++++ .../com/threerings/media/SafeScrollPane.java | 27 + .../media/animation/AnimatedPanel.java | 506 ------------------ .../media/animation/AnimatedView.java | 35 -- .../threerings/media/animation/Animation.java | 85 +-- .../media/animation/AnimationManager.java | 433 ++------------- .../media/animation/RainAnimation.java | 5 +- .../threerings/media/sprite/ImageSprite.java | 16 +- .../com/threerings/media/sprite/Sprite.java | 68 +-- .../media/sprite/SpriteManager.java | 106 ++-- src/java/com/threerings/media/util/Path.java | 9 +- .../media/util/PerformanceMonitor.java | 7 +- 18 files changed, 1596 insertions(+), 1093 deletions(-) create mode 100644 src/java/com/threerings/media/FrameManager.java create mode 100644 src/java/com/threerings/media/FrameParticipant.java create mode 100644 src/java/com/threerings/media/FrameRepaintManager.java create mode 100644 src/java/com/threerings/media/ManagedJFrame.java create mode 100644 src/java/com/threerings/media/MediaConstants.java create mode 100644 src/java/com/threerings/media/MediaPanel.java create mode 100644 src/java/com/threerings/media/RegionManager.java create mode 100644 src/java/com/threerings/media/SafeScrollPane.java delete mode 100644 src/java/com/threerings/media/animation/AnimatedPanel.java delete mode 100644 src/java/com/threerings/media/animation/AnimatedView.java 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);