/* JitterbugController.java History: 08 Jul 08 JitterbugGeometry.RADIUS becomes JitterbugModel.HALFMAX since the former was never really the radius of anything. Use Java sorting libraries instead of implementing heap sort. JitterbugModel extracted to a separate file. 07 Jul 08 Add a lower-level class to the hierarchy, JitterbugModel, which just manages the model coordinate values given the angle. This is extracted from JitterbugGeometry which is now called JitterbugRenderer. 28 Feb 07 Use nested Stepper class instead of Toggle and use interrupt feature to stop it. Synchronize more of the code to avoid choppiness at max speed. 08 Aug 06 JitterbugGeometry.Line and JitterbugGeometry.Triangle are not static nested classes, so deviceCoords arg to the member functions is needless. In Line.draw(), add a few more comments and don't bother with intermediate computation of coordinate values since they are only used in one place. 05 Aug 06 For clarity, rename toggle() as step() and write a new toggle() function which just calls step. Remove MIN_STAGE since DEFAULT_STAGE will serve as well. 03 Aug 06 Use JComponent instead of Canvas as display surface, BufferedImage instead of Image for the graphics buffer, and Graphics2D instead of Graphics for rendering into the buffer. Add INITIAL_TIME_DELAY label. Fix comments. 29 May 06 Substitute drag rotation for clunky scrollbar-driven rotation. Rename m_bufferImage as m_graphicsBuffer. Add some comments. 31 Oct 05 Remove semi-useless magnify facility. Add extra lines for VE_STAGE. 27 Oct 05 Remove wait() after requestStop() call in suspend(). 17 Oct 05 Encapsulate more stage information in JitterbugController rather than have JitterbugViewer deal with it. Hard code radius and don't bother with center (it's the origin). Remove M_ prefix from all constants. Rename FUDGE_FACTOR to more specific REDUCTION_FACTOR. 14 Oct 05 Construct figures with triangles rather than lines. Extensive reworking of code to simplify it and separate it more logically between JitterbugController and JitterbugGeometry. JitterbugPicture renamed to JitterbugController. 13 Oct 05 Replace needlessly complex masks and store line data in struct instead of a single integer. Replace deprecated size().width etc. calls. 24 Mar 99 Added javadoc comments. Added constants for various angle values. Get rid of m_nverts - use m_verts.length/3 instead. Get rid of unused zmid() member functions. 02 Mar 99 Allow acceleration of animation. 01 Mar 99 Allow starting and stopping and show extra lines. 23 Feb 99 Created from tview/ThreeD.java. To do: Optimize zmid computations by caching the values so they are only computed once for each Drawable during a sort. */ import java.awt.*; import java.awt.image.*; import java.lang.Math; import java.lang.Thread; /** Manage the way the jitterbug is drawn on Canvas. Much of the rendering technique and geometry handling was originally adapted from the JDK 1.1.1 wireframe example. @author Bob Burkhardt @see JitterbugRenderer */ public class JitterbugController extends javax.swing.JComponent { /** Utility to periodically "step" the model during animations. */ class Stepper extends Thread { /** Amount of time to sleep between steps. */ long m_ms_delay; /** @param ms_delay milliseconds to sleep between invocation of client's toggle member function */ Stepper(long ms_delay) { m_ms_delay = ms_delay; start(); } void setInterval(long ms_delay) { m_ms_delay = ms_delay; } long getInterval() { return m_ms_delay; } public void run() { for (;;) { try { sleep(m_ms_delay); } catch(InterruptedException e) { interrupt(); } if (isInterrupted()) { return; } else step(); } } } /** A reduction factor to make sure all the Jitterbug is visible. */ public static final float REDUCTION_FACTOR = 0.99f; /** Initial time (in ms) between animated steps. */ static final int INITIAL_TIME_DELAY = 100; /** The amount the jitterbug's triangles are rotated in each animation step. The value is negated whenever the jitterbug reaches either end of its range of permissible angle values. */ int m_increment; /** Indicates whether the jitterbug is currently being animated. */ boolean m_is_suspended; /** The jitterbug model. */ JitterbugRenderer m_renderer; /** Where all graphics are rendered. This is the same size as the Canvas and is drawn onto the Canvas any time the image needs updating. */ BufferedImage m_graphicsBuffer; /** Graphics context used for rendering the graphics. @see #m_graphicsBuffer */ Graphics2D m_bufferGC; /** Last recorded width of the Canvas. */ int m_width; /** Last recorded height of the Canvas. */ int m_height; /** Scale value which multiplies model coordinates to get the device coordinates used to render the jitterbug on Canvas. */ float m_scale; /** Transformation matrix which is passed to JitterbugRenderer to compute device coordinates from the model coordinates. @see #updateBuffer */ matrix3d m_deviceXform = new matrix3d(); /** Adjustment so dragging moves the front of the figure by more than the length of the drag by the given adjustment factor. */ final static float ROTATION_MAGNIFICATION_FACTOR = 3.0f; /** Rotation matrix which determines the orientation of the jitterbug. Used by updateBuffer as one component for computing m_deviceXform. @see #updateBuffer @see #m_deviceXform */ matrix3d m_rot = new matrix3d(); /** Coordinates of last observed mouse position (only updated during drags). */ int m_x = 0, m_y = 0; /** Dimension of largest square fitting on the Canvas. */ int m_dim = 0; /** Animates the jitterbug using the step() function. It is created anew when the animation starts, and interrupted when step mode is entered. */ Stepper m_stepper; public JitterbugController() { m_increment = JitterbugModel.ANGLE_GCM; m_is_suspended = false; m_renderer = new JitterbugRenderer(this, JitterbugModel.ICOSA_ANGLE); float f1 = REDUCTION_FACTOR*0.5f*getWidth(); float f2 = REDUCTION_FACTOR*0.5f*getHeight(); m_scale = (f1 < f2) ? f1 : f2 ; resetRotation(); m_stepper = new Stepper(INITIAL_TIME_DELAY); } /** Increments the angle of the jitterbug's triangles and redraws it with the new coordinates. */ public synchronized void step() { int angle = m_renderer.getAngle(); angle += m_increment; if (angle <= JitterbugModel.MIN_ANGLE || angle >= JitterbugModel.MAX_ANGLE) m_increment = -m_increment; m_renderer.setAngle(angle); if (m_graphicsBuffer != null) updateBuffer(); } /** Stops the jitterbug animation. */ public void suspend() { if (m_stepper != null) m_stepper.interrupt(); m_is_suspended = true; m_stepper = null; } /** Resumes the jitterbug animation. */ public void resume() { if (m_stepper == null) m_stepper = new Stepper(INITIAL_TIME_DELAY); m_is_suspended = false; } /** Accelerate the animation by halving the sleep time. */ public void accelerate() { if (!m_is_suspended && m_stepper != null) m_stepper.setInterval(m_stepper.getInterval()/2); } /** Set the rotation angle (in degrees) for the jitterbug's triangles. */ public void setAngle(int angle) { angle = m_renderer.setAngle(angle); if ((angle == JitterbugModel.MIN_ANGLE && m_increment < 0) || (angle == JitterbugModel.MAX_ANGLE && m_increment > 0)) m_increment = -m_increment; updateBuffer(); } /** Set the angle to correspond to a particular stage (stage value indicates when the jitterbug is in specific geometrical configurations). JitterbugModel.DEFAULT_STAGE is not admissible. */ public void setStage(int stage) { setAngle(JitterbugModel.getAngle(stage)); } /** Get the stage corresponding to the current angle setting. */ public int getStage() { return JitterbugModel.getStage(m_renderer.getAngle()); } /** Get the current rotation angle (in degrees) for the jitterbug's triangles. @return the angle value */ public int getAngle() { return m_renderer.getAngle(); } /** Indicates when the jitterbug's animation is stopped. */ public boolean isSuspended() { return m_is_suspended; } /** Set initial mouse coordinates for start of mouse-drag rotation. @param x x coordinate of mouse @param y y coordinate of mouse */ public void startRotation(int x, int y) { m_x = x; m_y = y; } /** Increment the rotation depending on new mouse coordinates. @param x x coordinate of mouse @param y y coordinate of mouse */ public void updateRotation(int x, int y) { if (x == m_x && y == m_y) return; float deltax = 2.0f*(x - m_x)/m_dim; float deltay = 2.0f*(y - m_y)/m_dim; // angle of rotation (radians) double angle = Math.sqrt(deltax*deltax + deltay*deltay); // axis of rotation (normalized to length == 1; z == 0) float axis_x = (float)(-deltay/angle); float axis_y = (float)(deltax/angle); angle *= (180.0/Math.PI)*ROTATION_MAGNIFICATION_FACTOR; // update rotation matrix synchronized (this) { m_rot.arot(angle, axis_x, axis_y, 0.0f); } m_x = x; m_y = y; if (m_is_suspended) updateBuffer(); } public void resetRotation() { m_rot.unit(); m_rot.arot(15.0, 1.0f, 1.0f, 0.0f); } /** Redraw m_graphicsBuffer according to current jitterbug geometry and transformations. This function basically sets things up. Most of the actual rendering is done by JitterbugRenderer.paint(). @see #m_graphicsBuffer @see JitterbugRenderer#paint */ public synchronized void updateBuffer() { if (m_renderer != null) { m_deviceXform.unit(); // rotate about the origin m_deviceXform.mult(m_rot); // do reality check on graphics buffer dimensions if (m_graphicsBuffer == null || m_width != getWidth() || m_height != getHeight()) { m_width = getWidth(); m_height = getHeight(); m_dim = m_width < m_height ? m_width : m_height; m_graphicsBuffer = (BufferedImage)createImage(m_width, m_height); if (m_graphicsBuffer != null) m_bufferGC = m_graphicsBuffer.createGraphics(); m_scale = REDUCTION_FACTOR*m_dim/2.0f; } if (m_graphicsBuffer != null && m_bufferGC == null) m_bufferGC = m_graphicsBuffer.createGraphics(); // scale graphics according to viewport m_deviceXform.scale(m_scale, m_scale, m_scale); // move graphics to center of viewport m_deviceXform.translate(m_width/2, m_height/2, 0); // clear buffer and draw graphics in it if (m_graphicsBuffer != null && m_bufferGC != null) { m_bufferGC.setColor(Color.lightGray); m_bufferGC.fillRect(0, 0, m_width, m_height); m_renderer.paint(m_bufferGC, m_deviceXform); repaint(0); } } } public synchronized void update(Graphics g) { if (m_renderer != null) { if (m_graphicsBuffer == null || m_width != getWidth() || m_height != getHeight()) updateBuffer(); g.drawImage(m_graphicsBuffer, 0, 0, this); } } public synchronized void paint(Graphics g) { update(g); } } /** The representation of the jitterbug. Used by JitterbugController. @see JitterbugController */ class JitterbugRenderer { public final static int NDRAWABLES = 32; /** The current rotational angle of the jitterbug's triangles in degrees. */ int m_angle; /** Model coordinates for all points. The origin is tacked on to the end and is always (0, 0, 0). */ private float m_modelCoords[] = new float[3*(JitterbugModel.NPOINTS + 1)]; /** Device coordinates for all points, including the origin (at the end -- varies according to the device transformation). */ private int m_deviceCoords[] = new int[3*(JitterbugModel.NPOINTS + 1)]; /** A graphic component of the jitterbug to be rendered. */ private abstract class Drawable implements Comparable { /** What is Z rank? */ public abstract int zmid(); /** Implement Comparable interface */ public int compareTo(Object o) { Drawable d = (Drawable)o; int other = d.zmid(); int me = zmid(); if (me < other) return -1; else if (me > other) return 1; else return 0; } /** Draw this object according to the stage of the jitterbug. The stage value determines whether the object is drawn or not. */ public abstract void draw(Graphics2D g, int stage); } /** All the objects which can be drawn. */ private Drawable m_drawables[] = new Drawable[NDRAWABLES]; /** The thing that's pulling all the strings. */ private JitterbugController m_controller; /** Outer color for the two groups of Triangles. */ public final static Color m_outer_colors[]; /** Inner color for the two groups of Triangles. */ public final static Color m_inner_colors[]; /** For each Triangle there are three integers indicating the indices of the vertices (0 based index). There are two colors, one of which is used to render depending on the sign of the z value of the Triangle's normal. */ private class Triangle extends Drawable { public final JitterbugModel.Triangle m_triangle; /** A representation for a triangle graphic. @param triangle the JitterbugModel.Triangle this Triangle corresponds to */ Triangle(JitterbugModel.Triangle triangle) { m_triangle = triangle; } /** The ranking value for the Triangle (the midpoint of the face). @return the z-coordinate rank for the Triangle. */ public int zmid() { return (m_deviceCoords[3*m_triangle.m_p1 + 2] + m_deviceCoords[3*m_triangle.m_p2 + 2] + m_deviceCoords[3*m_triangle.m_p3 + 2])/3; } /** Draw the Triangle to the specified context. Triangles are always drawn regardless of the stage value. @param g the graphics context @param stage the stage of the jitterbug */ public void draw(Graphics2D g, int stage) { int xs[] = new int[3]; int ys[] = new int[3]; xs[0] = m_deviceCoords[3*m_triangle.m_p1]; ys[0] = m_deviceCoords[3*m_triangle.m_p1 + 1]; xs[1] = m_deviceCoords[3*m_triangle.m_p2]; ys[1] = m_deviceCoords[3*m_triangle.m_p2 + 1]; xs[2] = m_deviceCoords[3*m_triangle.m_p3]; ys[2] = m_deviceCoords[3*m_triangle.m_p3 + 1]; int dx1 = xs[1] - xs[0]; int dy1 = ys[1] - ys[0]; int dx2 = xs[2] - xs[1]; int dy2 = ys[2] - ys[1]; // z component of vector pointing out from Triangle face determines // which side of the Triangle gets drawn. int znorm = dx1*dy2 - dy1*dx2; int group = m_triangle.m_group; Color color = (znorm > 0) ? m_inner_colors[group] : m_outer_colors[group]; g.setColor(color); g.fillPolygon(xs, ys, 3); g.setColor(Color.black); g.drawPolygon(xs, ys, 3); } } /** For each line there are two integers indicating the indices of the vertices (0 based index). A type value indicates at what stage it is to be drawn. */ private class TriangulatingLine extends Drawable { public final JitterbugModel.Line m_line; TriangulatingLine(JitterbugModel.Line line) { m_line = line; } public int zmid() { return (m_deviceCoords[3*m_line.m_p1 + 2] + m_deviceCoords[3*m_line.m_p2 + 2])/2; } public void draw(Graphics2D g, int stage) { // if this type of Line *shouldn't* be drawn at this stage, then // return -- for each type of Line, there are only one or two // stages when it should be drawn if (m_line.m_group == JitterbugModel.Line.GROUP_A) { if (stage != JitterbugModel.ICOSA2_STAGE && stage != JitterbugModel.TICOSA1_STAGE) return; } else if (stage != JitterbugModel.ICOSA1_STAGE && stage != JitterbugModel.TICOSA2_STAGE) return; // this Line is OK to draw, so draw it g.setColor(Color.black); g.drawLine(m_deviceCoords[3*m_line.m_p1], m_deviceCoords[3*m_line.m_p1 + 1], m_deviceCoords[3*m_line.m_p2], m_deviceCoords[3*m_line.m_p2 + 1]); } } /** A line from the center of the jitterbug to one of its vertices. */ private class RadialLine extends Drawable { /** Index of the vertex this RadialLine is drawn to. It is always drawn from the center of the jitterbug. */ public final int m_index; /** Create a RadialLine to the vertex specified. @param index index of the vertex (zero-based) in the JitterbugModel */ RadialLine(int index) { m_index = index; } public int zmid() { return (m_deviceCoords[3*m_index + 2] + m_deviceCoords[3*JitterbugModel.NPOINTS + 2])/2; } public void draw(Graphics2D g, int stage) { // RadialLines only get drawn at the VE stage. if (stage != JitterbugModel.VE_STAGE) return; // draw the line g.setColor(Color.black); g.drawLine(m_deviceCoords[3*m_index], m_deviceCoords[3*m_index + 1], m_deviceCoords[3*JitterbugModel.NPOINTS], m_deviceCoords[3*JitterbugModel.NPOINTS + 1]); } } /** @param controller the JitterbugController using this object */ JitterbugRenderer(JitterbugController controller, int angle) { m_controller = controller; // fill in value for origin (never changes) m_modelCoords[3*JitterbugModel.NPOINTS] = 0.0f; m_modelCoords[3*JitterbugModel.NPOINTS + 1] = 0.0f; m_modelCoords[3*JitterbugModel.NPOINTS + 2] = 0.0f; // initialize drawables m_drawables[ 0] = new Triangle(JitterbugModel.m_triangles[0]); m_drawables[ 1] = new Triangle(JitterbugModel.m_triangles[1]); m_drawables[ 2] = new Triangle(JitterbugModel.m_triangles[2]); m_drawables[ 3] = new Triangle(JitterbugModel.m_triangles[3]); m_drawables[ 4] = new Triangle(JitterbugModel.m_triangles[4]); m_drawables[ 5] = new Triangle(JitterbugModel.m_triangles[5]); m_drawables[ 6] = new Triangle(JitterbugModel.m_triangles[6]); m_drawables[ 7] = new Triangle(JitterbugModel.m_triangles[7]); m_drawables[ 8] = new TriangulatingLine(JitterbugModel.m_lines[ 0]); m_drawables[ 9] = new TriangulatingLine(JitterbugModel.m_lines[ 1]); m_drawables[10] = new TriangulatingLine(JitterbugModel.m_lines[ 2]); m_drawables[11] = new TriangulatingLine(JitterbugModel.m_lines[ 3]); m_drawables[12] = new TriangulatingLine(JitterbugModel.m_lines[ 4]); m_drawables[13] = new TriangulatingLine(JitterbugModel.m_lines[ 5]); m_drawables[14] = new TriangulatingLine(JitterbugModel.m_lines[ 6]); m_drawables[15] = new TriangulatingLine(JitterbugModel.m_lines[ 7]); m_drawables[16] = new TriangulatingLine(JitterbugModel.m_lines[ 8]); m_drawables[17] = new TriangulatingLine(JitterbugModel.m_lines[ 9]); m_drawables[18] = new TriangulatingLine(JitterbugModel.m_lines[10]); m_drawables[19] = new TriangulatingLine(JitterbugModel.m_lines[11]); m_drawables[20] = new RadialLine( 0); m_drawables[21] = new RadialLine( 1); m_drawables[22] = new RadialLine( 2); m_drawables[23] = new RadialLine( 3); m_drawables[24] = new RadialLine( 4); m_drawables[25] = new RadialLine( 5); m_drawables[26] = new RadialLine( 6); m_drawables[27] = new RadialLine( 7); m_drawables[28] = new RadialLine( 8); m_drawables[29] = new RadialLine( 9); m_drawables[30] = new RadialLine(10); m_drawables[31] = new RadialLine(11); setAngle(angle); } /** Initialize the Triangle colors. */ static { m_inner_colors = new Color[JitterbugModel.Triangle.NGROUPS]; m_inner_colors[0] = new Color(255, 0, 0); m_inner_colors[1] = new Color(0, 255, 0); m_outer_colors = new Color[JitterbugModel.Triangle.NGROUPS]; m_outer_colors[0] = new Color(255, 178, 178); m_outer_colors[1] = new Color(178, 255, 178); } /** Set the rotation angle (in degrees) for the jitterbug's triangles. @param int angle in degrees @return angle truncated to be within allowed range */ public synchronized int setAngle(int angle) { if (angle < JitterbugModel.MIN_ANGLE) angle = JitterbugModel.MIN_ANGLE; else if (angle > JitterbugModel.MAX_ANGLE) angle = JitterbugModel.MAX_ANGLE; if (angle != m_angle) { m_angle = angle; JitterbugModel.computeCoords(m_angle, m_modelCoords); } return angle; } /** Get the current rotation angle (in degrees) for the jitterbug's triangles. @return the current angle for the jitterbug being rendered. */ public int getAngle() { return m_angle; } /** Render this model. It uses the matrix associated with this model to map from model space to device space. @param g Graphics object to use for rendering @param deviceXform device transformation */ synchronized void paint(Graphics2D g, matrix3d deviceXform) { if (m_drawables[m_drawables.length - 1] == null) return; deviceXform.transform(m_modelCoords, m_deviceCoords, m_modelCoords.length/3); java.util.Arrays.sort(m_drawables); // g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, // RenderingHints.VALUE_ANTIALIAS_ON); int stage = m_controller.isSuspended() ? JitterbugModel.getStage(m_angle) : JitterbugModel.DEFAULT_STAGE; for (int i = 0; i < m_drawables.length; i++) m_drawables[i].draw(g, stage); } }