import java.applet.Applet; import java.awt.*; // for Graphics, Color, Image, Point, ... import java.awt.event.MouseEvent; import java.awt.event.MouseListener; import java.awt.event.MouseMotionListener; import java.lang.Math; import java.util.Vector; class ElectricCharge { public int x, y; // pixel coordinates public float q; // charge; could be positive or negative public ElectricCharge( int X, int Y, float Q ) { x = X; y = Y; q = Q; } } class ForceVector { public float x, y; public ForceVector( float X, float Y ) { x = X; y = Y; } } public class ElectricFieldVisualization extends Applet implements MouseListener, MouseMotionListener { private static final int chargeRadius = 8; // in pixels private static final int arrowPlotSpacing = 2*chargeRadius; // in pixels private static final int fluxLineSpacing = 2*arrowPlotSpacing; // in pixels private static final int fluxLinesPerCharge = 8; private boolean drawArrowPlot = true; private boolean drawFluxLines = false; private boolean isDragging = false; private int indexOfChargeBeingDragged = -1; private int previousX; private int previousY; Vector list = new Vector(); // vector of ElectricCharge objects int numColors = 16; Color[] spectrum; private Color bgcolor = new Color( 0.0f, 0.0f, 0.0f ); // background colour private Color fgcolor1 = new Color( 0.0f, 1.0f, 1.0f ); private Color fgcolor2 = new Color( 1.0f, 0.0f, 0.0f ); int width, height; // dimensions of applet, in pixels Image backbuffer; Graphics backg; boolean _painted = false; boolean _isBackbufferDirty = true; // a 2D array of flags, each corresponding to a region of the back buffer private boolean [][] map; public void init() { try { boolean b = Boolean.valueOf( getParameter("drawarrowplot") ).booleanValue(); drawArrowPlot = b; } catch (Exception E) { } try { boolean b = Boolean.valueOf( getParameter("drawfluxlines") ).booleanValue(); drawFluxLines = b; } catch (Exception E) { } try { Color c = new Color(Integer.parseInt( getParameter("bgcolor"), 16 )); bgcolor = c; } catch (Exception E) { } try { Color c = new Color(Integer.parseInt( getParameter("fgcolor1"), 16 )); fgcolor1 = c; } catch (Exception E) { } try { Color c = new Color(Integer.parseInt( getParameter("fgcolor2"), 16 )); fgcolor2 = c; } catch (Exception E) { } spectrum = new Color[ numColors ]; for ( int i = 0; i < numColors; ++i ) { float u = i/(float)(numColors-1); spectrum[i] = new Color( Math.round( (1-u)*fgcolor1.getRed() + u*fgcolor2.getRed() ), Math.round( (1-u)*fgcolor1.getGreen() + u*fgcolor2.getGreen() ), Math.round( (1-u)*fgcolor1.getBlue() + u*fgcolor2.getBlue() ) ); } width = getSize().width; height = getSize().height; backbuffer = createImage( width, height ); backg = backbuffer.getGraphics(); backg.setColor( bgcolor ); backg.fillRect( 0, 0, width, height ); map = new boolean[ width/fluxLineSpacing + 1 ] [ height/fluxLineSpacing + 1 ]; for ( int x = 0; x < map.length; ++x ) for ( int y = 0; y < map[0].length; ++y ) map[x][y] = false; addMouseListener( this ); addMouseMotionListener( this ); } // returns electric force field vector at the given pixel private ForceVector computeForce( float x, float y ) { ForceVector v = new ForceVector( 0.0f, 0.0f ); for ( int i = 0; i < list.size(); ++i ) { ElectricCharge c = (ElectricCharge)(list.elementAt(i)); float dx = (x-c.x)/(float)arrowPlotSpacing; float dy = (y-c.y)/(float)arrowPlotSpacing; float r2 = dx*dx + dy*dy; if ( r2 == 0 ) continue; float r = (float) Math.sqrt(r2); float forceMagnitude = c.q / r2; v.x += forceMagnitude * dx/r; v.y += forceMagnitude * dy/r; } return v; } private Color mapForceMagnitudeToColor( float forceMagnitude ) { // Consider radius r in units of arrowPlotSpacing, // and force f == 1/r^2. // If we map f linearly to colour, the high-intensity // colour are concentrated close to the centre of the charges, // and colour falls off very quickly as radius increases. // So, we instead map log_10( force ) linearly to colour. // // Maybe we would like r=0.3, i.e. f == 1/r^2 ~ 10, // to map to the max colour, // and r=4, i.e. f == 1/r^2 ~ 0.1, // to map to the min colour. // In other words, maybe we would like log_10( 10 ) == 1 // to map to the max colour, // and log_10( 0.1 ) == -1 // to map to the min colour. // This would correspond to // ( Math.log_10( forceMagnitude ) + 1 )/2 * numColors // int colorIndex = (int)( ( Math.log( forceMagnitude )/Math.log(10.0f) + 3 )/3 * numColors ); // clamp the result if ( colorIndex < 0 ) colorIndex = 0; else if ( colorIndex >= numColors ) colorIndex = numColors-1; return spectrum[ colorIndex ]; } // (x1,y1) is the origin of the arrow; (x2,y2) is the tip of the arrow private void drawArrow( float x1, float y1, float x2, float y2 ) { float dx = x2-x1; float dy = y2-y1; float f = 1/3.0f; // length of arrow head over length of arrow stem float f2 = 1/6.0f; // half-width of arrow head over length of arrow stem float x3 = x2 - f*dx - f2*dy; float y3 = y2 - f*dy + f2*dx; float x4 = x2 - f*dx + f2*dy; float y4 = y2 - f*dy - f2*dx; backg.drawLine( Math.round(x1), Math.round(y1), Math.round(x2), Math.round(y2) ); backg.drawLine( Math.round(x3), Math.round(y3), Math.round(x2), Math.round(y2) ); backg.drawLine( Math.round(x4), Math.round(y4), Math.round(x2), Math.round(y2) ); } private void drawFluxLine( float x, float y, // pixel location of point to start at float sign, // +1 to travel with the field, -1 to travel against it int maxLength // in pixels ) { for ( int k = 0; k < maxLength; ++k ) { ForceVector v = computeForce( x, y ); v.x *= sign; v.y *= sign; float forceMagnitude = (float)Math.sqrt( v.x*v.x+v.y*v.y ); if ( forceMagnitude == 0 ) break; // normalize the vector v.x /= forceMagnitude; v.y /= forceMagnitude; float new_x = x + v.x; float new_y = y + v.y; backg.setColor( mapForceMagnitudeToColor( forceMagnitude ) ); backg.drawLine( Math.round(x), Math.round(y), Math.round(new_x), Math.round(new_y) ); // every few pixels, draw an arrow if ( k > 0 && ( k % (5*arrowPlotSpacing) == 0 ) ) { drawArrow( x, y, x + sign*0.9f*arrowPlotSpacing*v.x, y + sign*0.9f*arrowPlotSpacing*v.y ); } x = new_x; y = new_y; if ( x < 0 || x >= width || y < 0 || y >= height ) // we're outside the image's boundaries break; // mark this part of the image as occupied by a flux line map[Math.round(x)/fluxLineSpacing] [Math.round(y)/fluxLineSpacing] = true; } } private void render() { backg.setColor( bgcolor ); backg.fillRect( 0, 0, width, height ); if ( drawArrowPlot ) { for ( int x = arrowPlotSpacing; x < width-arrowPlotSpacing; x+=arrowPlotSpacing ) for ( int y = arrowPlotSpacing; y < height-arrowPlotSpacing; y+=arrowPlotSpacing ) { ForceVector v = computeForce( x, y ); float forceMagnitude = (float)Math.sqrt( v.x*v.x+v.y*v.y ); if ( forceMagnitude == 0 ) continue; backg.setColor( mapForceMagnitudeToColor( forceMagnitude ) ); // normalize the vector v.x /= forceMagnitude; v.y /= forceMagnitude; // draw the arrow v.x *= 0.9f * arrowPlotSpacing; v.y *= 0.9f * arrowPlotSpacing; drawArrow( x, y, x+v.x, y+v.y ); } } if ( /* ! isDragging && */ drawFluxLines ) { // clear the map for ( int x = 0; x < map.length; ++x ) for ( int y = 0; y < map[0].length; ++y ) map[x][y] = false; for ( int i = list.size()-1; i >= 0; --i ) { ElectricCharge c = (ElectricCharge)(list.elementAt(i)); for ( int j = 0; j < fluxLinesPerCharge; ++j ) { float x0 = c.x+(float)Math.cos(2*Math.PI*j/fluxLinesPerCharge); float y0 = c.y+(float)Math.sin(2*Math.PI*j/fluxLinesPerCharge); drawFluxLine( x0, y0, // Use the sign of the charge, // so that we always travel away from the charge. c.q > 0 ? 1.0f : -1.0f, Math.max( width, height ) ); } } // look for regions that aren't occupied by flux lines for ( int x = 0; x < map.length; ++x ) for ( int y = 0; y < map[0].length; ++y ) if ( ! map[x][y] ) { // place a seed point in the centre of the region float x0 = (x+0.5f)*fluxLineSpacing; float y0 = (y+0.5f)*fluxLineSpacing; // draw flux lines forward and backward through the seed point drawFluxLine( x0, y0, 1.0f, Math.max( width, height ) ); drawFluxLine( x0, y0, -1.0f, Math.max( width, height ) ); } } // Draw the charges. // // When we do picking, we test for intersection in forward order. // Since we want occlusion to lead to predictable picking resolution, // we render in reverse order. for ( int i = list.size()-1; i >= 0; --i ) { ElectricCharge c = (ElectricCharge)(list.elementAt(i)); if ( c.q > 0 ) { backg.setColor( Color.white ); backg.fillOval( c.x-chargeRadius, c.y-chargeRadius, 2*chargeRadius, 2*chargeRadius ); backg.setColor( Color.black ); backg.drawOval( c.x-chargeRadius, c.y-chargeRadius, 2*chargeRadius, 2*chargeRadius ); backg.drawLine( c.x, c.y-chargeRadius/2, c.x, c.y+chargeRadius/2 ); backg.drawLine( c.x-chargeRadius/2, c.y, c.x+chargeRadius/2, c.y ); } else { backg.setColor( Color.black ); backg.fillOval( c.x-chargeRadius, c.y-chargeRadius, 2*chargeRadius, 2*chargeRadius ); backg.setColor( Color.white ); backg.drawOval( c.x-chargeRadius, c.y-chargeRadius, 2*chargeRadius, 2*chargeRadius ); backg.drawLine( c.x-chargeRadius/2, c.y, c.x+chargeRadius/2, c.y ); } } } // returns -1 if the given pixel doesn't fall on any charge private int indexOfChargeUnderPixel( int x, int y ) { for ( int i = 0; i < list.size(); ++i ) { ElectricCharge c = (ElectricCharge)(list.elementAt(i)); if ( (x-c.x)*(x-c.x)+(y-c.y)*(y-c.y) <= chargeRadius*chargeRadius ) return i; } return -1; } public void mouseEntered( MouseEvent e ) { } public void mouseExited( MouseEvent e ) { } public void mouseClicked( MouseEvent e ) { int i = indexOfChargeUnderPixel( e.getX(), e.getY() ); if ( ( e.getModifiers() & MouseEvent.SHIFT_MASK ) != 0 || ( e.getModifiers() & MouseEvent.CTRL_MASK ) != 0 || ( e.getModifiers() & MouseEvent.ALT_MASK ) != 0 ) { if ( i >= 0 ) { // delete the charge list.removeElementAt( i ); } else { // create a negative charge list.addElement( new ElectricCharge( e.getX(), e.getY(), -1 ) ); } } else { if ( i >= 0 ) { // do nothing } else { // create a positive charge list.addElement( new ElectricCharge( e.getX(), e.getY(), 1 ) ); } } markBackbufferDirty(); e.consume(); } public void mousePressed( MouseEvent e ) { int i = indexOfChargeUnderPixel( e.getX(), e.getY() ); if ( i >= 0 && ( e.getModifiers() & MouseEvent.SHIFT_MASK ) == 0 && ( e.getModifiers() & MouseEvent.CTRL_MASK ) == 0 && ( e.getModifiers() & MouseEvent.ALT_MASK ) == 0 ) { isDragging = true; indexOfChargeBeingDragged = i; previousX = e.getX(); previousY = e.getY(); } e.consume(); } public void mouseReleased( MouseEvent e ) { if ( isDragging ) { isDragging = false; indexOfChargeBeingDragged = -1; markBackbufferDirty(); } e.consume(); } public void mouseMoved( MouseEvent e ) { } public void mouseDragged( MouseEvent e ) { if ( isDragging ) { ((ElectricCharge)list.elementAt( indexOfChargeBeingDragged )).x += e.getX() - previousX; ((ElectricCharge)list.elementAt( indexOfChargeBeingDragged )).y += e.getY() - previousY; previousX = e.getX(); previousY = e.getY(); markBackbufferDirty(); } e.consume(); } private void markBackbufferDirty() { _isBackbufferDirty = true; if ( _painted ) { _painted = false; repaint(); } } public void update( Graphics g ) { if ( _isBackbufferDirty ) { render(); _isBackbufferDirty = false; } g.drawImage( backbuffer, 0, 0, this ); _painted = true; } public void paint( Graphics g ) { update( g ); } public String getAppletInfo() { return "Written by Michael J. McGuffin, November 2004, University of Toronto"; } }