/* * @(#)Animator.java 1.13 04/07/26 * * Copyright (c) 2004 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: * * -Redistribution of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * -Redistribution 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 Sun Microsystems, Inc. or the names of contributors may * be used to endorse or promote products derived from this software without * specific prior written permission. * * This software is provided "AS IS," without a warranty of any kind. ALL * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MIDROSYSTEMS, INC. ("SUN") * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL, * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE, * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. * * You acknowledge that this software is not designed, licensed or intended * for use in the design, construction, operation or maintenance of any * nuclear facility. */ /* * @(#)Animator.java 1.13 04/07/26 */ import java.awt.*; import java.awt.event.*; import java.applet.Applet; import java.applet.AudioClip; import java.util.Vector; import java.util.Hashtable; import java.util.Enumeration; import java.net.URL; import java.net.MalformedURLException; import java.util.List; import java.util.ArrayList; import java.util.Iterator; /** * 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. * See the Animator * home page for details and updates. * * @author Herb Jellinek * @version 1.13, 07/26/04 */ public class Animator extends Applet implements Runnable, MouseListener { int appWidth = 0; // Animator width int appHeight = 0; // Animator height Thread engine = null; // Thread animating the images boolean userPause = false; // True if thread currently paused by user boolean loaded = false; // Can we paint yet? boolean error = false; // Was there an initialization error? Animation animation = null; // Animation this animator contains String hrefTarget = null; // Frame target of reference URL if any URL hrefURL = null; // URL link for information if any static final String sourceLocation = "http://java.sun.com/applets/applets/Animator/"; static final String userInstructions = "shift-click for errors, info"; static final int STARTUP_ID = 0; static final int BACKGROUND_ID = 1; static final int ANIMATION_ID = 2; /** * Applet info. */ public String getAppletInfo() { return "Animator v1.10 (02/05/97), by Herb Jellinek"; } /** * Parameter info. */ public String[][] getParameterInfo() { String[][] info = { {"imagesource", "URL", "a directory"}, {"startup", "URL", "image displayed at start-up"}, {"backgroundcolor", "int", "background color (24-bit RGB number)"}, {"background", "URL", "image displayed as background"}, {"startimage", "int", "index of first image"}, {"endimage", "int", "index of last image"}, {"namepattern", "URL", "generates indexed names"}, {"images", "URLs", "list of image indices"}, {"href", "URL", "page to visit on mouse-click"}, {"target", "name", "frame to put that page in"}, {"pause", "int", "global pause, milliseconds"}, {"pauses", "ints", "individual pauses, milliseconds"}, {"repeat", "boolean", "repeat? true or false"}, {"positions", "coordinates", "path images will follow"}, {"soundsource", "URL", "audio directory"}, {"soundtrack", "URL", "background music"}, {"sounds", "URLs", "list of audio samples"}, }; return info; } /** * Show a crude "About" box. Displays credits, errors (if any), and * parameter values and documentation. */ void showDescription() { DescriptionFrame description = new DescriptionFrame(); description.tell("\t\t"+getAppletInfo()+"\n"); description.tell("Updates, documentation at "+sourceLocation+"\n\n"); description.tell("Document base: "+getDocumentBase()+"\n"); description.tell("Code base: "+getCodeBase()+"\n\n"); Object errors[] = animation.tracker.getErrorsAny(); if (errors != null) { description.tell("Applet image errors:\n"); for (int i = 0; i < errors.length; i++) { if (errors[i] instanceof Image) { AnimationFrame frame = (AnimationFrame) animation.frames.get(i); URL url = frame.imageLocation; if (url != null) { description.tell(" "+url+" not loaded\n"); } } } description.tell("\n"); } if (animation.frames == null || animation.frames.size() == 0) description.tell("\n** No images loaded **\n\n"); description.tell("Applet parameters:\n"); description.tell(" width = "+getParameter("WIDTH")+"\n"); description.tell(" height = "+getParameter("HEIGHT")+"\n"); String params[][] = getParameterInfo(); for (int i = 0; i < params.length; i++) { String name = params[i][0]; description.tell(" "+name+" = "+getParameter(name)+ "\t ["+params[i][2]+"]\n"); } description.show(); } /** * Local version of getParameter for debugging purposes. */ public String getParam(String key) { String result = getParameter(key); return result; } /** * Get parameters and parse them */ public void handleParams() { try { String param = getParam("IMAGESOURCE"); animation.imageSource = (param == null) ? getDocumentBase() : new URL(getDocumentBase(), param + "/"); String href = getParam("HREF"); if (href != null) { try { hrefURL = new URL(getDocumentBase(), href); } catch (MalformedURLException e) { showParseError(e); } } hrefTarget = getParam("TARGET"); if (hrefTarget == null) hrefTarget = "_top"; param = getParam("PAUSE"); if (param != null) animation.setGlobalPause(Integer.parseInt(param)); param = getParam("REPEAT"); animation.repeat = (param == null) ? true : (param.equalsIgnoreCase("yes") || param.equalsIgnoreCase("true")); int startImage = 1; int endImage = 1; param = getParam("ENDIMAGE"); if (param != null) { endImage = Integer.parseInt(param); param = getParam("STARTIMAGE"); if (param != null) { startImage = Integer.parseInt(param); } param = getParam("NAMEPATTERN"); animation.prepareImageRange(startImage, endImage, param); } else { param = getParam("STARTIMAGE"); if (param != null) { startImage = Integer.parseInt(param); param = getParam("NAMEPATTERN"); animation.prepareImageRange(startImage, endImage, param); } else { param = getParam("IMAGES"); if (param == null) { showStatus("No legal IMAGES, STARTIMAGE, or ENDIMAGE "+ "specified."); error = true; return; } else { animation.parseImages(param, getParam("NAMEPATTERN")); } } } param = getParam("BACKGROUND"); if (param != null) animation.backgroundImageURL = new URL(animation.imageSource, param); param = getParam("BACKGROUNDCOLOR"); if (param != null) animation.backgroundColor = decodeColor(param); param = getParam("STARTUP"); if (param != null) animation.startUpImageURL = new URL(animation.imageSource, param); param = getParam("SOUNDSOURCE"); animation.soundSource = (param == null) ? animation.imageSource : new URL(getDocumentBase(), param + "/"); param = getParam("SOUNDS"); if (param != null) animation.parseSounds(param); param = getParam("PAUSES"); if (param != null) animation.parseDurations(param); param = getParam("POSITIONS"); if (param != null) animation.parsePositions(param); param = getParam("SOUNDTRACK"); if (param != null) animation.soundTrackURL = new URL( animation.soundSource, param); } catch (MalformedURLException e) { showParseError(e); } catch (ParseException e) { showParseError(e); } } private Color decodeColor(String s) { int val = 0; try { if (s.startsWith("0x")) { val = Integer.parseInt(s.substring(2), 16); } else if (s.startsWith("#")) { val = Integer.parseInt(s.substring(1), 16); } else if (s.startsWith("0") && s.length() > 1) { val = Integer.parseInt(s.substring(1), 8); } else { val = Integer.parseInt(s, 10); } return new Color(val); } catch (NumberFormatException e) { return null; } } /** * Initialize the applet. Get parameters. */ public void init() { //animation.tracker = new MediaTracker(this); appWidth = getSize().width; appHeight = getSize().height; animation = new Animation(this); handleParams(); animation.init(); addMouseListener(this); Thread me = Thread.currentThread(); me.setPriority(Thread.MIN_PRIORITY); userPause = false; } public void destroy() { removeMouseListener(this); } 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(); } /** * Run the animation. This method is called by class Thread. * @see java.lang.Thread */ public void run() { Thread me = Thread.currentThread(); if (animation == null) return; if (animation.frames == null) return; if ((appWidth <= 0) || (appHeight <= 0)) return; try { animation.startPlaying(); while (engine == me) { // Get current frame and paint it, play its sound AnimationFrame thisFrame = (AnimationFrame) animation.frames.get(animation.currentFrame); repaint(); if (thisFrame.sound != null) thisFrame.sound.play(); animation.currentFrame++; // Check if we are done if (animation.currentFrame >= animation.frames.size()) { if (animation.repeat) animation.currentFrame = 0; else return; } // Pause for duration or longer if user paused try { Thread.sleep(thisFrame.duration); synchronized(this) { while (userPause) { animation.stopPlaying(); wait(); } } } catch (InterruptedException e) { } } } finally { synchronized(this) { if (engine == me) animation.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 (animation.startUpImage != null) { if (animation.tracker.checkID(STARTUP_ID)) { if (animation.backgroundColor != null) { g.setColor(animation.backgroundColor); g.fillRect(0, 0, appWidth, appHeight); } g.drawImage(animation.startUpImage, 0, 0, this); } } else { if ((animation.backgroundImage != null) && (animation.tracker.checkID(BACKGROUND_ID))) g.drawImage(animation.backgroundImage, 0, 0, this); else g.clearRect(0, 0, appWidth, appHeight); } } else { animation.paint(g); } } /** * Start the applet by forking an animation thread. */ public void start() { engine = new Thread(this); engine.start(); showStatus(getAppletInfo()); } /** * Stop the insanity, um, applet. */ public synchronized void stop() { engine = null; animation.stopPlaying(); if (userPause) { userPause = false; notify(); } } /** * 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 synchronized void mousePressed(MouseEvent event) { event.consume(); if ((event.getModifiers() & InputEvent.SHIFT_MASK) != 0) { showDescription(); return; } else if (hrefURL != null) { // Let mouseClicked handle this. return; } else if (loaded) { userPause = !userPause; if (!userPause) { animation.startPlaying(); notifyAll(); } } } public void mouseClicked(MouseEvent event) { if ((hrefURL != null) && ((event.getModifiers() & InputEvent.SHIFT_MASK) == 0)) { // Note: currently ignored by appletviewer getAppletContext().showDocument(hrefURL, hrefTarget); } showStatus(getAppletInfo() + " -- " + userInstructions); } public void mouseReleased(MouseEvent event) { } public void mouseEntered(MouseEvent event) { } public void mouseExited(MouseEvent event) { } } /** * A class that represents an animation to be displayed by the applet */ class Animation extends Object { static final int STARTUP_ID = 0; static final int BACKGROUND_ID = 1; static final int ANIMATION_ID = 2; static final String imageLabel = "image"; static final String soundLabel = "sound"; int globalPause = 1300; // global pause in milleseconds List frames = null; // List holding frames of animation int currentFrame; // Index into images for current position Image startUpImage = null; // The startup image if any Image backgroundImage = null; // The background image if any AudioClip soundTrack = null; // The soundtrack for this animation Color backgroundColor = null; // Background color if any URL backgroundImageURL = null; // URL of background image if any URL startUpImageURL = null; // URL of startup image if any URL soundTrackURL = null; // URL of soundtrack URL imageSource = null; // Directory or URL for images URL soundSource = null; // Directory or URL for sounds boolean repeat; // Repeat the animation if true Image offScrImage; // Offscreen image Graphics offScrGC; // Offscreen graphics context MediaTracker tracker; // MediaTracker used to load images Animator owner; // Applet that contains this animation Animation(Animator container) { super(); owner = container; } void init() { tracker = new MediaTracker(owner); currentFrame = 0; loadAnimationMedia(); } void setGlobalPause(int pause) { globalPause = pause; } /** * Loads the images and sounds involved with this animation */ void loadAnimationMedia() { URL badURL; boolean error; try { if (startUpImageURL != null) { owner.tellLoadingMsg(startUpImageURL, imageLabel); startUpImage = fetchImageAndWait(startUpImageURL, STARTUP_ID); if (tracker.isErrorID(STARTUP_ID)) { owner.loadError(startUpImageURL, "start-up image"); } owner.repaint(); } if (backgroundImageURL != null) { owner.tellLoadingMsg(backgroundImageURL, imageLabel); backgroundImage = fetchImageAndWait(backgroundImageURL, BACKGROUND_ID); if (tracker.isErrorID(BACKGROUND_ID)) owner.loadError(backgroundImageURL, "background image"); owner.repaint(); } // Fetch the animation frame images Iterator iterator = frames.iterator(); while(iterator.hasNext()) { AnimationFrame frame = (AnimationFrame) iterator.next(); owner.tellLoadingMsg(frame.imageLocation, imageLabel); frame.image = owner.getImage(frame.imageLocation); tracker.addImage(frame.image, ANIMATION_ID); try { tracker.waitForID(ANIMATION_ID); } catch (InterruptedException e) {} } if (soundTrackURL != null && soundTrack == null) { owner.tellLoadingMsg(soundTrackURL, imageLabel); soundTrack = owner.getAudioClip(soundTrackURL); if (soundTrack == null) { owner.loadError(soundTrackURL, "soundtrack"); return; } } // Load the sounds into their frames iterator = frames.iterator(); while(iterator.hasNext()) { AnimationFrame frame = (AnimationFrame) iterator.next(); if (frame.soundLocation != null) { owner.tellLoadingMsg(frame.soundLocation, soundLabel); try { frame.sound = owner.getAudioClip(frame.soundLocation); } catch (Exception ex) { owner.loadError(frame.soundLocation, soundLabel); } } } owner.clearLoadingMessage(); offScrImage = owner.createImage(owner.appWidth, owner.appHeight); offScrGC = offScrImage.getGraphics(); offScrGC.setColor(Color.lightGray); owner.loaded = true; error = false; } catch (Exception e) { error = true; e.printStackTrace(); } } /** * Fetch an image and wait for it to come in. Used to enforce a load * order for background and startup images. */ Image fetchImageAndWait(URL imageURL, int trackerClass) throws InterruptedException { Image image = owner.getImage(imageURL); tracker.addImage(image, trackerClass); tracker.waitForID(trackerClass); return image; } /** * Stuff a range of image names into a List * @return a List of image URLs. */ void prepareImageRange(int startImage, int endImage, String pattern) throws MalformedURLException { frames = new ArrayList(Math.abs(endImage - startImage) + 1); if (pattern == null) pattern = "T%N.gif"; if (startImage > endImage) { for (int i = startImage; i >= endImage; i--) { AnimationFrame frame = new AnimationFrame(); frames.add(frame); frame.duration = globalPause; frame.imageLocation = new URL(imageSource, doSubst(pattern, i+"")); } } else { for (int i = startImage; i <= endImage; i++) { AnimationFrame frame = new AnimationFrame(); frames.add(frame); frame.duration = globalPause; frame.imageLocation = new URL(imageSource, doSubst(pattern, i+"")); } } } /** * 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. */ void parseSounds(String attr) throws MalformedURLException { int frameIndex = 0; int numFrames = frames.size(); for (int i = 0; (i < attr.length()) && (frameIndex < numFrames); ) { int next = attr.indexOf('|', i); if (next == -1) next = attr.length(); String sound = attr.substring(i, next); if (sound.length() != 0) { AnimationFrame frame = (AnimationFrame) frames.get(frameIndex); frame.soundLocation = new URL(soundSource, sound); } i = next + 1; frameIndex++; } } /** * Parse the IMAGES parameter. It looks like * 1|2|3|4|5, etc., where each number (item) names a source image. */ void parseImages(String attr, String pattern) throws MalformedURLException { frames = new ArrayList(); if (pattern == null) pattern = "T%N.gif"; for (int i = 0; i < attr.length(); ) { int next = attr.indexOf('|', i); if (next == -1) next = attr.length(); String file = attr.substring(i, next); AnimationFrame frame = new AnimationFrame(); frames.add(frame); frame.imageLocation = new URL(imageSource, doSubst(pattern, file)); frame.duration = globalPause; i = next + 1; } } /** * 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. */ void parseDurations(String attr) { int imageNum = 0; int numImages = frames.size(); for (int i = 0; (i < attr.length()) && (imageNum < numImages); ) { int next = attr.indexOf('|', i); if (next == -1) next = attr.length(); AnimationFrame aFrame = (AnimationFrame) frames.get(imageNum); if (i != next) { int duration = Integer.parseInt(attr.substring(i, next)); aFrame.duration = duration; } i = next + 1; imageNum++; } } /** * 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. */ void parsePositions(String param) throws ParseException { int imageNum = 0; int numImages = frames.size(); for (int i = 0; (i < param.length()) && (imageNum < numImages); ) { int next = param.indexOf('|', i); if (next == -1) next = param.length(); if (i != next) { AnimationFrame frame = (AnimationFrame) frames.get(imageNum); frame.position = parsePoint(param.substring(i, next)); } i = next + 1; imageNum++; } } /** * 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. * %, for example %5 - substitute the integer left-padded with * zeros to digits wide. * %% - substitute a '%' here. * @param inStr the String to substitute within * @param theInt the int to substitute, as a String. */ String doSubst(String inStr, String 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' || 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(); } void startPlaying() { if (soundTrack != null) { soundTrack.loop(); } } void stopPlaying() { if (soundTrack != null) { soundTrack.stop(); } } public void paint(Graphics g) { int xPos = 0; int yPos = 0; if ((frames.size() > 0) && tracker.checkID(ANIMATION_ID) && (offScrGC != null)) { AnimationFrame frame = (AnimationFrame) frames.get(currentFrame); Image image = frame.image; if (backgroundImage == null) { offScrGC.clearRect(0, 0, owner.appWidth, owner.appHeight); } else offScrGC.drawImage(backgroundImage, 0, 0, owner); Point pos = null; if (frame.position != null) { xPos = frame.position.x; yPos = frame.position.y; } if (backgroundColor != null) { offScrGC.setColor(backgroundColor); offScrGC.fillRect(0, 0, owner.appWidth, owner.appHeight); offScrGC.drawImage(image, xPos, yPos, backgroundColor, owner); } else { offScrGC.drawImage(image, xPos, yPos, owner); } if (offScrImage != null) g.drawImage(offScrImage, 0, 0, owner); } } } /** * Instances of this class represent a single frame of an animation * There can be an image, sound, and position associated with each frame */ class AnimationFrame extends Object { static final String imageLabel = "image"; static final String soundLabel = "sound"; URL imageLocation = null; // Directory or URL of this frames image URL soundLocation = null; // Directory or URL of this frames sound int duration; // Duration time for this frame in milliseconds AudioClip sound; // Sound associated with this frame object Image image; // Image associated with this frame Point position; // Position of this frame } /** * ParseException: signals a parameter parsing problem. */ class ParseException extends Exception { ParseException(String s) { super(s); } } /** * DescriptionFrame: implements a pop-up "About" box. */ class DescriptionFrame extends Frame implements ActionListener { static final int rows = 27; static final int cols = 70; TextArea info; Button cancel; DescriptionFrame() { super("Animator v1.10"); add("Center", info = new TextArea(rows, cols)); info.setEditable(false); info.setBackground(Color.white); Panel buttons = new Panel(); add("South", buttons); buttons.add(cancel = new Button("Cancel")); cancel.addActionListener(this); pack(); } public void show() { info.select(0,0); super.show(); } void tell(String s) { info.append(s); } public void actionPerformed(ActionEvent e) { setVisible(false); } }