import java.applet.*; import java.awt.*; import java.awt.event.*; import java.awt.image.*; import javax.swing.*; import javax.swing.event.*; import java.util.*; import java.awt.geom.*; /*************************************************** /* This code was written in 2007 by Jason P. Modisette /* (jpmodisette at gmail dot com). I put it in /* the public domain, so you can use it however you /* want. I would be grateful if users of this code /* leave this notice intact, credit me, and if possible /* let me know what you are doing with it, but as it's /* in the public domain whether you do these things is /* up to you. /*************************************************** public class FTIRApplet extends JApplet { int N_BEAMS = 20; int opticalPanelHeight = 300; int opticalPanelWidth = 700; int evanPanelHeight = 200; int evanPanelWidth = 700; double evanPadLeft = 10; double evanPadTop = 10; double evanPadBottom = 10; double evanPadRight = 10; double evanTickHeight = 8; double evanLabelHeight = 15; double yTop; double yBottom; double xLeft; double xRight; double ySheetTop; double ySheetBottom; double xSheetTopLeft; double xSheetBottomLeft; double maxTIRAngle; int N_BINS = 350; double maxEvanescentDistance = 10.0; // in lambda/2Pi's double [] evanescentBins = new double[ N_BINS ]; double n_i; public double normalizeAngle( double theta ) { while ( theta < 0 ) theta += 2.0 * Math.PI; while ( theta > 2.0 * Math.PI ) theta -= 2.0 * Math.PI; return theta; } public double normalizeAngle2( double theta ) { while ( theta < Math.PI ) theta += 2.0 * Math.PI; while ( theta > Math.PI ) theta -= 2.0 * Math.PI; return theta; } JSlider ledAngleSlider; JSlider ledBeamWidthSlider; JSlider edgeAngleSlider; JSlider ledPositionSlider; JComboBox materialComboBox; JPanel opticalPanel = new JPanel() { Image buffer = null; public void update( Graphics g ) { this.paint( g ); } public void paint( Graphics gg ) { if ( buffer == null ) { buffer = createImage( opticalPanelWidth, opticalPanelHeight ); } Graphics2D g = ( Graphics2D ) buffer.getGraphics(); drawOptics( g, opticalPanelWidth, opticalPanelHeight ); gg.drawImage( buffer, 0, 0, this ); } public Dimension getPreferredSize() { return new Dimension( opticalPanelWidth, opticalPanelHeight ); } public Dimension getMinimumSize() { return new Dimension( opticalPanelWidth, opticalPanelHeight ); } public Dimension getMaximumSize() { return new Dimension( opticalPanelWidth, opticalPanelHeight ); } }; JPanel evanescentPanel = new JPanel() { Image buffer = null; public void update( Graphics g ) { this.paint( g ); } public void paint( Graphics gg ) { if ( buffer == null ) { buffer = createImage( evanPanelWidth + 1, evanPanelHeight + 1 ); } Graphics2D g = ( Graphics2D ) buffer.getGraphics(); drawEvanescentPlot( g, evanPanelWidth + 1, evanPanelHeight + 1 ); gg.drawImage( buffer, 0, 0, this ); } public Dimension getPreferredSize() { return new Dimension( evanPanelWidth, evanPanelHeight ); } public Dimension getMinimumSize() { return new Dimension( evanPanelWidth, evanPanelHeight ); } public Dimension getMaximumSize() { return new Dimension( evanPanelWidth, evanPanelHeight ); } }; public void setSliderColors( JSlider slider, Color bg, Color fg ) { slider.setBackground( bg ); slider.setForeground( fg ); Dictionary tickLabels = slider.getLabelTable(); if ( tickLabels != null ) { Enumeration keys = tickLabels.keys(); while ( keys.hasMoreElements() ) { Component label = ( Component ) tickLabels.get( keys.nextElement() ); if ( label instanceof JLabel ) { ( ( JLabel ) label ).setForeground( fg ); } } } } public void init() { this.getContentPane().setBackground( Color.black ); this.getContentPane().setForeground( Color.white ); GridBagLayout gbl = new GridBagLayout(); this.getContentPane().setLayout( gbl ); GridBagConstraints gbc = new GridBagConstraints(); // add the panel that draws the LED and rays this.getContentPane().add( opticalPanel ); gbc.gridx = 0; gbc.gridy = 0; gbc.gridwidth = 4; gbc.gridheight = 1; gbc.fill = GridBagConstraints.NONE; gbc.anchor = GridBagConstraints.CENTER; gbc.weightx = 1.0; gbc.weighty = 0.0; // gbc.ipadx = 3; // gbc.ipady = 3; gbl.setConstraints( opticalPanel, gbc ); // add the panel that draws the evanescent field decay plot this.getContentPane().add( evanescentPanel ); gbc.gridx = 0; gbc.gridy = 1; gbl.setConstraints( evanescentPanel, gbc ); // sliders ledAngleSlider = new JSlider( -90, 90, 0 ); ledAngleSlider.setMajorTickSpacing( 30 ); ledAngleSlider.setMinorTickSpacing( 10 ); ledAngleSlider.setPaintTicks( true ); ledAngleSlider.setPaintLabels( true ); setSliderColors( ledAngleSlider, Color.black, Color.white ); this.getContentPane().add( ledAngleSlider ); gbc.gridx = 1; gbc.gridy = 2; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.fill = GridBagConstraints.NONE; gbc.anchor = GridBagConstraints.WEST; gbc.weightx = 1.0; gbc.weighty = 0.0; gbl.setConstraints( ledAngleSlider, gbc ); ledAngleSlider.addChangeListener( new ChangeListener() { public void stateChanged( ChangeEvent e ) { opticalPanel.repaint(); } } ); JLabel ledAngleLabel = new JLabel( "LED Angle (deg)" ); ledAngleLabel.setBackground( Color.black ); ledAngleLabel.setForeground( Color.white ); this.getContentPane().add( ledAngleLabel ); gbc.gridx = 0; gbl.setConstraints( ledAngleLabel, gbc ); ledBeamWidthSlider = new JSlider( 0, 90, 20 ); ledBeamWidthSlider.setMajorTickSpacing( 15 ); ledBeamWidthSlider.setMinorTickSpacing( 5 ); ledBeamWidthSlider.setPaintTicks( true ); ledBeamWidthSlider.setPaintLabels( true ); setSliderColors( ledBeamWidthSlider, Color.black, Color.white ); this.getContentPane().add( ledBeamWidthSlider ); gbc.gridx = 1; gbc.gridy = 3; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.fill = GridBagConstraints.NONE; gbc.anchor = GridBagConstraints.WEST; gbc.weightx = 1.0; gbc.weighty = 0.0; gbl.setConstraints( ledBeamWidthSlider, gbc ); ledBeamWidthSlider.addChangeListener( new ChangeListener() { public void stateChanged( ChangeEvent e ) { opticalPanel.repaint(); } } ); JLabel ledBeamWidthLabel = new JLabel( "LED Beam Width (deg)" ); ledBeamWidthLabel.setBackground( Color.black ); ledBeamWidthLabel.setForeground( Color.white ); this.getContentPane().add( ledBeamWidthLabel ); gbc.gridx = 0; gbl.setConstraints( ledBeamWidthLabel, gbc ); edgeAngleSlider = new JSlider( 0, 90, 45 ); edgeAngleSlider.setMajorTickSpacing( 15 ); edgeAngleSlider.setMinorTickSpacing( 5 ); edgeAngleSlider.setPaintTicks( true ); edgeAngleSlider.setPaintLabels( true ); setSliderColors( edgeAngleSlider, Color.black, Color.white ); this.getContentPane().add( edgeAngleSlider ); gbc.gridx = 1; gbc.gridy = 4; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.fill = GridBagConstraints.NONE; gbc.anchor = GridBagConstraints.WEST; gbc.weightx = 1.0; gbc.weighty = 0.0; gbl.setConstraints( edgeAngleSlider, gbc ); edgeAngleSlider.addChangeListener( new ChangeListener() { public void stateChanged( ChangeEvent e ) { opticalPanel.repaint(); } } ); JLabel edgeAngleLabel = new JLabel( "Edge angle (deg)" ); edgeAngleLabel.setBackground( Color.black ); edgeAngleLabel.setForeground( Color.white ); this.getContentPane().add( edgeAngleLabel ); gbc.gridx = 0; gbl.setConstraints( edgeAngleLabel, gbc ); ledPositionSlider = new JSlider( 0, 150, 50 ); ledPositionSlider.setMajorTickSpacing( 20 ); ledPositionSlider.setPaintTicks( true ); ledPositionSlider.setBackground( Color.black ); ledPositionSlider.setForeground( Color.white ); setSliderColors( ledPositionSlider, Color.black, Color.white ); this.getContentPane().add( ledPositionSlider ); gbc.gridx = 1; gbc.gridy = 5; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.fill = GridBagConstraints.NONE; gbc.anchor = GridBagConstraints.WEST; gbc.weightx = 1.0; gbc.weighty = 0.0; gbl.setConstraints( ledPositionSlider, gbc ); ledPositionSlider.addChangeListener( new ChangeListener() { public void stateChanged( ChangeEvent e ) { opticalPanel.repaint(); } } ); JLabel ledPositionLabel = new JLabel( "LED Position" ); ledPositionLabel.setBackground( Color.black ); ledPositionLabel.setForeground( Color.white ); this.getContentPane().add( ledPositionLabel ); gbc.gridx = 0; gbl.setConstraints( ledPositionLabel, gbc ); // materials Object [] materialNames = new Object [] { "Polycarbonate", "Acrylic" }; materialComboBox = new JComboBox( materialNames ); materialComboBox.setBackground( Color.black ); materialComboBox.setForeground( Color.white ); this.getContentPane().add( materialComboBox ); gbc.gridx = 3; gbc.gridy = 2; gbc.gridwidth = 1; gbc.gridheight = 1; gbc.fill = GridBagConstraints.NONE; gbc.anchor = GridBagConstraints.CENTER; gbc.weightx = 1.0; gbc.weighty = 0.0; gbl.setConstraints( materialComboBox, gbc ); materialComboBox.addActionListener( new ActionListener() { public void actionPerformed( ActionEvent e ) { opticalPanel.repaint(); } } ); JLabel materialLabel = new JLabel( "Material" ); materialLabel.setBackground( Color.black ); materialLabel.setForeground( Color.white ); this.getContentPane().add( materialLabel ); gbc.gridx = 2; gbl.setConstraints( materialLabel, gbc ); } public void drawEvanescentPlot( Graphics2D g, int width, int height ) { g.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON ); g.setColor( Color.black ); g.fillRect( 0, 0, width + 10, height + 10 ); // title g.setColor( Color.white ); g.setFont( new Font( "SansSerif", Font.BOLD, 16 ) ); FontMetrics fontMetrics = g.getFontMetrics(); String titleString = "Evanescent Field Strength vs. Distance"; int xStringStart = evanPanelWidth / 2 - fontMetrics.stringWidth( titleString ) / 2; int yStringStart = ( int ) evanPadTop + 10 + fontMetrics.getAscent() + fontMetrics.getMaxDescent(); g.drawString( titleString, xStringStart, yStringStart ); // draw the x axis g.setFont( new Font( "SansSerif", Font.BOLD, 10 ) ); fontMetrics = g.getFontMetrics(); g.setColor( Color.white ); double xAxisY = evanPanelHeight - evanPadBottom - evanTickHeight - evanLabelHeight; g.drawLine( ( int ) evanPadLeft, ( int ) xAxisY, ( int ) ( evanPanelWidth - evanPadRight ), ( int ) xAxisY ); // x axis ticks every 100 nm double xSpan = evanPanelWidth - evanPadLeft - evanPadRight; double dxTick = 100.0 / ( 880.0 / ( 2.0 * Math.PI ) ); int labelNm = 100; for ( double xTick = dxTick; xTick < maxEvanescentDistance; xTick += dxTick ) { int screenX = ( int ) ( evanPadLeft + xTick * xSpan / 10.0 ); g.drawLine( screenX, ( int ) xAxisY, screenX, ( int ) ( xAxisY + evanTickHeight ) ); String labelString = labelNm + " nm"; g.drawString( labelString, screenX - fontMetrics.stringWidth( labelString ) / 2, ( int ) ( xAxisY + evanTickHeight + evanLabelHeight ) ); labelNm += 100; } // draw the y axis double yAxisX = evanPadLeft; g.drawLine( ( int ) yAxisX, ( int ) xAxisY, ( int ) yAxisX, ( int ) evanPadTop ); // draw the "max power" bar g.setColor( Color.yellow ); double yBottom = evanPanelHeight - evanPadBottom - evanTickHeight - evanLabelHeight; double ySpan = yBottom - evanPadTop - 10; double yBar = yBottom - ySpan; g.drawLine( ( int ) yAxisX, ( int ) yBar, ( int ) ( evanPanelWidth - evanPadRight ), ( int ) yBar ); // draw the plot g.setColor( Color.green ); for ( int i = 1; i < N_BINS; i++ ) { double x1 = ( 1.0 * ( i - 1 ) * xSpan / N_BINS ) + evanPadLeft; double y1 = yBottom - ySpan * evanescentBins[ i - 1 ]; double x2 = ( 1.0 * i * xSpan / N_BINS ) + evanPadLeft; double y2 = yBottom - ySpan * evanescentBins[ i ]; g.drawLine( ( int ) x1, ( int ) y1, ( int ) x2, ( int ) y2 ); } } public void drawOptics( Graphics2D g, int width, int height ) { // set up antialiasing g.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON ); g.setColor( Color.black ); g.fillRect( 0, 0, width, height ); double ledAngle = ( double ) ledAngleSlider.getValue(); double ledAngleDegrees = ledAngle; ledAngle *= Math.PI / 180.0; double ledBeamWidth = ( double ) ledBeamWidthSlider.getValue(); ledBeamWidth *= Math.PI / 180.0; double edgeAngle = ( double ) edgeAngleSlider.getValue(); edgeAngle *= Math.PI / 180.0; double ledCenterX = ( double ) ledPositionSlider.getValue(); String material = ( String ) materialComboBox.getSelectedItem(); if ( material.equals( "Polycarbonate" ) ) { n_i = 1.5; } else { n_i = 1.6; } maxTIRAngle = Math.asin( 1.0 / n_i ); int materialLeft = 200; int materialThickness = 100; int topPad = 100; int rightEdge = 700; int ledCenterY = topPad + materialThickness / 2; int ledRadius = 50; // draw the LED g.setColor( Color.green ); g.setStroke( new BasicStroke( ( float ) 2.0 ) ); g.draw( new Arc2D.Double( ( double ) ledCenterX - ledRadius, ( double ) ledCenterY - ledRadius, 2.0 * ledRadius, 2.0 * ledRadius, - 90.0 - ledAngleDegrees, 180.0, Arc2D.OPEN ) ); double theta1 = - ledAngle - Math.PI / 2.0; double r2 = ledRadius; int e1x = ( int ) ( ledCenterX - Math.cos( theta1 ) * r2 ); int e1y = ( int ) ( ledCenterY + Math.sin( theta1 ) * r2 ); double theta2 = - ledAngle + Math.PI / 2.0; int e2x = ( int ) ( ledCenterX - Math.cos( theta2 ) * r2 ); int e2y = ( int ) ( ledCenterY + Math.sin( theta2 ) * r2 ); int e3x = ( int ) ( e1x + Math.sin( theta1 ) * r2 * 0.5 ); int e3y = ( int ) ( e1y + Math.cos( theta1 ) * r2 * 0.5 ); int e4x = e2x + ( e3x - e1x ); int e4y = e2y + ( e3y - e1y ); g.drawLine( e1x, e1y, e3x, e3y ); g.drawLine( e3x, e3y, e4x, e4y ); g.drawLine( e4x, e4y, e2x, e2y ); // draw the material g.setColor( Color.white ); double dx = materialThickness * 0.5 / Math.tan( edgeAngle ); if ( dx > 200 ) dx = 200; xSheetBottomLeft = materialLeft - dx; ySheetBottom = topPad; xSheetTopLeft = materialLeft + dx; ySheetTop = topPad + materialThickness; g.drawLine( ( int ) xSheetTopLeft, ( int ) ySheetTop, ( int ) xSheetBottomLeft, ( int ) ySheetBottom ); g.drawLine( ( int ) xSheetTopLeft, ( int ) ySheetTop, rightEdge, ( int ) ySheetTop ); g.drawLine( ( int ) xSheetBottomLeft, ( int ) ySheetBottom, rightEdge, ( int ) ySheetBottom ); // draw the beams and accumulate the evanescent bins for ( int i = 0; i < N_BINS; i++ ) { evanescentBins[ i ] = 0.0; } g.setColor( Color.red ); xLeft = 0; xRight = opticalPanelWidth;; yBottom = 0; yTop = opticalPanelHeight; double dtheta = 2.0 * ledBeamWidth / ( N_BEAMS - 1.0 ); double theta0 = ledAngle - ledBeamWidth; for ( int i = 0; i < N_BEAMS; i++ ) { double theta = theta0 + dtheta * i; fireBeam( g, 1.0 * ledCenterX, 1.0 * ledCenterY, theta ); } evanescentPanel.repaint(); } public static int BOUNDARY = 0; public static int INTERFACE_FROM_INSIDE = 1; public static int INTERFACE_FROM_OUTSIDE = 2; private class Intersection { public double x; public double y; public double outAngle; public double d; public int type; public double decayLength; }; public Intersection checkIntersection( Intersection from, double x, double y, double theta, double dx, double dy, int which, double surfaceNormalAngle ) { double d = Math.sqrt( dx * dx + dy * dy ); if ( d < from.d ) { from.d = d; from.type = which; from.x = x + dx; from.y = y + dy; if ( which == BOUNDARY ) { from.outAngle = theta; } else if ( which == INTERFACE_FROM_INSIDE ) { double theta2 = normalizeAngle( theta + Math.PI ); // reversed double dTheta = normalizeAngle2( theta2 - surfaceNormalAngle ); if ( Math.abs( dTheta ) > maxTIRAngle ) { // reflection from.outAngle = normalizeAngle( theta + Math.PI - 2.0 * dTheta ); from.decayLength = 1.0 / ( n_i * n_i * Math.sin( dTheta ) * Math.sin( dTheta ) - 1.0 ); } else { // transmission: Snell's Law from.outAngle = normalizeAngle( surfaceNormalAngle + Math.PI + Math.asin( n_i * Math.sin( dTheta ) ) ); from.decayLength = -1.0; } } else { double backNormal = normalizeAngle( surfaceNormalAngle + Math.PI ); double dTheta = normalizeAngle2( theta - backNormal ); // transmission: Snell's Law from.outAngle = normalizeAngle( backNormal + Math.asin( Math.sin( dTheta ) / n_i ) ); } } return from; } public Intersection getNextIntersection( double x, double y, double theta, int count ) { // normalize theta to [0, 2 Pi] theta = normalizeAngle( theta ); Intersection retval = new Intersection(); retval.d = 1.0E+10; // check right side of screen if ( ( ( theta < 0.5 * Math.PI ) || ( theta > 1.5 * Math.PI ) ) && ( x < xRight ) ) { double dx = xRight - x; double dy = dx * Math.tan( theta ); retval = checkIntersection( retval, x, y, theta, dx, dy, BOUNDARY, Math.PI ); } // check the left side of the screen if ( ( theta > 0.5 * Math.PI ) && ( theta < 1.5 * Math.PI ) && ( x > xLeft ) ) { double dx = xLeft - x; double dy = dx * Math.tan( theta ); retval = checkIntersection( retval, x, y, theta, dx, dy, BOUNDARY, 0.0 ); } // check bottom of screen if ( ( theta > Math.PI ) && ( theta < 2.0 * Math.PI ) && ( y > yBottom ) ) { double dy = yBottom - y; double dx = dy / Math.tan( theta ); retval = checkIntersection( retval, x, y, theta, dx, dy, BOUNDARY, 0.5 * Math.PI ); } // check top of screen if ( ( theta > 0 ) && ( theta < Math.PI ) && ( y < yTop ) ) { double dy = yTop - y; double dx = dy / Math.tan( theta ); retval = checkIntersection( retval, x, y, theta, dx, dy, BOUNDARY, 1.5 * Math.PI ); } // check for top of sheet from below if ( ( theta > 0 ) && ( theta < Math.PI ) && ( y < ySheetTop ) ) { double dy = ySheetTop - y; double dx = dy / Math.tan( theta ); if ( x + dx > xSheetTopLeft ) { retval = checkIntersection( retval, x, y, theta, dx, dy, INTERFACE_FROM_INSIDE, 1.5 * Math.PI ); } } // check for bottom of sheet from above if ( ( theta > Math.PI ) && ( theta < 2.0 * Math.PI ) && ( y > ySheetBottom ) ) { double dy = ySheetBottom - y; double dx = dy / Math.tan( theta ); if ( x + dx > xSheetBottomLeft ) { retval = checkIntersection( retval, x, y, theta, dx, dy, INTERFACE_FROM_INSIDE, 0.5 * Math.PI ); } } // check for bottom of sheet from below if ( ( theta > 0 ) && ( theta < Math.PI ) && ( y < ySheetBottom ) ) { double dy = ySheetBottom - y; double dx = dy / Math.tan( theta ); if ( x + dx > xSheetBottomLeft ) { retval = checkIntersection( retval, x, y, theta, dx, dy, INTERFACE_FROM_OUTSIDE, 1.5 * Math.PI ); } } // check for top of sheet from above if ( ( theta > Math.PI ) && ( theta < 2.0 * Math.PI ) && ( y > ySheetTop ) ) { double dy = ySheetTop - y; double dx = dy / Math.tan( theta ); if ( x + dx > xSheetTopLeft ) { retval = checkIntersection( retval, x, y, theta, dx, dy, INTERFACE_FROM_OUTSIDE, 0.5 * Math.PI ); } } // check for left edge of sheet from outside; it goes from // (xSheetTopLeft, ySheetTop) to (xSheetBottomLeft, ySheetBottom) if ( count == 0 ) { double st = Math.sin( theta ); double ct = Math.cos( theta ); double dxs = xSheetBottomLeft - xSheetTopLeft; double dys = ySheetBottom - ySheetTop; double ds = Math.sqrt( dxs * dxs + dys * dys ); double cts = dxs / ds; double sts = dys / ds; double tts = sts / cts; double s = ( ( y - ySheetTop ) - tts * ( x - xSheetTopLeft ) ) / ( tts * ct - st ); if ( s > 0.0 ) { double dx = s * Math.cos( theta ); double dy = s * Math.sin( theta ); dys = y + dy; if ( ( dys >= ySheetBottom ) && ( dys <= ySheetTop ) ) { double edgeAngle = ( double ) edgeAngleSlider.getValue(); edgeAngle *= Math.PI / 180.0; double normalAngle = edgeAngle + 0.5 * Math.PI; retval = checkIntersection( retval, x, y, theta, dx, dy, INTERFACE_FROM_OUTSIDE, normalAngle ); } } } if ( ( count == 1 ) && ( retval.type == INTERFACE_FROM_INSIDE ) && ( retval.decayLength > 0 ) ) { // this is a surface hit; figure out double alpha = 1.0 / N_BEAMS; double dx = maxEvanescentDistance / ( N_BINS - 1.0 ); double decayFactor = Math.exp( - dx / retval.decayLength ); for ( int i = 0; i < N_BINS; i++ ) { evanescentBins[ i ] += alpha; alpha *= decayFactor; } } return retval; } public void fireBeam( Graphics2D g, double x0, double y0, double theta ) { boolean done = false; int count = 0; while ( ( !done ) && ( count < 50 ) ) { Intersection i = getNextIntersection( x0, y0, theta, count ); g.drawLine( ( int ) x0, ( int ) y0, ( int ) i.x, ( int ) i.y ); x0 = i.x; y0 = i.y; theta = i.outAngle; if ( i.type == BOUNDARY ) done = true; count++; } } }