From 7253a68c7c6a30118372d1eaa04b5d25f0b54b2f Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Tue, 18 Jun 2002 22:25:33 +0000 Subject: [PATCH] Death, taxes and refactoring: seriously revamped the low-level media code to handle scrolling in a much cleaner and happier way. There's now a VirtualMediaPanel which operates in virtual coordinates and can be made to "view" any location in those virtual coordinates. By changing the view, the view can be scrolled and the view can be made to center on a pathable (generally a sprite) which will cause it to scroll around as the pathable moves around in the virtual coordinate space. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@1475 542714f4-19e9-0310-aa3c-eee0fc999fb1 --- .../com/threerings/media/FrameManager.java | 10 +- .../threerings/media/FrameParticipant.java | 13 +- src/java/com/threerings/media/MediaPanel.java | 586 ++++-------------- .../com/threerings/media/RegionManager.java | 17 +- .../threerings/media/VirtualMediaPanel.java | 219 +++++++ .../com/threerings/media/sprite/Sprite.java | 86 +-- .../media/sprite/SpriteManager.java | 19 +- .../com/threerings/media/util/LinePath.java | 29 +- .../media/util/LineSegmentPath.java | 14 +- src/java/com/threerings/media/util/Path.java | 9 +- 10 files changed, 387 insertions(+), 615 deletions(-) create mode 100644 src/java/com/threerings/media/VirtualMediaPanel.java diff --git a/src/java/com/threerings/media/FrameManager.java b/src/java/com/threerings/media/FrameManager.java index 614120091..bab8dee8a 100644 --- a/src/java/com/threerings/media/FrameManager.java +++ b/src/java/com/threerings/media/FrameManager.java @@ -1,5 +1,5 @@ // -// $Id: FrameManager.java,v 1.10 2002/06/11 00:52:37 mdb Exp $ +// $Id: FrameManager.java,v 1.11 2002/06/18 22:25:33 mdb Exp $ package com.threerings.media; @@ -511,8 +511,12 @@ public class FrameManager } try { - // render this participant - _g.setClip(_bounds); + // render this participant; we don't set the clip because + // frame participants are expected to handle clipping + // themselves; otherwise we might pointlessly set the clip + // here, creating a few Rectangle objects in the process, + // only to have the frame participant immediately set the + // clip to something more sensible _g.translate(_bounds.x, _bounds.y); pcomp.paint(_g); _g.translate(-_bounds.x, -_bounds.y); diff --git a/src/java/com/threerings/media/FrameParticipant.java b/src/java/com/threerings/media/FrameParticipant.java index 18d058683..c007b22a6 100644 --- a/src/java/com/threerings/media/FrameParticipant.java +++ b/src/java/com/threerings/media/FrameParticipant.java @@ -1,5 +1,5 @@ // -// $Id: FrameParticipant.java,v 1.1 2002/04/23 01:16:27 mdb Exp $ +// $Id: FrameParticipant.java,v 1.2 2002/06/18 22:25:33 mdb Exp $ package com.threerings.media; @@ -26,9 +26,14 @@ public interface FrameParticipant * 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. + * Component#paint} called on it once per frame with a translated but + * unclipped graphics object. + * + *

Because clipping is expensive in terms of rectangle object + * allocation, frame participants are given the opportunity to do + * their own clipping because they are likely to want to clip to a + * more fine grained region than their entire bounds. If a particpant + * does not wish to be actively rendered, it can safely return null. */ public Component getComponent (); } diff --git a/src/java/com/threerings/media/MediaPanel.java b/src/java/com/threerings/media/MediaPanel.java index 6eeebbfb1..44c4d8db0 100644 --- a/src/java/com/threerings/media/MediaPanel.java +++ b/src/java/com/threerings/media/MediaPanel.java @@ -1,33 +1,21 @@ // -// $Id: MediaPanel.java,v 1.13 2002/06/12 00:51:24 mdb Exp $ +// $Id: MediaPanel.java,v 1.14 2002/06/18 22:25:33 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.Point; import java.awt.Rectangle; import java.awt.Shape; import javax.swing.JComponent; import javax.swing.RepaintManager; -import javax.swing.SwingUtilities; 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.util.Path; -import com.threerings.media.util.Pathable; - import com.threerings.media.animation.Animation; import com.threerings.media.animation.AnimationManager; @@ -60,7 +48,7 @@ public class MediaPanel extends JComponent implements FrameParticipant, MediaConstants { /** - * Constructs an animated panel. + * Constructs a media panel. */ public MediaPanel (FrameManager framemgr) { @@ -88,94 +76,6 @@ public class MediaPanel extends JComponent }); } - /** - * 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; - -// Log.info("Scrolling [dx=" + dx + ", dy=" + dy + -// ", millis=" + millis + "ms."); - } - - /** - * Instructs the view to allow the supplied path to "scroll" it such - * that the center point of the view follows the supplied path. The - * view will initially report its position as being in the center of - * the view and will update those coordinates as it is scrolled by the - * path. - */ - public void setPath (Path path) - { - _pathable = createPathable(); - _path = path; - _path.init(_pathable, System.currentTimeMillis()); - } - - /** - * Creates a pathable that will be used to scroll the view along a - * path. - */ - protected Pathable createPathable () - { - return new PanelPathable(); - } - - /** - * If this media panel is following a path and that path attempts to - * set the orientation of the pathable it is controlling, this method - * will be called. Derived classes can override the method and effect - * whatever display of the panel's orientation they might desire. - */ - protected void setOrientation (int orient) - { - } - - /** - * When the media panel is made to follow a path, this method is - * called when the path begins. - */ - protected void pathBeginning () - { - } - - /** - * When the media panel is made to follow a path, this method is - * called when the path ends. {@link #viewFinishedScrolling} will - * still be called (and is in fact called by this method). - */ - protected void pathCompleted () - { - // we're all done; clear out our business - _path = null; - _pathable = null; - - // call our standard callback - viewFinishedScrolling(); - } - /** * Pauses the sprites and animations that are currently active on this * media panel. Also stops listening to the frame tick while paused. @@ -201,30 +101,11 @@ public class MediaPanel extends JComponent _animmgr.fastForward(delta); _spritemgr.fastForward(delta); - // fast forward our path as well (if we have one) - if (_path != null) { - _path.fastForward(delta); - } - // clear out our pause time _pauseTime = 0; } } - /** - * Configures a region of interest which will be displayed in the - * center of the viewport in situations where the media panel's actual - * size is smaller than its view size. - * - * @param region the region of interest or null if there is to be no - * region of interest (in which case the view will simply be - * centered). - */ - public void setRegionOfInterest (Rectangle region) - { - _interest = region; - } - /** * Returns a reference to the region manager that is used to * accumulate dirty regions each frame. @@ -266,53 +147,16 @@ public class MediaPanel extends JComponent _animmgr.unregisterAnimation(anim); } - // documentation inherited - public void doLayout () - { - super.doLayout(); - - // figure out our viewport offsets - Dimension size = getSize(), vsize = getViewSize(); - - // we need to compute one dimension at a time to make things work; - // if we have a region of interest (and our actual size is smaller - // than our view size), use that to compute our offset - if (_interest != null && size.width < vsize.width) { - _tx = centerWithInterest(size.width, vsize.width, - _interest.x, _interest.width); - } else { - // otherwise just center the display - _tx = (vsize.width - size.width)/2; - } - if (_interest != null && size.height < vsize.height) { - _ty = centerWithInterest(size.height, vsize.height, - _interest.y, _interest.height); - } else { - _ty = (vsize.height - size.height)/2; - } - -// Log.info("Size: " + size + ", vsize: " + vsize + -// ", tx: " + _tx + ", ty: " + _ty + "."); - } - - /** - * Used to compute our viewport offsets. - */ - protected int centerWithInterest (int len, int vlen, int ix, int ilen) - { - // start out by centering on the region of interest - int tx = (ix - (len - ilen)/2); - // make sure that didn't push us off of the screen - return Math.min(Math.max(tx, 0), vlen-len); - } - // documentation inherited public void invalidate () { super.invalidate(); // invalidate our bounds with the region manager - _remgr.invalidateRegion(_tx, _ty, getWidth(), getHeight()); + int width = getWidth(), height = getHeight(); + if (width > 0 && height > 0) { + dirtyScreenRect(new Rectangle(0, 0, width, height)); + } } // documentation inherited from interface @@ -323,124 +167,39 @@ public class MediaPanel extends JComponent return; } - int width = getWidth(), height = getHeight(); - _dx = 0; _dy = 0; - - // figure out if we should be scrolling and by how much - computeScrollDeltas(tickStamp); - - // 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 really 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; - - // if we've reached our desired position, finish the job; if - // we're being scrolled by a path, the path will let us know - // when we're done (via pathCompleted()) and that will trigger - // a call to viewFinishedScrolling() - if (_ttime != 0 && _scrollx == 0 && _scrolly == 0) { - _ttime = 0; - viewFinishedScrolling(); - } - } + // let derived classes do their business + willTick(tickStamp); // now tick our animations and sprites _animmgr.tick(tickStamp); _spritemgr.tick(tickStamp); + // let derived classes do their business + didTick(tickStamp); + // make a note that the next paint will correspond to a call to // tick() _tickPaintPending = true; } /** - * Called at the beginning of each tick, this method determines - * whether or not the view should be scrolled because of standard - * methods. These methods include a call to {@link #setScrolling} or a - * call to {@link #setPath} which causes the view to follow a path. A - * derived class could override this method to compute custom scroll - * deltas, which it would supply via a call to {@link - * #setScrollDeltas} at some point during the execution of the - * overridden method. + * Derived classes can override this method and perform computation + * prior to the ticking of the sprite and animation managers. */ - protected void computeScrollDeltas (long tickStamp) + protected void willTick (long tickStamp) { - // if scrolling is enabled, determine the scrolling delta to be - // used and do the business - if (_ttime != 0) { - // if we've blown past our allotted time, we want to scroll - // the rest of the way - if (tickStamp > _ttime) { - setScrollDeltas(_scrollx, _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 - setScrollDeltas(Math.round((float)(_scrollx * dt) / rt), - Math.round((float)(_scrolly * dt) / rt)); - -// Log.info("Scrolling delta [dt=" + dt + ", rt=" + rt + -// ", dx=" + _dx + ", dy=" + _dy + -// ", leftx=" + (_scrollx-_dx) + -// ", lefty=" + (_scrolly-_dy) + "]."); - } - - // subtract our scrolled deltas from the distance remaining - _scrollx -= _dx; - _scrolly -= _dy; - - // otherwise, if we're following a path, give that a chance to - // scroll us along - } else if (_path != null) { - // this will update our _dx and _dy if the path wishes us to - // scroll - _path.tick(_pathable, tickStamp); - } } /** - * This can be called by derived classes during the beginning of the - * tick to instruct the view to scroll by the specified amount on the - * tick in question. + * Derived classes can override this method and perform computation + * subsequent to the ticking of the sprite and animation managers. */ - protected void setScrollDeltas (int dx, int dy) + protected void didTick (long tickStamp) { - _dx = dx; - _dy = dy; } /** - * We want to be painted every tick. + * Returns this component, as we want to be painted with the tick. */ public Component getComponent () { @@ -450,14 +209,15 @@ public class MediaPanel extends JComponent // documentation inherited public void repaint (long tm, int x, int y, int width, int height) { - _remgr.invalidateRegion(_tx + x, _ty + y, width, height); + if (width > 0 && height > 0) { + dirtyScreenRect(new Rectangle(x, y, width, height)); + } } // documentation inherited public void paint (Graphics g) { Graphics2D gfx = (Graphics2D)g; - int width = getWidth(), height = getHeight(); // no use in painting if we're not showing or if we've not yet // been validated @@ -469,218 +229,134 @@ public class MediaPanel extends JComponent // rectangle and mark it as dirty if (!_tickPaintPending) { Shape clip = g.getClip(); - Rectangle dirty = (clip == null) ? getBounds() : clip.getBounds(); - dirty.translate(_tx, _ty); - _remgr.invalidateRegion(dirty); + if (clip == null) { + // mark the whole component as invalid + invalidate(); + } else { + dirtyScreenRect(clip.getBounds()); + } + return; } 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) { + // if we have no invalid rects, there's no need to repaint + if (!_remgr.haveDirtyRegions()) { return; } - // we may need to do some scrolling - if (_dx != 0 || _dy != 0) { - gfx.copyArea(0, 0, width, height, -_dx, -_dy); - } - - // get our dirty rectangles - Rectangle[] dirty = _remgr.getDirtyRegions(); - int dcount = dirty.length; - - // clip the dirty regions to the viewport - for (int ii = 0; ii < dcount; ii++) { - SwingUtilities.computeIntersection( - _tx, _ty, width, height, dirty[ii]); - } - - // translate into happy space - gfx.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); - -// if (_path != null) { -// Dimension vsize = getViewSize(); -// int ox = _pathable.getX() - vsize.width/2; -// int oy = _pathable.getY() - vsize.height/2; -// gfx.translate(-ox, -oy); -// _path.paint(gfx); -// gfx.translate(ox, oy); -// } - - // translate back out of happy space - gfx.translate(_tx, _ty); + // get our dirty rectangles and delegate the main painting to a + // method that can be more easily overridden + paint(gfx, _remgr.getDirtyRegions()); } /** - * 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. + * Performs the actual painting of the media panel. Derived methods + * can override this method if they wish to perform pre- and/or + * post-paint activities or if they wish to provide their own painting + * mechanism entirely. */ - protected void paintBehind (Graphics2D gfx, Rectangle[] dirtyRects) + protected void paint (Graphics2D gfx, Rectangle[] dirty) + { + int dcount = dirty.length; + + for (int ii = 0; ii < dcount; ii++) { + Rectangle clip = dirty[ii]; + // ignore rectangles that were reduced to nothingness + if (clip.width == 0 || clip.height == 0) { + continue; + } + clipToDirtyRegion(gfx, clip); + + // paint the behind the scenes stuff + paintBehind(gfx, clip); + + // paint back sprites and animations + paintBits(gfx, AnimationManager.BACK, clip); + + // paint the between the scenes stuff + paintBetween(gfx, clip); + + // paint front sprites and animations + paintBits(gfx, AnimationManager.FRONT, clip); + + // paint anything in front + paintInFront(gfx, clip); + } + } + + /** + * This is called to clip the rendering output to the supplied dirty + * region. This should use {@link Graphics#setClip} because the + * clipping region will need to be replaced as we iterate through our + * dirty regions. By default, a region is assumed to represent screen + * coordinates, but if a derived class wishes to maintain dirty + * regions in non-screen coordinates, it can override this method to + * properly clip to the dirty region. + */ + protected void clipToDirtyRegion (Graphics2D gfx, Rectangle dirty) + { +// Log.info("MP: Clipping to [clip=" + StringUtil.toString(dirty) + "]."); + gfx.setClip(dirty); + } + + /** + * Called to mark the specified rectangle (in screen coordinates) as + * dirty. The rectangle will be bent, folded and mutilated, so be sure + * you're not passing a rectangle into this method that is being used + * elsewhere. + * + *

If derived classes wish to convert from screen coordinates to + * some virtual coordinate system to be used by their repaint manager, + * this is the place to do it. + */ + protected void dirtyScreenRect (Rectangle rect) + { + _remgr.addDirtyRegion(rect); + } + + /** + * Paints behind all sprites and animations. The supplied invalid + * rectangle should be redrawn in the supplied graphics context. + * Sub-classes should override this method to do the actual rendering + * for their display. + */ + protected void paintBehind (Graphics2D gfx, Rectangle dirtyRect) { } /** * Paints between the front and back layer of sprites and animations. - * The supplied 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. + * The supplied invalid rectangle should be redrawn in the supplied + * graphics context. Sub-classes should override this method to do the + * actual rendering for their display. */ - protected void paintBetween (Graphics2D gfx, Rectangle[] dirtyRects) + protected void paintBetween (Graphics2D gfx, Rectangle dirtyRect) { } /** - * 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. + * Paints in front of all sprites and animations. The supplied invalid + * rectangle should be redrawn in the supplied graphics context. + * Sub-classes should override this method to do the actual rendering + * for their display. */ - protected void paintInFront (Graphics2D gfx, Rectangle[] dirtyRects) + protected void paintInFront (Graphics2D gfx, Rectangle dirtyRect) { } /** * 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. + * dirty region in the specified layer. Derived classes can override + * this method if they need to do custom sprite or animation rendering + * (if they need to do special sprite z-order handling, for example). + * The clipping region will already be set appropriately. */ - protected void paintBits (Graphics2D gfx, int layer, Rectangle clip) + protected void paintBits (Graphics2D gfx, int layer, Rectangle dirty) { - Shape oclip = gfx.getClip(); - gfx.clipRect(clip.x, clip.y, clip.width, clip.height); - _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) - { - // if we have a path, let it know that we're scrolling - if (_path != null) { - _path.viewWillScroll(dx, dy); - } - } - - /** - * 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 the paint methods. - */ - protected Dimension getViewSize () - { - return getSize(); - } - - /** - * Used when causing the view to follow a path. - */ - protected class PanelPathable implements Pathable - { - public PanelPathable () - { - Dimension vsize = getViewSize(); - _porigin = new Point(vsize.width/2, vsize.height/2); - } - - // documentation inherited from interface - public int getX () { - return _porigin.x; - } - - // documentation inherited from interface - public int getY () { - return _porigin.y; - } - - // documentation inherited from interface - public void setLocation (int x, int y) { - // make a note of the scrolling that we'll need to do to - // actually follow this path - setScrollDeltas(x - _porigin.x, y - _porigin.y); - - // update our origin - _porigin.x = x; - _porigin.y = y; - } - - // documentation inherited from interface - public void setOrientation (int orient) { - MediaPanel.this.setOrientation(orient); - } - - // documentation inherited from interface - public void pathBeginning () { - MediaPanel.this.pathBeginning(); - } - - // documentation inherited from interface - public void pathCompleted () { - MediaPanel.this.pathCompleted(); - } - - protected Point _porigin; + _animmgr.renderAnimations(gfx, layer, dirty); + _spritemgr.renderSprites(gfx, layer, dirty); } /** The frame manager with whom we register. */ @@ -695,30 +371,6 @@ public class MediaPanel extends JComponent /** Used to accumulate and merge dirty regions on each tick. */ protected RegionManager _remgr; - /** A region of interest which we'll try to keep visible. */ - protected Rectangle _interest; - - /** 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; - - /** The path we're following. */ - protected Path _path; - - /** The pathable we use to follow the path. */ - protected Pathable _pathable; - /** 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 index 7492df40d..1d346dca2 100644 --- a/src/java/com/threerings/media/RegionManager.java +++ b/src/java/com/threerings/media/RegionManager.java @@ -1,5 +1,5 @@ // -// $Id: RegionManager.java,v 1.2 2002/06/11 00:52:37 mdb Exp $ +// $Id: RegionManager.java,v 1.3 2002/06/18 22:25:33 mdb Exp $ package com.threerings.media; @@ -8,6 +8,8 @@ import java.awt.Rectangle; import java.util.ArrayList; import java.util.List; +import com.samskivert.util.StringUtil; + /** * Manages regions (rectangles) that are invalidated in the process of * ticking animations and sprites and generally doing other display @@ -20,7 +22,9 @@ public class RegionManager */ public void invalidateRegion (int x, int y, int width, int height) { - _dirty.add(new Rectangle(x, y, width, height)); + if (width != 0 && height != 0) { + addDirtyRegion(new Rectangle(x, y, width, height)); + } } /** @@ -30,7 +34,9 @@ public class RegionManager */ public void invalidateRegion (Rectangle rect) { - _dirty.add((Rectangle)rect.clone()); + if (rect.width != 0 && rect.height != 0) { + addDirtyRegion((Rectangle)rect.clone()); + } } /** @@ -41,7 +47,10 @@ public class RegionManager */ public void addDirtyRegion (Rectangle rect) { - _dirty.add(rect); + if (rect.width != 0 && rect.height != 0) { +// Log.info("Invalidating " + StringUtil.toString(rect)); + _dirty.add(rect); + } } /** diff --git a/src/java/com/threerings/media/VirtualMediaPanel.java b/src/java/com/threerings/media/VirtualMediaPanel.java new file mode 100644 index 000000000..d1c401f2f --- /dev/null +++ b/src/java/com/threerings/media/VirtualMediaPanel.java @@ -0,0 +1,219 @@ +// +// $Id: VirtualMediaPanel.java,v 1.1 2002/06/18 22:25:33 mdb Exp $ + +package com.threerings.media; + +import java.awt.Dimension; +import java.awt.Graphics2D; +import java.awt.Point; +import java.awt.Rectangle; +import java.awt.Shape; +import java.awt.event.MouseEvent; + +import javax.swing.SwingUtilities; + +import com.samskivert.util.StringUtil; + +import com.threerings.media.util.Path; +import com.threerings.media.util.Pathable; + +/** + * Extends the base media panel with the notion of a virtual coordinate + * system. All entities in the virtual media panel have virtual + * coordinates and the virtual media panel displays a window into that + * virtual view. The panel can be made to scroll by adjusting the view + * offset slightly at the start of each tick and it will efficiently copy + * the unmodified view data and generate repaint requests for the exposed + * regions. + */ +public class VirtualMediaPanel extends MediaPanel +{ + /** + * Constructs a virtual media panel. + */ + public VirtualMediaPanel (FrameManager framemgr) + { + super(framemgr); + } + + /** + * Sets the upper-left coordinate of the view port in virtual + * coordinates. The view will be as efficient as possible about + * repainting itself to achieve this new virtual location (meaning + * that if we need only to move one pixel to the left, it will use + * {@link Graphics#copyArea} to move our rendered view over one pixel + * and generate a dirty region for the exposed area). The new location + * will not take effect until the view is {@link #tick}ed, so only the + * last call to this method during a tick will have any effect. + */ + public void setViewLocation (int x, int y) + { + // make a note of our new x and y offsets + _nx = x; + _ny = y; + } + + /** + * Instructs the view to follow the supplied pathable; ensuring that + * the pathable always remains in the center of the view. The virtual + * coordinates will be adjusted after every tick to center the view on + * the sprite. + */ + public void setFollowsPathable (Pathable pable) + { + _fpath = pable; + } + + /** + * Configures a region of interest which will be displayed in the + * center of the viewport in situations where the media panel's actual + * size is smaller than its view size. + * + * @param region the region of interest or null if there is to be no + * region of interest (in which case the view will simply be + * centered). + */ + public void setRegionOfInterest (Rectangle region) + { + _interest = region; + } + + /** + * We overload this to translate mouse events into the proper + * coordinates before they are dispatched to any of the mouse + * listeners. + */ + protected void processMouseEvent (MouseEvent event) + { + event.translatePoint(_tx, _ty); + super.processMouseEvent(event); + } + + /** + * We overload this to translate mouse events into the proper + * coordinates before they are dispatched to any of the mouse + * listeners. + */ + protected void processMouseMotionEvent (MouseEvent event) + { + event.translatePoint(_tx, _ty); + super.processMouseMotionEvent(event); + } + + // documentation inherited + protected void dirtyScreenRect (Rectangle rect) + { + // translate the screen rect into happy coordinates + rect.translate(_tx, _ty); + _remgr.addDirtyRegion(rect); + } + + // documentation inherited + protected void didTick (long tickStamp) + { + super.didTick(tickStamp); + + int width = getWidth(), height = getHeight(); + + // if we're following a pathable, adjust our view coordinates such + // that the pathable is at the center of the view + if (_fpath != null) { + setViewLocation(_fpath.getX() - width/2, _fpath.getY() - height/2); + } + + // if we have a new target location, we'll need to generate dirty + // regions for the area exposed by the scrolling + if (_nx != _tx || _ny != _ty) { + // determine how far we'll be moving on this tick + _dx = _nx - _tx; _dy = _ny - _ty; + + // these are used to prevent the vertical strip from + // overlapping the horizontal strip + int sy = _ny, shei = height; + + // and add invalid rectangles for the exposed areas + if (_dy > 0) { + shei -= _dy; + _remgr.invalidateRegion(_nx, _ny + height - _dy, width, _dy); + } else if (_dy < 0) { + sy -= _dy; + _remgr.invalidateRegion(_nx, _ny, width, -_dy); + } + if (_dx > 0) { + _remgr.invalidateRegion(_nx + width - _dx, sy, _dx, shei); + } else if (_dx < 0) { + _remgr.invalidateRegion(_nx, sy, -_dx, shei); + } + + // now go ahead and update our location so that changes in + // between here and the call to paint() for this tick don't + // booch everything + _tx = _nx; _ty = _ny; + +// Log.info("Scrolling into place " + +// "[dx=" + _dx + ", dy=" + _dy + "]."); + } + } + + // documentation inherited + protected void paint (Graphics2D gfx, Rectangle[] dirty) + { + int width = getWidth(), height = getHeight(); + int dcount = dirty.length; + + // if we're scrolling, go ahead and do the business + if (_dx != 0 || _dy != 0) { + int cx = (_dx > 0) ? _dx : 0; + int cy = (_dy > 0) ? _dy : 0; + gfx.copyArea(cx, cy, width - Math.abs(_dx), + height - Math.abs(_dy), -_dx, -_dy); + // and clear out our scroll deltas + _dx = 0; _dy = 0; + } + + // clip the dirty regions to the viewport + for (int ii = 0; ii < dcount; ii++) { + SwingUtilities.computeIntersection( + _tx, _ty, width, height, dirty[ii]); +// Log.info("Intersected [rect=" + StringUtil.toString(dirty[ii]) + +// ", tx=" + _tx + ", ty=" + _ty + "]."); + } + + // translate into happy space + gfx.translate(-_tx, -_ty); + + // now do the actual painting + super.paint(gfx, dirty); + + gfx.setColor(java.awt.Color.white); + gfx.drawOval(_tx, _tx, 10, 10); + + // translate back out of happy space + gfx.translate(_tx, _ty); + } + + /** + * Used to compute our viewport offsets. + */ + protected int centerWithInterest (int len, int vlen, int ix, int ilen) + { + // start out by centering on the region of interest + int tx = (ix - (len - ilen)/2); + // make sure that didn't push us off of the screen + return Math.min(Math.max(tx, 0), vlen-len); + } + + /** Our viewport offsets. */ + protected int _tx, _ty; + + /** Our target offsets to be effected on the next tick. */ + protected int _nx, _ny; + + protected int _dx, _dy; + + /** A region of interest which we'll try to keep visible. */ + protected Rectangle _interest; + + /** The pathable being followed. */ + protected Pathable _fpath; +} diff --git a/src/java/com/threerings/media/sprite/Sprite.java b/src/java/com/threerings/media/sprite/Sprite.java index a769ed589..68c89f611 100644 --- a/src/java/com/threerings/media/sprite/Sprite.java +++ b/src/java/com/threerings/media/sprite/Sprite.java @@ -1,5 +1,5 @@ // -// $Id: Sprite.java,v 1.46 2002/06/11 05:56:44 mdb Exp $ +// $Id: Sprite.java,v 1.47 2002/06/18 22:25:33 mdb Exp $ package com.threerings.media.sprite; @@ -44,25 +44,6 @@ public abstract class Sprite _y = y; } - /** - * Returns true if this sprite should scroll along with the view or - * false if it should remain fixed relative to the view when it - * scrolls. - */ - public boolean scrollsWithView () - { - return _scrollsWithView; - } - - /** - * Specifies whether this sprite scrolls when the view scrolls or - * remains fixed relative to the view. - */ - public void setScrollsWithView (boolean scrollsWithView) - { - _scrollsWithView = scrollsWithView; - } - /** * Called by the sprite manager to initialize the sprite when a sprite * is added to said manager for management. @@ -182,8 +163,8 @@ public abstract class Sprite */ public void setLocation (int x, int y) { - // dirty our current bounds - invalidate(); + // start with our current bounds + Rectangle dirty = new Rectangle(_bounds); // move ourselves _x = x; @@ -193,8 +174,19 @@ public abstract class Sprite // size of our current bounds updateRenderOrigin(); - // dirty our new bounds - invalidate(); + // grow the dirty rectangle to incorporate our new bounds and pass + // the dirty region to our region manager + if (_spritemgr != null) { + // if our new bounds intersect our old bounds, grow a single + // dirty rectangle to incorporate them both + if (_bounds.intersects(dirty)) { + dirty.add(_bounds); + } else { + // otherwise invalidate our new bounds separately + _spritemgr.getRegionManager().invalidateRegion(_bounds); + } + _spritemgr.getRegionManager().addDirtyRegion(dirty); + } } /** @@ -405,47 +397,6 @@ public abstract class Sprite _observers.add(obs); } - /** - * Called by the sprite manager to let us know that the view we occupy - * 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 void viewWillScroll (int dx, int dy) - { - if (_scrollsWithView) { - // update our coordinates - _x -= dx; - _y -= dy; - _bounds.x -= dx; - _bounds.y -= dy; - - // and let any path that we're following know what's up - if (_path != null) { - _path.viewWillScroll(dx, dy); - } - - } else { - // 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 - int ex = dirty.x - dx, ey = dirty.y - dy; - if (dx < 0) { - ex += dirty.width; - } - if (dy < 0) { - ey += dirty.height; - } - dirty.add(ex, ey); - - // give the dirty rectangle to the region manager - _spritemgr.getRegionManager().addDirtyRegion(dirty); - } - } - /** * Inform all sprite observers of a sprite event. * @@ -497,11 +448,6 @@ public abstract class Sprite /** The orientation of this sprite. */ protected int _orient = NONE; - /** True if we should move along with the view when it scrolls, false - * if we should remain fixed relative to the viewport. We are fixed by - * default. */ - protected boolean _scrollsWithView = false; - /** When moving, the path the sprite is traversing. */ protected Path _path; diff --git a/src/java/com/threerings/media/sprite/SpriteManager.java b/src/java/com/threerings/media/sprite/SpriteManager.java index 512a78eea..1bd7c2964 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.30 2002/04/25 16:23:31 mdb Exp $ +// $Id: SpriteManager.java,v 1.31 2002/06/18 22:25:33 mdb Exp $ package com.threerings.media.sprite; @@ -38,23 +38,6 @@ public class SpriteManager _remgr = remgr; } - /** - * Lets the sprite manager know that the view is about to scroll by - * 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). - */ - 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); - sprite.viewWillScroll(dx, dy); - } - } - /** * When an animated view processes its dirty rectangles, it may * require an expansion of the dirty region which may in turn diff --git a/src/java/com/threerings/media/util/LinePath.java b/src/java/com/threerings/media/util/LinePath.java index 0d2ffb604..d518a6ae4 100644 --- a/src/java/com/threerings/media/util/LinePath.java +++ b/src/java/com/threerings/media/util/LinePath.java @@ -1,5 +1,5 @@ // -// $Id: LinePath.java,v 1.8 2002/06/12 01:11:55 mdb Exp $ +// $Id: LinePath.java,v 1.9 2002/06/18 22:25:33 mdb Exp $ package com.threerings.media.util; @@ -46,28 +46,6 @@ public class LinePath implements Path _duration = duration; } - /** - * Instructs this path to adjust itself when the view scrolls, or not. - * If the path scrolls with the view, it will adjust its starting and - * ending positions by the amount scrolled by the view so that they - * remain fixed relative to the scrolled view. If it doesn't they will - * remain the same regardless of whether the view scrolls. - */ - public void setScrollsWithView (boolean scrollsWithView) - { - _scrollsWithView = scrollsWithView; - } - - // documentation inherited from interface - public void viewWillScroll (int dx, int dy) - { - // adjust our source and dest points if we're tracking the view - if (_scrollsWithView) { - _source.translate(-dx, -dy); - _dest.translate(-dx, -dy); - } - } - // documentation inherited public void init (Pathable pable, long timestamp) { @@ -153,9 +131,4 @@ public class LinePath implements Path /** The duration that we're to spend following the path. */ protected long _duration; - - /** Whether or not we scroll along with the view (which we accomplish - * by scrolling our start and end positions when the view scrolls) or - * if we should leave our coordinates alone. */ - protected boolean _scrollsWithView = true; } diff --git a/src/java/com/threerings/media/util/LineSegmentPath.java b/src/java/com/threerings/media/util/LineSegmentPath.java index 53e0b60ef..049d03cff 100644 --- a/src/java/com/threerings/media/util/LineSegmentPath.java +++ b/src/java/com/threerings/media/util/LineSegmentPath.java @@ -1,5 +1,5 @@ // -// $Id: LineSegmentPath.java,v 1.24 2002/06/12 23:33:03 ray Exp $ +// $Id: LineSegmentPath.java,v 1.25 2002/06/18 22:25:33 mdb Exp $ package com.threerings.media.util; @@ -156,18 +156,6 @@ public class LineSegmentPath setVelocity(distance/millis); } - // documentation inherited from interface - public void viewWillScroll (int dx, int dy) - { - // adjust the coordinates of our path nodes - int ncount = _nodes.size(); - for (int ii = 0; ii < ncount; ii++) { - PathNode node = (PathNode)_nodes.get(ii); - node.loc.x -= dx; - node.loc.y -= dy; - } - } - // documentation inherited public void init (Pathable pable, long timestamp) { diff --git a/src/java/com/threerings/media/util/Path.java b/src/java/com/threerings/media/util/Path.java index ffb4253eb..c5447f550 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.7 2002/05/31 03:38:03 mdb Exp $ +// $Id: Path.java,v 1.8 2002/06/18 22:25:33 mdb Exp $ package com.threerings.media.util; @@ -48,13 +48,6 @@ public interface Path */ public void fastForward (long timeDelta); - /** - * Called when the view that contains the pathable following this path - * is scrolling by the specified amount. Gives the path an opportunity - * to adjust its internal coordinates by the scrolled amount. - */ - public void viewWillScroll (int dx, int dy); - /** * Paint this path on the screen (used for debugging purposes only). */