// NOTE: much of the code in this file, and many of the comments, // came directly from the Mac version of Kali by Jeff Weeks. Thanks // to Jeff for his lucid programming style and copius comments! // mbp Mon Sep 16 17:06:26 1996 import java.awt.*; /** * The Panorama object is where the math happens in Kali. It keeps * track of the current group, and applies that group's action to the * line segments as they are drawn. (The DrawPanel object, which * stores the list of line segments, called the Panorama's * drawSegment() method once for each line segment.) * * @see DrawPanel * @see KaliCanvas */ class Panorama implements Constants { /** * The current group. */ SymmetryGroup symmetryGroup; /** * prepareToDraw() computes preparedTranslations, referenceVector, * and maxReference, based on the current group and current * coordinate system. */ DVector[] preparedTranslations = new DVector[2]; DVector referenceVector; double maxReference; /** * The KaliCanvas object that we draw on. */ KaliCanvas kaliCanvas; /** * Handy rotation matrices. */ DMatrix[] rotationMatrix = { new DMatrix(0.0, 0.0, 0.0, 0.0), new DMatrix(1.0, 0.0, 0.0, 1.0), new DMatrix(-1.0, 0.0, 0.0, -1.0), new DMatrix(-0.5, -0.5*ROOT3, 0.5*ROOT3, -0.5 ), new DMatrix(0.0, -1.0, 1.0, 0.0), new DMatrix(COS2PIOVER5, -SIN2PIOVER5, SIN2PIOVER5, COS2PIOVER5), new DMatrix(0.5, -0.5*ROOT3, 0.5*ROOT3, 0.5 ) }; /** * Create a new Panorama object * @param kaliCanvas The KaliCanvas object that this Panorama * should use for drawing. */ public Panorama(KaliCanvas kaliCanvas) { this.kaliCanvas = kaliCanvas; } /** * Set the current group. * @param index The group index; should be one of the constants * defined in Constants.java, e.g. GROUP_w2222 or * GROUP_w3x3, etc. */ public void setGroup(int index) { symmetryGroup = SymmetryGroups.group[index]; } /** * Return the current group */ public SymmetryGroup getGroup() { return symmetryGroup; } /** * Do the internal calculations necessary to prepare for drawing with * the current KaliCanvas coordinate systems. This should be called * immediately before a sequence of drawSegment() calls to draw the * screen, but after the KaliCanvas object's coordinate systems have * been set. */ public boolean prepareToDraw() { int i; double area, width, altitude, det, dotProduct; // Transfer symmetryGroup's translations to the screen // coordinate system. for (i = 0; i < symmetryGroup.numTranslations; i++) { preparedTranslations[i] = kaliCanvas.internalToScreen(symmetryGroup.translations[i]); // kaliCanvas.position.times(symmetryGroup.translations[i]); } // Make sure they're nondegenerate, and do further preparation as necessary. switch (symmetryGroup.numTranslations) { case 0: // No preparation is needed for a rosette group. return true; case 1: // Insist that the translation length be at least MIN_TRANSLATION pixels. if (preparedTranslations[0].length() < MIN_TRANSLATION) { return false; } // If the translation vector is "mainly horizontal", make it point to the right. // If the translation vector is "mainly vertical", make it point upwards. // Vectors on the 45 degree lines are considered horizontal. if (Math.abs(preparedTranslations[0].c[0]) >= Math.abs(preparedTranslations[0].c[1])) { if (preparedTranslations[0].c[0] < 0.0) { preparedTranslations[0].negate(); } } else { if (preparedTranslations[0].c[1] < 0.0) { preparedTranslations[0].negate(); } } return true; case 2: // Insist that the area of a fundamental parallelogram // be at least MIN_AREA, so that we don't end up drawing // 100,000 copies of a one-pixel image. area = Math.abs(preparedTranslations[0].c[0] * preparedTranslations[1].c[1] - preparedTranslations[0].c[1] * preparedTranslations[1].c[0]); if (area < MIN_AREA) { return false; } // In addition, insist that the parallelogram have altitude // at least MIN_ALTITUDE pixels in each direction, to avoid long // skinny parallelograms. for (i = 0; i < 2; i++) { width = preparedTranslations[i].length(); altitude = area / width; if (altitude < MIN_ALTITUDE) return false; } // Let preparedTranslations[0] be the more horizontal pointing Vector. if (Math.abs(preparedTranslations[0].c[0]) < Math.abs(preparedTranslations[1].c[0])) { DVector temp = preparedTranslations[0].copy(); preparedTranslations[0] = preparedTranslations[1]; preparedTranslations[1] = temp; } // Make sure preparedTranslations[0] points to the right. if (preparedTranslations[0].c[0] < 0.0) { preparedTranslations[0].negate(); } // Make sure preparedTranslations form a right-handed basis. det = preparedTranslations[0].c[0] * preparedTranslations[1].c[1] - preparedTranslations[0].c[1] * preparedTranslations[1].c[0]; if (det < 0.0) { preparedTranslations[1].negate(); } // Imagine ruling the window with lines parallel // to preparedTranslations[0]. To help us decide which // of these lines a given point lies on, we define a // reference vector such that // (1) referenceVector is parallel to preparedTranslations[0], and // (2) . referenceVector = new DVector(- preparedTranslations[0].c[1], preparedTranslations[0].c[0]); dotProduct = referenceVector.dot(preparedTranslations[1]); referenceVector.scale( 1.0 / dotProduct ); // At this point we should have referenceVector[1] > 0.0 // Find the maximum absolute value of // as pt ranges over the window frame. Use the fact that the frame // is symmetrical about the origin. The minimum will be the // negative of the maximum. maxReference = Math.abs(referenceVector.c[0]) * kaliCanvas.screenRight + Math.abs(referenceVector.c[1]) * kaliCanvas.screenTop; return true; default: return false; // should never occur } } /** * Draw a single segment in the given color under the * action of the current group. */ public void drawSegment(Segment s, Color c) { drawSegmentRfGlRtTr(s, c); } /** * Draw the Segment under the action of symmetryGroup. * We take care of the possible reflection ("Rf") here, * and let other methods deal with the possible glide * reflection ("Gl"), rotation ("Rt") and translations ("Tr"). */ private void drawSegmentRfGlRtTr(Segment s, Color c) { // Draw the Segment under the action of the possible // glide reflection, rotation and translations. drawSegmentGlRtTr(s, c); // If a reflection is called for, do it here, and then // pass it to DrawSegmentGlRtTr() for the remaining operations. if (symmetryGroup.reflectionType != AXIS_NONE) { Segment reflectedSegment = s.copy(); int i; switch (symmetryGroup.reflectionType) { case AXIS_X0: for (i = 0; i < 2; i++) { reflectedSegment.p[i].c[0] = - reflectedSegment.p[i].c[0]; } break; case AXIS_X4: for (i = 0; i < 2; i++) { reflectedSegment.p[i].c[0] = 0.5 - reflectedSegment.p[i].c[0]; } break; case AXIS_Y0: for (i = 0; i < 2; i++) { reflectedSegment.p[i].c[1] = - reflectedSegment.p[i].c[1]; } break; case AXIS_Y4: for (i = 0; i < 2; i++) { reflectedSegment.p[i].c[1] = 0.5 - reflectedSegment.p[i].c[1]; } break; } drawSegmentGlRtTr(reflectedSegment, c); } } /** * Take care of the possible glide reflection ("Gl") here, and let * other methods deal with the possible rotation ("Rt") and * translations ("Tr"). */ private void drawSegmentGlRtTr(Segment s, Color c) { // Draw the Segment under the action of the possible // rotation and translations. drawSegmentRtTr(s, c); // If a glide reflection is called for, do it here, and then // pass it to DrawSegmentRtTr() for the remaining operations. if (symmetryGroup.glideReflectionType != AXIS_NONE) { Segment glideSegment = s.copy(); int i; switch (symmetryGroup.glideReflectionType) { case AXIS_X0: for (i = 0; i < 2; i++) { glideSegment.p[i].c[0] = - glideSegment.p[i].c[0]; glideSegment.p[i].c[1] += 0.5; } break; case AXIS_X4: for (i = 0; i < 2; i++) { glideSegment.p[i].c[0] = 0.5 - glideSegment.p[i].c[0]; glideSegment.p[i].c[1] += 0.5; } break; case AXIS_Y0: for (i = 0; i < 2; i++) { glideSegment.p[i].c[1] = - glideSegment.p[i].c[1]; glideSegment.p[i].c[0] += 0.5; } break; case AXIS_Y4: for (i = 0; i < 2; i++) { glideSegment.p[i].c[1] = 0.5 - glideSegment.p[i].c[1]; glideSegment.p[i].c[0] += 0.5; } break; } drawSegmentRtTr(glideSegment, c); } } /** * Take care of the possible rotation ("Rt") here, and let * another method deal with the possible translations ("Tr"). */ private void drawSegmentRtTr(Segment s, Color c) { Segment rotatedSegment = s.copy(); int i, j; for (i = 0; i < symmetryGroup.rotationOrder; i++) { drawSegmentTr(rotatedSegment, c); for (j = 0; j < 2; j++) { rotatedSegment.p[j] = rotationMatrix[symmetryGroup.rotationOrder].times(rotatedSegment.p[j]); } } } /** * Draw a Segment under the action of symmetryGroup's translations. */ private void drawSegmentTr(Segment s, Color c) { // Here we must finally leave the pristine world of the internal // coordinate system and deal with the realities of the window // coordinate system. Segment screenSegment = new Segment(kaliCanvas.internalToScreen(s.p[0]), kaliCanvas.internalToScreen(s.p[1])); // Break into separate cases for rosette, frieze and wallpaper groups. switch (symmetryGroup.numTranslations) { case 0: drawSegmentRosette(screenSegment, c); break; case 1: drawSegmentFrieze(screenSegment, c); break; case 2: drawSegmentWallpaper(screenSegment, c); break; } } private void drawSegmentRosette(Segment s, Color c) { kaliCanvas.drawSegment(s, c); } private void drawSegmentFrieze(Segment segment, Color color) { int i, j; double multiple; Segment translatedSegment; // At the moment I can't think of a more elegant way to do it, // so let's break into cases according to whether the translation // is mainly horizontal or mainly vertical. // [prepareToDraw() has checked that it's not // completely degenerate.] if ( Math.abs(preparedTranslations[0].c[0]) >= Math.abs(preparedTranslations[0].c[1])) { // mainly horiz // mainly horizontal // // prepareToDraw() has already made sure that // preparedTranslations[0] points to the right. // Copy segment to translatedEndpoints in such a way that // translatedSegment.p[0] is not to the right of // translatedSegment.p[1]. translatedSegment = segment.copy(); if (segment.p[0].c[0] > segment.p[1].c[0]) { translatedSegment.reverse(); } // Subtract off some multiple of preparedTranslations[0] to move // to the left most image which might be visible. multiple = Math.floor((translatedSegment.p[1].c[0] - kaliCanvas.screenLeft) / preparedTranslations[0].c[0]); for (i = 0; i < 2; i++) { for (j = 0; j < 2; j++) { translatedSegment.p[i].c[j] -= multiple * preparedTranslations[0].c[j]; } } // Draw images until we move off the right side of the frame. while (translatedSegment.p[0].c[0] < kaliCanvas.screenRight) { kaliCanvas.drawSegment(translatedSegment, color); for (i = 0; i < 2; i++) { for (j = 0; j < 2; j++) { translatedSegment.p[i].c[j] += preparedTranslations[0].c[j]; } } } } else { // mainly vertical // // prepareToDraw() has already made sure that // preparedTranslations[0] points upward. // Copy endpoints to translatedSegment.p in such a way that // translatedSegment.p[0] is not above translatedSegment.p[1]. translatedSegment = segment.copy(); if (segment.p[0].c[1] > segment.p[1].c[1]) { translatedSegment.reverse(); } // Subtract off some multiple of preparedTranslations[0] to move // to the lowest image which might be visible. multiple = Math.floor((translatedSegment.p[1].c[1] - kaliCanvas.screenBottom) / preparedTranslations[0].c[1]); for (i = 0; i < 2; i++) { for (j = 0; j < 2; j++) { translatedSegment.p[i].c[j] -= multiple * preparedTranslations[0].c[j]; } } // Draw images until we move off the top of the frame. while (translatedSegment.p[0].c[1] < kaliCanvas.screenTop) { kaliCanvas.drawSegment(translatedSegment, color); for (i = 0; i < 2; i++) { for (j = 0; j < 2; j++) { translatedSegment.p[i].c[j] += preparedTranslations[0].c[j]; } } } } } private void drawSegmentWallpaper(Segment segment, Color color) { int i, j, numRows, row; double[] dotProduct = new double[2]; double higherDotProduct, dotProductDifference, multiple; Segment translatedSegment; // prepareToDraw() has already set up preparedTranslations so that // (1) preparedTranslations[0] points to the right, // (2) preparedTranslations[] for a right-handed basis, and // (3) the corresponding parallelogram has MIN_AREA and MIN_ALTITUDE. // We want to draw all translates whose initial endpoint lies within the frame. // Copy segment to translatedSegment in such a way that // translatedSegment.p[0] is not to the right of translatedSegment.p[1]. translatedSegment = segment.copy(); if (segment.p[0].c[0] > segment.p[1].c[0]) { translatedSegment.reverse(); } // Imagine the coordinate grid defined by preparedTranslations[]. // Compute to see which horizontal // grid line we're near (cf. the definition of referenceVector in // prepareToDraw()). E.g. if dotProduct is 2.25, we're // a quarter of the way from grid line 2 to grid line 3. for (i = 0; i < 2; i++) { dotProduct[i] = referenceVector.dot(translatedSegment.p[i]); } higherDotProduct = Math.max(dotProduct[0], dotProduct[1]); dotProductDifference = Math.abs(dotProduct[0] - dotProduct[1]); // Subtract off some multiple of preparedTranslations[1] so that // we move to the lowest parallel line which might still intersect the window. multiple = Math.floor(higherDotProduct - (-maxReference)); for (i = 0; i < 2; i++) { for (j = 0; j < 2; j++) { translatedSegment.p[i].c[j] -= multiple * preparedTranslations[1].c[j]; } } // The number of rows we need to consider is given by // [maxReference - (-maxReference)] + dotProductDifference // = 2*maxReference + dotProductDifference. numRows = (int)Math.round(Math.ceil(2.0*maxReference + dotProductDifference)); for (row = 0; row < numRows; row++) { // Draw one row. // Subtract some multiple of preparedTranslations[0] to move // to the left most position within the row. multiple = Math.floor( (translatedSegment.p[1].c[0] - kaliCanvas.screenLeft) / preparedTranslations[0].c[0]); for (i = 0; i < 2; i++) { for (j = 0; j < 2; j++) { translatedSegment.p[i].c[j] -= multiple * preparedTranslations[0].c[j]; } } // Draw images until we move off the right side of the frame. while (translatedSegment.p[0].c[0] < kaliCanvas.screenRight) { kaliCanvas.drawSegment(translatedSegment, color); for (i = 0; i < 2; i++) { for (j = 0; j < 2; j++) { translatedSegment.p[i].c[j] += preparedTranslations[0].c[j]; } } } // Add preparedTranslations[1] to each of translatedSegment // to move up to the next row. for (i = 0; i < 2; i++) { for (j = 0; j < 2; j++) { translatedSegment.p[i].c[j] += preparedTranslations[1].c[j]; } } } } }