In this post we will write some functions to draw geometric shapes. We will see how to draw lines, rectangles, triangles and circles. Other shapes, such as curves, ellipses, rounded rectangles, polygons, etc. will be the subject of another post in the future.

 

Basic functions

To develop the drawing functions, we will start from three basic functions, that will be called by all the others.

The first of these basic functions is drawPixel, that we have already developed in the previous post. The other two are drawHorizontalLine and drawVerticalLine. The name of these functions already says everything about what they do!

Declare them in the GFX class in GFX.h:

void drawHorizontalLine(int16_t x, int16_t y, uint16_t width, uint8_t color);
void drawVerticalLine(int16_t x, int16_t y, uint16_t height, uint8_t color);

and add this code to GFX.cpp:

void GFX::drawHorizontalLine(int16_t x, int16_t y, uint16_t width, uint8_t color) {
    int sector = SCREENBUFFER_SECTOR(y);
    if(sector)
        y = y - 256;
    int offset = 320 * y + x; 
    memset(screenBuffer[sector] + offset, color, width);
}

void GFX::drawVerticalLine(int16_t x, int16_t y, uint16_t height, uint8_t color) {
    int16_t y2 = y + height - 1;
    int startSector = SCREENBUFFER_SECTOR(y);
    int endSector = SCREENBUFFER_SECTOR(y2);
    if(startSector == endSector) {
        if(startSector) {
            y = y - 256;
            y2 = y2 - 256;
        }
        for( ; y <= y2; y++)
            screenBuffer[startSector][320 * y + x] = color;
    } else {
        y2 = y2 - 256;
        for( ; y < 256; y++)
            screenBuffer[0][320 * y + x] = color;
        for(y = 0; y <= y2; y++)
            screenBuffer[1][320 * y + x] = color;
    }
}

Some comments on these functions:

  • They are the only drawing functions that write directly into the screen buffer.
  • Since they are called by all the other drawing functions, it’s essential that they are fast. When we will try to optimize the speed of the graphics library in the future, we will mainly focus on these.
  • To make them faster, the validity of the coordinates where they draw is not checked. The individual drawing functions will take care of doing the checks (having more possibilities to optimize for specific cases)
  • drawHorizontalLine is much faster than drawVerticalLine because it uses the memset function. For this reason it will be used for drawing filled shapes.

 

Useful functions and macros

There are some operations that will be repeated very often. It is therefore useful to develop some macros or functions to avoid rewriting the same code several times.

A condition that is checked multiple times is whether a pixel is inside the screen or not. To do this check, we add a macro to GFX.h:

#define ONSCREEN(x,y) (x >= 0 && x < 320 && y >= 0 && y < 480)

A second operation performed very often is checking if a horizontal or vertical line is partially on the screen, and in that case calculating the portion that must be actually drawn. All this will be done by the function cropToViewSize.

Declare it in the private section of the GFX class:

int16_t cropToViewSize(int16_t* start, uint16_t* length, uint16_t viewSize);

and add this code to GFX.cpp:

int16_t GFX::cropToViewSize(int16_t* start, uint16_t* length, uint16_t viewSize) {
    // Check if the line is completely outside the screen
    if(*start >= viewSize) {
        *start = 0;
        *length = 0;
        return -1;
    }
    int16_t end = *start + *length - 1;
    if(end < 0) {
        *start = 0;
        *length = 0;
        return -1;
    }

    // If we get here, the line is at least partially on the screen.
    // Check if it starts outside the screen and recalculate the length if necessary.
    if(*start < 0) {
        *length += *start;
        *start = 0;
    }

    // Check if the line ends outside the screen and recalculate the length if necessary.
    *length = (end < viewSize) ? *length : (viewSize - *start);

    // Return the end coordinate of the line
    return (*start + *length - 1);
}

Straight lines

Let’s start with the simplest shape, a straight line. To draw straight lines, we will use Bresenham’s line algorithm. I will not go into the details of how this algorithm works, follow the previous link to read the Wikipedia page about it.

Add the declaration in GFX class:

void drawLine(int16_t xStart, int16_t yStart, int16_t xEnd, int16_t yEnd, uint8_t color);

and the following code in GFX.cpp:

void GFX::drawLine(int16_t xStart, int16_t yStart, int16_t xEnd, int16_t yEnd, uint8_t color) {
    // Check for horizontal/vertical line to use faster functions
    if(yStart == yEnd) {
        // Check if the line is outside the screen
        if(yStart < 0 || yStart >= 480)
            return;

        // Sort horizontal coordinates
        if(xEnd < xStart) {
            int16_t tmp;
            tmp = xStart; xStart = xEnd; xEnd = tmp;
        }

        // Check if the line is outside the screen
        if(xEnd < 0 || xStart >= 320)
            return;
        
        // Draw line
        uint16_t width = xEnd - xStart + 1;
        cropToViewSize(&xStart, &width, 320);
        drawHorizontalLine(xStart, yStart, width, color);
        return;
    } else if(xStart == xEnd) {
        // Check if the line is outside the screen
        if(xStart < 0 || xStart >= 320)
            return;        

        // Sort vertical coordinates
        if(yEnd < yStart) {
            int16_t tmp;
            tmp = yStart; yStart = yEnd; yEnd = tmp;
        }

        // Check if the line is outside the screen
        if(yEnd < 0 || yStart >= 480)
            return;
        
        // Draw line
        uint16_t height = yEnd - yStart + 1;
        cropToViewSize(&yStart, &height, 480);
        drawVerticalLine(xStart, yStart, height, color);
        return;
    }

    // Bresenham's line algorithm
    int16_t steep = abs(yEnd - yStart) > abs(xEnd - xStart);
    if (steep) {
        int16_t tmp;
        tmp = xStart; xStart = yStart; yStart = tmp;
        tmp = xEnd; xEnd = yEnd; yEnd = tmp;
    }
    if (xStart > xEnd) {
        int16_t tmp;
        tmp = xStart; xStart = xEnd; xEnd = tmp;
        tmp = yStart; yStart = yEnd; yEnd = tmp;
    }

    int16_t dx, dy;
    dx = xEnd - xStart;
    dy = abs(yEnd - yStart);

    int16_t err = dx / 2;
    int16_t yStep;

    if (yStart < yEnd) {
        yStep = 1;
    } else {
        yStep = -1;
    }

    for ( ; xStart<=xEnd; xStart++) {
        if (steep) {
            if(ONSCREEN(yStart, xStart))
                drawPixel(yStart, xStart, color);
        } else {
            if(ONSCREEN(xStart, yStart))
                drawPixel(xStart, yStart, color);
        }
        err -= dy;
        if (err < 0) {
            yStart += yStep;
            err += dx;
        }
    }
}

 

Triangles and Rectangles

Once you have the function to draw a line, the functions for drawing the outline of polygons are very simple.

Add these declarations to GFX class:

void drawRectangle(int16_t x, int16_t y, uint16_t width, uint16_t height, uint8_t color);
void drawTriangle(int16_t x0, int16_t y0,int16_t x1, int16_t y1,int16_t x2, int16_t y2, uint8_t color);

and add this code to GFX.cpp:

void GFX::drawRectangle(int16_t x, int16_t y, uint16_t width, uint16_t height, uint8_t color) {
    drawLine(x, y, x + width - 1, y, color);
    drawLine(x, y + height -1, x + width - 1, y + height - 1, color);
    drawLine(x, y, x, y + height - 1, color);
    drawLine(x + width - 1, y, x + width - 1, y + height - 1, color);
}

void GFX::drawTriangle(int16_t x0, int16_t y0,int16_t x1, int16_t y1,int16_t x2, int16_t y2, uint8_t color) {
    drawLine(x0, y0, x1, y1, color);
    drawLine(x1, y1, x2, y2, color);
    drawLine(x2, y2, x0, y0, color);
}

 

Filled shapes

Drawing filled shapes is a little more complicated. The idea is to draw the shape line by line, calculating the horizontal starting and ending coordinates of each line.

Using this idea, drawing a filled rectangle is very simple.

Add this declaration to GFX class:

void drawFilledRectangle(int16_t x, int16_t y, uint16_t width, uint16_t height, uint8_t color);

and add this code to GFX.cpp:

void GFX::drawFilledRectangle(int16_t x, int16_t y, uint16_t width, uint16_t height, uint8_t color) {
    // Check if the rectangle is outside the screen
    if(x >= 320 || y >= 480)
        return;
    if(x + width - 1 < 0)
        return;
    if(y + height - 1 < 0)
        return;

    // Draw the filled rectangle
    int16_t y2;
    cropToViewSize(&x, &width, 320);
    y2 = cropToViewSize(&y, &height, 480);
    for( ; y <= y2; y++)
        drawHorizontalLine(x, y, width, color);
}

Instead, drawing a filled triangle is a bit more complicated. The algorithm for drawing filled triangles is based on the fact that there are two easy-to-draw cases: the flat bottom and flat top triangles. They are simple cases because the difference in vertical coordiantes is equal for both sides. The idea of this algorithm is to traverse both legs step by step in y-direction and draw a straight horizontal line between both endpoints. For a generic triangle, we will decompose it into two triangles, one with flat bottom and the other with flat top, and draw them separately.

Add the declaration in the GFX class:

void drawFilledTriangle(int16_t x0, int16_t y0,int16_t x1, int16_t y1,int16_t x2, int16_t y2, uint8_t color);

and add this to GFX.cpp:

void GFX::drawFilledTriangle(int16_t x0, int16_t y0,int16_t x1, int16_t y1,int16_t x2, int16_t y2, uint8_t color) {
    int dx01, dx02, dx12, dy01, dy02, dy12;
    int u01, u02, u12;

    // Sort vertices by y value
    if(y0 > y1) {
        int16_t tmp;
        tmp = y0; y0 = y1; y1 = tmp;
        tmp = x0; x0 = x1; x1 = tmp;
    }
    if(y0 > y2) {
        int16_t tmp;
        tmp = y0; y0 = y2; y2 = tmp;
        tmp = x0; x0 = x2; x2 = tmp;
    }
    if(y1 > y2) {
        int16_t tmp;
        tmp = y1; y1 = y2; y2 = tmp;
        tmp = x1; x1 = x2; x2 = tmp;
    }

    // Set up long side (0 => 2)
    dx02 = x2 - x0;
    dy02 = y2 - y0;
    u02 = x0 * dy02;

    // Upper triangle
    if(y1 > y0) {
        // Set up upper side (0 => 1)
        dx01 = x1 - x0;
        dy01 = y1 - y0;
        u01 = x0 * dy01;

        // Draw upper triangle
        for(int y=y0; y<y1; y++) {
            int16_t xStart = u01 / dy01;
            int16_t xEnd = u02 / dy02;
            if(xEnd > xStart) {
                if(y >= 0 && y < 480) {
                    uint16_t width = xEnd - xStart + 1;
                    cropToViewSize(&xStart, &width, 320);
                    drawHorizontalLine(xStart, y, width, color);
                }
            } else {
                if(y >= 0 && y < 480) {
                    uint16_t width = xStart - xEnd + 1;
                    cropToViewSize(&xEnd, &width, 320);
                    drawHorizontalLine(xEnd, y, width, color);
                }
            }

            // Next line
            u02 = u02 + dx02;
            u01 = u01 + dx01;
        }
    }

    // Lower triangle
    if(y2 > y1) {
        // Set up lower side (1 => 2)
        dx12 = x2 - x1;
        dy12 = y2 - y1;
        u12 = x1 * dy12;

        // Draw lower triangle
        for(int y=y1; y<=y2; y++) {
            int16_t xStart = u12 / dy12;
            int16_t xEnd = u02 / dy02;
            if(xEnd > xStart) {
                if(y >= 0 && y < 480) {
                    uint16_t width = xEnd - xStart + 1;
                    cropToViewSize(&xStart, &width, 320);
                    drawHorizontalLine(xStart, y, width, color);
                }
            } else {
                if(y >= 0 && y < 480) {
                    uint16_t width = xStart - xEnd + 1;
                    cropToViewSize(&xEnd, &width, 320);
                    drawHorizontalLine(xEnd, y, width, color);
                }
            }

            // Next line
            u02 = u02 + dx02;
            u12 = u12 + dx12;
        }
    } else {
        // y1 == y2
        int16_t xStart = x1;
        int16_t xEnd = x2;
        if(xEnd > xStart) {
            if(y1 >= 0 && y1 < 480) {
                uint16_t width = xEnd - xStart + 1;
                cropToViewSize(&xStart, &width, 320);
                drawHorizontalLine(xStart, y1, width, color);
            }
        } else {
            if(y1 >= 0 && y1 < 480) {
                uint16_t width = xStart - xEnd + 1;
                cropToViewSize(&xEnd, &width, 320);
                drawHorizontalLine(xEnd, y1, width, color);
            }
        }
    }
}

Circles

To draw a circle we use a variation of the Bresenham’s algorithm. You can find a well-done explanation here: A Fast Bresenham Type Algorithm For Drawing Circles.

The “filled” version is almost identical but, as we saw earlier, the circle is filled using horizontal lines.

Add these declarations in the GFX class:

void drawCircle(int16_t x, int16_t y, uint16_t radius, uint8_t color);
void drawFilledCircle(int16_t x, int16_t y, uint16_t radius, uint8_t color);

and add this code in GFX.cpp:

void GFX::drawCircle(int16_t x, int16_t y, uint16_t radius, uint8_t color) {
    int16_t px = radius;
    int16_t py = 0;
    int16_t dx = 1 - 2 * radius;
    int16_t dy = 1;
    int16_t err = 0;

    while(px >= py) {
        if(ONSCREEN(x + px, y + py)) drawPixel(x + px, y + py, color);
        if(ONSCREEN(x + py, y + px)) drawPixel(x + py, y + px, color);
        if(ONSCREEN(x - py, y + px)) drawPixel(x - py, y + px, color);
        if(ONSCREEN(x - px, y + py)) drawPixel(x - px, y + py, color);
        if(ONSCREEN(x - px, y - py)) drawPixel(x - px, y - py, color);
        if(ONSCREEN(x - py, y - px)) drawPixel(x - py, y - px, color);
        if(ONSCREEN(x + py, y - px)) drawPixel(x + py, y - px, color);
        if(ONSCREEN(x + px, y - py)) drawPixel(x + px, y - py, color);

        py++;
        err += dy;
        dy += 2;
        if(2 * err + dx > 0) {
            px--;
            err += dx;
            dx += 2;
        }
    }
}

void GFX::drawFilledCircle(int16_t x, int16_t y, uint16_t radius, uint8_t color) {
    int16_t px = radius;
    int16_t py = 0;
    int16_t dx = 1 - 2 * radius;
    int16_t dy = 1;
    int16_t err = 0;

    while(px >= py) {
        int16_t xStart, yStart;
        uint16_t width;
        
        xStart = x - px; yStart = y + py; width = 2 * px + 1;
        if(yStart >= 0 && yStart < 480) {
            cropToViewSize(&xStart, &width, 320);
            drawHorizontalLine(xStart, yStart, width, color);
        }
        xStart = x - py; yStart = y + px; width = 2 * py + 1;
        if(yStart >= 0 && yStart < 480) {
            cropToViewSize(&xStart, &width, 320);
            drawHorizontalLine(xStart, yStart, width, color);
        }
        xStart = x - px; yStart = y - py; width = 2 * px + 1;
        if(yStart >= 0 && yStart < 480) {
            cropToViewSize(&xStart, &width, 320);
            drawHorizontalLine(xStart, yStart, width, color);
        }
        xStart = x - py; yStart = y - px; width = 2 * py + 1;
        if(yStart >= 0 && yStart < 480) {
            cropToViewSize(&xStart, &width, 320);
            drawHorizontalLine(xStart, yStart, width, color);
        }

        py++;
        err += dy;
        dy += 2;
        if(2 * err + dx > 0) {
            px--;
            err += dx;
            dx += 2;
        }
    }
}

 

Filling the screen

The last function of this post is not related to the drawing of geometric shapes, but it’s a function that we will use often, starting from the example we will see on the next page.

Add this declaration in the GFX class:

void fillScreen(uint8_t color);

and this code to GFX.cpp:

void GFX::fillScreen(uint8_t color) {
    memset(screenBuffer[0], color, 81920);
    memset(screenBuffer[1], color, 71680);
}

The last operation we performed during the initialization of the screen in the begin function was to fill the screen with black. Now we can replace that code with the function we just created.

In GFX.cpp, at the end of the begin function replace this code:

memset(screenBuffer[0], 15, 81920);
memset(screenBuffer[1], 15, 71680);

with:

fillScreen(15);

Let’s have fun with geometric shapes!

With this example program we will demonstrate that even using a single geometric shape (in this case the circle) it is possible to realize some very nice effects! Our goal is to show on the screen a countryside landscape, with hills in bloom and a beautiful cloudy sky.

Let’s create a new project and insert this code to draw the sky and the hills in bloom:

/* main.cpp or your_project_name.ino file */

#include <Arduino.h>
#include "GFX.h"

GFX gfx;

void setup() {
    // Draw sky, hills and flowers
    gfx.begin();
    gfx.fillScreen(7);
    gfx.drawFilledCircle(260, 70, 15, 1);
    gfx.drawFilledCircle(80, 480, 240, 8);
    for(int i=0; i<200; i++) {
        double radius = random(135, 235);
        double theta = random(225, 315);
        double x = radius * cos(theta / 180 * 3.14);
        double y = radius * sin(theta / 180 * 3.14);
        gfx.drawFilledCircle(80+x, 480+y, 1, 1 + random(2));
    }
    gfx.drawFilledCircle(200, 700, 400, 9);
    for(int i=0; i<400; i++) {
        double radius = random(200, 395);
        double theta = random(190, 315);
        double x = radius * cos(theta / 180 * 3.14);
        double y = radius * sin(theta / 180 * 3.14);
        gfx.drawCircle(200+x, 700+y, 1, 2 + random(3));
    }
}

void loop() {
    // Update screen
    gfx.update();
}

Upload the firmare. Somethink like this will appear:

The real interesting part of this project is the creation of animated clouds. We will draw the clouds as a set of overlapping circles. We will use 3 shades of gray to give the illusion of depth.

Since the goal is to animate these clouds, we create two data structures that allow us to keep track of the position of the clouds and of the circles that make them. We also define two constants that tell us how many clouds there are and how many circles each cloud is made of:

#define NUMBER_OF_CLOUDS            30
#define NUMBER_OF_CLOUD_COMPONENTS  30

// Clouds data structures
struct CloudComponent {
    float size;
    float x;
    float y;
    float t;
};
struct Cloud {
    float x;
    float y;
    float z;
    CloudComponent component[NUMBER_OF_CLOUD_COMPONENTS];
};

Immediately under the declaration of the graphics library, declare the clouds and the 3 colors used to draw them:

uint8_t cloudColors[3] = { 13, 12, 0 };
Cloud clouds[NUMBER_OF_CLOUDS];

In the setup function, we randomly generate the clouds:

// Random generations of clouds
for(int i=0; i<NUMBER_OF_CLOUDS; i++) {
    clouds[i].x = random(-2500, 2500);
    clouds[i].y = random(-120, 120);
    clouds[i].z = 1.0 + (float)(NUMBER_OF_CLOUDS - i) / 15;
    for(int j=0; j<NUMBER_OF_CLOUD_COMPONENTS; j++) {
        clouds[i].component[j].size = (random(10) + 15);
        clouds[i].component[j].x = random(-50, 50);
        clouds[i].component[j].y = random(-12, 12);
        clouds[i].component[j].t = (float)random(360);
    }
}

Now, in the loop function, add this code to draw the clouds:

// Draw the clouds
for(int i=0; i<NUMBER_OF_CLOUDS; i++) {
    for(int j=0; j<=2; j++) {
        for(int k=0; k<NUMBER_OF_CLOUD_COMPONENTS; k++) {
            gfx.drawFilledCircle(
                160 + (clouds[i].x + clouds[i].component[k].x + 5*j) / clouds[i].z,
                95 + (clouds[i].y + clouds[i].component[k].y - 3*j) / clouds[i].z,
                (clouds[i].component[k].size - 4.5*j) / clouds[i].z,
                cloudColors[j]
            );
        }
    }
}

Upload the firmware; something like this should appear:

It could happen that all clouds are off the screen, in which case reset the feather board until something appears.

To animate the clouds, we need a variable that keeps track of the passage of time. Add the following declaration:

unsigned long lastUpdate;

At the end of the setup function, add this code:

// Start counting the time
lastUpdate = micros();

The speed of the clouds must be independent from the frame rate, so at the beginning of the loop function we add this code which calculates how much time has elapsed from the previous frame:

// Calculate frame delta time
unsigned long now = micros();
float deltaTime = (float)(now - lastUpdate) / 1000000;
lastUpdate = now;

Finally, we must update the position of the clouds. In the loop function, update the code to draw the clouds:

// Clear the sky to update clouds
gfx.drawFilledRectangle(0, 0, 320, 240, 7);

// Redraw the sun
gfx.drawFilledCircle(260, 70, 15, 1);

// Update and redraw clouds
for(int i=0; i<NUMBER_OF_CLOUDS; i++) {
    clouds[i].x = (clouds[i].x + 20 * deltaTime);
    if(clouds[i].x > 2500)
        clouds[i].x -= 5000;
    
    for(int j=0; j<=2; j++) {
        for(int k=0; k<NUMBER_OF_CLOUD_COMPONENTS; k++) {
            gfx.drawFilledCircle(
                160 + (clouds[i].x + clouds[i].component[k].x + 5*j) / clouds[i].z,
                95 + (clouds[i].y + clouds[i].component[k].y - 3*j + 20*sin(clouds[i].component[k].t + (double)now/10000000)) / clouds[i].z,
                (clouds[i].component[k].size - 4.5*j) / clouds[i].z,
                cloudColors[j]
            );
        }
    }
}

Upload the firmware. Now the clouds are moving!

You can find the complete code for this post here.

 

In the next post…

In the next post we will start drawing sprites!


0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

By continuing to browse this Website, you consent to the use of cookies. More information

This Website uses:

By continuing to browse this Website without changing the cookies settings of your browser or by clicking "Accept" below, you consent to the use of cookies.

Close