Scientific Programming II

ModSim

Graphics using Applets


An applet is a Java program designed to be run in a web browser. Originally, they were designed to use the older AWT and Swing GUI APIs, so we will be touching on these as well. Mostly, we are using applets as a way to easily do animations. JavaFX is primarily designed for GUI applications with perhaps dynamic content, but not much animation (though it is possible).

First, let's write a simple applet. This applet will simulate a screensaver, by drawing lines of random size, location, and color every 500 milliseconds until 100 lines have been drawn. The screen is then erased and the process repeated.

package edu.govschool;

import java.awt.*;
import java.awt.image.*;
import javax.swing.*;

public class ScreenSaver extends JApplet
{
    // This will store our temporary image as we draw
    private BufferedImage image;
    // To set how frequently we update our image
    private Timer timer;
    private int count = 0;
    
    @Override
    public void init()
    {
        // Set the overall size of the applet. This should be the first
        // line in init()
        setSize(800, 800);
        
        // We're creating a image buffer to draw RGB into.
        // This allows us to prepare a bunch of re-draws, then
        // all at once update the screen.
        image = new BufferedImage(getWidth(), getHeight(), 
                                    BufferedImage.TYPE_INT_RGB);
        
        // Our Timer will set how frequently we update, in milliseconds
        timer = new Timer(500, e -> updateScreen());
        timer.start();
        
        // Do our initial drawing. First, we need a reference to the
        // Graphics of the image, onto which we draw. Nothing is actually
        // shown on the screen, however, until the end of init() or when
        // repaint() is called.
        Graphics g = image.getGraphics();
        g.setColor(Color.LIGHT_GRAY);
        g.fillRect(0, 0, image.getWidth(), image.getHeight());
    }
    
    // This is our 'loop' method. This is what will be called on each screen
    // update.
    public void updateScreen()
    {
        // As before, we need a reference to the Graphics of the image
        // to draw on it.
        Graphics g = image.getGraphics();
        
        // Colors can be represented in several ways on a computer.
        // One of the most common is RGBa, or just RGB. This represents
        // colors based on their components of red, blue, and green.
        int r = (int) (Math.random() * 255);
        int gr = (int) (Math.random() * 255);
        int b = (int) (Math.random() * 255);
        
        // We need a random start and random end coordinate
        int x1 = (int) (Math.random() * getWidth());
        int y1 = (int) (Math.random() * getHeight());
        int x2 = (int) (Math.random() * getWidth());
        int y2 = (int) (Math.random() * getHeight());
        
        // Set the color
        g.setColor(new Color(r, gr, b));
        // Draw the line in our buffer, but not on the screen.
        g.drawLine(x1, y1, x2, y2);
        // Increment our count
        count++;
        
        // Erase the screen if we've drawn 100 lines
        if (count % 100 == 0) {
            g.setColor(Color.LIGHT_GRAY);
            g.fillRect(0, 0, getWidth(), getHeight());
        }
        // This calls the paint() method (strange, I know), which
        // actually puts the image buffer onto the screen.
        repaint();
    }
    
    @Override
    public void paint(Graphics g)
    {
        // Put whatever's in our image buffer on the screen.
        g.drawImage(image, 0, 0, this);
    }
}

Lines are pretty cool, but what if we wanted shapes? Well, luckily the Graphics class provides us with several methods to create shapes instead of just lines. Let's modify our applet to draw shapes instead.

package edu.govschool;

import java.awt.*;
import java.awt.image.*;
import java.util.Random;
import javax.swing.*;

public class ScreenSaver extends JApplet
{
    private BufferedImage image;
    private Timer timer;
    private int count = 0;
    private Random random = new Random();
    
    @Override
    public void init()
    {
        image = new BufferedImage(getWidth(), getHeight(), 
                                    BufferedImage.TYPE_INT_RGB);
        timer = new Timer(500, e -> updateScreen());
        timer.start();
        
        Graphics g = image.getGraphics();
        g.setColor(Color.LIGHT_GRAY);
        g.fillRect(0, 0, image.getWidth(), image.getHeight());
    }
    
    public void updateScreen()
    {
        Graphics g = image.getGraphics();
        int r = (int) (Math.random() * 255);
        int gr = (int) (Math.random() * 255);
        int b = (int) (Math.random() * 255);
        
        int x1 = (int) (Math.random() * getWidth());
        int y1 = (int) (Math.random() * getHeight());
        int x2 = (int) (Math.random() * getWidth());
        int y2 = (int) (Math.random() * getHeight());
        
        // Generate a number 1-4
        int toss = random.nextInt(4) + 1;
        
        g.setColor(new Color(r, gr, b));
        
        if (toss == 1) {
            // Draw a line from (x1, y1) to (x2, y2)
            g.drawLine(x1, y1, x2, y2);
        } else if (toss == 2) {
            // Draw a filled rectangle with a top-left corner
            // at (x1, y1) that's 50x70px (w/h)
            g.fillRect(x1, y1, 50, 70);
        } else if (toss == 3) {
            // Draw a filled oval with a top-left corner
            // at (x1, y1) that's 50x70px (w/h) (bounding box)
            g.fillOval(x1, y1, 50, 70);
        } else {
            // Draw a filled rectangle with a top-left corner
            // at (x1, y1) that's 50x70px and rounded corners
            // with an arc-width of 10px and arc-height of 
            // 20px
            g.fillRoundRect(x1, y1, 50, 70, 10, 20);
        }
        
        count++;
        
        if (count % 100 == 0) {
            g.setColor(Color.LIGHT_GRAY);
            g.fillRect(0, 0, getWidth(), getHeight());
        }
        repaint();
    }
    
    @Override
    public void paint(Graphics g)
    {
        g.drawImage(image, 0, 0, this);
    }
}

Neato. Finally, we'll see an example of displaying images in an applet. This applet loads an image and begins erasing pixels from it. Once over half of the pixels are gone, the entire image is erased

package edu.govschool;

import java.awt.*;
import java.awt.image.*;
import java.io.*;
import javax.imageio.*;
import javax.swing.*;

public class PixelErase extends JApplet
{
    private BufferedImage image;
    private Timer timer;
    private int count = 0, total = 0;
    
    @Override
    public void init()
    {
        // Creating a File object just like JavaFX
        File file = new File("/Users/bryce/NetBeansProjects/Applets/src/edu/govschool/compiles.jpg");
        
        try {
            // We have an API to read in a file in AWT
            image = ImageIO.read(file);
        } catch (IOException e) {}
        
        // setSize() is here because we're basing the size of the
        // entire applet on the size of the image, which we need
        // to load in first
        setSize(image.getWidth(), image.getHeight());
        
        // The lambda expression replaces an object implementing
        // the ActionListener interface
        timer = new Timer(100, e -> updateScreen());
        timer.start();
        total = image.getHeight() * image.getWidth();
    }
    
    public void updateScreen()
    {
        Graphics g = image.getGraphics();
        g.setColor(Color.RED);
        
        // Generate a random (x, y) bounded by the image size
        for (int i = 1; i < 5000; i++) {
            int x = (int) (Math.random() * image.getWidth());
            int y = (int) (Math.random() * image.getHeight());
            // Drawing a line with the same start and end coords
            // is like drawing one single pixel
            g.drawLine(x, y, x, y);
        }
        
        count += 5000;
        
        if (count > 0.5 * total) {
            g.setColor(Color.BLACK);
            g.fillRect(0, 0, image.getWidth(), image.getHeight());
        }
        
        repaint();
    }
    
    @Override
    public void paint(Graphics g)
    {
        g.drawImage(image, 0, 0, this);
    }
}

Let's start looking at more complex GUIs with applets. Just like JavaFX, AWT and Swing provide many layouts and components to put together like Legos into more complex applets. Many of the components are similar to their JavaFX cousins, but may have slightly different methods or usages.

package edu.govschool;

import java.awt.*;
import javax.swing.*;

public class AppletGUIExample extends JApplet
{
    private static JTextField field;
    private static JButton button;
    private static JLabel label;
    
    @Override
    public void init()
    {
        // A FlowLayout organizes objects in the order
        // that they are added to the applet
        setLayout(new FlowLayout());
        // Create a new JTextField 20 chars wide
        field = new JTextField(20);
        // Create a new JButton with the text "Click"
        button = new JButton("Click");
        // Create a new JLabel with the text "Enter
        // a sentence". Additionally, if the mouse is
        // hovered over the label, a tooltip appears.
        label = new JLabel("Enter a sentence");
        label.setToolTipText("Upper/lowercase letters");
        
        add(label);
        add(field);
        add(button);
        button.addActionListener(e -> btnClick());
    }
    
    /**
     * Just like JavaFX, we use event handlers to deal
     * with events. With Java 8, however, we can substitute
     * old school interfaces or anonymous classes with
     * lambdas!
     */
    private static void btnClick()
    {
        String text = field.getText();
        
        if (text.equals("")) {
            JOptionPane.showMessageDialog(null, "No sentence entered.");
        } else if (isPalindrome(text)) {
            JOptionPane.showMessageDialog(null, "That's no moon, that's a palindrome!");
        } else {
            JOptionPane.showMessageDialog(null, "This is not the palindrome you're looking for");
        }
        field.setText("");
    }
    
    /**
     * Converts a <code>String</code> to alphabetic characters
     * and whitespace only.
     * @param s the <code>String</code> to alphabetize
     * @return the alphabetized <code>String</code>
     */
    private static String toLetters(String s)
    {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            if (ch == ' ' || Character.isAlphabetic(ch)) {
                sb.append(ch);
            }
        }
        return sb.toString();
    }
    
    /**
     * Reverses the characters in a <code>String</code>.
     * @param s the <code>String</code> to reverse
     * @return the reversed <code>String</code>
     */
    private static String reverse(String s)
    {
        StringBuilder sb = new StringBuilder();
        for (int i = (s.length() - 1); i >= 0; i--) {
            sb.append(s.charAt(i));
        }
        return sb.toString();
    }
    
    /**
     * Tests if the input <code>String</code> is a palindrome.
     * A palindrome is defined as the same characters forwards
     * and backwards
     * @param s a <code>String</code> to test
     * @return <code>true</code> if the <code>String</code> is a palindrome
     */
    private static boolean isPalindrome(String s)
    {
        String sLetters = toLetters(s);
        String sRLetters = toLetters(reverse(s));
        return sLetters.equalsIgnoreCase(sRLetters);
    }
}

Applets can get as complex as you want! Components can even be used in different event handlers at different times. In the next example, the code from above is kept, but a new set of components added to display the same words within two sentences, reusing the first JTextField.

package edu.govschool;

import java.awt.*;
import java.util.LinkedList;
import javax.swing.*;

public class AppletGUIExample2 extends JApplet
{
    private static JTextField field1, field2;
    private static JButton button1, button2;
    private static JLabel label1, label2;
    
    @Override
    public void init()
    {
        // A FlowLayout organizes objects in the order
        // that they are added to the applet
        setLayout(new FlowLayout());
        // Create a new JTextField 20 chars wide
        field1 = new JTextField(20);
        // Create a new JButton with the text "Click"
        button1 = new JButton("Click");
        // Create a new JLabel with the text "Enter
        // a sentence". Additionally, if the mouse is
        // hovered over the label, a tooltip appears.
        label1 = new JLabel("Enter a sentence");
        label1.setToolTipText("Upper/lowercase letters");
        
        label2 = new JLabel("Enter a second sentence");
        label2.setToolTipText("Upper/lowercase letters");
        field2 = new JTextField(20);
        button2 = new JButton("Same words");
        
        add(label1);
        add(field1);
        add(button1);
        add(label2);
        add(field2);
        add(button2);
        button1.addActionListener(e -> btn1Click());
        button2.addActionListener(e -> btn2Click());
    }
    
    /**
     * Just like JavaFX, we use event handlers to deal
     * with events. With Java 8, however, we can substitute
     * old school interfaces or anonymous classes with
     * lambdas!
     */
    private static void btn1Click()
    {
        String text = field1.getText();
        
        if (text.equals("")) {
            JOptionPane.showMessageDialog(null, "No sentence entered.");
        } else if (isPalindrome(text)) {
            JOptionPane.showMessageDialog(null, "That's no moon, that's a palindrome!");
        } else {
            JOptionPane.showMessageDialog(null, "This is not the palindrome you're looking for");
        }
        field1.setText("");
    }
    
    private static void btn2Click()
    {
        String text1 = toLetters(field1.getText().toLowerCase());
        String text2 = toLetters(field2.getText().toLowerCase());
        
        if (text1.equals("") && text2.equals("")) {
            JOptionPane.showMessageDialog(null, "Both sentences empty");
        } else if (text1.equals("")) {
            JOptionPane.showMessageDialog(null, "First sentence empty");
        } else if (text2.equals("")) {
            JOptionPane.showMessageDialog(null, "Second sentence emoty");
        } else {
            String str = sameWords(split(text1), split(text2));
            
            if (str.equals("")) {
                JOptionPane.showMessageDialog(null, "None");
            } else {
                JOptionPane.showMessageDialog(null, str);
            }
        }
        field1.setText("");
        field2.setText("");
    }
    
    /**
     * Converts a <code>String</code> to alphabetic characters
     * and whitespace only.
     * @param s the <code>String</code> to alphabetize
     * @return the alphabetized <code>String</code>
     */
    private static String toLetters(String s)
    {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            if (ch == ' ' || Character.isAlphabetic(ch)) {
                sb.append(ch);
            }
        }
        return sb.toString();
    }
    
    /**
     * Reverses the characters in a <code>String</code>.
     * @param s the <code>String</code> to reverse
     * @return the reversed <code>String</code>
     */
    private static String reverse(String s)
    {
        StringBuilder sb = new StringBuilder();
        for (int i = (s.length() - 1); i >= 0; i--) {
            sb.append(s.charAt(i));
        }
        return sb.toString();
    }
    
    /**
     * Tests if the input <code>String</code> is a palindrome.
     * A palindrome is defined as the same characters forwards
     * and backwards
     * @param s a <code>String</code> to test
     * @return <code>true</code> if the <code>String</code> is a palindrome
     */
    private static boolean isPalindrome(String s)
    {
        String sLetters = toLetters(s);
        String sRLetters = toLetters(reverse(s));
        return sLetters.equalsIgnoreCase(sRLetters);
    }
    
    /**
     * Splits a <code>String</code> based on whitespace and punctuation.
     * @param s the <code>String</code> to the split
     * @return a <code>String</code> array of split components
     */
    private static String[] split(String s)
    {
        // String.split() takes in a regular expression as a parameter.
        // A regular expression, or regex, is a way of expressing patterns
        // of text. They are a whole lesson in themselves, but just know 
        // that this regular expression will match any of the characters
        // within the [] that appears one or more times.
        return s.split("[? ,!.;]+");
    }
    
    /**
     * Gets all of the same words between two <code>String</code> arrays.
     * @param arr1 the first array
     * @param arr2 the second array
     * @return an <code>String</code> representation of <code>String</code>s within both arrays
     */
    private static String sameWords(String[] arr1, String[] arr2)
    {
        LinkedList<String> list = new LinkedList<>();
        for (String s1 : arr1) {
            if (contains(arr2, s1)) {
                list.add(s1);
            }
        }
        StringBuilder sb = new StringBuilder();
        for (String s : list) {
            sb.append(s + " ");
        }
        return sb.toString();
    }
    
    /**
     * Tests if a <code>String</code> array contains a <code>String</code>.
     * @param arr the array to test
     * @param s the <code>String</code> to find
     * @return <code>true</code> if the array contains the <code>String</code>
     */
    private static boolean contains(String[] arr, String s) {
        for (String str : arr) {
            if (str.equals(s)) return true;
        }
        return false;
    }
}

Applets have the interesting property of being able to handle MouseEvents and MouseMotionEvents. Using this, we can get information about the mouse from the user, expanding our applet capabilities.

package edu.govschool;

import java.awt.*;
// Needed for the event handlers, since we can't use lambdas
import java.awt.event.*;
import javax.swing.*;

public class MouseExample extends JApplet
{
    // JPanels can be thought of as different layout panes from JavaFX
    private static JPanel mousePanel;
    private static JLabel statusBar;
    
    @Override
    public void init()
    {
        mousePanel = new JPanel();
        mousePanel.setBackground(Color.WHITE);
        // BorderLayout is analogous to BorderPane from JavaFX. Instead of
        // relational directions, however, cardinal directions are used for
        // positioning
        add(mousePanel, BorderLayout.CENTER);
        
        statusBar = new JLabel("Recording Mouse Events");
        add(statusBar, BorderLayout.SOUTH);
        
        // An object implementing the MouseListener interface, or an anonymous
        // class, is required for this event handler. Since there are more than
        // one method to implement, we cannot use a lambda
        addMouseListener(
            new MouseListener() {
                // The mouse is clicked
                @Override
                public void mouseClicked(MouseEvent e)
                {
                    statusBar.setText("Clicked at (" + e.getX() + ", " + e.getY() + ")");
                }
                
                // The mouse is clicked and held
                @Override
                public void mousePressed(MouseEvent e)
                {
                    statusBar.setText("Pressed at (" + e.getX() + ", " + e.getY() + ")");
                }
                
                // The mouse is released after being held
                @Override
                public void mouseReleased(MouseEvent e)
                {
                    statusBar.setText("Released at (" + e.getX() + ", " + e.getY() + ")");
                }
                
                // The mouse is on the object 
                @Override
                public void mouseEntered(MouseEvent e)
                {
                    statusBar.setText("Entered at (" + e.getX() + ", " + e.getY() + ")");
                    mousePanel.setBackground(Color.GREEN);
                }
                
                // The mouse left the object
                @Override
                public void mouseExited(MouseEvent e)
                {
                    statusBar.setText("Exited at (" + e.getX() + ", " + e.getY() + ")");
                    mousePanel.setBackground(Color.WHITE);
                }
            }
        );
        
        // The MouseMotionListener is similar, but handles events from the mouse
        // moving about
        addMouseMotionListener(
            new MouseMotionListener() {
                // The mouse was pressed, held, and moved
                @Override
                public void mouseDragged(MouseEvent e)
                {
                    statusBar.setText("Dragged at (" + e.getX() + ", " + e.getY() + ")");
                }
                
                // The mouse was moved without being pressed
                @Override
                public void mouseMoved(MouseEvent e)
                {
                    statusBar.setText("Moved at (" + e.getX() + ", " + e.getY() + ")");
                }
            }
        );
    }
}