/* Developed by Erez Kaplan April-97 Email: calclist@webcom.com Web site: http://webcom.com/~calc Applet: http://webcom.com/~calc/applets/felt Applet code: http://webcom.com/~calc/applets/felt/felt_code.java Applet photos:http://webcom.com/~calc/applets/felt/felt.jpeg Applet HTML: http://webcom.com/~calc/applets/felt/welcome.html Developed on Mac Peforma 6360, Java 1.02 ====== Prolog: ====== Having a large "virtual museum" displaying my collection of "Mechanical Calculating Machines", I have always wanted to give users the opportunity to operate such an historical machine "on-line". The appearance of Java supplied me with a great tool to do this. The idea is simple. Put as the background the photo of the machine, and enable the user to operate it with the aid of animation and sound. To ensure realism, no graphics are used, everything is photographed. ================== Java Documentation: ================== ================================ Background and assumptions: ================================ This is my first attempt at writing a Java applat, but I think it came out quit well. I will not discuss here things such as sound and cursor manipulation which are well documented in Java books and on the Web. How ever I will largely discuss 2 areas which I think Java books and the Web offer poor solutions to: 1) Initial loading of a large background image. 2) Initial loading of animation frames. Though my solutions may not be elegant, they work like MAGIC, since I believe practice is more important than theory. (Well maybe that's the way I was brought up in Israel (;-) Trial and error was my motto, and tough it calls for more work than cut & paste, the rewards for finding new ways of implementing things are great both in the results and in the feeling of accomplishment. Before commencing development, I check around the Web to see what other applets have to offer, and it was not much. Most of them take ages to load, and when (and if) they finally appear the performance is poor. This brought me to the notion that most applet developers do not consider the fact that it needs to run over the Web. Developing and testing at home is very nice, but results over the Web are poor. Further more testing the applet when the images are already in the chache is completely wrong. The user will NOT run this way, you must run every test with the cache clear of images and Java code. Better more, upload your code and images to the Web, and test it over a slow connection. Then find as many different stations as you can (Win 95, Win NT, Mac, UNIX) and test it with NetScape and Explorer. As a starting point I designed this applet based against an 'average' home user : 1) Monitor 14' or 15' 2) Resolution: 600-800 or 1024-768 3) Number of colors: 256 or thousands 4) Modem speed 14,400 or 28,800. (The applet is part of a hobby and the users would be typical home users, and not commercial users which work over ISDN) ================================ Initial display: ================================ The next target was simple. The user must get immediate display on the screen before loading the applet. This will give him something to read while the applet is loading. There is no problem in having text + images appear on the page before the applet is loaded. (Examine the HTML code at:) http://webcom.com/~calc/applets/felt/welcome.html ================================ Loading the image: ================================ Most applets use the MediaTracker and wait for the image to load. MediaTracker.addImage(bg_pic, 0); MediaTracker.waitForID(0); This may look very nice in theory, but in practice it's not useful, since you loose control during this time. (It's very nice to say "please wait for 10 minutes..." but why not show the image while it is loading? Other applets just draw the image while it is loading, this to gives poor performance since the rendering of each line at a time results in a grater time than loading the entire image. That's where I came with the following solution. Consider this code from pain() //================================================================= public void paint( Graphics g ) { //================================================================= if (First) { showStatus("Loading images..."); if (!g.drawImage(bg_pic, 10, 10, this)) { try { Thread.sleep(1000);} catch (InterruptedException e) { } return; } First = false; ...... } 1) MediaTracker is not used. 2) The image is displayed as soon as the applet is loaded, and if loading isn't completed a sleep of 1 second is implemented. (control will automatically come back to paint a second later) 3) The image begins to display immediately and is updated every second while it's loading, until completion. ==================== Animation preparation: ==================== So, here comes the fun part. I really enjoyed every minute of work involved. I used a Sony Video camera connected via an Apple Video System to my Performa 6360. The Animation uses only 6 frames, which are displayed once in regular order and then backwards in reverse order. The camera was located on a tripod above the machine. I first used a simple "frame grab" to get the full picture. Next I tied a long thin transparent fishing string to the handle of the machine. After shouting ACTION, I rolled the camera and gently pulled the string. This time I grabbed the full video movie. (Note: the camera was not moved at all during all this operation) Using "Avid" Videoshop, I cropped the area of the handle, and by using "cut & paste" I grabbed each frame and transported it into PhotoShop. =========================== Animation loading and display: ============================ Once again, loading and display time is critical. Begging your pardon, but the books, including Sun's example are completely wrong in expelling how to do this with each frame being loaded via a file of it's own. This is CRAZY, if your remember you must be Web aware. It may work fine on your local environment but over the Web the overhead for loading one file can vary from 3 -15 seconds, so if you have 24 frames for a 1 second animation, it can take up to six minutes. No sir, I decided there must be a better and faster way to do this, and there is!!!. I located all the frames in one JPEG file, side by side, with a small gap between then. (This may take some extra work, but the results are worth it) Consider this simple code which loads all of the frames from one file. ... full_pic = getImage(getCodeBase(), "felt.jpeg"); animate = new Image[frameNums]; ... //================================================================= void loadAnimation() { //================================================================= for (int ctr = 0; ctr < frameNums; ctr++){ animate[ctr] = createImage(new FilteredImageSource(full_pic.getSource(), new CropImageFilter(ctr * aniGap + 1 ,0,aniWidth,aniHight))); } } CropImageFilter does the trick and it's great !!! PS - for the same price I included the full background photo and the frames in one file, JPEG, 39 KB. You can see the original frames and background photo in one file at: http://webcom.com/~calc/applets/felt/felt.jpeg Note: Animated Gif. I considered using Animated Gif however Java 1.02 does not support it and you loose the ability to reduce file size using JPEG format. Well, that's it, hope you enjoy what I have done, and found it useful. */ import java.awt.*; import java.awt.Frame; import java.applet.Applet; import java.applet.AudioClip; import java.lang.Math; import java.lang.Boolean; import java.awt.image.*; //================================================================= public class felt extends Applet //================================================================= { public static Image bg_pic; public static Image down_pic; public static Image tmp_pic; public static Image full_pic; public static Image first_pic; public static Image animate_bg; public static Image animate[]; public static String last_val[]; AudioClip click_au, bell_au; // public static Applet my_app; public static boolean crop = false; public static boolean First = true; public static boolean debug = false; public static boolean Zero = false; public static boolean Click = false; public static int displayVal = 0; public static int lastVal =9; public static int currX =0; public static int currY =0; public static final int frameNums =6; public static final int aniWidth = 36; public static final int aniHight = 142; public static final int aniGap = 40; public static Frame my_frame; //================================ void main(String argv[]){ init(); } //================================================================= public void init (){ //================================================================= animate = new Image[frameNums]; last_val = new String[9]; for (int ctr = 0; ctr < 9 ; ctr++) last_val[ctr] = "U"; my_frame = getFrame (); String param1 = getParameter("DebugF"); if (param1 != null) if (param1.equals("true")) { debug = true; } else debug = false; showStatus("Loading images..."); full_pic = getImage(getCodeBase(), "felt.jpeg"); bg_pic = createImage(new FilteredImageSource(full_pic.getSource(), new CropImageFilter(0,156,250,350))); first_pic = createImage(new FilteredImageSource(bg_pic.getSource(), new CropImageFilter(210,10,40,150))); ImageFilter imgfilter = new CropImageFilter(270,200,18,18); down_pic = createImage(new FilteredImageSource(bg_pic.getSource(),imgfilter)); loadAnimation(); click_au = getAudioClip(getDocumentBase(), "Launcher.au"); bell_au = getAudioClip(getDocumentBase(), "bell.au"); resize(270,370); First = true; // my_app = this; setBackground(new Color(255,206,49)); } //================================================================= void loadAnimation() { //================================================================= for (int ctr = 0; ctr < frameNums; ctr++){ animate[ctr] = createImage(new FilteredImageSource(full_pic.getSource(), new CropImageFilter(ctr * aniGap + 1 ,0,aniWidth,aniHight))); } } //================================================================= void playAnimation(Graphics g) { //================================================================= if (debug) System.out.println("playAnimation========"); bell_au.play(); for (int ctr = 0; ctr < frameNums; ctr++){ g.drawImage(animate[ctr], 221, 23, this); try { Thread.sleep(100);} catch (InterruptedException e) { break; } } bell_au.play(); drawResult(0, g); for (int ctr = frameNums-1; ctr >= 0; ctr--){ g.drawImage(animate[ctr], 221, 23, this); try { Thread.sleep(100);} catch (InterruptedException e) { break; } } // redraw original 'top' frame g.drawImage(first_pic, 220, 20, this); } //================================================================= public void paint( Graphics g ) { //================================================================= //if (debug) //System.out.println("from Paint"); if (First) { showStatus("Loading images..."); if (!g.drawImage(bg_pic, 10, 10, this)) { try { Thread.sleep(1000);} catch (InterruptedException e) { } return; } showStatus("Click on the keys"); drawResult(displayVal, g); if (debug) System.out.println("first draw"); First = false; } if (Zero){ playAnimation(g); Zero = false; return; } if (Click){ // g.drawImage(down_pic, currX-7, currY-7, this); //for (int x=1; x<2; x++ ) { // try { Thread.sleep(100);} // catch (InterruptedException e) { break; } // } // g.drawImage(tmp_pic, currX-7, currY-7, this); Click = false; } if (lastVal != displayVal){ drawResult(displayVal, g); lastVal = displayVal; } } //------------------ public void update(Graphics g){ //------------------ if (debug) System.out.println("update"); paint(g); } //------------------ public int clickVal( int x, int y) { //------------------ if (debug) System.out.println("clickVal"); int xVal = 8 - (int)(Math.round(((x-48.0) / 20.0))); if (debug) System.out.println("xVal = " + xVal); int yVal = 10 - (int)(Math.round(((y-73.0) / 18.0))); if (debug) System.out.println("yVal = " + yVal); double xD = xVal; return ( (int)Math.pow(10,xVal) * yVal); } //------------------ public boolean mouseDown(Event evt, int x, int y) { //------------------ int retVal = 0; currX = x; currY = y; Zero = false; Click = false; if (debug) { System.out.println("mouseDown"); System.out.println(x); System.out.println(y); } // user has pressed the RESET keys if (inReset(x,y)){ displayVal = 0; // bell_au.play(); if (debug) System.out.println("RESET"); Zero = true; repaint(); return(true); } // click not in selectable area, do nothing if (!inKeys(x,y)) return(true); // cropPicture(x, y); retVal = clickVal(x, y); if (retVal >= 0 ) { displayVal += retVal; if (debug) System.out.println("retVal = " + retVal); click_au.play(); Click = true; repaint(); } return(true); } //------------------ public int drawResult(int result, java.awt.Graphics g ) { //------------------ String val = String.valueOf(result); int len = val.length(); String one = new String(); int tmp_val, tmp_result; tmp_result = result; int limit = Math.min(9, len); for (int ctr = 0 ; ctr < limit; ctr++) { tmp_val = tmp_result - ((tmp_result / 10) *10) ; tmp_result = tmp_result / 10; one = String.valueOf(tmp_val); drawOneNumber(one, ctr, g ); } for (int ctr = len ; ctr < 9; ctr++) drawOneNumber("0", ctr, g ); return(0); } //------------------ public boolean handleEvent(Event evt){ //------------------ if (debug) System.out.println("EVENT !!!!! = " + evt.id); // redraw screen incase foucs was changed // System.out.println("mouseCord=" + evt.x + " "+ evt.y); switch (evt.id) { case Event.MOUSE_ENTER: case Event.LOST_FOCUS: case Event.GOT_FOCUS: { First = true; // System.out.println("EVENT restart"); repaint(); return(true); } case Event.MOUSE_MOVE: { if (inReset(evt.x, evt.y)) { if (my_frame != null ) my_frame.setCursor(Frame.HAND_CURSOR); } else if (inKeys(evt.x, evt.y)){ if (my_frame != null ) my_frame.setCursor(Frame.HAND_CURSOR); } else { if (my_frame != null ) my_frame.setCursor(Frame.DEFAULT_CURSOR); } return(true); } } return(super.handleEvent(evt)); } //------------------ boolean inReset(int x, int y){ //------------------ if ((x > 216) && (y > 26) && (x < 256) && (y < 118)) return(true); else return(false); } //------------------ boolean inKeys(int x, int y){ //------------------ if ((x < 64) || (y < 85) || (x > 215) || (y > 245)) return(false); int xVal = x; xVal -= 48; int xDiv = xVal / 20; int xDif = xVal - (xDiv * 20); if (Math.abs(10 - xDif) < 4 ) return(false); int yVal = y; yVal -= 73; int yDiv = yVal / 18; int yDif = yVal - (yDiv * 18); if (Math.abs(10 - yDif) < 4 ) return(false); return(true); } //------------------ int drawOneNumber(String number, int pos, java.awt.Graphics g ) { //------------------ int xPos; int yPos; // draw number only if value changed or redraw if (( last_val[pos].equals(number) ) && (!First)) return(0); last_val[pos] = number; xPos = 200 - (pos * 20); //yPos = 383 + pos*2; yPos = 270 - (int)(pos * 0.75); g.fillRoundRect(xPos, yPos, 12, 12, 12, 12); g.setColor(Color.white); g.drawString(number, xPos + 3, yPos + 9); g.setColor(Color.black); return(0); } //------------------ int cropPicture(int x, int y) { //------------------ ImageFilter imgfilter = new CropImageFilter(x-17,y-17,18,18); tmp_pic = createImage(new FilteredImageSource(bg_pic.getSource(),imgfilter)); return(0); } //================================ private Frame getFrame () //================================ { for (Container parent = getParent (); parent != null; parent = parent.getParent ()) if (parent instanceof Frame) return (Frame) parent; return null; } }