// AtomApplet.java Copyright (c) Kari Laitinen // http://www.naturalprogramming.com // 2007-11-29 File created. // 2007-12-13 Latest modification. /* This program displays an "atom" whose electrons are circulating around its nucleus. As the atom has two electrons, it is a helium atom. The nucleus of the atom consists of two protons and two neutrons. This program demonstrates the use of following "geometric" and "graphic" classes: - PathIterator - FlatteningPathIterator - AffineTransform - GradientPaint This program also demonstrates so-called active rendering. Active rendering means that a program actively decides when it wants to draw its drawings to its window. Active rendering is thus a kind of opposite to using the repaint() and paint() methods, the use of which can be described as "passive rendering". By using active rendering drawing operations are faster, which is necessary, for example, when game programs are written. More notes at the end of this file. */ import java.awt.* ; import javax.swing.* ; import java.awt.image.BufferStrategy; import java.awt.image.BufferedImage; import java.awt.geom.* ; // Classes GeneralPath etc. /* Classes NucleusParticle, Proton, and Neutron are used to draw the nucleus of the atom so that the particles are drawn with a GradientPaint that goes from white to the specified particle color. The draw() method of class NucleusParticle temporarily modifies the coordinate system in order to make a nice color for the balls. When a GradientPaint is specified, the coordinates refer to points on the drawing surface, not to points belonging to any particular graphical shape. Thus, if we want to draw similar GradientPaint'ed graphical shapes in various positions in the coordinate system, we can achieve this most easily if we temporarily move the zero point of the coordinate system where a graphical shape should be drawn. */ class NucleusParticle { static final int PARTICLE_RADIUS = 20 ; int particle_center_point_x, particle_center_point_y ; Color particle_color ; public void draw( Graphics2D graphics2D ) { AffineTransform saved_graphics_transform = graphics2D.getTransform() ; graphics2D.translate( particle_center_point_x - PARTICLE_RADIUS, particle_center_point_y - PARTICLE_RADIUS ) ; graphics2D.setPaint( new GradientPaint( 0, 0, Color.WHITE, PARTICLE_RADIUS * 2, PARTICLE_RADIUS * 2, particle_color, true ) ); graphics2D.fill( new Ellipse2D.Double( 0, 0, PARTICLE_RADIUS * 2, PARTICLE_RADIUS * 2 ) ) ; graphics2D.setTransform( saved_graphics_transform ) ; } } class Proton extends NucleusParticle { public Proton( int given_proton_center_point_x, int given_proton_center_point_y ) { particle_center_point_x = given_proton_center_point_x ; particle_center_point_y = given_proton_center_point_y ; particle_color = Color.BLACK ; } } class Neutron extends NucleusParticle { public Neutron( int given_neutron_center_point_x, int given_neutron_center_point_y ) { particle_center_point_x = given_neutron_center_point_x ; particle_center_point_y = given_neutron_center_point_y ; particle_color = Color.BLUE ; } } public class AtomApplet extends JApplet implements Runnable { int applet_width, applet_height ; Thread animation_thread = null ; boolean thread_must_be_executed = false ; BufferedImage offscreen_drawing_surface ; BufferStrategy buffer_strategy ; public void init() { applet_width = getSize().width ; applet_height = getSize().height ; setIgnoreRepaint( true ) ; Canvas canvas_for_drawing = new Canvas(); canvas_for_drawing.setIgnoreRepaint( true ); canvas_for_drawing.setSize( applet_width, applet_height ); add( canvas_for_drawing ) ; // Add canvas_for_drawing to this applet canvas_for_drawing.createBufferStrategy( 2 ); buffer_strategy = canvas_for_drawing.getBufferStrategy(); GraphicsEnvironment graphics_environment = GraphicsEnvironment.getLocalGraphicsEnvironment() ; GraphicsDevice graphics_device = graphics_environment.getDefaultScreenDevice() ; GraphicsConfiguration graphics_configuration = graphics_device.getDefaultConfiguration() ; offscreen_drawing_surface = graphics_configuration.createCompatibleImage( applet_width, applet_height ) ; } public void start() { if ( animation_thread == null ) { animation_thread = new Thread( this ) ; thread_must_be_executed = true ; animation_thread.start() ; } System.out.print( "\n Method start() executed. " ) ; } public void stop() { if ( animation_thread != null ) { animation_thread.interrupt() ; thread_must_be_executed = false ; animation_thread = null ; } System.out.print( "\n Method stop() executed. " ) ; } public void run() { System.out.print( "\n Method run() started." ) ; // Objects needed for rendering... Graphics graphics = null; Graphics2D graphics2D = null; Color background_color = Color.ORANGE ; // Variables for counting frames per seconds int frames_per_second = 0; int frames_during_last_second = 0; long total_time = 0; long current_time = System.currentTimeMillis(); long previous_time = current_time; // The nucleus of a helium atom consists of two protons and // two neutrons. The coordinates of these nucleus particles // refer to their center points, the zero point being // the center of the atom. Proton upper_left_proton = new Proton( -15, -15 ) ; Proton lower_right_proton = new Proton( 15, 15 ) ; Neutron lower_left_neutron = new Neutron( -15, 15 ) ; Neutron upper_right_neutron = new Neutron( 15, -15 ) ; // To make an electron orbit, we first define an ellipse that // is converted to a GeneralPath object. The second electron // orbit is made by cloning the first orbit. Then the transform() // method of class GeneralPath is used to turn the two ellipses // so that their angle is 90 degrees ( Math.PI / 2 radians ). GeneralPath first_electron_orbit = new GeneralPath( new Ellipse2D.Double( -200, -100, 400, 200 )) ; GeneralPath second_electron_orbit = (GeneralPath) first_electron_orbit.clone() ; first_electron_orbit.transform( AffineTransform.getRotateInstance( Math.PI / 4 ) ) ; second_electron_orbit.transform( AffineTransform.getRotateInstance( - Math.PI / 4 ) ) ; // Next we create an iterator with which we get a set of points // that belong to the orbit of the first electron. The "raw" iterator // provides data that describe the orbit as curves. When it is // converted to a FlatteningPathIterator, we can get coordinates // that describe the orbit as line-to segments. PathIterator raw_first_orbit_iterator = first_electron_orbit.getPathIterator( new AffineTransform() ) ; FlatteningPathIterator first_orbit_iterator = new FlatteningPathIterator( raw_first_orbit_iterator, 2.0, 20 ) ; double[] first_orbit_points = new double[ 6 ] ; first_orbit_iterator.currentSegment( first_orbit_points ) ; // Next we'll do the same activities to the second electron orbit. PathIterator raw_second_orbit_iterator = second_electron_orbit.getPathIterator( new AffineTransform() ) ; FlatteningPathIterator second_orbit_iterator = new FlatteningPathIterator( raw_second_orbit_iterator, 2.0, 20 ); double[] second_orbit_points = new double[ 6 ] ; second_orbit_iterator.currentSegment( second_orbit_points ) ; // The following while loop produces an image (a frame) to the screen // each time it is executed. By executing the program you can find // out that something like 60 frames can be produced in a second. // The speed of the used computer affects the frame rate. while ( thread_must_be_executed == true ) // almost eternal loop { try { // clear back buffer... graphics2D = offscreen_drawing_surface.createGraphics(); graphics2D.setColor( background_color ); graphics2D.fillRect( 0, 0, applet_width, applet_height ); // display frames per second... graphics2D.setFont( new Font( "Monospaced", Font.PLAIN, 12 ) ); graphics2D.setColor( Color.BLACK ); graphics2D.drawString( "FRAMES PER SECOND: " + frames_per_second, 20, 20 ); // The translate() method is used to adjust the coordinate // system so that the zero point is in the center of the applet. // The atom will be drawn around this center point. graphics2D.translate( applet_width / 2, applet_height / 2 ) ; graphics2D.draw( first_electron_orbit ) ; graphics2D.draw( second_electron_orbit ) ; graphics2D.setColor( Color.RED ) ; // First electron is red. graphics2D.fill( new Ellipse2D.Double( first_orbit_points[ 0 ] - 6, first_orbit_points[ 1 ] - 6, 12, 12 ) ) ; graphics2D.setColor( Color.BLUE ) ; // Second electron is blue. graphics2D.fill( new Ellipse2D.Double( second_orbit_points[ 0 ] - 6, second_orbit_points[ 1 ] - 6, 12, 12 ) ) ; upper_left_proton.draw( graphics2D ) ; lower_left_neutron.draw( graphics2D ) ; upper_right_neutron.draw( graphics2D ) ; lower_right_proton.draw( graphics2D ) ; // Now the atom is drawn. Let's blit the image and flip. graphics = buffer_strategy.getDrawGraphics(); graphics.drawImage( offscreen_drawing_surface, 0, 0, null ) ; if( ! buffer_strategy.contentsLost() ) { buffer_strategy.show(); } first_orbit_iterator.next() ; if ( ! first_orbit_iterator.isDone() ) { // After the following statement is executed the coordinates // of a point in the elliptical orbit are stored in the // first two positions in the array that is supplied as // a parameter for the currentSegment() method. first_orbit_iterator.currentSegment( first_orbit_points ) ; } else { // The electron has now circulated its whole orbit. // We must re-create the necessary iterators in order to // continue circulation. raw_first_orbit_iterator = first_electron_orbit.getPathIterator( new AffineTransform() ) ; first_orbit_iterator = new FlatteningPathIterator( raw_first_orbit_iterator, 2.0, 20 ) ; first_orbit_iterator.currentSegment( first_orbit_points ) ; } second_orbit_iterator.next() ; if ( ! second_orbit_iterator.isDone() ) { second_orbit_iterator.currentSegment( second_orbit_points ) ; } else { // Let's re-create the necessary iterators raw_second_orbit_iterator = second_electron_orbit.getPathIterator( new AffineTransform() ) ; second_orbit_iterator = new FlatteningPathIterator( raw_second_orbit_iterator, 2.0, 20 ); second_orbit_iterator.currentSegment( second_orbit_points ) ; } } finally { if ( graphics != null ) { graphics.dispose() ; // release resource } if ( graphics2D != null ) { graphics2D.dispose() ; // release resource } } try { Thread.sleep( 10 ) ; } catch ( InterruptedException caught_exception ) { thread_must_be_executed = false ; } // Finally we'll count how fast frames are created, i.e., // how many times per second this loop is executed. This speed // depends on how much the heart is "resting" within the second // in question. previous_time = current_time ; current_time = System.currentTimeMillis() ; total_time += current_time - previous_time ; if( total_time > 1000 ) { total_time -= 1000; frames_per_second = frames_during_last_second ; frames_during_last_second = 0 ; } frames_during_last_second ++ ; } // end of the almost eternal while loop System.out.print( "\n Method run() terminated. " ) ; } } /* NOTES: The electrons will orbit counter clockwise if you define an electron orbit by using the Arc2D.Double class in place of the Ellipse2D.Double class, in the following way GeneralPath first_electron_orbit = new GeneralPath( new Arc2D.Double( -200, -100, 400, 200, 0, 360, Arc2D.OPEN )) ; If you want to make an applet that uses a Canvas-based drawing surface to react to mouse events, you have to make the canvas as a mouse listener, in the following way: canvas_for_drawing.addMouseListener( this ) ; ACKNOWLEDGEMENTS: Miikka Liukkonen providied me invaluable advice in the use of the FlatteningPathIterator class. */