Smooth Moves
import java.awt.AlphaComposite; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.GraphicsConfiguration; import java.awt.Image; import java.awt.Transparency; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.image.BufferedImage; import java.net.URL; import javax.imageio.ImageIO; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.SwingUtilities; import javax.swing.Timer; /* * SmoothMoves.java * * Created on May 2, 2007, 4:49 PM * * Copyright (c) 2007, Sun Microsystems, Inc * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * Neither the name of the TimingFramework project nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /** * * @author Chet */ public class SmoothMoves extends JComponent implements ActionListener, KeyListener { /** image holds the graphics we render for each animating object */ BufferedImage image = null; static int imageW = 100; static int imageH = 150; /** Location of fading animation */ int fadeX = 50; int fadeY = 50; /** X values that moving animation will move between */ static int moveMinX = 150; static int moveMaxX = 350; /** Current x/y location of moving animation */ int moveX = moveMinX; int moveY = 50; /** Current opacity of fading animation */ float opacity = 0.0f; /** Toggles for various demo options (key to toggle in parentheses) */ boolean useImage = false; // (i) image instead of rectangle boolean useAA = false; // (a) anti-aliased edges (rectangle only) boolean motionBlur = false; // (b) ghost images behind moving animation boolean alterColor = false; // (c) light-gray instead of black rectangle boolean linear = true; // (l) linear vs. non-linear motion /** Used for motion blur rendering; holds information for ghost trail */ int blurSize = 5; int prevMoveX[]; int prevMoveY[]; float trailOpacity[]; /** Basic Timer animation info */ final static int CYCLE_TIME = 2000; // One cycle takes 2 seconds int currentResolution = 50; // current Timer resolution Timer timer = null; // animation Timer long cycleStart; // track start time for each cycle /** Creates a new instance of SmoothAnimation */ public SmoothMoves() { // createAnimationImage(); cycleStart = System.nanoTime() / 1000000; startTimer(currentResolution); } /** * Create the image that will be animated. This image may be an actual image * (duke.gif), or some graphics (a variation on a black filled rectangle) that * are rendered into an image. The contents of this image are dependent upon * the runtime toggles that have been set when this method is called. */ void createAnimationImage() { GraphicsConfiguration gc = getGraphicsConfiguration(); image = gc.createCompatibleImage(imageW, imageH, Transparency.TRANSLUCENT); Graphics2D gImg = image.createGraphics(); if (useImage) { try { URL url = getClass().getResource("duke.gif"); Image originalImage = ImageIO.read(url); gImg.drawImage(originalImage, 0, 0, imageW, imageH, null); } catch (Exception e) { } } else { // use graphics Color graphicsColor; if (alterColor) { graphicsColor = Color.LIGHT_GRAY; } else { graphicsColor = Color.BLACK; } gImg.setColor(graphicsColor); gImg.fillRect(0, 0, imageW, imageH); if (useAA) { // Antialiasing hack - just draw a fading-out border around the // rectangle gImg.setComposite(AlphaComposite.Src); int red = graphicsColor.getRed(); int green = graphicsColor.getRed(); int blue = graphicsColor.getRed(); gImg.setColor(new Color(red, green, blue, 50)); gImg.drawRect(0, 0, imageW - 1, imageH - 1); gImg.setColor(new Color(red, green, blue, 100)); gImg.drawRect(1, 1, imageW - 3, imageH - 3); gImg.setColor(new Color(red, green, blue, 150)); gImg.drawRect(2, 2, imageW - 5, imageH - 5); gImg.setColor(new Color(red, green, blue, 200)); gImg.drawRect(3, 3, imageW - 7, imageH - 7); gImg.setColor(new Color(red, green, blue, 225)); gImg.drawRect(4, 4, imageW - 9, imageH - 9); } } gImg.dispose(); } public void paintComponent(Graphics g) { if (image == null) { createAnimationImage(); } // Erase the background g.setColor(Color.WHITE); g.fillRect(0, 0, getWidth(), getHeight()); // Draw the fading image Graphics2D gFade = (Graphics2D) g.create(); gFade.setComposite(AlphaComposite.SrcOver.derive(opacity)); gFade.drawImage(image, fadeX, fadeY, null); gFade.dispose(); // Draw the moving image if (motionBlur) { // Draw previous locations of the image as a trail of // ghost images if (prevMoveX == null) { // blur location array not yet created; create it now prevMoveX = new int[blurSize]; prevMoveY = new int[blurSize]; trailOpacity = new float[blurSize]; float incrementalFactor = .2f / (blurSize + 1); for (int i = 0; i < blurSize; ++i) { // default values, act as flag to not render these // until they have real values prevMoveX[i] = -1; prevMoveY[i] = -1; // vary the translucency by the number of the ghost // image; the further away it is from the current one, // the more faded it will be trailOpacity[i] = (.2f - incrementalFactor) - i * incrementalFactor; } } else { Graphics2D gTrail = (Graphics2D) g.create(); for (int i = 0; i < blurSize; ++i) { if (prevMoveX[i] >= 0) { // Render each blur image with the appropriate // amount of translucency gTrail.setComposite(AlphaComposite.SrcOver.derive(trailOpacity[i])); gTrail.drawImage(image, prevMoveX[i], prevMoveY[i], null); } } gTrail.dispose(); } } g.drawImage(image, moveX, moveY, null); if (motionBlur) { // shift the ghost positions to add the current position and // drop the oldest one for (int i = blurSize - 1; i > 0; --i) { prevMoveX[i] = prevMoveX[i - 1]; prevMoveY[i] = prevMoveY[i - 1]; } prevMoveX[0] = moveX; prevMoveY[0] = moveY; } } /** * This method handles the events from the Swing Timer */ public void actionPerformed(ActionEvent ae) { // calculate the fraction elapsed of the animation and call animate() // to alter the values accordingly long currentTime = System.nanoTime() / 1000000; long totalTime = currentTime - cycleStart; if (totalTime > CYCLE_TIME) { cycleStart = currentTime; } float fraction = (float) totalTime / CYCLE_TIME; fraction = Math.min(1.0f, fraction); fraction = 1 - Math.abs(1 - (2 * fraction)); animate(fraction); } /** * Animate the opacity and location factors, according to the current * fraction. */ public void animate(float fraction) { float animationFactor; if (linear) { animationFactor = fraction; } else { // Our "nonlinear" motion just uses a sin function to get a // simple bounce behavior animationFactor = (float) Math.sin(fraction * (float) Math.PI / 2); } // Clamp the value to make sure it does not exceed the bounds animationFactor = Math.min(animationFactor, 1.0f); animationFactor = Math.max(animationFactor, 0.0f); // The opacity, used by the fading animation, will just use the // animation fraction directly opacity = animationFactor; // The move animation will calculate a location based on a linear // interpolation between its start and end points using the fraction moveX = moveMinX + (int) (.5f + animationFactor * (float) (moveMaxX - moveMinX)); // redisplay our component with the new animated values repaint(); } /** * Moves the frame rate up or down by changing the Timer resolution */ private void changeResolution(boolean faster) { if (faster) { currentResolution -= 5; } else { currentResolution += 5; } currentResolution = Math.max(currentResolution, 0); currentResolution = Math.min(currentResolution, 500); startTimer(currentResolution); } /** * Starts the animation */ private void startTimer(int resolution) { if (timer != null) { timer.stop(); timer.setDelay(resolution); } else { timer = new Timer(resolution, this); } timer.start(); } /** * Toggles various rendering flags */ public void keyPressed(KeyEvent ke) { int keyCode = ke.getKeyCode(); if (keyCode == KeyEvent.VK_B) { // B: Motion blur - displays trail of ghost images motionBlur = !motionBlur; } else if (keyCode == KeyEvent.VK_A) { // A: Antialiasing - Displays soft edges around graphics useAA = !useAA; createAnimationImage(); } else if (keyCode == KeyEvent.VK_C) { // C: Color - Toggles rectangle color between dark and light colors alterColor = !alterColor; createAnimationImage(); } else if (keyCode == KeyEvent.VK_I) { // I: Image - Toggles use of image or filled rectangle to show how // straight edges affect animation perception useImage = !useImage; createAnimationImage(); } else if (keyCode == KeyEvent.VK_UP) { // Up Arrow: Speed - Speeds up frame rate changeResolution(true); } else if (keyCode == KeyEvent.VK_DOWN) { // Down Arrow: Speed - Slows down frame rate changeResolution(false); } else if (keyCode == KeyEvent.VK_L) { // L: Linearity: Toggles linear/nonlinear motion linear = !linear; } else if (keyCode >= KeyEvent.VK_1 && keyCode <= KeyEvent.VK_9) { // 0-9: Blur size: Toggles size of ghost trail for motion blur blurSize = keyCode - KeyEvent.VK_0; prevMoveX = prevMoveY = null; } } // Unused KeyListener implementations public void keyReleased(KeyEvent ke) { } public void keyTyped(KeyEvent ke) { } private static void createAndShowGUI() { JFrame f = new JFrame("Smooth Moves"); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setSize(moveMaxX + imageW + 50, 300); SmoothMoves component = new SmoothMoves(); f.add(component); f.setVisible(true); f.addKeyListener(component); } public static void main(String[] args) { Runnable doCreateAndShowGUI = new Runnable() { public void run() { createAndShowGUI(); } }; SwingUtilities.invokeLater(doCreateAndShowGUI); } }