// CSC220F19DemoG_ClassInterfaceInheritance Parson's, csc220 new stuff // This version adds Interface Inheritance on 9/5/2019. // Extensions to use java.lang.Math.sin() and java.lang.Math.cos() and // some other functions from class java.lang.Math for // bounding boxes on rotating Avatars, added 9/6/2019, see calls to these below and // https://docs.oracle.com/javase/8/docs/api/index.html for java.lang.Math. // See functions rotatePoint and rotateBB below, added 9/6/2019. import java.lang.Math ; /** * KEYBOARD COMMANDS: * * 'f' toggles between freeze and unfreeze the display. * 'b' toggles between show and hide bounding boxes for debugging collisions. **/ boolean isFrozen = false ; // 'f' toggles this display freeze boolean isBounding = false ; // 'b' toggles this bounding box display Avatar [] av1 = new Avatar [10]; // setup() called once at the start of Run void setup() { size(800, 700); // At a minimum set display window size in setup(). frameRate(60); // Must be explicit in newer Macs after setting size. rectMode(CENTER); ellipseMode(CENTER); for (int i = 0; i < av1.length; i+=2) { av1[i] = new Avatar1(int(random(0, width)), int(random(0, height))); } for (int i = 1; i < av1.length; i+=2) { av1[i] = new Avatar2(int(random(0, width)), int(random(0, height))); } } // draw() is called in a loop at the frameRate, defaults to once every 60th sec. void draw() { if (isFrozen) { return ; } background(255, 255, 0); // screen color, use Tools -> Color Selector, RGB int i = 0 ; while (i < av1.length) { av1[i].display(); // function call to the display() function to display avatar av1[i].move(); i++ ; } } // Processing calls function keyPressed() whenever a keyboard key is pressed. // See additonal Keyboard functions in the help page. void keyPressed() { if (key == CODED) { // CODED needed only for special characters if (keyCode == RIGHT) { // do the right thing } else if (keyCode == LEFT) { // do the left thing } else if (keyCode == UP) { // do the up thing } else if (keyCode == DOWN) { // do the down thing } } else { // else it is a regular key if (key == 'f') { isFrozen = ! isFrozen ; // invert the value println("isFrozen = " + isFrozen); } else if (key == 'b') { isBounding = ! isBounding ; // invert the value println("isBounding = " + isBounding); } } } /** overlap checks whether two objects' bounding boxes overlap **/ boolean overlap(Avatar avatar1, Avatar avatar2) { int [] bb1 = avatar1.getBoundingBox(); int [] bb2 = avatar2.getBoundingBox(); // If bb1 is completely above, below, // left or right of bb2, we have an easy reject. if (bb1[0] > bb2[2] // bb1_left is right of bb2_right || bb1[1] > bb2[3] // bb1_top is below bb2_bottom, now reverse them || bb2[0] > bb1[2] // bb2_left is right of bb1_right || bb2[1] > bb1[3] // bb2_top is below bb1_bottom, now reverse them ) { return false ; } // In this case one contains the other or they overlap. return true ; } /** * Helper function supplied by Dr. Parson, student can just call it. * rotatePoint takes the unrotated coordinates local to an object's * 0,0 reference location and rotates them by angleDegrees in degrees. * Applying it to global coordinates rotates around the global 0,0 reference * point in the LEFT,TOP corner of the display. x, y are the unrotated coords. * Return value is 2-element array of the rotated x,y values. **/ double [] rotatePoint(double x, double y, double angleDegrees) { double [] result = new double [2]; double angleRadians = Math.toRadians(angleDegrees); // I have kept local variables as doubles instead of floats until the // last possible step. I was seeing rounding errors in the displayed BBs // when they are scaled when using floats. Using doubles for these calculations // appears to have eliminated those occasionally noticeable errors. // SEE: https://en.wikipedia.org/wiki/Rotation_matrix double cosAngle = (Math.cos(angleRadians)); // returns a double double sinAngle = (Math.sin(angleRadians)); result[0] = (x * cosAngle - y * sinAngle) ; result[1] = (x * sinAngle + y * cosAngle); // println("angleD = " + angleDegrees + ", cos = " + cosAngle + ", sin = " + sinAngle + ", x = " + x + ", y = " // + y + ", newx = " + result[0] + ", newy = " + result[1]); return result ; } /** * Helper function supplied by Dr. Parson, student can just call it. * rotateBB takes the (leftx, topy) and (rightx, bottomy) extents of an * unrotated bounding box and determines the leftmost x, uppermost y, * rightmost x, and bottommost y of the rotated BB, and returns these * rotated extents in a 4-element array of coodinates. * rotateBB needs to rotate every corner of the original bounding box * in turn to find the rotated bounding box as min and max values for X and Y. * Parameters: * leftx, topy and rightx, bottomy are the original, unrotated extents. * angle is the angle of rotation in degrees. * scaleXfactor and scaleYfactor are the scalings of the shape with the BB. * referencex,referencey is the "center" 0,0 point within the shape * being rotated, with is also the reference point of the BB, in global coord. * Return 4-element array holds the minx,miny and maxx,maxy rotated extents. * See http://faculty.kutztown.edu/parson/fall2019/RotateBB2D.png **/ int [] rotateBB(double leftx, double topy, double rightx, double bottomy, double angle, double scaleXfactor, double scaleYfactor, double referencex, double referencey) { int [] result = new int [4]; leftx = leftx * scaleXfactor ; rightx = rightx * scaleXfactor ; topy = topy * scaleYfactor ; bottomy = bottomy * scaleYfactor ; double [] ul = rotatePoint(leftx, topy, angle); // rotate each of the 4 corners double [] ll = rotatePoint(leftx, bottomy, angle); double [] ur = rotatePoint(rightx, topy, angle); double [] lr = rotatePoint(rightx, bottomy, angle); double minx = Math.min(ul[0], ll[0]);// find minx,miny and max,maxy from all 4 minx = Math.min(minx, ur[0]); minx = Math.min(minx, lr[0]); double maxx = Math.max(ul[0], ll[0]); maxx = Math.max(maxx, ur[0]); maxx = Math.max(maxx, lr[0]); double miny = Math.min(ul[1], ll[1]); miny = Math.min(miny, ur[1]); miny = Math.min(miny, lr[1]); double maxy = Math.max(ul[1], ll[1]); maxy = Math.max(maxy, ur[1]); maxy = Math.max(maxy, lr[1]); // scale by this shapes scale result[0] = (int)Math.round(referencex + minx) ; // left extreme result[1] = (int)Math.round(referencey + miny); // top result[2] = (int)Math.round(referencex + maxx); // right side result[3] = (int)Math.round(referencey + maxy); // bottom return result ; } // An interface is a specification of functions that class must provide, along with documentation comments. interface Avatar { // display the Avatar, make sure to restore global ccordinates via popMatrix() if there is a pushMatrix() in display. void display() ; // move() updates any location variables within the Avatar void move(); // getBoundingBox() returns a 4-element array of // lowestX, lowestY, biggestX, biggestY for a bounding box that gives the extents // of the Avatar in global coordinate space int [] getBoundingBox(); } class Avatar1 implements Avatar { int elx, ely ; // X, Y location int exspeed, eyspeed ; // X, Y speed int ewidth = 75, eheight = 50 ; // w & h of Avatar1 // added in 020 section at 1:30 float scaleFactor, scaleSpeed ; // scale "location" and speed Avatar1(int initialx, int initialy) { // constructor for a class builds an object of that class elx = initialx ; ely = initialy ; exspeed = round(random(1, 4)) ; eyspeed = round(random(1, 5)) ; ; scaleFactor = random(.1, 2.0); scaleSpeed = .01 ; } void display() { pushMatrix(); translate(elx, ely); scale(scaleFactor); // added in 1:30 class // display my avatar // body stroke(0); fill(255); rect(0, 0, ewidth/2, eheight/2); //head stroke(0, 0, 255); // Sets stroke around shapes, alternatively noStroke() fill(255, 128, 0); // Sets fill within closed shape, alternatively noFill() ellipse(0, -eheight/2, ewidth, eheight); // X, Y, Ewidth, Eheight popMatrix(); if (isBounding) { // Optionally show BB around Avatar for debugging int [] bb = getBoundingBox(); // get "this" object's BB, display it noFill(); stroke(0); strokeWeight(1); rectMode(CORNER); rect(bb[0], bb[1], bb[2]-bb[0], bb[3]-bb[1]); rectMode(CENTER); } } void move() { // move my avatar elx = elx + exspeed ; ely += eyspeed ; if (elx > width+ewidth) { // if horizontal loc is off right side of display exspeed = - exspeed ; // reverse direction } else if (elx < 0-ewidth) { // if horizontal loc is off left side of display exspeed = - exspeed ; } if (ely < 0-eheight || ely > height+eheight) { // if vertical off top or bottom eyspeed = - eyspeed ; } scaleFactor += scaleSpeed ; if (scaleFactor < 0 || scaleFactor > 2) { scaleSpeed = - scaleSpeed ; } // Add collision detection 9/3/2019. // Look at every *other* avatar and see whether we overlap. // On first overlap detected, change directions and break loop. for (Avatar other : av1) { if (other == this) { // this is ME! "this" is this Avatar object. continue ; // Go back to top of loop & get the next one. } if (overlap(this, other)) { // If I (this) overlap with the other Avatar. exspeed = - exspeed ; // reverse direction eyspeed = - eyspeed ; // reverse direction break ; // Go past the loop, i.e., break out of the loop. // You only want to change direction because of collision once. } } } int [] getBoundingBox() { int [] result = new int[4]; result[0] = (elx - round (scaleFactor * ewidth/2)) ; // left extreme result[1] = (ely - round (scaleFactor * eheight)); // top result[2] = (elx + round (scaleFactor * ewidth/2)); // right side result[3] = (ely + round (scaleFactor * eheight/4)); // bottom return result ; } } class Avatar2 implements Avatar { int elx, ely ; // X, Y location int exspeed, eyspeed ; // X, Y speed int ewidth = 75, eheight = 50 ; // w & h of Avatar2 // added in 020 section at 1:30 float scaleFactor, scaleSpeed ; // scale "location" and speed float rotateFactor, rotateSpeed ; // rotate shape in degrees Avatar2(int initialx, int initialy) { // constructor for a class builds an object of that class elx = initialx ; ely = initialy ; exspeed = round(random(1, 4)) ; eyspeed = round(random(1, 5)) ; ; scaleFactor = random(.1, 2.0); scaleSpeed = .01 ; rotateFactor = random(0, 360); rotateSpeed = 1 ; } void display() { pushMatrix(); translate(elx, ely); rotate(radians(rotateFactor)); scale(scaleFactor); // added in 1:30 class // display my avatar // body //head stroke(255, 0, 0); // Sets stroke around shapes, alternatively noStroke() fill(0, 128, 255); // Sets fill within closed shape, alternatively noFill() //ellipse(0, -eheight/2, ewidth, eheight); // X, Y, Ewidth, Eheight // I want an isoceles triangle. triangle(-ewidth/2, eheight/2, ewidth/2, eheight/2, 0, -eheight/2); stroke(0); strokeWeight(4); noFill(); // rect(0, 0, ewidth/2, eheight/2); quad(-ewidth/2, -eheight/2, ewidth/2, eheight/2, ewidth/2, -eheight/2, -ewidth/2, eheight/2); popMatrix(); if (isBounding) { // Optionally show BB around Avatar for debugging int [] bb = getBoundingBox(); // get "this" object's BB, display it noFill(); stroke(0); strokeWeight(1); rectMode(CORNER); rect(bb[0], bb[1], bb[2]-bb[0], bb[3]-bb[1]); rectMode(CENTER); } } void move() { // move my avatar elx = elx + exspeed ; ely += eyspeed ; if (elx > width+ewidth) { // if horizontal loc is off right side of display exspeed = - exspeed ; // reverse direction } else if (elx < 0-ewidth) { // if horizontal loc is off left side of display exspeed = - exspeed ; } if (ely < 0-eheight || ely > height+eheight) { // if vertical off top or bottom eyspeed = - eyspeed ; } scaleFactor += scaleSpeed ; if (scaleFactor < 0 || scaleFactor > 2) { scaleSpeed = - scaleSpeed ; } rotateFactor += rotateSpeed ; if (rotateFactor < -360 || rotateFactor > 360) { rotateSpeed = - rotateSpeed ; } // Add collision detection 9/3/2019. // Look at every *other* avatar and see whether we overlap. // On first overlap detected, change directions and break loop. for (Avatar other : av1) { if (other == this) { // this is ME! "this" is this Avatar object. continue ; // Go back to top of loop & get the next one. } if (overlap(this, other)) { // If I (this) overlap with the other Avatar. exspeed = - exspeed ; // reverse direction eyspeed = - eyspeed ; // reverse direction break ; // Go past the loop, i.e., break out of the loop. // You only want to change direction because of collision once. } } } int [] getBoundingBox() { int [] result = new int[4]; // Rotation complicates matters because it is necessary to rotate BB // around this object's 0,0 reference point, not around the global 0,0 // at the LEFT,TOP of the display window. getBoundingBox() returns values // in the global coordinates, but rotation must happen using the object's 0,0. if (rotateFactor == 0) { result[0] = (elx - round (scaleFactor * ewidth/2)) ; // left extreme result[1] = (ely - round (scaleFactor * eheight/2)); // top result[2] = (elx + round (scaleFactor * ewidth/2)); // right side result[3] = (ely + round (scaleFactor * eheight/2)); // bottom } else { // Added 9/6/2019 to rotate around local 0,0, and then convert to global. result = rotateBB(-ewidth/2, -eheight/2, ewidth/2, eheight/2, rotateFactor, scaleFactor, scaleFactor, elx, ely); } return result ; } }