001 /* 002 * DotPanel.java 003 * Part of the Spirograph problem set 004 * 005 * Developed for "Rethinking CS101", a project of Lynn Andrea Stein's AP Group. 006 * For more information, see <a href="http://www.ai.mit.edu/projects/cs101/">the 007 * CS101 homepage</a> or email <las@ai.mit.edu>. 008 * 009 * Copyright (C) 1998 Massachusetts Institute of Technology. 010 * Please do not redistribute without obtaining permission. 011 */ 012 013 package spirograph; 014 015 import java.awt.*; 016 import java.util.*; 017 018 /** 019 * This class keeps track of the coordinates of the Dot. It has a seperate 020 * Thread to update the "state" of the two coordinates and to repaint 021 * itself. It also handles the dot bouncing off either the walls of the 022 * square or the circle. It also keeps track of all of the gravity points 023 * and all of the places where the dot has been before and draws the trail. 024 * 025 * <p>Copyright © 1998 Massachusetts Institute of Technology.<br /> 026 * Copyright © 2002-2003 Franklin W. Olin College of Engineering.</p> 027 * 028 * 029 * @author Luis Sarmenta, lfgs@cag.lcs.mit.edu 030 * @author Henry Wong, henryw@mit.edu 031 * @author Patrick G. Heck, gus.heck@olin.edu 032 * @version $Id: DotPanel.java,v 1.5 2004/02/09 20:55:03 gus Exp $ 033 * @see Coord 034 * @see Spirograph 035 * @see AccelHandler 036 */ 037 public class DotPanel extends Canvas implements Runnable { 038 039 // This vector holds all of the points representing the ball's 040 // various positions. 041 private Vector v = new Vector(100); 042 043 // All of the gravitational points that have been added 044 private Vector grav = new Vector(5); 045 046 // This image is used for the double buffering. 047 private Image buf; 048 049 // These fields represent the size of the Panel. They should be 050 // changed using the setter methods whenever the window is resized. 051 private int width = Spirograph.WIDTH; 052 private int height = Spirograph.HEIGHT; 053 054 // The coords represent the ball's coordinates along the x and 055 // y axis. 056 private Coord x; 057 private Coord y; 058 059 // Whether or not the DotPanel is in circular mode. 060 private boolean circMode = false; 061 062 // The two focii of the ellipse when in circular mode 063 private Point focusA = new Point(0,0); 064 private Point focusB = new Point(0,0); 065 066 // Bounce and Wrap Modes 067 private boolean bounceOn = false; 068 private boolean wrapOn = false; 069 070 // Mode corresponding to accelMode for AccelHandlers 071 private int myMode = AccelHandler.POSMODE; 072 073 /** 074 * Create a new dot panel that displays a dot by poling two {@link Coord} objects. 075 * 076 * @param x Defines the position and movement atributes along the x axis 077 * @param y Defines the position and movement atributes along the y axis 078 */ 079 public DotPanel(Coord x, Coord y) { 080 this.x = x; 081 this.y = y; 082 this.setBackground (Color.white); 083 this.setSize(width,height); 084 } 085 086 /** 087 * Set the operational mode for both coordinate axis. 088 * 089 * @param mode {@link AccelHandler#POSMODE}, {@link AccelHandler#VELMODE}, 090 * or {@link AccelHandler#ACCELMODE} 091 */ 092 public void setMode( int mode ) 093 { 094 this.myMode = mode; 095 x.setMode( mode ); 096 y.setMode( mode ); 097 } 098 099 /** 100 * Overides the superclass to return our constants. 101 * 102 * @return new Dimension({@link Spirograph#WIDTH},{@link Spirograph#HEIGHT}) 103 */ 104 public Dimension getPreferredSize() { 105 return (new Dimension(Spirograph.WIDTH,Spirograph.HEIGHT)); 106 } 107 108 /** 109 * Overide the parent class to return the prefered size as the minimum size. 110 * 111 * @return {@link DotPanel#getPreferredSize()} 112 */ 113 public Dimension getMinimumSize() { 114 return getPreferredSize(); 115 } 116 117 /** 118 * The maximum distance from the center of the drawing area that the ball can be 119 * placed in the X direction. 120 * 121 * @return half the width minus the size of the ball 122 */ 123 public double getMaxX() 124 { 125 return ( width/2 - Spirograph.BALLSIZE ); 126 } 127 128 /** 129 * The maximum distance from the center of the drawing area that the ball can be 130 * placed in the X direction. 131 * 132 * @return half the height minus the size of the ball 133 */ 134 public double getMaxY() 135 { 136 return ( height/2 - Spirograph.BALLSIZE ); 137 } 138 139 /** 140 * Conducts the drawing of all objects in the DotPanel. 141 * 142 */ 143 public void run() { 144 // In order to make sure that the x and y axis go through the 145 // same number of "timesteps" I use a seperate Thread to handle 146 // painting and timestepping. 147 148 // If you put the nextStep command in the AccelHandler threads 149 // than you can't guarentee that the two timesteps will be 150 // called with equal frequency. 151 152 153 while (true) { 154 double step; 155 156 // I scale the timesteps so that there are more of them as 157 // the dot get's nearer the edge. This way, the dot is 158 // much more acccurate near the edge and it cuts down on 159 // the flickering of the screen near the middle, where the 160 // dot doesn't need to be as accurate 161 162 if (circMode) { 163 step = Spirograph.TIMESTEP*Math.abs(1 164 - (Math.pow(2*x.getPos()/height,2) 165 + Math.pow(2*y.getPos()/width,2))/2); 166 } else { 167 step = 0.25; 168 } 169 170 // getDistVect creates a Vector containing the distance of 171 // the dot from every gravitational point (see below) 172 x.nextStep(getDistVect(true),step); 173 y.nextStep(getDistVect(false),step); 174 175 double xVel = x.getVel(); 176 double yVel = y.getVel(); 177 178 if (Math.sqrt(Math.pow(xVel,2) + Math.pow(yVel,2)) > 179 Spirograph.MAXVEL) { 180 xVel = Spirograph.MAXVEL*xVel/(xVel+yVel); 181 yVel = Spirograph.MAXVEL*yVel/(xVel+yVel); 182 x.setVel(xVel); 183 y.setVel(yVel); 184 } 185 186 double xPos = x.getPos(); 187 double yPos = y.getPos(); 188 189 if (circMode) { 190 // This is a lot of math to figure out whether or not 191 // the dot hit the wall and if it did, where it should 192 // have gone and what it's new velocity should be. 193 194 // from (x/a)^2 + (y/b)^2 = 1 195 double a = width/2; 196 double b = height/2; 197 198 if (!SpiroUtils.inEllipse(xPos,yPos,a,b)) { 199 200 // Need to figure out where dot went out of circle. 201 202 double outXPos; 203 double outYPos; 204 205 if (xVel == 0) { 206 // If this is true will get div 0 errors 207 208 outXPos = xPos; 209 210 if (yVel > 0) { 211 outYPos = b*Math.sqrt(1-Math.pow(xPos/a,2)); 212 } else { 213 outYPos = -b*Math.sqrt(1-Math.pow(xPos/a,2)); 214 } 215 } else { 216 217 // Figure out "m" and "c" from (y = mx + c) 218 double m = yVel/xVel; 219 double c = yPos - (yVel*xPos/xVel); 220 221 // Figure out b^2-4ac... 222 double det = 4*Math.pow(a*b,2)*(Math.pow(b,2) + 223 Math.pow(a*m,2) - Math.pow(c,2)); 224 225 226 //Figure out +/- 227 228 if (xVel > 0) { 229 outXPos = (-2*m*c*Math.pow(a,2) + 230 Math.sqrt(det)) / 231 (2*(Math.pow(b,2)+(Math.pow(m*a,2)))); 232 } else { 233 outXPos = (-2*m*c*Math.pow(a,2) - 234 Math.sqrt(det)) / 235 (2*(Math.pow(b,2)+(Math.pow(m*a,2)))); 236 } 237 238 // now do same for y... 239 240 det = 4*Math.pow(a*m*b,2)*(Math.pow(a*m,2)+ 241 Math.pow(b,2)-Math.pow(c,2)); 242 243 if (yVel > 0) { 244 outYPos = (2*c*Math.pow(b,2) + Math.sqrt(det))/ 245 (2*(Math.pow(b,2)+Math.pow(m*a,2))); 246 } else { 247 outYPos = (2*c*Math.pow(b,2) - Math.sqrt(det))/ 248 (2*(Math.pow(b,2)+Math.pow(m*a,2))); 249 } 250 251 } // matches else from if (xVel == 0) 252 253 if (outXPos == 0) { 254 //Special case, refSlope = infinity 255 256 y.setVel(-y.getVel()); 257 if (yPos > 0) { 258 y.setPos(height - yPos); 259 } else { 260 y.setPos(-height - yPos); 261 } 262 263 } else { 264 265 double refSlope = -1*(outYPos*Math.pow(a,2)/ 266 (-outXPos*Math.pow(b,2))); 267 double coef = (xVel + yVel * refSlope)/ 268 (1 + Math.pow(refSlope,2)); 269 270 x.setVel(xVel- 2 * coef); 271 y.setVel(yVel- 2 * coef * refSlope); 272 273 coef = ((xPos - outXPos) + (yPos - outYPos) * refSlope) 274 /(1 + Math.pow(refSlope,2)); 275 276 x.setPos(outXPos - 2 * coef); 277 y.setPos(outYPos - 2 * coef * refSlope); 278 279 } // closes else from if (outX == 0) 280 281 } // ends circMode out of bounds 282 283 } else if ( bounceOn ) { 284 // not circMode 285 286 // If the ball moved past one of the boundaries, 287 // calculate where it would have gone if it had bounced 288 // and negate it's velocity. 289 290 if (xPos > getMaxX() ) { 291 x.setPos(width - xPos - (Spirograph.BALLSIZE * 2)); 292 x.setVel(-xVel); 293 } 294 295 if (xPos < -getMaxX() ) { 296 x.setPos(-width - xPos + (Spirograph.BALLSIZE * 2)); 297 x.setVel(-xVel); 298 } 299 300 if (yPos > getMaxY() ) { 301 y.setPos(height - yPos - (Spirograph.BALLSIZE * 2)); 302 y.setVel(-yVel); 303 } 304 305 if (yPos < -getMaxY() ) { 306 y.setPos(-height - yPos + (Spirograph.BALLSIZE * 2)); 307 y.setVel(-yVel); 308 } 309 } else if (wrapOn) { 310 if(xPos == getMaxX()) 311 x.setPos(-getMaxX()); 312 else 313 if(xPos == -getMaxX()) 314 x.setPos(getMaxX()); 315 if(yPos == getMaxY()) 316 y.setPos(-getMaxY()); 317 else 318 if(yPos == -getMaxY()) 319 y.setPos(getMaxY()); 320 } else { 321 if(xPos == getMaxX()) 322 x.setPos(getMaxX()); 323 else 324 if(xPos == -getMaxX()) 325 x.setPos(-getMaxX()); 326 if(yPos == getMaxY()) 327 y.setPos(getMaxY()); 328 else 329 if(yPos == -getMaxY()) 330 y.setPos(-getMaxY()); 331 } 332 333 paintBuf(); 334 try { 335 Thread.sleep((int)(100*step)); 336 //Thread.yield(); 337 } catch (InterruptedException e) { 338 System.out.println ("Error sleeping."); 339 } 340 } 341 } 342 343 /** 344 * Convenience wrapper for {@link SpiroUtils#inEllipse}. This is called by the 345 * AdvArg to see whether or not the ball is in the circle. If the ball isn't 346 * inside the circle, AdvArg won't turn on circle mode. 347 * 348 * @return True if the ball is currently inside the elipse used by circular mode, 349 * false otherwise 350 */ 351 public boolean inEllipse() { 352 return SpiroUtils.inEllipse(x.getPos(), y.getPos(), width/2, height/2); 353 } 354 355 /** 356 * This method returns a vector of Points. The x coordinate of each 357 * Point represents the distance of the point from the gravitational 358 * dot along the appropriate axis, the y coordinate represents the 359 * total distance of the dot from each point. 360 * 361 * @see Coord 362 */ 363 private Vector getDistVect(boolean xDim) { 364 double xPos = x.getPos(); 365 double yPos = y.getPos(); 366 Vector temp = new Vector(); 367 368 for (Enumeration e = grav.elements(); e.hasMoreElements();) { 369 370 Point o = (Point)e.nextElement(); 371 372 if (xDim) { 373 temp.addElement(new Point((int)(o.x-xPos), 374 (int)SpiroUtils.dist(o.x,o.y,xPos,yPos))); 375 } else { 376 temp.addElement(new Point((int)(o.y-yPos), 377 (int)SpiroUtils.dist(o.x,o.y,xPos,yPos))); 378 } 379 } 380 381 return temp; 382 } 383 384 /** 385 * This method paints an off-screen image to be used for the double 386 * buffering.<p> 387 * 388 * It adds the balls current position to the vector of Points, then 389 * reconstructs the balls path from the Points. 390 * 391 */ 392 public void paintBuf() { 393 394 Graphics g; 395 396 if (buf == null) { 397 buf = createImage(width, height); 398 } 399 400 try { 401 g = buf.getGraphics(); 402 } catch (NullPointerException e) { 403 return; 404 } 405 406 g.clearRect(0,0,width,height); 407 408 myMode = x.getMode(); 409 g.drawString("Pos: (" + x.getPos() + "," + y.getPos() + ")",5,15); 410 if ( myMode != AccelHandler.POSMODE) 411 g.drawString("Vel: (" + x.getVel() + "," + y.getVel() + ")",5,30); 412 if ( myMode == AccelHandler.ACCELMODE) 413 g.drawString("Accel: ("+x.getAccel() +","+ y.getAccel() + ")",5,45); 414 415 // Draw the ellipse 416 if (circMode) { 417 g.setColor(Color.black); 418 g.drawOval(0,0,width,height); 419 420 // Draw the focii 421 g.drawOval(focusA.x+width/2-Spirograph.FOCUSSIZE/2, 422 height/2-focusA.y-Spirograph.FOCUSSIZE/2, 423 Spirograph.FOCUSSIZE, Spirograph.FOCUSSIZE); 424 g.drawOval(focusB.x+width/2-Spirograph.FOCUSSIZE/2, 425 height/2-focusB.y-Spirograph.FOCUSSIZE/2, 426 Spirograph.FOCUSSIZE, Spirograph.FOCUSSIZE); 427 } 428 429 // Draw the gravity sources 430 g.setColor(Color.blue); 431 432 for (Enumeration e = grav.elements(); e.hasMoreElements();) { 433 434 Point o = (Point)e.nextElement(); 435 436 g.fillOval(o.x + width/2 - Spirograph.BALLSIZE/2, 437 height/2 - o.y - Spirograph.BALLSIZE/2, 438 Spirograph.BALLSIZE, Spirograph.BALLSIZE); 439 } 440 441 // Draw the dot's trail 442 double oldX; 443 double oldY; 444 445 // Set up the beginning of the trail. 446 if (v.isEmpty()) { 447 oldX = x.getPos(); 448 oldY = y.getPos(); 449 v.addElement(new Point((int)x.getPos(),(int)y.getPos()) ); // add element now so call to v.lastElement later will work 450 } else { 451 oldX = ((Point)(v.elementAt(0))).x; 452 oldY = ((Point)(v.elementAt(0))).y; 453 } 454 455 // Add the position of the dot to the vector of Points. 456 Point newPoint = new Point((int)x.getPos(),(int)y.getPos()); 457 Point prevPoint = (Point)v.lastElement(); 458 459 if ( ! (( Math.abs(newPoint.x-prevPoint.x) < 1.0 ) && 460 ( Math.abs(newPoint.y-prevPoint.y) < 1.0 )) ) 461 v.addElement ( newPoint ); 462 // else no need to add a new point 463 464 g.setColor (Color.red); 465 466 // Step through the vector and draw the a line representing the 467 // dot's path. 468 for (Enumeration e = v.elements(); e.hasMoreElements();) { 469 470 Point o = (Point)e.nextElement(); 471 // if the difference between the old and new position 472 // is almost as big as the width/height of the box, 473 // then a wraparound probably happened, so don't draw a line. 474 // otherwise, draw a line 475 if( (Math.abs(o.x - oldX) < 2*(getMaxX()-Spirograph.BALLSIZE)) 476 && 477 (Math.abs(o.y - oldY) < 2*(getMaxY()-Spirograph.BALLSIZE)) 478 ) { 479 g.drawLine((int)(oldX+width/2), (int)(-oldY+height/2), 480 (int)(o.x+width/2), (int)(-o.y+height/2)); 481 } 482 483 oldX = o.x; 484 oldY = o.y; 485 486 } 487 488 g.setColor(Color.black); 489 490 // Draw the oval 491 g.fillOval ((int)(x.getPos()-Spirograph.BALLSIZE/2 492 +(width/2)), 493 (int)(-y.getPos()-Spirograph.BALLSIZE/2 494 +(height/2)), 495 Spirograph.BALLSIZE,Spirograph.BALLSIZE); 496 497 // System.out.println ("NOW AT: " + x.getPos() + "," + y.getPos()); 498 499 repaint(); 500 g.dispose(); 501 } // end of paintbuf(); 502 503 /** 504 * Overide the superclass paint method, to do our own drawing. 505 * 506 * @param out The <code>Graphics</code> object for this component. 507 */ 508 public void paint (Graphics out) { 509 // Draw the ball on screen. If buf hasn't been initialized yet, 510 // initialized it. 511 512 try { 513 out.drawImage(buf,0,0,this); 514 } catch (NullPointerException e) { 515 paintBuf(); 516 } 517 518 } 519 520 /** 521 * Recalculate the foci and repaint of the elipse before calling the 522 * super.setSize(). Called by the DotFrame whenever the DotFrame is resized 523 * 524 * @param width The new width 525 * @param height The new height 526 */ 527 public void setSize(int width, int height) { 528 529 buf = null; 530 531 this.width = width; 532 this.height = height; 533 534 x.setMaxPos( getMaxX() ); 535 y.setMaxPos( getMaxY() ); 536 537 // get new focii 538 539 if (width < height) { 540 focusA.x = 0; 541 focusA.y = (int)Math.sqrt(Math.pow(height/2,2)- 542 Math.pow(width/2,2)); 543 focusB.x = 0; 544 focusB.y = -(int)Math.sqrt(Math.pow(height/2,2)- 545 Math.pow(width/2,2)); 546 } else { 547 focusA.x = (int)Math.sqrt(Math.pow(width/2,2)- 548 Math.pow(height/2,2)); 549 focusA.y = 0; 550 focusB.x = -(int)Math.sqrt(Math.pow(width/2,2)- 551 Math.pow(height/2,2)); 552 focusB.y = 0; 553 } 554 555 if (circMode) { 556 System.out.println ("Focus A at: "+focusA.x+":"+focusA.y); 557 System.out.println ("Focus B at: "+focusB.x+":"+focusB.y); 558 } 559 560 paintBuf(); 561 super.setSize(width,height); 562 } 563 564 /** 565 * Delete the trail of the dot. The points defining the trail of the dot 566 * are removed from the containing <code>Vector</code>. The position of the dot 567 * is uneffected, and the panel is repainted. 568 * 569 */ 570 public void flushLines() { 571 v.removeAllElements(); 572 paintBuf(); 573 } 574 575 /** 576 * Delete all gravity sources, and repaint. 577 * 578 */ 579 public void flushGrav() { 580 grav.removeAllElements(); 581 paintBuf(); 582 } 583 584 /** 585 * Place a gravity source at the coordinates specified. Usually this is called 586 * by a <code>MouseListener</code> that has been added to the DotPanel. 587 * 588 * @param x The horizontal coordinate for the gravity source. 589 * @param y The vertical coordinate for the gravity source. 590 */ 591 public void addGrav(int x, int y) { 592 grav.addElement(new Point(x,y)); 593 } 594 595 /** 596 * Turn circular mode on or off. 597 * 598 * @param circMode <code>true</code> to turn circular mode on, <code>false</code> to turn it off 599 */ 600 public void setCirc(boolean circMode) { 601 this.circMode = circMode; 602 paintBuf(); 603 } 604 605 /** 606 * Query the state of circular mode. 607 * 608 * @return True if circular mode is on, false otherwise 609 */ 610 public boolean getCirc() { 611 return circMode; 612 } 613 614 /** 615 * Get the current height of the DotPanel. This method reports the value of the 616 * height variable, which in turn shadows the same value as getHeight() in the 617 * component class. 618 * 619 * @return The current height of the panel 620 * @deprecated This method and the associated shadow variable violate the DRY 621 * (Do not Repeat Yourself) principle. The only visible benefit is a 622 * slightly reduced typing. With modern compilers this won't even yield 623 * a performance increase because the getWidth or getHeight methods will 624 * be inlined. The Sun code for these methods in <code>Component</code> 625 * is:<code> 626 * 627 * public int getWidth() { 628 * return width; 629 * } 630 * </code> 631 * For this reason, this method and the associated variables will be removed. 632 */ 633 public int curHeight() { 634 return height; 635 } 636 637 /** 638 * Get the current width of the DotPanel. This method reports the value of the 639 * height variable, which in turn shadows the same value as getHeight() in the 640 * component class. 641 * 642 * @return The current width of the panel 643 * @deprecated This method and the associated shadow variable violate the DRY 644 * (Do not Repeat Yourself) principle. The only visible benefit is a 645 * slightly reduced typing. With modern compilers this won't even yield 646 * a performance increase because the getWidth or getHeight methods will 647 * be inlined. The Sun code for these methods in <code>Component</code> 648 * is:<code> 649 * 650 * public int getWidth() { 651 * return width; 652 * } 653 * </code> 654 * For this reason, this method and the associated variable will be removed. 655 */ 656 public int curWidth() { 657 return width; 658 } 659 660 /** 661 * Query the state of bounce mode. 662 * 663 * @return True if bounce mode is on, false otherwise 664 */ 665 public boolean getBounce() 666 { 667 return bounceOn; 668 } 669 670 /** 671 * Turn bounce mode on or off. 672 * 673 * @param bounceOn <code>true</code> to turn bounce mode on, <code>false</code> to turn it off 674 */ 675 public void setBounce(boolean bounceOn) 676 { 677 this.bounceOn = bounceOn; 678 } 679 680 /** 681 * Query the state of wrap mode. 682 * 683 * @return True if wrap mode is on, false otherwise 684 */ 685 public boolean getWrap() 686 { 687 return wrapOn; 688 } 689 690 /** 691 * Turn wrap mode on or off. 692 * 693 * @param wrapOn <code>true</code> to turn wrap mode on, <code>false</code> to turn it off 694 */ 695 public void setWrap(boolean wrapOn) 696 { 697 this.wrapOn = wrapOn; 698 } 699 } 700 701 /* 702 * $Log: DotPanel.java,v $ 703 * Revision 1.5 2004/02/09 20:55:03 gus 704 * javadoc fixes 705 * 706 * Revision 1.4 2003/01/15 17:36:10 gus 707 * adding log keywords to files that don't have them 708 * 709 */ 710