/* * [ScrollingImage.java] * * Summary: Allows smooth scrolling with VolatileImage. * * Copyright: (c) 1998-2017 Roedy Green, Canadian Mind Products, http://mindprod.com * * Licence: This software may be copied and used freely for any purpose but military. * http://mindprod.com/contact/nonmil.html * * Requires: JDK 1.8+ * * Created with: JetBrains IntelliJ IDEA IDE http://www.jetbrains.com/idea/ * * Version History: * 1.0 2002-06-28 original. */ package com.mindprod.smooth; import java.awt.Component; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Image; import java.awt.image.VolatileImage; /** * Allows smooth scrolling with VolatileImage. * * @author Roedy Green, Canadian Mind Products * @version 1.0 2002-06-28 original. * @since 2002-06-28 */ public class ScrollingImage extends Component implements Runnable { /** * true if debugging. */ private static final boolean DEBUGGING = false; /** * Width of the image we are scrolling. */ int imageWidth; /** * Height of the image we are scrolling. It will * normally be considerably taller than the * height of the ScrollingImage component. */ int imageHeight; /** * preferred size for this component. */ int preferredWidth; /** * preferred height for this component. */ int preferredHeight; /** * The large image we are scrolling. Offscreen in * regular RAM. */ Image bigImage; /** * Copy of bigImage stored in video ram. * Volatile. May have to be recreated from time to time. */ VolatileImage vramImage; /** * How long to delay between 1-pixel scrolls * in milliseconds. */ int delay; /** * How many pixels we will scroll the big image up the next time * we paint. */ int pixelShift; /** * true if we have completed the cycle of * scrolling the image */ boolean done; /** * Timer thread that issues repaints to keep screen moving */ Thread animator; /** * We called this object's run method to notfy * the calller when the scrolling cycles is complete. */ Runnable notify; /** * Constructor. * * @param image Image to display. May be considerably * taller than the displayable region. * Normally would have the same * width as the component. * @param slowness How slowly to pan the image. This parameter * represents the delay in milliseconds between * screen animation refreshes. * 0=pan as fast as possible. * @param height height in pixels of the displayable region in pixels, * must be taller than the image. * @param notify We called this object's run method to notfy * the calller when the scrolling cycles is complete. * null if no notification desired. */ public ScrollingImage( Image image, int slowness, int height, Runnable notify ) { if ( image == null ) { throw new IllegalArgumentException( "ScrollingImage: no image" ); } this.bigImage = image; this.imageWidth = bigImage.getWidth( this ); this.imageHeight = bigImage.getHeight( this ); this.preferredWidth = imageWidth; this.preferredHeight = height; if ( imageHeight <= height ) { throw new IllegalArgumentException( "ScrollingImage: image not tall enough to scroll" ); } this.delay = slowness; this.notify = notify; this.pixelShift = 0; } /** * Make sure the vramImage volatile copy of bigImage in * video ram is good. If not recreate it. */ private void ensureGoodVramImage() { // should only be null to get started. if ( vramImage == null ) { vramImage = createVolatileImage( imageWidth, imageHeight ); } do { switch ( vramImage.validate( getGraphicsConfiguration() ) ) { case VolatileImage.IMAGE_INCOMPATIBLE: // we don't have a good vramImage, recreate it vramImage = createVolatileImage( imageWidth, imageHeight ); // no break, deliberate fallthrough!!! case VolatileImage.IMAGE_RESTORED: // vramImage needs to be restored if ( false ) { System.out.println( "restoring vramImage" ); } Graphics2D vramg = vramImage.createGraphics(); // copy image from ordinary ram to video ram, hopefully this happens rarely vramg.drawImage( bigImage, /* Image to draw */ 0, /* top left dest x */ 0, /* top left dest y */ imageWidth - 1, /* bottom right dest x */ imageHeight - 1, /* bottom right dest y */ 0, /* top left source x */ 0, /* top left source y */ imageWidth - 1, /* bottom right source x */ imageHeight - 1, /* bottom right source y */ this ); /* this ScrollingImage observer */ vramg.dispose(); break; default: // vramImage is fine, nothing to do } // end switch // vramImage could have gone south at any time, if it does, try again } while ( vramImage.contentsLost() ); // finally we have a good vramImage, at least for now. } /** * Just a convenient place to start the animation * after peer is ready. */ public void addNotify() { super.addNotify(); // start up the animator thread animator = new Thread( this ); animator.start(); } /** * Maximum layout size. * * @return the maximum dimensions to properly display the Image */ public Dimension getMaximumSize() { return getPreferredSize(); } /** * Minimum layout size. * * @return the minimum dimensions to properly display the Image */ public Dimension getMinimumSize() { return getPreferredSize(); } /** * Preferred Layout size. * * @return the recommended dimensions to display this component. */ public Dimension getPreferredSize() { return new Dimension( preferredWidth, preferredHeight ); } /** * Paints this component using the given graphics context. * * @param g Graphics context where to paint, e.g. to screen, printer, RAM. */ public void paint( Graphics g ) { super.paint( g ); // get size of box we have to draw in Dimension dim = getSize(); int actualWidth = dim.width; int actualHeight = dim.height; // border should be 0 int border = ( actualWidth - imageWidth ) / 2; int imageLeftEdge = border; int imageRightEdge = imageLeftEdge + imageWidth - 1; int imageTopEdge = border; int imageBottomEdge = actualHeight - 1 - border; int firstDisplayableRow = pixelShift; int firstDisplayableColumn = 0; int lastDisplayableRow = imageBottomEdge - imageTopEdge + pixelShift; int lastDisplayableColumn = imageWidth - 1; if ( false ) { System.out.println( " pixelShift:" + pixelShift + " actualWidth:" + actualWidth + " actualHeight:" + actualHeight + " imageWidth:" + imageWidth + " imageHeight:" + imageHeight + " border:" + border + " imageLeftEdge:" + imageLeftEdge + " imageRightEdge:" + imageRightEdge + " imageTopEdge:" + imageTopEdge + " imageBottomEdge:" + imageBottomEdge + " firstDisplayableRow:" + firstDisplayableRow + " firstDisplayableColumn:" + firstDisplayableColumn + " lastDisplayableRow:" + lastDisplayableRow + " lastDisplayableColumn:" + lastDisplayableColumn ); } do { // repeat until we manage to complete drawImage without losing the volatile vramImage ensureGoodVramImage(); /* drawing image from scratch, source is offscreen in video ram. */ /* center Image left/right in box. */ /* Overflow is no problem, drawImage will clip. */ /* destination: top-left/bottom-right, source: top-left/bottom-right */ g.drawImage( vramImage, /* Image to draw */ imageLeftEdge, /* top left dest x */ imageTopEdge, /* top left dest y */ imageRightEdge, /* bottom right dest x */ imageBottomEdge, /* bottom right dest y */ firstDisplayableColumn, /* top left source x */ firstDisplayableRow, /* top left source y */ lastDisplayableColumn, /* bottom right source x */ lastDisplayableRow, /* bottom right source y */ this ); /* this ScrollingImage observer */ } while ( vramImage.contentsLost() ); done = lastDisplayableRow >= imageHeight - 1; if ( done ) { // we are done, freeze screen (pixelShift) the way it is. // More repaints won't hurt anything. } else { // keep going pixelShift++; } } /** * For internal use only. * Thread that triggers the repaints to scroll each pixel. * Perhaps in a future version the * animator thread could hold the first frame somewhat * longer before starting the scroll. */ public void run() { // Remember the starting time long tm = System.currentTimeMillis(); // don't do anything if others use this method while ( !done && Thread.currentThread() == animator ) { // Display the next frame of animation. repaint(); // Delay depending on how far we are behind. try { tm += delay; Thread.sleep( Math.max( 0, tm - System.currentTimeMillis() ) ); } catch ( InterruptedException e ) { break; } } // scrolling is complete if ( notify != null ) { notify.run(); } } /** * Update does not clear the background * for speed. Image always completely fills the component. * * @param g graphics context. */ public void update( Graphics g ) { paint( g ); } } // end class ScrollingImage