Animation How animation is done • The illusion of movement is created by displaying a series of images, each slightly different – The eye fills in the gaps – More images per second makes smoother animation • 12 images per second is sometimes “good enough” • 20 images per second (one every 50 ms) is quite good – The computer has to work to compute each new image • For complex images, such as most visually exciting computer games, this is an extremely difficult problem • For simple animations, modern computers generally don’t have any problems – That is, if you don’t do anything too stupid Tweaking • When developing an animation, you often have several parameters (time, x and y distances, etc.) that you need to experiment with – It’s a nuisance to recompile your program each time – Instead, pass in parameters • You can pass parameters to both applets and applications – For an application, each parameter comes in as a String – public static void main(String args[]) – This is just an array of Strings; use it in the obvious way Passing parameters to applets • In the HTML file, – <applet code="MyApplet.class" width="250" height="60"> <param name="delay" value="50"> <param name="version" value="Version 1.0"> </applet> • In the Applet code: – String version = getParameter("version"); – int delay = Integer.valueOf(getParameter("delay")).intValue(); • Bounce Applet Model-View-Controller • The MVC Design Pattern is usually an excellent choice for doing animations – The Controller handles user input, and controls the Model • The best way to do this is to call methods in the Model – The Model does the computational work; it should be independent of the other classes – The View examines the state of the Model and displays the results on the screen • For a “pure” animation, sometimes the View is in charge, at least to the extent of telling the Model when to make a “step” • Controller.html Example Model I: Bouncing ball • class Model { final int BALL_SIZE = 20; int xPosition = 0; int yPosition = 0; int xLimit, yLimit; int xDelta = 4; int yDelta = 3; } void makeOneStep( ) { ...} Example Model II: Bouncing ball void makeOneStep() { xPosition += xDelta; if(xPosition < 0 || xPosition >= xLimit) { xDelta = -xDelta; xPosition += xDelta; } yPosition += yDelta; if(yPosition < 0 || yPosition >= yLimit) { yDelta = -yDelta; yPosition += yDelta; } } Example: View.run() public void run() { // we set "alive" false when applet is destroyed while(alive) { // we set okToRun true if the Applet is started if(okToRun) { model.makeOneStep(); repaint(); } // We use "delay" to control the speed of execution try { Thread.sleep(delay); } catch(InterruptedException e) {} } } Example: View.paint() • public void paint(Graphics g) { // Erase previous ball g.setColor(Color.white); g.fillRect(oldX, oldY, model.BALL_SIZE, model.BALL_SIZE); // Draw new ball and remember its position g.setColor(Color.red); g.fillOval(model.xPosition, model.yPosition, model.BALL_SIZE, model.BALL_SIZE); oldX = model.xPosition; oldY = model.yPosition; } How repainting is actually done • • • Remember, you write paint but you call repaint Applets inherit an update(Graphics g) method When you call repaint(), Java really calls • What the inherited version of update does is: update(Graphics g) 1. Erases the Applet (set it to the background color) 2. Call your paint(Graphics g) method • • Your gets a nice, clean background to draw on, but erasing and drawing may cause flicker You can override update(Graphics g) and erase only the part that needs erasing Example: View.update(Graphics g) public void update(Graphics g) { // Erase anything that needs erasing, then... paint(g); } Backgrounds • To use a background, simply paint it before you paint any objects in the foreground: void background(Graphics g) { int width = getSize().width; int height = getSize().height; int squareSize = 2 * model.BALL_SIZE; g.setColor(Color.blue); g.fillRect(0, 0, width, height); g.setColor(Color.green); for (int x = 0; x < width; x += 2 * squareSize) { for (int y = 0; y < height; y += 2 * squareSize) { g.fillRect(x, y, squareSize, squareSize); g.fillRect(x + squareSize, y + squareSize, squareSize, squareSize); } } } Example: View.update(Graphics g) public void update(Graphics g) { paintBackground(g); paint(g); } public void paint(Graphics g) { // Draw new ball g.setColor(Color.red); g.fillOval(model.xPosition, model.yPosition model.BALL_SIZE, model.BALL_SIZE); } Controller Applet Flicker, again • Flicker occurs because you are trying to change the display at the same time your computer is trying to show you the display • The secret is to get the display ready first, then give the computer the new, completed display • Instead of painting onto the same Graphics g that the computer is using, – Create a new, “offscreen” Graphics – Do your work there – Move it to g when it's all painted and ready to be seen • This technique is called double buffering Double buffering • public void update(Graphics g) { Image offscreenImage = null; Graphics offscreenGraphics = null; // Create the offscreen Graphics if (offscreenImage == null) { offscreenImage = createImage(getSize().width, getSize().height); offscreenGraphics = offscreenImage.getGraphics(); } // Paint into the offscreen Graphics background(offscreenGraphics); paint(offscreenGraphics); // Copy the offscreen onto the screen g.drawImage(offscreenImage, 0, 0, null); } • Controlling flicker Controls • User controls have to be handled in a separate Thread from the animation • All the usual techniques for working with Threads apply • Adding user controls Using images • Good news: Displaying images is easy g.drawImage(controller.skull, model.xPosition, model.yPosition, null); • Bad news: Loading images into memory is slightly complicated • Here’s how: – – – – – Create a new MediaTracker object Get the URLs for your images Start loading your images Tell the MediaTracker about each image Tell the MediaTracker to wait until all images are loaded Example: loadImages() I void loadImages() { // Create a MediaTracker MediaTracker tracker = new MediaTracker(this); // Declare variables for the URLs URL appletLocation = getCodeBase(); URL imageURL = null; Example: loadImages() II • try { // Start loading the graveyard image imageURL = new URL(appletLocation + "/images/boothill-blue.jpg"); graveyard = getImage(imageURL); tracker.addImage(graveyard, 0); // Start loading the skull image imageURL = new URL(appletLocation + "/images/skull-1.gif"); skull = getImage(imageURL); tracker.addImage(skull, 0); } catch (MalformedURLException e) { e.printStackTrace(); } Example: loadImages() III • // Wait for the images to be loaded // (should do error checking, but I didn’t) try { tracker.waitForAll(5000); } catch (InterruptedException e) {} } // end of method • Using Images Animating individual elements I • To make an individual image “do something” we need a series of images – Examples: A bird that flaps its wings, a rotating skull • The more images, the smoother the motion. • There are two ways to animate an individual image: – Use an animated GIF • You only need to load and use the one image • Your program can't control the speed of the animation – Use a series of GIFs or JPGs • You have lots of separate files and images • You have full control over the animation Animating individual elements II for (int i = 0; i < 25; i++) { skullURL = new URL(appletLocation + "/images/t-skull-" + i + ".gif"); skull[i] = getImage(skullURL); tracker.addImage(skull[i], 0); } skullNumber = (skullNumber + 1) % 24; g.drawImage(controller.skull[skullNumber], model.xPosition, model.yPosition, null); Animating elements Multiple images Listening to the mouse • Your program can detect (and use) mouse events • This doesn’t really have anything to do with animation, but it can be used along with animation • view.addMouseListener(myMouseAdapter); • class myMouseAdapter extends MouseAdapter { public void mousePressed(MouseEvent event) {...} public void mouseReleased(MouseEvent event) {...} } • view.addMouseMotionListener(myMouseDragger); • class myMouseDragger extends MouseMotionAdapter) { public void mouseDragged(MouseEvent event) {...} } Reacting to mouse clicks • public void mousePressed(MouseEvent event) { // Get mouse position int mouseX = event.getX(); int mouseY = event.getY(); // Check whether the mouse is inside the skull if (mouseX >= model.xPosition && mouseX <= model.xPosition +model.skullWidth && mouseY >= model.yPosition && mouseY <= model.yPosition +model.skullHeight) { draggingSkull = true; } } • public void mouseReleased(MouseEvent event) { draggingSkull = false; draggingSkeleton = false; } Responding to mouse drags • public void mouseDragged(MouseEvent event) { // Get mouse position int mouseX = event.getX(); int mouseY = event.getY(); if (draggingSkull) { // Center skull over mouse position model.xPosition = mouseX – (model.skullWidth / 2); model.yPosition = mouseY - (model.skullHeight / 2); } • Mouse control The End