a88799d4a1
instead we say that we want to scroll by a certain amount in a certain amount of time. This allows the animated panel (who knows about this stuff) to figure out when to stop scrolling without overshooting by a pixel or two. git-svn-id: svn+ssh://src.earth.threerings.net/narya/trunk@1244 542714f4-19e9-0310-aa3c-eee0fc999fb1
505 lines
17 KiB
Java
505 lines
17 KiB
Java
//
|
|
// $Id: AnimatedPanel.java,v 1.20 2002/04/15 17:46:41 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.
|
|
*
|
|
* <p> 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.
|
|
*
|
|
* <p> 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 (_last != 0) {
|
|
// int delta = (int)(now-_last);
|
|
// _histo.addValue(delta);
|
|
|
|
// // dump the histogram every ten seconds
|
|
// if (_last % (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);
|
|
}
|
|
|
|
// subtract our scrolled deltas from the time remaining
|
|
_scrollx -= dx;
|
|
_scrolly -= dy;
|
|
if (_scrollx == 0 && _scrolly == 0) {
|
|
_ttime = 0;
|
|
viewFinishedScrolling();
|
|
}
|
|
}
|
|
|
|
// keep track of the last time we rendered
|
|
_last = now;
|
|
|
|
// 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();
|
|
}
|