/* turtle.hpp Simple array-based turtle graphics engine in CPP. Exports to BMP files. Author: Damian Kurpiewski, December 2021 Based on a work by Mike Lam, James Madison University (https://w3.cs.jmu.edu/lam2mo/cs240_2015_08/turtle.html) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #ifndef TURTLEGRAPHICS_YATG_HPP #define TURTLEGRAPHICS_YATG_HPP #include #include #include #include #include #define MAX_POLYGON_VERTICES 128 struct rgb { unsigned char red; unsigned char green; unsigned char blue; }; struct turtleState { double xpos; // current position and heading double ypos; // (uses floating-point numbers for double heading; // increased accuracy) rgb strokeColor; // current pen color rgb fillColor; // current fill color bool pendown; // currently drawing? bool filled; // currently filling? }; struct BMPHeader { char bfType[2]; // "BM" int bfSize; // size of file in bytes int bfReserved; // set to 0 int bfOffBits; // byte offset to actual bitmap data (= 54) int biSize; // size of BITMAPINFOHEADER, in bytes (= 40) int biWidth; // width of image, in pixels int biHeight; // height of images, in pixels short biPlanes; // number of planes in target device (set to 1) short biBitCount; // bits per pixel (24 in this case) int biCompression; // type of compression (0 if no compression) int biSizeImage; // image size, in bytes (0 if no compression) int biXPelsPerMeter; // resolution in pixels/meter of display device int biYPelsPerMeter; // resolution in pixels/meter of display device int biClrUsed; // number of colors in the color table (if 0, use maximum allowed by biBitCount) int biClrImportant; // number of important colors. If 0, all colors are important }; class Turtle { turtleState mainTurtle{}; turtleState backupTurtle{}; rgb *mainTurtleImage = nullptr; // 2d pixel data field unsigned int mainFieldWidth = 0; // size in pixels unsigned int mainFieldHeight = 0; bool mainFieldSaveFrames = false; // currently saving video frames? int mainFieldFrameCount = 0; // current video frame counter int mainFieldFrameInterval = 10; // pixels per frame int mainFieldPixelCount = 0; // total pixels drawn by turtle since // beginning of video int mainTurtlePolyVertexCount = 0; // polygon vertex count double mainTurtlePolyX[MAX_POLYGON_VERTICES]{}; // polygon vertex x-coords double mainTurtlePolyY[MAX_POLYGON_VERTICES]{}; // polygon vertex y-coords unsigned long long int numPixelsOutOfBounds; const int TURTLE_DIGITS[10][20] = { {0, 1, 1, 0, // 0 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0}, {0, 1, 1, 0, // 1 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 1, 1}, {1, 1, 1, 0, // 2 0, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 0, 1, 1, 1, 1}, {1, 1, 1, 0, // 3 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0}, {0, 1, 0, 1, // 4 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 0, 0, 1}, {1, 1, 1, 1, // 5 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0}, {0, 1, 1, 0, // 6 1, 0, 0, 0, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0}, {1, 1, 1, 1, // 7 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0}, {0, 1, 1, 0, // 8 1, 0, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0}, {0, 1, 1, 0, // 9 1, 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 0}, }; public: /** * Initializes the 2d field that the turtle moves on. * @param width field width * @param height field height */ Turtle(unsigned int width, unsigned int height) { numPixelsOutOfBounds = 0; auto totalSize = sizeof(rgb) * width * height; // allocate new image and initialize it to white mainTurtleImage = (rgb *) malloc(totalSize); if (mainTurtleImage == nullptr) { fprintf(stderr, "Can't allocate memory for turtle image.\n"); exit(EXIT_FAILURE); } memset(mainTurtleImage, 255, totalSize); // save field size for later mainFieldWidth = width; mainFieldHeight = height; // disable video mainFieldSaveFrames = false; // reset turtle position and color reset(); // create backup at the initial position backup(); } ~Turtle() { cleanup(); } /** * Resets the turtle's location, orientation, color, and pen status to the default values: * center of the field (0,0), facing right (0 degrees), black stroke and green fill color, pen down. */ void reset() { // move turtle to middle of the field mainTurtle.xpos = 0.0; mainTurtle.ypos = 0.0; // orient to the right (0 deg) mainTurtle.heading = 0.0; // default stroke color is black mainTurtle.strokeColor.red = 0; mainTurtle.strokeColor.green = 0; mainTurtle.strokeColor.blue = 0; // default fill color is black mainTurtle.fillColor.red = 0; mainTurtle.fillColor.green = 255; mainTurtle.fillColor.blue = 0; // default pen position is down mainTurtle.pendown = true; // default fill status is off mainTurtle.filled = false; mainTurtlePolyVertexCount = 0; } /** * Creates a backup of the current turtle. * The backup can be restored by using restore(). * Useful in complex drawing situations. */ void backup() { backupTurtle = mainTurtle; } /** * Restores the turtle from the backup. */ void restore() { mainTurtle = backupTurtle; } /** * Moves the turtle forward, drawing a straight line if the pen is down. * @param pixels movement distance */ void forward(int pixels) { // calculate (x,y) movement vector from heading double radians = mainTurtle.heading * M_PI / 180.0; double dx = cos(radians) * pixels; double dy = sin(radians) * pixels; // delegate to another method to actually move goTo(mainTurtle.xpos + dx, mainTurtle.ypos + dy); } /** * Moves the turtle backward, drawing a straight line if the pen is down. * @param pixels movement distance */ void backward(int pixels) { // opposite of "forward" forward(-pixels); } /** * Moves the turtle left. * @param pixels movement distance */ void strafeLeft(int pixels) { turnLeft(90); forward(pixels); turnRight(90); } /** * Moves the turtle right. * @param pixels movement distance */ void strafeRight(int pixels) { turnRight(90); forward(pixels); turnLeft(90); } /** * Turns the turtle to the left by the specified number of degrees. * @param angle */ void turnLeft(double angle) { // rotate turtle heading mainTurtle.heading += angle; // constrain heading to range: [0.0, 360.0) if (mainTurtle.heading < 0.0) { mainTurtle.heading += 360.0; } else if (mainTurtle.heading >= 360.0) { mainTurtle.heading -= 360.0; } } /** * Turns the turtle to the right by the specified number of degrees. * @param angle */ void turnRight(double angle) { // opposite of "turn left" turnLeft(-angle); } /** * Sets the pen status to "up" (do not draw). */ void penUp() { mainTurtle.pendown = false; } /** * Sets the pen status to "down" (draw). */ void penDown() { mainTurtle.pendown = true; } /** * Starts filling. * Call this before drawing a polygon to activate the bookkeeping required to run the filling algorithm later. */ void beginFill() { mainTurtle.filled = true; mainTurtlePolyVertexCount = 0; } /** * Ends filling. * Call this after drawing a polygon to trigger the fill algorithm. * The filled polygon may have up to 128 sides. */ void endFill() { // based on public-domain fill algorithm in C by Darel Rex Finley, 2007 // from http://alienryderflex.com/polygon_fill/ double nodeX[MAX_POLYGON_VERTICES]; // x-coords of polygon intercepts int nodes; // size of nodeX int x, y, i, j; // current pixel and loop indices double temp; // temporary variable for sorting // loop through the rows of the image for (y = -(mainFieldHeight / 2); y < mainFieldHeight / 2; y++) { // build a list of polygon intercepts on the current line nodes = 0; j = mainTurtlePolyVertexCount - 1; for (i = 0; i < mainTurtlePolyVertexCount; i++) { if ((mainTurtlePolyY[i] < (double) y && mainTurtlePolyY[j] >= (double) y) || (mainTurtlePolyY[j] < (double) y && mainTurtlePolyY[i] >= (double) y)) { // intercept found; record it nodeX[nodes++] = (mainTurtlePolyX[i] + ((double) y - mainTurtlePolyY[i]) / (mainTurtlePolyY[j] - mainTurtlePolyY[i]) * (mainTurtlePolyX[j] - mainTurtlePolyX[i])); } j = i; if (nodes >= MAX_POLYGON_VERTICES) { fprintf(stderr, "Too many intercepts in fill algorithm!\n"); exit(EXIT_FAILURE); } } // sort the nodes via simple insertion sort for (i = 1; i < nodes; i++) { temp = nodeX[i]; for (j = i; j > 0 && temp < nodeX[j - 1]; j--) { nodeX[j] = nodeX[j - 1]; } nodeX[j] = temp; } // fill the pixels between node pairs for (i = 0; i < nodes; i += 2) { for (x = (int) floor(nodeX[i]) + 1; x < (int) ceil(nodeX[i + 1]); x++) { fillPixel(x, y); } } } mainTurtle.filled = false; // redraw polygon (filling is imperfect and can occasionally occlude sides) for (i = 0; i < mainTurtlePolyVertexCount; i++) { int x0 = (int) round(mainTurtlePolyX[i]); int y0 = (int) round(mainTurtlePolyY[i]); int x1 = (int) round(mainTurtlePolyX[(i + 1) % mainTurtlePolyVertexCount]); int y1 = (int) round(mainTurtlePolyY[(i + 1) % mainTurtlePolyVertexCount]); drawLine(x0, y0, x1, y1); } } /** * Moves the turtle to the specified location, drawing a straight line if the pen is down. * Takes integer coordinate parameters. * @param x * @param y */ void goTo(int x, int y) { goTo((double) x, (double) y); } /** * Moves the turtle to the specified location, drawing a straight line if the pen is down. * Takes real-numbered coordinate parameters, and is also used internally to implement forward and backward motion. * @param x * @param y */ void goTo(double x, double y) { // draw line if pen is down if (mainTurtle.pendown) { drawLine((int) round(mainTurtle.xpos), (int) round(mainTurtle.ypos), (int) round(x), (int) round(y)); } // change current turtle position mainTurtle.xpos = (double) x; mainTurtle.ypos = (double) y; // track coordinates for filling if (mainTurtle.filled && mainTurtle.pendown && mainTurtlePolyVertexCount < MAX_POLYGON_VERTICES) { mainTurtlePolyX[mainTurtlePolyVertexCount] = x; mainTurtlePolyY[mainTurtlePolyVertexCount] = y; mainTurtlePolyVertexCount++; } } /** * Rotates the turtle to the given heading (in degrees). * 0 degrees means facing to the right. * 90 degrees means facing straight up. * @param angle */ void setHeading(double angle) { mainTurtle.heading = angle; } /** * Set the current drawing color. * Each component (red, green, and blue) may be any value between 0 and 255 (inclusive). * Black is (0,0,0) and white is (255,255,255). * @param red * @param green * @param blue */ void setPenColor(int red, int green, int blue) { mainTurtle.strokeColor.red = red; mainTurtle.strokeColor.green = green; mainTurtle.strokeColor.blue = blue; } /** * Set the current filling color. * Each component (red, green, and blue) may be any value between 0 and 255 (inclusive). * Black is (0,0,0) and white is (255,255,255). * @param red * @param green * @param blue */ void setFillColor(int red, int green, int blue) { mainTurtle.fillColor.red = red; mainTurtle.fillColor.green = green; mainTurtle.fillColor.blue = blue; } /** * Draws a 1-pixel dot at the current location, regardless of pen status. */ void dot() { // draw a pixel at the current location, regardless of pen status drawPixel((int) round(mainTurtle.xpos), (int) round(mainTurtle.ypos)); } /** * Draw a 1-pixel dot at the given location using the current draw color, * regardless of current turtle location or pen status. * @param x * @param y */ void drawPixel(int x, int y) { if (x < (-mainFieldWidth / 2) || x > (mainFieldWidth / 2) || y < (-mainFieldHeight / 2) || y > (mainFieldHeight / 2)) { // only print the first 100 error messages (prevents runaway output) if (++numPixelsOutOfBounds < 100) { fprintf(stderr, "Pixel out of bounds: (%d,%d)\n", x, y); } return; } // calculate pixel offset in image data array int idx = mainFieldWidth * (y + mainFieldHeight / 2) + (x + mainFieldWidth / 2); // "draw" the pixel by setting the color values in the image matrix if (idx >= 0 && idx < mainFieldWidth * mainFieldHeight) { mainTurtleImage[idx].red = mainTurtle.strokeColor.red; mainTurtleImage[idx].green = mainTurtle.strokeColor.green; mainTurtleImage[idx].blue = mainTurtle.strokeColor.blue; } // track total pixels drawn and emit video frame if a frame interval has // been crossed (and only if video saving is enabled, of course) if (mainFieldSaveFrames && mainFieldPixelCount++ % mainFieldFrameInterval == 0) { saveFrame(); } } /** * Draws a 1-pixel dot at the given location using the current fill color, * regardless of current turtle location or pen status. * @param x * @param y */ void fillPixel(int x, int y) { // calculate pixel offset in image data array int idx = mainFieldWidth * (y + mainFieldHeight / 2) + (x + mainFieldWidth / 2); // check to make sure it's not out of bounds if (idx >= 0 && idx < mainFieldWidth * mainFieldHeight) { mainTurtleImage[idx].red = mainTurtle.fillColor.red; mainTurtleImage[idx].green = mainTurtle.fillColor.green; mainTurtleImage[idx].blue = mainTurtle.fillColor.blue; } } /** * Draw a straight line between the given coordinates, regardless of current turtle location or pen status. * @param x0 * @param y0 * @param x1 * @param y1 */ void drawLine(int x0, int y0, int x1, int y1) { // uses a variant of Bresenham's line algorithm: // https://en.wikipedia.org/wiki/Talk:Bresenham%27s_line_algorithm int absX = abs(x1 - x0); // absolute value of coordinate distances int absY = abs(y1 - y0); int offX = x0 < x1 ? 1 : -1; // line-drawing direction offsets int offY = y0 < y1 ? 1 : -1; int x = x0; // incremental location int y = y0; int err; drawPixel(x, y); if (absX > absY) { // line is more horizontal; increment along x-axis err = absX / 2; while (x != x1) { err = err - absY; if (err < 0) { y += offY; err += absX; } x += offX; drawPixel(x, y); } } else { // line is more vertical; increment along y-axis err = absY / 2; while (y != y1) { err = err - absX; if (err < 0) { x += offX; err += absY; } y += offY; drawPixel(x, y); } } } /** * Draws a circle at the given coordinates with the given radius, regardless of current turtle location or pen status. * @param x0 * @param y0 * @param radius */ void drawCircle(int x0, int y0, int radius) { // implementation based on midpoint circle algorithm: // https://en.wikipedia.org/wiki/Midpoint_circle_algorithm int x = radius; int y = 0; int switch_criteria = 1 - x; if (mainTurtle.filled) { fillCircle(x0, y0, radius); } while (x >= y) { drawPixel(x + x0, y + y0); drawPixel(y + x0, x + y0); drawPixel(-x + x0, y + y0); drawPixel(-y + x0, x + y0); drawPixel(-x + x0, -y + y0); drawPixel(-y + x0, -x + y0); drawPixel(x + x0, -y + y0); drawPixel(y + x0, -x + y0); y++; if (switch_criteria <= 0) { switch_criteria += 2 * y + 1; // no x-coordinate change } else { x--; switch_criteria += 2 * (y - x) + 1; } } } /** * Fill a circle at the given coordinates with the given radius, regardless of current turtle location or pen status. * @param x0 * @param y0 * @param radius */ void fillCircle(int x0, int y0, int radius) { int rad_sq = radius * radius; // Naive algorithm, pretty ugly due to no antialiasing: for (int x = x0 - radius; x < x0 + radius; x++) { for (int y = y0 - radius; y < y0 + radius; y++) { int dx = x - x0; int dy = y - y0; int dsq = (dx * dx) + (dy * dy); if (dsq < rad_sq) fillPixel(x, y); } } } /** * Fills a circle at the current coordinates with the given radius. * @param radius */ void fillCircle(int radius) { fillCircle(mainTurtle.xpos, mainTurtle.ypos, radius); } /** * Draws a turtle at the current location. */ void drawTurtle() { // We are going to make our own backup of the turtle, since turtle_backup() // only gives us one level of undo. turtleState original_turtle = mainTurtle; penUp(); // Draw the legs for (int i = -1; i < 2; i += 2) { for (int j = -1; j < 2; j += 2) { backup(); forward(i * 7); strafeLeft(j * 7); setFillColor( mainTurtle.strokeColor.red, mainTurtle.strokeColor.green, mainTurtle.strokeColor.blue ); fillCircle(5); setFillColor( original_turtle.fillColor.red, original_turtle.fillColor.green, original_turtle.fillColor.blue ); fillCircle(3); restore(); } } // Draw the head backup(); forward(10); setFillColor( mainTurtle.strokeColor.red, mainTurtle.strokeColor.green, mainTurtle.strokeColor.blue ); fillCircle(5); setFillColor( original_turtle.fillColor.red, original_turtle.fillColor.green, original_turtle.fillColor.blue ); fillCircle(3); restore(); // Draw the body for (int i = 9; i >= 0; i -= 4) { backup(); setFillColor( mainTurtle.strokeColor.red, mainTurtle.strokeColor.green, mainTurtle.strokeColor.blue ); fillCircle(i + 2); setFillColor( original_turtle.fillColor.red, original_turtle.fillColor.green, original_turtle.fillColor.blue ); fillCircle(i); restore(); } // Restore the original turtle position: mainTurtle = original_turtle; } /** * Saves current field to a .bmp file. * @param filename */ void saveBMP(const char *filename) { int i, j, ipos; unsigned int bytesPerLine; unsigned char *line; FILE *file; BMPHeader bmph{}; auto width = mainFieldWidth; auto height = mainFieldHeight; char *rgb = (char *) mainTurtleImage; // the length of each line must be a multiple of 4 bytes bytesPerLine = (3 * (width + 1) / 4) * 4; strncpy(bmph.bfType, "BM", 2); bmph.bfOffBits = 54; bmph.bfSize = bmph.bfOffBits + bytesPerLine * height; bmph.bfReserved = 0; bmph.biSize = 40; bmph.biWidth = width; bmph.biHeight = height; bmph.biPlanes = 1; bmph.biBitCount = 24; bmph.biCompression = 0; bmph.biSizeImage = bytesPerLine * height; bmph.biXPelsPerMeter = 0; bmph.biYPelsPerMeter = 0; bmph.biClrUsed = 0; bmph.biClrImportant = 0; file = fopen(filename, "wb"); if (file == nullptr) { fprintf(stderr, "Could not write to file: %s\n", filename); exit(EXIT_FAILURE); } fwrite(&bmph.bfType, 2, 1, file); fwrite(&bmph.bfSize, 4, 1, file); fwrite(&bmph.bfReserved, 4, 1, file); fwrite(&bmph.bfOffBits, 4, 1, file); fwrite(&bmph.biSize, 4, 1, file); fwrite(&bmph.biWidth, 4, 1, file); fwrite(&bmph.biHeight, 4, 1, file); fwrite(&bmph.biPlanes, 2, 1, file); fwrite(&bmph.biBitCount, 2, 1, file); fwrite(&bmph.biCompression, 4, 1, file); fwrite(&bmph.biSizeImage, 4, 1, file); fwrite(&bmph.biXPelsPerMeter, 4, 1, file); fwrite(&bmph.biYPelsPerMeter, 4, 1, file); fwrite(&bmph.biClrUsed, 4, 1, file); fwrite(&bmph.biClrImportant, 4, 1, file); line = (unsigned char *) malloc(bytesPerLine); memset(line, 0, bytesPerLine); if (line == nullptr) { fprintf(stderr, "Can't allocate memory for BMP file.\n"); exit(EXIT_FAILURE); } for (i = 0; i < height; i++) { for (j = 0; j < width; j++) { ipos = 3 * (width * i + j); line[3 * j] = rgb[ipos + 2]; line[3 * j + 1] = rgb[ipos + 1]; line[3 * j + 2] = rgb[ipos]; } fwrite(line, bytesPerLine, 1, file); } free(line); fclose(file); } /** * Enables the video output. * When enabled, periodic frame bitmaps will be saved with sequentially-ordered filenames matching the following pattern: * "frameXXXX.bmp" (X is a digit). * Frames are emitted after a regular number of pixels have been drawn. * This number is set by the parameter to this function. * Some experimentation may be required to find a optimal values for different shapes. * @param pixelsPerFrame */ void beginVideo(int pixelsPerFrame) { mainFieldSaveFrames = true; mainFieldFrameCount = 0; mainFieldFrameInterval = pixelsPerFrame; mainFieldPixelCount = 0; } /** * Emits a single video frame containing the current field image. */ void saveFrame() { char filename[32]; sprintf(filename, "frame%05d.bmp", ++mainFieldFrameCount); saveBMP(filename); } /** * Disables the video output. */ void endVideo() { mainFieldSaveFrames = false; } /** * Returns the current x-coordinate. * @return current x-coordinate */ double getX() { return mainTurtle.xpos; } /** * Returns the current y-coordinate. * @return current y-coordinate */ double getY() { return mainTurtle.ypos; } /** * Draws an integer at the current location. * @param number number to draw */ void drawInt(int number) { int digitsCount = countDigits(number); for (int i = digitsCount - 1; i >= 0; i--) { drawDigit(number % 10, i); number = number / 10; } } private: /** * Cleans up any memory used by the turtle graphics system. */ void cleanup() { if (mainTurtleImage != nullptr) { free(mainTurtleImage); mainTurtleImage = nullptr; } } /** * Draws single digit depending on it location in the number * @param digit digit to draw * @param digitIndex digit location in the number */ void drawDigit(unsigned char digit, int digitIndex) { for (int y = 0; y < 5; y++) { for (int x = 0; x < 4; x++) { if (TURTLE_DIGITS[digit][y * 4 + x] == 1) { drawPixel(mainTurtle.xpos + digitIndex * 5 + x, mainTurtle.ypos - y); } } } } /** * Counts the number of digits in the given integer number. * @param number * @return Number of digits in number */ static unsigned short countDigits(int number) { if (number > 9) { (int) (ceil(log10(number))); } else { return 1; } } }; #endif //TURTLEGRAPHICS_YATG_HPP