Animator.java Source code

Java tutorial

Introduction

Here is the source code for Animator.java

Source

/* From http://java.sun.com/docs/books/tutorial/index.html */

/*
 * @(#)Animator.java   1.5 95/11/29 Herb Jellinek
 *
 * Copyright (c) 1994-1995 Sun Microsystems, Inc. All Rights Reserved.
 *
 * Permission to use, copy, modify, and distribute this software
 * and its documentation for NON-COMMERCIAL or COMMERCIAL purposes and
 * without fee is hereby granted.
 * Please refer to the file http://java.sun.com/copy_trademarks.html
 * for further important copyright and trademark information and to
 * http://java.sun.com/licensing.html for further important licensing
 * information for the Java (tm) Technology.
 *
 * SUN MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY OF
 * THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
 * TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
 * PARTICULAR PURPOSE, OR NON-INFRINGEMENT. SUN SHALL NOT BE LIABLE FOR
 * ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
 * DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
 *
 * THIS SOFTWARE IS NOT DESIGNED OR INTENDED FOR USE OR RESALE AS ON-LINE
 * CONTROL EQUIPMENT IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE
 * PERFORMANCE, SUCH AS IN THE OPERATION OF NUCLEAR FACILITIES, AIRCRAFT
 * NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, DIRECT LIFE
 * SUPPORT MACHINES, OR WEAPONS SYSTEMS, IN WHICH THE FAILURE OF THE
 * SOFTWARE COULD LEAD DIRECTLY TO DEATH, PERSONAL INJURY, OR SEVERE
 * PHYSICAL OR ENVIRONMENTAL DAMAGE ("HIGH RISK ACTIVITIES").  SUN
 * SPECIFICALLY DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY OF FITNESS FOR
 * HIGH RISK ACTIVITIES.
 */

import java.applet.Applet;
import java.applet.AudioClip;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Event;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Point;
import java.awt.image.ImageProducer;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

/**
 * An applet that plays a sequence of images, as a loop or a one-shot. Can have
 * a soundtrack and/or sound effects tied to individual frames.
 * 
 * @author Herb Jellinek
 * @version 1.5, 29 Nov 1995
 */

public class Animator extends Applet implements Runnable {

    /**
     * The images, in display order (Images).
     */
    Vector images = null;

    /**
     * Duration of each image (Integers, in milliseconds).
     */
    Hashtable durations = null;

    /**
     * Sound effects for each image (AudioClips).
     */
    Hashtable sounds = null;

    /**
     * Position of each image (Points).
     */
    Hashtable positions = null;

    /**
     * MediaTracker 'class' ID numbers.
     */

    static final int STARTUP_ID = 0;

    static final int BACKGROUND_ID = 1;

    static final int ANIMATION_ID = 2;

    /**
     * Start-up image URL, if any.
     */
    URL startUpImageURL = null;

    /**
     * Start-up image, if any.
     */
    Image startUpImage = null;

    /**
     * Background image URL, if any.
     */
    URL backgroundImageURL = null;

    /**
     * Background image, if any.
     */
    Image backgroundImage = null;

    /**
     * The soundtrack's URL.
     */
    URL soundtrackURL = null;

    /**
     * The soundtrack.
     */
    AudioClip soundtrack;

    /**
     * Largest width.
     */
    int maxWidth = 0;

    /**
     * Largest height.
     */
    int maxHeight = 0;

    /**
     * Was there a problem loading the current image?
     */
    boolean imageLoadError = false;

    /**
     * The directory or URL from which the images are loaded
     */
    URL imageSource = null;

    /**
     * The directory or URL from which the sounds are loaded
     */
    URL soundSource = null;

    /**
     * The thread animating the images.
     */
    Thread engine = null;

    /**
     * The current loop slot - index into 'images.'
     */
    int frameNum;

    /**
     * frameNum as an Object - suitable for use as a Hashtable key.
     */
    Integer frameNumKey;

    /**
     * The current X position (for painting).
     */
    int xPos = 0;

    /**
     * The current Y position (for painting).
     */
    int yPos = 0;

    /**
     * The default number of milliseconds to wait between frames.
     */
    public static final int defaultPause = 3900;

    /**
     * The global delay between images, which can be overridden by the PAUSE
     * parameter.
     */
    int globalPause = defaultPause;

    /**
     * Whether or not the thread has been paused by the user.
     */
    boolean userPause = false;

    /**
     * Repeat the animation? If false, just play it once.
     */
    boolean repeat;

    /**
     * The offscreen image, used in double buffering
     */
    Image offScrImage;

    /**
     * The offscreen graphics context, used in double buffering
     */
    Graphics offScrGC;

    /**
     * The MediaTracker we use to load our images.
     */
    MediaTracker tracker;

    /**
     * Can we paint yet?
     */
    boolean loaded = false;

    /**
     * Was there an initialization error?
     */
    boolean error = false;

    /**
     * What we call an image file in messages.
     */
    final static String imageLabel = "image";

    /**
     * What we call a sound file in messages.
     */
    final static String soundLabel = "sound";

    /**
     * Print silly debugging info?
     */
    final boolean debug = false;

    /**
     * Applet info.
     */
    public String getAppletInfo() {
        return "Animator v1.5, by Herb Jellinek";
    }

    /**
     * Parameter info.
     */
    public String[][] getParameterInfo() {
        String[][] info = { { "imagesource", "URL", "a directory" }, { "startup", "URL", "displayed at startup" },
                { "background", "URL", "displayed as background" }, { "startimage", "int", "start index" },
                { "endimage", "int", "end index" }, { "namepattern", "URL", "used to generate indexed names" },
                { "pause", "int", "milliseconds" }, { "pauses", "ints", "milliseconds" },
                { "repeat", "boolean", "repeat or not" }, { "positions", "coordinates", "path" },
                { "soundsource", "URL", "audio directory" }, { "soundtrack", "URL", "background music" },
                { "sounds", "URLs", "audio samples" }, };
        return info;
    }

    /**
     * Print silly debugging info.
     */
    void dbg(String s) {
        if (debug) {
            System.out.println("> " + s);
        }
    }

    /**
     * Local version of getParameter for debugging purposes.
     */
    public String getParameter(String key) {
        String result = super.getParameter(key);
        dbg("getParameter(" + key + ") = " + result);
        return result;
    }

    final int setFrameNum(int newFrameNum) {
        frameNumKey = new Integer(frameNum = newFrameNum);
        return frameNum;
    }

    void updateMaxDims(Dimension dim) {
        maxWidth = Math.max(dim.width, maxWidth);
        maxHeight = Math.max(dim.height, maxHeight);
        dbg("New width = " + maxWidth + ", height = " + maxHeight);
    }

    /**
     * Parse the IMAGES parameter. It looks like 1|2|3|4|5, etc., where each
     * number (item) names a source image.
     * 
     * @return a Vector of (URL) image file names.
     */
    Vector parseImages(String attr) throws MalformedURLException {
        Vector result = new Vector(10);
        for (int i = 0; i < attr.length();) {
            int next = attr.indexOf('|', i);
            if (next == -1)
                next = attr.length();
            String file = attr.substring(i, next);
            result.addElement(new URL(imageSource, "T" + file + ".gif"));
            i = next + 1;
        }
        return result;
    }

    /**
     * Fetch the images named in the argument, updating maxWidth and maxHeight
     * as we go. Is restartable.
     * 
     * @param images
     *            a Vector of URLs
     * @return true if all went well, false otherwise.
     */
    boolean fetchImages(Vector images) {
        int i;
        int size = images.size();
        for (i = 0; i < size; i++) {
            Object o = images.elementAt(i);
            if (o instanceof URL) {
                URL url = (URL) o;
                tellLoadingMsg(url, imageLabel);
                Image im = getImage(url);
                tracker.addImage(im, ANIMATION_ID);
                images.setElementAt(im, i);
            }
        }

        try {
            tracker.waitForID(ANIMATION_ID);
        } catch (InterruptedException e) {
        }
        if (tracker.isErrorID(ANIMATION_ID)) {
            return false;
        }

        for (i = 0; i < size; i++) {
            updateMaxDims(getImageDimensions((Image) images.elementAt(i)));
        }

        return true;
    }

    /**
     * Parse the SOUNDS parameter. It looks like train.au||hello.au||stop.au,
     * etc., where each item refers to a source image. Empty items mean that the
     * corresponding image has no associated sound.
     * 
     * @return a Hashtable of SoundClips keyed to Integer frame numbers.
     */
    Hashtable parseSounds(String attr, Vector images) throws MalformedURLException {
        Hashtable result = new Hashtable();

        int imageNum = 0;
        int numImages = images.size();
        for (int i = 0; i < attr.length();) {
            if (imageNum >= numImages)
                break;

            int next = attr.indexOf('|', i);
            if (next == -1)
                next = attr.length();

            String sound = attr.substring(i, next);
            if (sound.length() != 0) {
                result.put(new Integer(imageNum), new URL(soundSource, sound));
            }
            i = next + 1;
            imageNum++;
        }

        return result;
    }

    /**
     * Fetch the sounds named in the argument. Is restartable.
     * 
     * @return URL of the first bogus file we hit, null if OK.
     */
    URL fetchSounds(Hashtable sounds) {
        for (Enumeration e = sounds.keys(); e.hasMoreElements();) {
            Integer num = (Integer) e.nextElement();
            Object o = sounds.get(num);
            if (o instanceof URL) {
                URL file = (URL) o;
                tellLoadingMsg(file, soundLabel);
                try {
                    sounds.put(num, getAudioClip(file));
                } catch (Exception ex) {
                    return file;
                }
            }
        }
        return null;
    }

    /**
     * Parse the PAUSES parameter. It looks like 1000|500|||750, etc., where
     * each item corresponds to a source image. Empty items mean that the
     * corresponding image has no special duration, and should use the global
     * one.
     * 
     * @return a Hashtable of Integer pauses keyed to Integer frame numbers.
     */
    Hashtable parseDurations(String attr, Vector images) {
        Hashtable result = new Hashtable();

        int imageNum = 0;
        int numImages = images.size();
        for (int i = 0; i < attr.length();) {
            if (imageNum >= numImages)
                break;

            int next = attr.indexOf('|', i);
            if (next == -1)
                next = attr.length();

            if (i != next - 1) {
                int duration = Integer.parseInt(attr.substring(i, next));
                result.put(new Integer(imageNum), new Integer(duration));
            } else {
                result.put(new Integer(imageNum), new Integer(globalPause));
            }
            i = next + 1;
            imageNum++;
        }

        return result;
    }

    /**
     * Parse a String of form xxx@yyy and return a Point.
     */
    Point parsePoint(String s) throws ParseException {
        int atPos = s.indexOf('@');
        if (atPos == -1)
            throw new ParseException("Illegal position: " + s);
        return new Point(Integer.parseInt(s.substring(0, atPos)), Integer.parseInt(s.substring(atPos + 1)));
    }

    /**
     * Parse the POSITIONS parameter. It looks like 10@30|11@31|||12@20, etc.,
     * where each item is an X@Y coordinate corresponding to a source image.
     * Empty items mean that the corresponding image has the same position as
     * the preceding one.
     * 
     * @return a Hashtable of Points keyed to Integer frame numbers.
     */
    Hashtable parsePositions(String param, Vector images) throws ParseException {
        Hashtable result = new Hashtable();

        int imageNum = 0;
        int numImages = images.size();
        for (int i = 0; i < param.length();) {
            if (imageNum >= numImages)
                break;

            int next = param.indexOf('|', i);
            if (next == -1)
                next = param.length();

            if (i != next) {
                result.put(new Integer(imageNum), parsePoint(param.substring(i, next)));
            }
            i = next + 1;
            imageNum++;
        }

        return result;
    }

    /**
     * Get the dimensions of an image.
     * 
     * @return the image's dimensions.
     */
    Dimension getImageDimensions(Image im) {
        return new Dimension(im.getWidth(null), im.getHeight(null));
    }

    /**
     * Substitute an integer some number of times in a string, subject to
     * parameter strings embedded in the string. Parameter strings: %N -
     * substitute the integer as is, with no padding. % <digit>, for example %5 -
     * substitute the integer left-padded with zeros to <digits>digits wide. %% -
     * substitute a '%' here.
     * 
     * @param inStr
     *            the String to substitute within
     * @param theInt
     *            the int to substitute.
     */
    String doSubst(String inStr, int theInt) {
        String padStr = "0000000000";
        int length = inStr.length();
        StringBuffer result = new StringBuffer(length);

        for (int i = 0; i < length;) {
            char ch = inStr.charAt(i);
            if (ch == '%') {
                i++;
                if (i == length) {
                    result.append(ch);
                } else {
                    ch = inStr.charAt(i);
                    if (ch == 'N') {
                        // just stick in the number, unmolested
                        result.append(theInt + "");
                        i++;
                    } else {
                        int pad;
                        if ((pad = Character.digit(ch, 10)) != -1) {
                            // we've got a width value
                            String numStr = theInt + "";
                            String scr = padStr + numStr;
                            result.append(scr.substring(scr.length() - pad));
                            i++;
                        } else {
                            result.append(ch);
                            i++;
                        }
                    }
                }
            } else {
                result.append(ch);
                i++;
            }
        }
        return result.toString();
    }

    /**
     * Stuff a range of image names into a Vector.
     * 
     * @return a Vector of image URLs.
     */
    Vector prepareImageRange(int startImage, int endImage, String pattern) throws MalformedURLException {
        Vector result = new Vector(Math.abs(endImage - startImage) + 1);
        if (pattern == null) {
            pattern = "T%N.gif";
        }
        if (startImage > endImage) {
            for (int i = startImage; i >= endImage; i--) {
                result.addElement(new URL(imageSource, doSubst(pattern, i)));
            }
        } else {
            for (int i = startImage; i <= endImage; i++) {
                result.addElement(new URL(imageSource, doSubst(pattern, i)));
            }
        }
        return result;
    }

    /**
     * Initialize the applet. Get parameters.
     */
    public void init() {

        tracker = new MediaTracker(this);

        try {
            String param = getParameter("IMAGESOURCE");
            imageSource = (param == null) ? getDocumentBase() : new URL(getDocumentBase(), param + "/");

            param = getParameter("PAUSE");
            globalPause = (param != null) ? Integer.parseInt(param) : defaultPause;

            param = getParameter("REPEAT");
            repeat = (param == null) ? true : (param.equalsIgnoreCase("yes") || param.equalsIgnoreCase("true"));

            int startImage = 1;
            int endImage = 1;
            param = getParameter("ENDIMAGE");
            if (param != null) {
                endImage = Integer.parseInt(param);
                param = getParameter("STARTIMAGE");
                if (param != null) {
                    startImage = Integer.parseInt(param);
                }
                param = getParameter("NAMEPATTERN");
                images = prepareImageRange(startImage, endImage, param);
            } else {
                param = getParameter("STARTIMAGE");
                if (param != null) {
                    startImage = Integer.parseInt(param);
                    param = getParameter("NAMEPATTERN");
                    images = prepareImageRange(startImage, endImage, param);
                } else {
                    param = getParameter("IMAGES");
                    if (param == null) {
                        showStatus("No legal IMAGES, STARTIMAGE, or ENDIMAGE " + "specified.");
                        return;
                    } else {
                        images = parseImages(param);
                    }
                }
            }

            param = getParameter("BACKGROUND");
            if (param != null) {
                backgroundImageURL = new URL(imageSource, param);
            }

            param = getParameter("STARTUP");
            if (param != null) {
                startUpImageURL = new URL(imageSource, param);
            }

            param = getParameter("SOUNDSOURCE");
            soundSource = (param == null) ? imageSource : new URL(getDocumentBase(), param + "/");

            param = getParameter("SOUNDS");
            if (param != null) {
                sounds = parseSounds(param, images);
            }

            param = getParameter("PAUSES");
            if (param != null) {
                durations = parseDurations(param, images);
            }

            param = getParameter("POSITIONS");
            if (param != null) {
                positions = parsePositions(param, images);
            }

            param = getParameter("SOUNDTRACK");
            if (param != null) {
                soundtrackURL = new URL(soundSource, param);
            }
        } catch (MalformedURLException e) {
            showParseError(e);
        } catch (ParseException e) {
            showParseError(e);
        }

        setFrameNum(0);
    }

    void tellLoadingMsg(String file, String fileType) {
        showStatus("Animator: loading " + fileType + " " + file);
    }

    void tellLoadingMsg(URL url, String fileType) {
        tellLoadingMsg(url.toExternalForm(), fileType);
    }

    void clearLoadingMessage() {
        showStatus("");
    }

    void loadError(String fileName, String fileType) {
        String errorMsg = "Animator: Couldn't load " + fileType + " " + fileName;
        showStatus(errorMsg);
        System.err.println(errorMsg);
        error = true;
        repaint();
    }

    void loadError(URL badURL, String fileType) {
        loadError(badURL.toExternalForm(), fileType);
    }

    void showParseError(Exception e) {
        String errorMsg = "Animator: Parse error: " + e;
        showStatus(errorMsg);
        System.err.println(errorMsg);
        error = true;
        repaint();
    }

    void startPlaying() {
        if (soundtrack != null) {
            soundtrack.loop();
        }
    }

    void stopPlaying() {
        if (soundtrack != null) {
            soundtrack.stop();
        }
    }

    /**
     * Run the animation. This method is called by class Thread.
     * 
     * @see java.lang.Thread
     */
    public void run() {
        Thread me = Thread.currentThread();
        URL badURL;

        me.setPriority(Thread.MIN_PRIORITY);

        if (!loaded) {
            try {
                // ... to do a bunch of loading.
                if (startUpImageURL != null) {
                    tellLoadingMsg(startUpImageURL, imageLabel);
                    startUpImage = getImage(startUpImageURL);
                    tracker.addImage(startUpImage, STARTUP_ID);
                    tracker.waitForID(STARTUP_ID);
                    if (tracker.isErrorID(STARTUP_ID)) {
                        loadError(startUpImageURL, "start-up image");
                    }
                    Dimension size = getImageDimensions(startUpImage);
                    resize(size.width, size.height);
                    repaint();
                }

                if (backgroundImageURL != null) {
                    tellLoadingMsg(backgroundImageURL, imageLabel);
                    backgroundImage = getImage(backgroundImageURL);
                    tracker.addImage(backgroundImage, BACKGROUND_ID);
                    tracker.waitForID(BACKGROUND_ID);
                    if (tracker.isErrorID(BACKGROUND_ID)) {
                        loadError(backgroundImageURL, "background image");
                    }
                    updateMaxDims(getImageDimensions(backgroundImage));
                    repaint();
                }

                // Fetch the animation frames
                if (!fetchImages(images)) {
                    // Need to add method to MediaTracker to return
                    // files that caused errors during loading.
                    loadError("an image", imageLabel);
                    return;
                }

                if (soundtrackURL != null && soundtrack == null) {
                    tellLoadingMsg(soundtrackURL, imageLabel);
                    soundtrack = getAudioClip(soundtrackURL);
                    if (soundtrack == null) {
                        loadError(soundtrackURL, "soundtrack");
                        return;
                    }
                }

                if (sounds != null) {
                    badURL = fetchSounds(sounds);
                    if (badURL != null) {
                        loadError(badURL, soundLabel);
                        return;
                    }
                }

                clearLoadingMessage();

                offScrImage = createImage(maxWidth, maxHeight);
                offScrGC = offScrImage.getGraphics();
                offScrGC.setColor(Color.white);

                resize(maxWidth, maxHeight);
                loaded = true;
                error = false;
            } catch (Exception e) {
                error = true;
                e.printStackTrace();
            }
        }

        if (userPause) {
            return;
        }

        if (repeat || frameNum < images.size()) {
            startPlaying();
        }

        try {
            if (images.size() > 1) {
                while (maxWidth > 0 && maxHeight > 0 && engine == me) {
                    if (frameNum >= images.size()) {
                        if (!repeat) {
                            return;
                        }
                        setFrameNum(0);
                    }
                    repaint();

                    if (sounds != null) {
                        AudioClip clip = (AudioClip) sounds.get(frameNumKey);
                        if (clip != null) {
                            clip.play();
                        }
                    }

                    try {
                        Integer pause = null;
                        if (durations != null) {
                            pause = (Integer) durations.get(frameNumKey);
                        }
                        if (pause == null) {
                            Thread.sleep(globalPause);
                        } else {
                            Thread.sleep(pause.intValue());
                        }
                    } catch (InterruptedException e) {
                        // Should we do anything?
                    }
                    setFrameNum(frameNum + 1);
                }
            }
        } finally {
            stopPlaying();
        }
    }

    /**
     * No need to clear anything; just paint.
     */
    public void update(Graphics g) {
        paint(g);
    }

    /**
     * Paint the current frame.
     */
    public void paint(Graphics g) {
        if (error || !loaded) {
            if (startUpImage != null) {
                if (tracker.checkID(STARTUP_ID)) {
                    g.drawImage(startUpImage, 0, 0, this);
                }
            } else {
                if (backgroundImage != null) {
                    if (tracker.checkID(BACKGROUND_ID)) {
                        g.drawImage(backgroundImage, 0, 0, this);
                    }
                } else {
                    g.clearRect(0, 0, maxWidth, maxHeight);
                }
            }
        } else {
            if ((images != null) && (images.size() > 0)) {
                if (frameNum < images.size()) {
                    if (backgroundImage == null) {
                        offScrGC.fillRect(0, 0, maxWidth, maxHeight);
                    } else {
                        offScrGC.drawImage(backgroundImage, 0, 0, this);
                    }

                    Image image = (Image) images.elementAt(frameNum);
                    Point pos = null;
                    if (positions != null) {
                        pos = (Point) positions.get(frameNumKey);
                    }
                    if (pos != null) {
                        xPos = pos.x;
                        yPos = pos.y;
                    }
                    offScrGC.drawImage(image, xPos, yPos, this);
                    g.drawImage(offScrImage, 0, 0, this);
                } else {
                    // no more animation, but need to draw something
                    dbg("No more animation; drawing last image.");
                    if (backgroundImage == null) {
                        g.fillRect(0, 0, maxWidth, maxHeight);
                    } else {
                        g.drawImage(backgroundImage, 0, 0, this);
                    }
                    g.drawImage((Image) images.lastElement(), 0, 0, this);
                }
            }
        }
    }

    /**
     * Start the applet by forking an animation thread.
     */
    public void start() {
        if (engine == null) {
            engine = new Thread(this);
            engine.start();
        }
    }

    /**
     * Stop the insanity, um, applet.
     */
    public void stop() {
        if (engine != null && engine.isAlive()) {
            engine.stop();
        }
        engine = null;
    }

    /**
     * Pause the thread when the user clicks the mouse in the applet. If the
     * thread has stopped (as in a non-repeat performance), restart it.
     */
    public boolean handleEvent(Event evt) {
        if (evt.id == Event.MOUSE_DOWN) {
            if (loaded) {
                if (engine != null && engine.isAlive()) {
                    if (userPause) {
                        engine.resume();
                        startPlaying();
                    } else {
                        engine.suspend();
                        stopPlaying();
                    }
                    userPause = !userPause;
                } else {
                    userPause = false;
                    setFrameNum(0);
                    engine = new Thread(this);
                    engine.start();
                }
            }
            return true;
        } else {
            return super.handleEvent(evt);
        }
    }

}

class ParseException extends Exception {
    ParseException(String s) {
        super(s);
    }
}

class ImageNotFoundException extends Exception {
    ImageNotFoundException(ImageProducer source) {
        super(source + "");
    }
}