import java.awt.*; import java.awt.image.*; import java.applet.*; import java.io.*; import java.net.URL; import java.util.*; public class Anim extends Applet implements Runnable { static final boolean DEBUG = true; //---------------------------------------------------------------------- // // Constants // //---------------------------------------------------------------------- // NOTE: These constants are not exposed externally; they can be changed final static int TYPE_UNKNOWN = -1; final static int TYPE_PAR = 0; final static int TYPE_SEQ = 1; final static int TYPE_SCENE = 2; final static int TYPE_BITMAP = 3; final static int TYPE_VECTOR = 4; final static int TYPE_TEXT = 5; final static int TYPE_SOUND = 6; final static int TYPE_PATH = 7; final static int TYPE_SCALE = 8; final static int TYPE_ROTATE = 9; final static int TYPE_COLOR = 10; final static int GA_INIT = 0; final static int GA_CURR = 1; //final static long MILLIS_PER_FRAME = 50; final static long MILLIS_PER_FRAME = 10; //---------------------------------------------------------------------- // // Globals // //---------------------------------------------------------------------- static Hashtable g_typeHash = null; static { g_typeHash = new Hashtable(); g_typeHash.put( "par", new Integer( TYPE_PAR ) ); g_typeHash.put( "seq", new Integer( TYPE_SEQ ) ); g_typeHash.put( "scene", new Integer( TYPE_SCENE ) ); g_typeHash.put( "bitmap", new Integer( TYPE_BITMAP ) ); g_typeHash.put( "vector", new Integer( TYPE_VECTOR ) ); g_typeHash.put( "text", new Integer( TYPE_TEXT ) ); g_typeHash.put( "sound", new Integer( TYPE_SOUND ) ); g_typeHash.put( "path", new Integer( TYPE_PATH ) ); g_typeHash.put( "scale", new Integer( TYPE_SCALE ) ); g_typeHash.put( "rotate", new Integer( TYPE_ROTATE ) ); g_typeHash.put( "color", new Integer( TYPE_COLOR ) ); } public static Applet g_applet = null; public static Hashtable g_idTable = null; private static MediaTracker g_mediaTracker = null; private static Thread g_animThread = null; private static Image g_offscreenImage = null; private static Graphics g_offscreenGraphics = null; private static long g_tmStart = 0; //---------------------------------------------------------------------- // // Members // //---------------------------------------------------------------------- // Data Structure public Hashtable m_properties = new Hashtable(); public Hashtable m_currentProperties = null; public Vector m_children = new Vector(); public Anim m_parent = null; public int m_type = TYPE_UNKNOWN; public boolean m_fActive = false; //---------------------------------------------------------------------- // // Applet/Component methods // // void init() // void start() // void stop() // void destroy() // void update() // void paint() // void processMouseEvent() // void processMouseMotionEvent() // //---------------------------------------------------------------------- //------------------------------------------------------------ public void init() //------------------------------------------------------------ { if( DEBUG ) System.out.println("init"); g_applet = this; g_mediaTracker = new MediaTracker( this ); g_animThread = new Thread( this ); g_idTable = new Hashtable(); try { URL url = new URL( getDocumentBase(), getParameter( "href" ) ); InputStream stream = url.openStream(); //File file = new File( "test.jck.html" ); //InputStream stream = new FileInputStream( file ); PushbackReader reader = new PushbackReader( new BufferedReader( new InputStreamReader( stream ) ), 2 ); parseElement( reader ); reader.close(); // Add root to the ID table (special case, see parser) String id = getAttribString( "id", null, GA_INIT ); if( id != null ) g_idTable.put( id, this ); if( DEBUG ) dump( "\t" ); int width = (int)getAttribLong( "width", 100, GA_INIT ); int height = (int)getAttribLong( "height", 100, GA_INIT ); setSize( width, height ); onLoad(); g_mediaTracker.waitForAll(); enableEvents( AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK ); g_animThread.start(); } catch ( Exception e ) { if( DEBUG ) e.printStackTrace(); } if( DEBUG ) System.out.println("/init"); } // init //------------------------------------------------------------ public void start() //------------------------------------------------------------ { if( DEBUG ) System.out.println("start"); // TODO: Should not use resume/suspend/stop - use flags to allow thread to self-terminate if( g_animThread != null ) g_animThread.resume(); if( DEBUG ) System.out.println("/start"); } //------------------------------------------------------------ public void stop() //------------------------------------------------------------ { if( DEBUG ) System.out.println("stop"); // TODO: Should not use resume/suspend/stop - use flags to allow thread to self-terminate if( g_animThread != null ) g_animThread.suspend(); if( DEBUG ) System.out.println("/stop"); } //------------------------------------------------------------ public void destroy() //------------------------------------------------------------ { if( DEBUG ) System.out.println("destroy"); // TODO: Should not use resume/suspend/stop - use flags to allow thread to self-terminate if( g_animThread != null ) g_animThread.stop(); if( DEBUG ) System.out.println("/destroy"); } //------------------------------------------------------------ public void update( Graphics g ) //------------------------------------------------------------ { // override to stop erasing background; paint() will do all the right work paint( g ); } //------------------------------------------------------------ public void paint( Graphics g ) //------------------------------------------------------------ { Dimension dim = getSize(); if( g_offscreenImage == null ) { g_offscreenImage = createImage( dim.width, dim.height ); g_offscreenGraphics = g_offscreenImage.getGraphics(); } g_offscreenGraphics.setColor( Color.black ); g_offscreenGraphics.fillRect( 0, 0, dim.width, dim.height ); onPaint( g_offscreenGraphics ); g.drawImage( g_offscreenImage, 0, 0, this ); } // paint //------------------------------------------------------------ public void processMouseEvent( java.awt.event.MouseEvent e ) //------------------------------------------------------------ { if( e.getID() == java.awt.event.MouseEvent.MOUSE_CLICKED ) onMouseEvent( "click", e.getX(), e.getY() ); } // processMouseEvent //------------------------------------------------------------ public void processMouseMotionEvent( java.awt.event.MouseEvent e ) //------------------------------------------------------------ { if( e.getID() == java.awt.event.MouseEvent.MOUSE_MOVED ) onMouseEvent( "move", e.getX(), e.getY() ); } // processMouseMotionEvent //---------------------------------------------------------------------- // Runnable methods //---------------------------------------------------------------------- //------------------------------------------------------------ public void run() //------------------------------------------------------------ { if( DEBUG ) System.out.println("run"); try { g_tmStart = System.currentTimeMillis(); for(;;) { Thread.sleep( MILLIS_PER_FRAME ); long m_tmCur = System.currentTimeMillis() - g_tmStart; //System.out.println("tick"); onTick( m_tmCur / 1000.0 ); Graphics g = this.getGraphics(); if( g != null ) paint( g ); } } catch( Exception e ) { if( DEBUG ) e.printStackTrace(); } if( DEBUG ) System.out.println("/run"); } // run //---------------------------------------------------------------------- // // Anim object methods // // void onLoad(); // void onTick( double t ); // void onPaint( Graphics g ); // void onMouseEvent( int x, int y ); // void onEvent( String eventName, String param ); // //---------------------------------------------------------------------- //------------------------------------------------------------ public void onLoad() throws InterruptedException //------------------------------------------------------------ { switch( m_type ) { case TYPE_BITMAP: { if( m_properties.containsKey("src") ) { // Load image Image image = g_applet.getImage( g_applet.getDocumentBase(), (String)m_properties.get("src") ); g_mediaTracker.addImage( image, 0 ); g_mediaTracker.waitForAll(); g_mediaTracker.removeImage( image ); // And split into individual frames; this makes rendering easier // later when dealing with rotation int hframes = (int)getAttribLong( "hframes", 1, GA_INIT ); int vframes = (int)getAttribLong( "vframes", 1, GA_INIT ); int frames = hframes * vframes; Image[] images = new Image[ frames ]; int dxImg = image.getWidth( null ); int dyImg = image.getHeight( null ); int dxFrame = (int)Math.round( dxImg / hframes ); int dyFrame = (int)Math.round( dyImg / vframes ); int index = 0; for( int y = 0; y < vframes; y++ ) { for( int x = 0; x < hframes; x++ ) { int xSrc = x * dxFrame; int ySrc = y * dyFrame; Image imgFrame = g_applet.createImage( dxFrame, dyFrame ); Graphics g = imgFrame.getGraphics(); g.drawImage( image, -xSrc, -ySrc, null ); images[ index++ ] = imgFrame; } } m_properties.put( "_images", images ); } break; } case TYPE_SOUND: { if( m_properties.containsKey("src") ) { AudioClip audio = g_applet.getAudioClip( g_applet.getDocumentBase(), (String)m_properties.get("src") ); if( audio != null ) { m_properties.put( "_audio", audio ); } } break; } case TYPE_VECTOR: { String points = getAttribString( "points", "", GA_INIT ); Vector xsv = new Vector(); Vector ysv = new Vector(); pathToPoints( points, xsv, ysv ); int nPoints = xsv.size(); int[] xs = new int[ nPoints ]; int[] ys = new int[ nPoints ]; for( int i = 0; i < nPoints; i++ ) { xs[i] = (int)Math.round( ((Double)xsv.elementAt( i )).doubleValue() ); ys[i] = (int)Math.round( ((Double)ysv.elementAt( i )).doubleValue() ); } m_properties.put( "_xs", xs ); m_properties.put( "_ys", ys ); break; } case TYPE_PATH: { String points = getAttribString( "points", "", GA_INIT ); Vector xsv = new Vector(); Vector ysv = new Vector(); pathToPoints( points, xsv, ysv ); int nPoints = xsv.size(); double[] xs = new double[ nPoints ]; double[] ys = new double[ nPoints ]; for( int i = 0; i < nPoints; i++ ) { xs[i] = ((Double)xsv.elementAt( i )).doubleValue(); ys[i] = ((Double)ysv.elementAt( i )).doubleValue(); } m_properties.put( "_xs", xs ); m_properties.put( "_ys", ys ); break; } } // recurse for( Enumeration e = m_children.elements(); e.hasMoreElements(); ) ((Anim)e.nextElement()).onLoad(); } // onLoad //------------------------------------------------------------ public void onTick( double t ) //------------------------------------------------------------ { m_properties.put( "_tick", Double.toString( t ) ); double tmCur = Double.NaN; m_fActive = false; { double tmBegin = getAttribDouble( "begin", 0.0, GA_INIT ); double tmEnd = getAttribDouble( "end", Double.POSITIVE_INFINITY, GA_INIT ); double tmDur = getAttribDouble( "dur", Double.POSITIVE_INFINITY, GA_INIT ); double repeatCount = getAttribDouble( "repeatCount", 1.0, GA_INIT ); String fill = getAttribString( "fill", "remove", GA_INIT ); double tmForceBegin = getAttribDouble( "_forceBegin", Double.NEGATIVE_INFINITY, GA_INIT ); double tmForceEnd = getAttribDouble( "_forceEnd", Double.NEGATIVE_INFINITY, GA_INIT ); if( tmForceBegin != Double.NEGATIVE_INFINITY /*&& (tmBegin == Double.POSITIVE_INFINITY || tmForceBegin > tmBegin)*/ ) tmBegin = tmForceBegin; if( tmForceEnd != Double.NEGATIVE_INFINITY && tmForceEnd < tmEnd ) tmEnd = tmForceEnd; // special case - sound if( m_type == TYPE_SOUND ) { AudioClip audio = (AudioClip)m_properties.get( "_audio" ); if( audio == null ) return; long lastIter = getAttribLong( "_lastIter", 1, GA_INIT ); boolean fPlaying = "playing".equals( getAttribString( "_status", "stopped", GA_INIT ) ); boolean fShouldPlay = false; boolean fRestart = false; long iter = 1; // If not explicitly ended... if( t < tmEnd ) { double tm = t - tmBegin; if( tm < 0 ) { // Before start - therefore not playing fShouldPlay = false; } else if( tmDur == Double.POSITIVE_INFINITY ) { // No duration specified - therefore, playing fShouldPlay = true; } else { iter = (long)Math.floor( tm / tmDur ); fShouldPlay = iter < repeatCount; fRestart = ( iter != lastIter ); } } if( fRestart ) resetTiming( false ); if( fShouldPlay ) { if( !fPlaying || fRestart ) audio.play(); } else { if( fPlaying ) audio.stop(); } m_properties.put( "_lastIter", Double.toString( iter ) ); m_properties.put( "_status", fShouldPlay ? "playing" : "stopped" ); m_fActive = fShouldPlay; } else // non-Sounds { // Compute the (local) current time of this node if( tmDur == 0 ) return; double tm = t - tmBegin; if( tm < 0 ) return; double iter = tm / tmDur; if( iter >= repeatCount || t >= tmEnd ) { if( "hold".equals( fill ) ) { tmCur = tmDur; } else { return; } } else { tmCur = tm % tmDur; } double lastIter = getAttribDouble( "_lastIter", 1, GA_INIT ); if( Math.floor( lastIter ) != Math.floor( iter ) ) resetTiming( false ); m_properties.put( "_lastIter", Double.toString( iter ) ); m_fActive = true; } } m_properties.put( "_tmCur", Double.toString( tmCur ) ); // This is relevant only for behaviors Anim m_actor = m_parent; String actorID = getAttribString( "actor", null, GA_INIT ); if( actorID != null ) m_actor = (Anim)g_idTable.get( actorID ); switch( m_type ) { case TYPE_PAR: case TYPE_SEQ: // no-op break; case TYPE_SCENE: case TYPE_BITMAP: case TYPE_VECTOR: case TYPE_TEXT: case TYPE_UNKNOWN: // Not necessary for behavior extensions, but required for actor extensions { // Make a transient copy of the properties, for modification by behaviors m_currentProperties = (Hashtable)m_properties.clone(); break; } case TYPE_PATH: { double[] xs = (double[])m_properties.get( "_xs" ); double[] ys = (double[])m_properties.get( "_ys" ); int nPoints = xs.length; double tmDur = getAttribDouble( "dur", Double.POSITIVE_INFINITY, GA_INIT ); double frac = tmCur / tmDur; int interval = (int)Math.floor( frac * (nPoints-1) ); if( interval >= 0 && interval < nPoints-1 ) { double lastX = xs[ interval ]; double lastY = ys[ interval ]; double nextX = xs[ interval + 1 ]; double nextY = ys[ interval + 1 ]; double tmIntervalDur = tmDur / nPoints; double tmIntervalStart = interval * tmIntervalDur; double p = ( tmCur - tmIntervalStart ) / tmIntervalDur; double x = ( (nextX - lastX) * p ) + lastX; double y = ( (nextY - lastY) * p ) + lastY; x += m_actor.getAttribDouble( "x", 0, GA_CURR ); y += m_actor.getAttribDouble( "y", 0, GA_CURR ); m_actor.m_currentProperties.put( "x", Double.toString( x ) ); m_actor.m_currentProperties.put( "y", Double.toString( y ) ); } break; } case TYPE_SCALE: { double initialScale = getAttribDouble( "initialScale", 1.0, GA_INIT ); double finalScale = getAttribDouble( "finalScale", 1.0, GA_INIT ); double tmDur = getAttribDouble( "dur", Double.POSITIVE_INFINITY, GA_INIT ); double frac = tmCur / tmDur; double scale = ( (finalScale - initialScale) * frac + initialScale ) * m_actor.getAttribDouble( "scale", 1.0, GA_CURR ); m_actor.m_currentProperties.put( "scale", Double.toString( scale ) ); break; } case TYPE_ROTATE: { double initialAngle = getAttribDouble( "initialAngle", 0.0, GA_INIT ); double finalAngle = getAttribDouble( "finalAngle", 0.0, GA_INIT ); double tmDur = getAttribDouble( "dur", Double.POSITIVE_INFINITY, GA_INIT ); double frac = tmCur / tmDur; double angle = ( (finalAngle - initialAngle) * frac + initialAngle ) + m_actor.getAttribDouble( "angle", 0.0, GA_CURR ); m_actor.m_currentProperties.put( "angle", Double.toString( angle ) ); break; } case TYPE_COLOR: { String colorProp = getAttribString( "colorProp", "color", GA_INIT ); Color curColor = m_actor.getAttribColor( colorProp, Color.black, GA_CURR ); Color initialColor = getAttribColor( "initialColor", curColor, GA_INIT ); Color finalColor = getAttribColor( "finalColor", curColor, GA_INIT ); double tmDur = getAttribDouble( "dur", Double.POSITIVE_INFINITY, GA_INIT ); double frac = tmCur / tmDur; int r1 = initialColor.getRed(); int r2 = finalColor .getRed(); int g1 = initialColor.getGreen(); int g2 = finalColor .getGreen(); int b1 = initialColor.getBlue(); int b2 = finalColor .getBlue(); int r = (int)( r1 + (r2 - r1) * frac ); int g = (int)( g1 + (g2 - g1) * frac ); int b = (int)( b1 + (b2 - b1) * frac ); int rgb = ((r&0xff)<<16) | ((g&0xff)<<8) | (b&0xff); m_actor.m_currentProperties.put( colorProp, Integer.toHexString( rgb ) ); break; } } if( m_type == TYPE_SEQ ) { double tm = tmCur; for( Enumeration e = m_children.elements(); e.hasMoreElements(); ) { Anim child = (Anim)e.nextElement(); double tmBegin = child.getAttribDouble( "begin", 0.0, GA_INIT ); double tmDur = child.getAttribDouble( "dur", Double.POSITIVE_INFINITY, GA_INIT ); double repeatCount = child.getAttribDouble( "repeatCount", 1.0, GA_INIT ); double tmFullDur = tmBegin + (tmDur * repeatCount); if( tm < tmFullDur ) { child.onTick( tm ); break; } tm -= tmFullDur; } } else // PAR-type { // Recurse for( Enumeration e = m_children.elements(); e.hasMoreElements(); ) ((Anim)e.nextElement()).onTick( tmCur ); } } // onTick //------------------------------------------------------------ public void onPaint( Graphics g ) //------------------------------------------------------------ { if( !m_fActive ) return; switch( m_type ) { case TYPE_PAR: case TYPE_SEQ: { // recurse for( Enumeration e = m_children.elements(); e.hasMoreElements(); ) ((Anim)e.nextElement()).onPaint( g ); return; } case TYPE_SCENE: case TYPE_BITMAP: case TYPE_VECTOR: case TYPE_TEXT: // no-op break; // Behaviors - bail now default: return; } // Slurp in common attributes int x = (int)getAttribLong( "x", 0, GA_CURR ); int y = (int)getAttribLong( "y", 0, GA_CURR ); int width = (int)getAttribLong( "width", 0, GA_CURR ); int height = (int)getAttribLong( "height", 0, GA_CURR ); double scale = getAttribDouble( "scale", 1.0, GA_CURR ); double angle = getAttribDouble( "angle", 0.0, GA_CURR ) * Math.PI / 180.0; switch( m_type ) { case TYPE_SCENE: { g.translate( x, y ); Color bgcolor = getAttribColor( "bgcolor", null, GA_CURR ); Shape clip = null; if( width != 0 && height != 0 ) { // Scale width = (int)Math.round( width * scale ); height = (int)Math.round( height * scale ); clip = g.getClip(); g.clipRect( 0, 0, width, height ); if( bgcolor != null ) { g.setColor( bgcolor ); g.fillRect( 0, 0, width, height ); } } // recurse for( Enumeration e = m_children.elements(); e.hasMoreElements(); ) ((Anim)e.nextElement()).onPaint( g ); if( clip != null ) g.setClip( clip ); g.translate( -x, -y ); Polygon hitRegion = new Polygon( new int[] {x, x+width, x+width, x }, new int[] {y, y, y+height, y+height }, 4 ); m_properties.put( "_hitPoly", hitRegion ); m_properties.put( "_offsetX", Double.toString( x ) ); m_properties.put( "_offsetY", Double.toString( y ) ); break; } case TYPE_BITMAP: { Image[] images = (Image[])m_currentProperties.get( "_images" ); double tmCur = getAttribDouble( "_tmCur", 1, GA_INIT ); double rate = getAttribDouble( "rate", 1, GA_CURR ); int frames = images.length; int frame = (int)Math.floor( (tmCur * rate) % frames ); Image image = images[frame]; // Rotation image = rotateImage( g_applet, image, angle ); // Scale int dxSrc = image.getWidth( null ); int dySrc = image.getHeight( null ); int dxDst = (int)Math.round( dxSrc * scale ); int dyDst = (int)Math.round( dySrc * scale ); // Render int xOffset = dxDst/2; int yOffset = dyDst/2; g.drawImage( image, x-xOffset, y-yOffset, dxDst, dyDst, g_applet ); // Compute bounds for hit testing int[] xs = new int[] { x-xOffset, x-xOffset+dxDst, x-xOffset+dxDst, x-xOffset }; int[] ys = new int[] { y-yOffset, y-yOffset, y-yOffset+dyDst, y-yOffset+dyDst }; rotateScalePoints( xs, ys, 4, angle, 1.0 ); Polygon hitRegion = new Polygon( xs, ys, 4 ); m_properties.put( "_hitPoly", hitRegion ); break; } case TYPE_VECTOR: { int[] xs = (int[])m_properties.get( "_xs" ); int[] ys = (int[])m_properties.get( "_ys" ); int nPoints = xs.length; // Clone for scaling & rotation xs = (int[])xs.clone(); ys = (int[])ys.clone(); rotateScalePoints( xs, ys, nPoints, angle, scale ); Color lineColor = getAttribColor( "linecolor", null, GA_CURR ); Color fillColor = getAttribColor( "fillcolor", null, GA_CURR ); g.translate( x, y ); if( fillColor != null ) { g.setColor( fillColor ); g.fillPolygon( xs, ys, nPoints ); } if( lineColor != null ) { g.setColor( lineColor ); g.drawPolygon( xs, ys, nPoints ); } g.translate( -x, -y ); // Compute bounds for hit testing Polygon hitRegion = new Polygon( xs, ys, nPoints ); hitRegion.translate( x, y ); m_properties.put( "_hitPoly", hitRegion ); break; } case TYPE_TEXT: { String text = getAttribString( "text", "", GA_CURR ); String family = getAttribString( "family", "SansSerif", GA_CURR ); double size = getAttribDouble( "size", 12.0f, GA_CURR ); String weight = getAttribString( "weight", "normal", GA_CURR ); String style = getAttribString( "style", "normal", GA_CURR ); Color color = getAttribColor( "color", Color.white, GA_CURR ); // Scale size *= scale; Font font = new Font( family, Font.PLAIN | ( ( "bold" .equals( weight ) ) ? Font.BOLD : 0 ) | ( ( "italic".equals( style ) ) ? Font.ITALIC : 0 ), (int)Math.round(size) ); FontMetrics fm = g.getFontMetrics(font); width = fm.stringWidth(text); height = fm.getHeight(); Color bgcolor = getAttribColor( "bgcolor", null, GA_CURR ); int xOffset = width / 2; int yOffset = height / 2; // Rotation if( angle == 0 ) { if( bgcolor != null ) { g.setColor( bgcolor ); g.fillRect( x-xOffset, y-yOffset, width, height ); } g.setColor( color ); g.setFont( font ); g.drawString( text, x-xOffset, y+fm.getAscent()-yOffset ); } else { // PERF: This runs every frame; should cache the surface Image imgText = g_applet.createImage( width, height ); Graphics gText = imgText.getGraphics(); if( bgcolor != null ) { gText.setColor( bgcolor ); gText.fillRect( 0, 0, width, height ); gText.setColor( color ); } else { gText.setColor( Color.black ); gText.fillRect( 0, 0, width, height ); gText.setColor( Color.white ); } gText.setFont( font ); gText.drawString( text, 0, fm.getAscent() ); // PERF: Try and cache the chroma image if not scaled if( bgcolor == null ) imgText = chromaImage( g_applet, imgText, color ); imgText = rotateImage( g_applet, imgText, angle ); int dx = imgText.getWidth( null ); int dy = imgText.getHeight( null ); g.drawImage( imgText, x-dx/2, y-dy/2, null ); } // Compute bounds for hit testing int[] xs = new int[] { x-xOffset, x-xOffset+width, x-xOffset+width, x-xOffset }; int[] ys = new int[] { y-yOffset, y-yOffset, y-yOffset+height, y-yOffset+height }; rotateScalePoints( xs, ys, 4, angle, 1.0 ); Polygon hitRegion = new Polygon( xs, ys, 4 ); m_properties.put( "_hitPoly", hitRegion ); break; } } } // onPaint //------------------------------------------------------------ public void onMouseEvent( String type, int x, int y ) //------------------------------------------------------------ { if( !m_fActive ) return; // BUG: Not working for rotating bitmaps Polygon hitRegion = (Polygon)m_properties.get( "_hitPoly" ); if( hitRegion == null ) return; if( hitRegion.contains( x, y ) ) { // TODO: Should the event be consumed? if( type == "click" ) { String onClick = getAttribString( "onclick", null, GA_INIT ); if( onClick != null ) { evaluateScriptlet( onClick ); } } else if( type == "move" && !m_properties.containsKey( "_hover" ) ) { m_properties.put( "_hover", true ); String onEnter = getAttribString( "onenter", null, GA_INIT ); if( onEnter != null ) { evaluateScriptlet( onEnter ); } } } else if( type == "move" && m_properties.containsKey( "_hover" ) ) { m_properties.remove( "_hover" ); String onExit = getAttribString( "onexit", null, GA_INIT ); if( onExit != null ) { evaluateScriptlet( onExit ); } } // Recurse, offset appropriately x += (int)getAttribLong( "_offsetX", 0, GA_INIT ); y += (int)getAttribLong( "_offsetY", 0, GA_INIT ); for( Enumeration e = m_children.elements(); e.hasMoreElements(); ) { // TODO: This descends in the same order as rendering; needs to be in reverse order // so top-most item gets the event first if we consume events ((Anim)e.nextElement()).onMouseEvent( type, x, y ); } } // onMouseEvent //------------------------------------------------------------ private void evaluateScriptlet( String script ) //------------------------------------------------------------ { // scriptlet ::= statement [ ";" scriptlet ] // statement ::= object "." member [ "=" parameter ] StringTokenizer statements = new StringTokenizer( script, ";" ); while( statements.hasMoreTokens() ) { StringTokenizer tokens = new StringTokenizer( statements.nextToken(), ".=" ); if( tokens.countTokens() >= 2 ) { String object = tokens.nextToken().trim(); String event = tokens.nextToken( ".=" ).trim(); String param = tokens.hasMoreTokens() ? tokens.nextToken( "" ).trim() : null; if( object == "window" ) { if( event == "href" ) { g_applet.getAppletContext().showDocument( getDocumentBase(), param ); } } else { Anim target = (Anim)g_idTable.get( object ); if( target != null ) { target.onEvent( event, param ); if( DEBUG ) System.out.println( "target: " + object + ", event: " + event + ", param: " + param ); } } } } } // evaluateScriptlet //------------------------------------------------------------ public void onEvent( String eventName, String param ) //------------------------------------------------------------ { if( "start".equals( eventName ) ) { resetTiming( true ); double tmCur = getAttribDouble( "_tick", 1, GA_INIT ); m_properties.put( "_forceBegin", Double.toString( tmCur ) ); m_properties.put( "_status", "stopped" ); // For sounds } else if( "stop".equals( eventName ) ) { double tmCur = getAttribDouble( "_tick", 1, GA_INIT ); m_properties.put( "_forceEnd", Double.toString( tmCur ) ); } } // onEvent //------------------------------------------------------------ private void resetTiming( boolean fSelf ) //------------------------------------------------------------ { if( fSelf ) { m_properties.put( "_forceBegin", Double.toString( Double.NEGATIVE_INFINITY ) ); m_properties.put( "_forceEnd", Double.toString( Double.NEGATIVE_INFINITY ) ); } for( Enumeration e = m_children.elements(); e.hasMoreElements(); ) ((Anim)e.nextElement()).resetTiming( true ); } //---------------------------------------------------------------------- // // File Parser // //---------------------------------------------------------------------- // Grammar: // element ::= whitespace* ( ( opentag element* closetag ) | emptytag )? // emptytag ::= '<' alphanumeric+ attribute* '/' '>' // opentag ::= '<' alphanumeric+ attribute* '>' // closetag ::= '<' '/' alphanumeric* '>' // attribute ::= whitespace* (alphanumeric+ whitespace* '=' whitespace* '"' [^"]* '"')? // NOTE: To streamline parsing, the distinction between an // open tag and an empty tag is handled by the same // parsing function and distinguished by return value. // NOTE: Close tags names are not checked, and the name is optional public static final int TAG_ERROR = 0; public static final int TAG_OPEN = 1; public static final int TAG_EMPTY = 2; //------------------------------------------------------------ public boolean parseElement( PushbackReader reader ) throws Exception //------------------------------------------------------------ { // element ::= whitespace* ( ( opentag element* closetag ) | emptytag )? char c; //---------------------------------------- // ' '* while( Character.isWhitespace( c = (char)reader.read() ) ); reader.unread( c ); //---------------------------------------- // opentag int tagType = parseTag( reader ); if( tagType == TAG_ERROR ) return false; { // Determine the type String typeName = getAttribString( "_type", "", GA_INIT ); if( g_typeHash.containsKey( typeName ) ) { m_type = ( (Integer)g_typeHash.get( typeName ) ).intValue(); } else { m_type = TYPE_UNKNOWN; if( DEBUG ) System.out.println( "Unknown tag: " + typeName + " (may be an extension)" ); } } if( tagType == TAG_OPEN ) { //---------------------------------------- // element* for(;;) { Anim child = new Anim(); if( child.parseElement( reader ) ) { // If the child specifies a class name, instantiate and copy // over all of the parsed data. String className = child.getAttribString( "class", null, GA_INIT ); if( className != null ) { if( DEBUG ) System.out.println( "Extension requested: " + className ); Anim child2 = (Anim)Class.forName( className ).newInstance(); // Use the current child as a prototype, and copy properties over child2.m_type = child.m_type; child2.m_properties = child.m_properties; child2.m_children = child.m_children; for( Enumeration e = child2.m_children.elements(); e.hasMoreElements(); ) ((Anim)e.nextElement()).m_parent = child2; child = child2; } // Add to parent/child tree child.m_parent = this; m_children.addElement( child ); // Add to the ID table String id = child.getAttribString( "id", null, GA_INIT ); if( id != null ) g_idTable.put( id, child ); } else { break; } } //---------------------------------------- // closetag parseCloseTag( reader ); } return true; } // parseElement //------------------------------------------------------------ public int parseTag( PushbackReader reader ) throws Exception //------------------------------------------------------------ { // tag ::= '<' alphanumeric+ attribute* '/'? '>' char c; //---------------------------------------- // '<' parse_expect( reader, '<' ); // Distinguish between another open tag and a close tag; // the grammar is ambiguous here so we have to rewind by hand if( parse_test(reader,'/') ) { reader.unread('<'); return TAG_ERROR; } //---------------------------------------- // alphanumeric+ StringBuffer nameBuf = new StringBuffer(); while( Character.isLetterOrDigit( c = (char)reader.read() ) ) nameBuf.append( c ); String name = nameBuf.toString(); reader.unread( c ); m_properties.put( "_type", name ); //---------------------------------------- // attribute* while( parseAttribute( reader ) ); //---------------------------------------- // '/'? '>' if( parse_test( reader, '/' ) ) { parse_expect( reader, '/' ); parse_expect( reader, '>' ); return TAG_EMPTY; } else { parse_expect( reader, '>' ); return TAG_OPEN; } } // parseTag //------------------------------------------------------------ public void parseCloseTag( PushbackReader reader ) throws Exception //------------------------------------------------------------ { // closetag ::= '<' '/' alphanumeric* '>' char c; //---------------------------------------- // '<' parse_expect( reader, '<' ); //---------------------------------------- // '/' parse_expect( reader, '/' ); //---------------------------------------- // alphanumeric* while( Character.isLetterOrDigit( c = (char)reader.read() ) ); reader.unread( c ); //---------------------------------------- // '>' parse_expect( reader, '>' ); } // parseCloseTag //------------------------------------------------------------ public boolean parseAttribute( PushbackReader reader ) throws Exception //------------------------------------------------------------ { // attribute ::= whitespace* (alphanumeric+ whitespace* '=' whitespace* '"' [^"]* '"')? char c; //---------------------------------------- // ' '* while( Character.isWhitespace( c = (char)reader.read() ) ); reader.unread( c ); // Distinguish between an attribute and the end of a tag; // the grammar is ambiguous here so we have to rewind by hand reader.unread( c = (char)reader.read() ); if( !Character.isLetterOrDigit( c ) ) return false; //---------------------------------------- // alphanumeric+ StringBuffer attribBuf = new StringBuffer(); while( Character.isLetterOrDigit( c = (char)reader.read() ) ) attribBuf.append( c ); String attrib = attribBuf.toString(); reader.unread( c ); //---------------------------------------- // ' '* while( Character.isWhitespace( c = (char)reader.read() ) ); reader.unread( c ); //---------------------------------------- // '=' parse_expect( reader, '=' ); //---------------------------------------- // ' '* while( Character.isWhitespace( c = (char)reader.read() ) ); reader.unread( c ); //---------------------------------------- // '"' parse_expect( reader, '"' ); //---------------------------------------- // [^']* StringBuffer valueBuf = new StringBuffer(); while( ( c = (char)reader.read() ) != '"' ) valueBuf.append( c ); String value = valueBuf.toString(); reader.unread( c ); //---------------------------------------- // '"' parse_expect( reader, '"' ); // Decode escaped characters value = stringReplace( value, """, "\"" ); value = stringReplace( value, "<", "<" ); value = stringReplace( value, ">", ">" ); value = stringReplace( value, "&", "&" ); m_properties.put( attrib, value ); return true; } // parseAttribute //------------------------------------------------------------ public static void parse_expect( PushbackReader reader, char chExpect ) throws Exception //------------------------------------------------------------ { char chRead = (char)reader.read(); if( chRead != chExpect ) { reader.unread( chRead ); throw new Exception( "Expected \""+chExpect+"\", saw \""+chRead+"\"" ); } } // parse_expect //------------------------------------------------------------ public static boolean parse_test( PushbackReader reader, char chExpect ) throws IOException //------------------------------------------------------------ { char chRead = (char)reader.read(); reader.unread( chRead ); return chRead == chExpect; } // parse_test //---------------------------------------------------------------------- // // Attributes // //---------------------------------------------------------------------- //------------------------------------------------------------ public double getAttribDouble( String attrib, double defVal, int flags ) //------------------------------------------------------------ { // BUG: There's a null pointer dereference here on Applet Restart Hashtable properties = ((flags & GA_CURR)!=0) ? m_currentProperties : m_properties; try { if( properties.containsKey( attrib ) ) { String value = (String)properties.get( attrib ); return "Infinity".equals(value) ? Double.POSITIVE_INFINITY : Double.valueOf( value ).doubleValue(); } } catch( NumberFormatException e ) {} return defVal; } // getAttribDouble //------------------------------------------------------------ public long getAttribLong( String attrib, long defVal, int flags ) //------------------------------------------------------------ { return Math.round( getAttribDouble( attrib, defVal, flags ) ); } // getAttribLong //------------------------------------------------------------ public String getAttribString( String attrib, String defVal, int flags ) //------------------------------------------------------------ { Hashtable properties = ((flags & GA_CURR)!=0) ? m_currentProperties : m_properties; if( properties.containsKey( attrib ) ) return properties.get( attrib ).toString(); return defVal; } // getAttribString //------------------------------------------------------------ public Color getAttribColor( String attrib, Color defVal, int flags ) //------------------------------------------------------------ { Hashtable properties = ((flags & GA_CURR)!=0) ? m_currentProperties : m_properties; try { if( properties.containsKey( attrib ) ) { String val = properties.get( attrib ).toString(); return new Color( Integer.parseInt( val, 16 ) ); } } catch( NumberFormatException e ) {} return defVal; } // getAttribColor //---------------------------------------------------------------------- // // Utility Functions // //---------------------------------------------------------------------- //------------------------------------------------------------ public static String stringReplace( String orig, String match, String repl ) //------------------------------------------------------------ { int idx; while( -1 != ( idx = orig.indexOf( match ) ) ) orig = orig.substring( 0, idx ) + repl + orig.substring( idx + match.length() ); return orig; } // For evaluating Bezier curves; smaller = more points public static double TIME_STEP = 0.025; //------------------------------------------------------------ public static void pathToPoints( String path, Vector xs, Vector ys ) //------------------------------------------------------------ { double curX = 0.0; double curY = 0.0; try { StringTokenizer tokens = new StringTokenizer( path, " \t\r\n,;()[]{}" ); while( tokens.hasMoreTokens() ) { String tok = tokens.nextToken(); if( "m".equals(tok) || "l".equals(tok) ) { curX = Double.valueOf( tokens.nextToken() ).doubleValue(); curY = Double.valueOf( tokens.nextToken() ).doubleValue(); xs.addElement( new Double( curX ) ); ys.addElement( new Double( curY ) ); } else if( "c".equals(tok) || "v".equals(tok) ) { double x0 = curX; double y0 = curY; double x1 = Double.valueOf( tokens.nextToken() ).doubleValue(); double y1 = Double.valueOf( tokens.nextToken() ).doubleValue(); double x2 = Double.valueOf( tokens.nextToken() ).doubleValue(); double y2 = Double.valueOf( tokens.nextToken() ).doubleValue(); double x3 = Double.valueOf( tokens.nextToken() ).doubleValue(); double y3 = Double.valueOf( tokens.nextToken() ).doubleValue(); for( double t = TIME_STEP; t <= 1+TIME_STEP ; t += TIME_STEP ) { // use Berstein polynomials // 3 2 2 3 // p(t) = (1 - t) p + 3t(1 - t) p + 3t (1 - t)p + t p // 0 1 2 3 curX = (1-t)*(1-t)*(1-t)*x0 + 3*t*(1-t)*(1-t)*x1 + 3*t*t*(1-t)*x2 + t*t*t*x3; curY = (1-t)*(1-t)*(1-t)*y0 + 3*t*(1-t)*(1-t)*y1 + 3*t*t*(1-t)*y2 + t*t*t*y3; xs.addElement( new Double( curX ) ); ys.addElement( new Double( curY ) ); } } else if( "x".equals(tok) || "e".equals(tok) ) { break; } else { if( DEBUG ) System.out.println("Unknown curve token: " + tok ); break; } } } catch( NumberFormatException e ) { if( DEBUG ) e.printStackTrace(); } } // pathToPoints //------------------------------------------------------------ public static Image rotateImage( Component comp, Image srcImg, double angle ) //------------------------------------------------------------ { if( angle == 0 ) return srcImg; int dxSrc = srcImg.getWidth( null ); int dySrc = srcImg.getHeight( null ); double sa = Math.sin( angle ); double ca = Math.cos( angle ); int dxDst = (int)Math.round( Math.abs( ca * dxSrc ) + Math.abs( sa * dySrc ) ); int dyDst = (int)Math.round( Math.abs( ca * dySrc ) + Math.abs( sa * dxSrc ) ); int[] srcPixels = null; try { // PERF: Consider caching pixel array PixelGrabber g = new PixelGrabber( srcImg, 0, 0, dxSrc, dySrc, true ); if( g.grabPixels() ) srcPixels = (int[])g.getPixels(); else return null; } catch( InterruptedException e ) { e.printStackTrace(); return null; } int[] dstPixels = new int[ dxDst * dyDst ]; int index = 0; double cxSrc = dxSrc / 2; double cySrc = dySrc / 2; double cxDst = dxDst / 2; double cyDst = dyDst / 2; double xIncr = ca; double yIncr = -sa; for( int y = 0; y < dyDst; y++ ) { double xDst = 0 - cxDst; double yDst = y - cyDst; double xSrc = ( xDst * ca + yDst * sa ) + cxSrc; double ySrc = ( yDst * ca - xDst * sa ) + cySrc; for( int x = 0; x < dxDst; x++ ) { xSrc += xIncr; ySrc += yIncr; // NOTE: Rounding would be more accurate, but slower int pxSrc = (int)xSrc; int pySrc = (int)ySrc; if( pxSrc < 0 || pySrc < 0 || pxSrc >= dxSrc || pySrc >= dySrc ) { dstPixels[ index++ ] = 0x00000000; // Transparent } else { int idx = pxSrc + dxSrc * pySrc; int argb = srcPixels[ idx ]; dstPixels[ index++ ] = argb; } } } return comp.createImage( new MemoryImageSource( dxDst, dyDst, dstPixels, 0, dxDst ) ); } // rotateImage //------------------------------------------------------------ public static Image chromaImage( Component comp, Image srcImg, Color fgColor ) //------------------------------------------------------------ { int dx = srcImg.getWidth( null ); int dy = srcImg.getHeight( null ); int[] pixels = null; try { PixelGrabber g = new PixelGrabber( srcImg, 0, 0, dx, dy, true ); if( g.grabPixels() ) pixels = (int[])g.getPixels(); else return null; } catch( InterruptedException e ) { e.printStackTrace(); return null; } int rgb = fgColor.getRGB() & 0x00ffffff; int nPixels = pixels.length; for( int i = nPixels-1; i >= 0; i-- ) { pixels[i] = ((pixels[i] & 0xff) << 24) | rgb; } return comp.createImage( new MemoryImageSource( dx, dy, pixels, 0, dx ) ); } // chromaImage //------------------------------------------------------------ public static Image createTransparentImage( Component comp, int width, int height ) //------------------------------------------------------------ { int nPixels = width * height; int[] dstPixels = new int[ nPixels ]; for( int i = 0; i < nPixels; i++ ) { dstPixels[ i ] = 0x00000000; } return comp.createImage( new MemoryImageSource( width, height, dstPixels, 0, width ) ); } // createTransparentImage //------------------------------------------------------------ public static void rotateScalePoints( int[] xs, int[] ys, int nPoints, double angle, double scale ) //------------------------------------------------------------ { double ca = Math.cos(angle); double sa = Math.sin(angle); for( int i = 0; i < nPoints; i++ ) { double x = xs[i] * scale; double y = ys[i] * scale; xs[i] = (int)Math.round( x * ca + y * sa ); ys[i] = (int)Math.round( y * ca - x * sa ); } } // rotateScalePoints //------------------------------------------------------------ public void dump( String prefix ) //------------------------------------------------------------ { if( DEBUG ) { String tagName = m_properties.get( "_type" ).toString(); System.out.println( prefix + "<" + tagName ); for( Enumeration e = m_properties.keys(); e.hasMoreElements(); ) { String key = (String)e.nextElement(); System.out.println( prefix + " " + key + "=\"" + m_properties.get(key) + "\"" ); } System.out.println( prefix + ">" ); for( Enumeration e = m_children.elements(); e.hasMoreElements(); ) { ((Anim)e.nextElement()).dump( prefix + " " ); } System.out.println( prefix + "" ); } } // dump }