Coding

Shape drawing on the SSD1351 display

Based on the previous few posts, we now have lots of things we can do with the SSD1351 display. One additional thing we can play with this time is drawing shapes. There are a few tricks to doing this quickly. As with previous posts, this will work on other displays with minimal effort to convert it.

Placing Pixels

The first function that is going to be useful here is placing single pixels down on the screen. This is a rather simple routine that works out where in the 2-byte-per-pixel buffer the pixel is and copies the colour to it:

inline void ssd1351_draw_pixel(buffer_type type, uint16_t colour, point_t pos)
{
    uint8_t *buffer = get_buffer(type);

    memcpy(buffer + (pos.y*WIDTH*2) + (pos.x*2), &colour, 2);
}

Not much more to add here, this should be pretty self-explanatory based on our work so far, but it is a useful function for everything else coming up.

The following photo is part of an animation that just cycles through colours for each pixel with a diagonal colour shift, whilst also using text rendering. I would show a video here, but I get a terrible rolling-shutter effect on my camera with this demo and I haven’t been able to resolve it.

The code for this demo is:

static void colour_pattern()
{
    ssd1351_clear();
    point_t pos = {0, 0};
    point_t text_pos;
    uint8_t *buffer;
    for (int j = 0; j <= HEIGHT * 2; j+=4)
    {
        for (int i = 0; i < WIDTH * HEIGHT; i++)
        {
            uint16_t colour = ssd1351_make_colour((j + pos.x * 2), (j + pos.y * 2), (255 - (j + pos.x * 2)));
            ssd1351_draw_pixel(BUFFER_TYPE_DISPLAY, colour, pos);
            pos.x++;
            if (pos.x == 128)
            {
                pos.x = 0;
                pos.y++;
            }
        }
        text_pos = (point_t){20, 0};
        ssd1351_write_text(BUFFER_TYPE_DISPLAY, "LinuxJedi's", text_pos, 0, FONT_TOPAZ, true, true);
        text_pos = (point_t){4, 10};

        ssd1351_write_text(BUFFER_TYPE_DISPLAY, "OLED Pico Badge", text_pos, 0, FONT_TOPAZ, true, true);
        ssd1351_refresh();
        pos.x = 0;
        pos.y = 0;
    }
}

Drawing Lines

Now that we can put pixels on the screen at given coordinates, we can start drawing things. First of is lines. These again will become quite useful for subsequent functions. You would think this would be a simple case of drawing a line of pixels from A to B, but it gets a bit complicated when you draw at an angle. You can often end up with half or quarter pixels. So a simple way of handling this is when you get over 50% of a half pixel you call that a full pixel.

void ssd1351_draw_line(buffer_type type, uint16_t colour, point_t start, point_t end)
{
    bool steep = false;
    if (abs(start.x - end.x) < abs(start.y - end.y))
    {
        uint8_t tmp;
        tmp = start.y;
        start.y = start.x;
        start.x = tmp;

        tmp = end.y;
        end.y = end.x;
        end.x = tmp;
        steep = true;
    }
    if (start.x > end.x)
    {
        uint8_t tmp = end.x;
        end.x = start.x;
        start.x = tmp;

        tmp = end.y;
        end.y = start.y;
        start.y = tmp;
    }

    int dx = end.x - start.x;
    int dy = end.y - start.y;
    float derror = fabs(dy / (float)dx);
    float error = 0;
    point_t pos = {0, start.y};

    for (pos.x = start.x; pos.x <= end.x; pos.x++)
    {
        if (steep)
        {
            point_t tmp = {pos.y, pos.x};
            ssd1351_draw_pixel(type, colour, tmp);
        }
        else
        {
            ssd1351_draw_pixel(type, colour, pos);
        }
        error += derror;
        if (error > 0.5)
        {
            pos.y += (end.y > start.y ? 1 : -1);
            error -= 1;
        }
    }
}

We do a trick here if the line is steeper than it is long, we switch the X and Y temporarily so it is the X we are doing the half-pixel calculations on. We switch them back again at drawing time. Then it is just a case of drawing each pixel for the line.

Drawing Rectangles

Now that we have line primitives, rectangles are quite easy, just four lines. For filled rectangles, we have all the pixel lines in the middle drawn as well.

void ssd1351_draw_rectangle(buffer_type type, uint16_t colour, point_t start, point_t end, bool filled)
{
    if (end.x < start.x)
    {
        uint8_t tmp = end.x;
        end.x = start.x;
        start.x = tmp;
    }

    if (end.y < start.y)
    {
        uint8_t tmp = end.y;
        end.y = start.y;
        start.y = tmp;
    }

    ssd1351_draw_line(type, colour, (point_t){start.x, start.y}, (point_t){end.x, start.y});
    ssd1351_draw_line(type, colour, (point_t){start.x, end.y}, (point_t){end.x, end.y});
    ssd1351_draw_line(type, colour, (point_t){start.x, start.y}, (point_t){start.x, end.y});
    ssd1351_draw_line(type, colour, (point_t){end.x, start.y}, (point_t){end.x, end.y});
    if (filled)
    {
        for (uint8_t x = start.x + 1; x < end.x; x++)
        {
            ssd1351_draw_line(type, colour, (point_t){x, start.y}, (point_t){x, end.y});
        }
    }
}

We could also just detect if we are doing filled at the beginning and just draw horizontal lines, this will give the same effect.

Drawing Triangles

Again, drawing an outline of a triangle is very simple, it is just three lines. When we want filled triangles things get a bit tricky. There are a few possible techniques available here, the one I went with is based on this software rasterisation trick.

What we end up doing is breaking the triangle up into two, one with a flat bottom and one with a flat top. We can then render scanlines for the triangles relatively easily.

So, here is the setup:

static void swap_coords(point_t *p1, point_t *p2)
{
    point_t tmp = *p1;
    *p1 = *p2;
    *p2 = tmp;
}

void ssd1351_draw_triangle(buffer_type type, uint16_t colour, point_t p1, point_t p2, point_t p3, bool filled)
{
    if (!filled)
    {
        ssd1351_draw_line(type, colour, p1, p2);
        ssd1351_draw_line(type, colour, p1, p3);
        ssd1351_draw_line(type, colour, p2, p3);
        return;
    }

    // Sort so p1.y is the lowest
    if (p1.y > p2.y)
    {
        swap_coords(&p2, &p1);
    }
    if (p2.y > p3.y)
    {
        swap_coords(&p2, &p3);
    }
    if (p1.y > p2.y)
    {
        swap_coords(&p2, &p1);
    }

    if (p2.y == p3.y)
    {
        ssd1351_fill_bottom_flat_triangle(type, colour, p1, p2, p3);
    }
    else if (p1.y == p2.y)
    {
        ssd1351_fill_top_flat_triangle(type, colour, p1, p2, p3);
    }
    else
    {
        point_t p4;
        p4.x = (uint8_t)(p1.x + ((float)(p2.y - p1.y) / (float)(p3.y - p1.y)) * (p3.x - p1.x));
        p4.y = p2.y;
        ssd1351_fill_bottom_flat_triangle(type, colour, p1, p2, p4);
        ssd1351_fill_top_flat_triangle(type, colour, p2, p4, p3);
    }
}

This basically short-cuts everything if we aren’t filling and draws the 3 lines. But if we are filling it does the following:

  1. Sort all the coordinates based on the Y axis for each, smallest to largest.
  2. If the Y axis for the second and third coordinates are the same, then this is a flat bottom triangle, so draw that
  3. If the Y axis for the first and second coordinates are the same, then this is a flat top triangle, so draw that
  4. Finally is the case where it is neither a flat top nor bottom triangle already. In this case, we add another point on the triangle which splits it into two, a flat top and bottom, we can then render the top and bottom half.

All we need now are the top and bottom rendering functions:

static void ssd1351_fill_bottom_flat_triangle(buffer_type type, uint16_t colour, point_t p1, point_t p2, point_t p3)
{
    float invslope1 = ((float)p2.x - (float)p1.x) / ((float)p2.y - (float)p1.y);
    float invslope2 = ((float)p3.x - (float)p1.x) / ((float)p3.y - (float)p1.y);

    float curx1 = p1.x;
    float curx2 = p1.x;

    for (int scanline = p1.y; scanline <= p2.y; scanline++)
    {
        ssd1351_draw_line(type, colour, (point_t){(uint8_t)curx1, scanline}, (point_t){(uint8_t)curx2, scanline});
        curx1 += invslope1;
        curx2 += invslope2;
    }
}

static void ssd1351_fill_top_flat_triangle(buffer_type type, uint16_t colour, point_t p1, point_t p2, point_t p3)
{
    float invslope1 = ((float)p3.x - (float)p1.x) / ((float)p3.y - (float)p1.y);
    float invslope2 = ((float)p3.x - (float)p2.x) / ((float)p3.y - (float)p2.y);

    float curx1 = p3.x;
    float curx2 = p3.x;

    for (int scanline = p3.y; scanline > p1.y; scanline--)
    {
        ssd1351_draw_line(type, colour, (point_t){(uint8_t)curx1, scanline}, (point_t){(uint8_t)curx2, scanline});
        curx1 -= invslope1;
        curx2 -= invslope2;
    }
}

Essentially we calculate the left and right slopes of the triangle and for each line increment or decrement the start and end X axis by this for every Y axis increment, then draw the line.

Drawing Circles

There are a number of different algorithms you can use for this and I did start out with an extremely accurate algorithm using sine and cosine. For filled circles, it would basically draw smaller circles inside. Unfortunately, this ended up being very computationally heavy. At one point taking 2 seconds to render a filled circle with a radius of 30.

I then discovered an algorithm to use 8-way symmetry to draw the circle. Essentially if you know a point on a circle is {X, Y} from the centre, you also know there is a point {X, -Y}, {-X, Y} and {-X, -Y}. But also you can swap X and Y for these four and generate four more points of the circle. This is actually really great for filled circles because it means we can draw lines between the points. It is also incredibly fast.

The first function we need to do this is:

void ssd1351_draw_circle(buffer_type type, uint16_t colour, point_t pos, uint8_t r, bool filled)
{
    int p = (5 - r * 4) / 4;
    uint8_t yoff = r;
    uint8_t xoff = 0;
    while (xoff < yoff)
    {
        xoff++;
        if (p < 0)
        {
            p += 2*xoff+1;
        }
        else
        {
            yoff--;
            p += 2*(xoff-yoff)+1;
        }
        ssd1351_circle_points(type, colour, filled, pos, (point_t){xoff, yoff});
    }
    if (filled)
    {
        ssd1351_draw_line(type, colour, (point_t){pos.x + r, pos.y}, (point_t){pos.x - r, pos.y});
    }
}

How this works is detailed in the algorithm link above, but essentially it is a very clever way of getting 1/8th of the points of a circle’s edge without duplication and avoiding an expensive square root calculation with every step.

We just need the function that draws the points found:

static void ssd1351_circle_points(buffer_type type, uint16_t colour, bool filled, point_t c, point_t off)
{
    if (filled)
    {
        ssd1351_draw_line(type, colour, (point_t){c.x + off.x, c.y + off.y}, (point_t){c.x - off.x, c.y + off.y});
        ssd1351_draw_line(type, colour, (point_t){c.x + off.x, c.y - off.y}, (point_t){c.x - off.x, c.y - off.y});
        ssd1351_draw_line(type, colour, (point_t){c.x + off.y, c.y + off.x}, (point_t){c.x - off.y, c.y + off.x});
        ssd1351_draw_line(type, colour, (point_t){c.x + off.y, c.y - off.x}, (point_t){c.x - off.y, c.y - off.x});
        return;
    }

    if (off.x == 0)
    {
        ssd1351_draw_pixel(type, colour, (point_t){c.x, c.y + off.y});
        ssd1351_draw_pixel(type, colour, (point_t){c.x, c.y - off.y});
        ssd1351_draw_pixel(type, colour, (point_t){c.x + off.y, c.y});
        ssd1351_draw_pixel(type, colour, (point_t){c.x - off.y, c.y});
    }
    else if (off.x == off.y)
    {
        ssd1351_draw_pixel(type, colour, (point_t){c.x + off.x, c.y + off.y});
        ssd1351_draw_pixel(type, colour, (point_t){c.x - off.x, c.y + off.y});
        ssd1351_draw_pixel(type, colour, (point_t){c.x + off.x, c.y - off.y});
        ssd1351_draw_pixel(type, colour, (point_t){c.x - off.x, c.y - off.y});
    }
    else if (off.x < off.y)
    {
        ssd1351_draw_pixel(type, colour, (point_t){c.x + off.x, c.y + off.y});
        ssd1351_draw_pixel(type, colour, (point_t){c.x - off.x, c.y + off.y});
        ssd1351_draw_pixel(type, colour, (point_t){c.x + off.x, c.y - off.y});
        ssd1351_draw_pixel(type, colour, (point_t){c.x - off.x, c.y - off.y});
        ssd1351_draw_pixel(type, colour, (point_t){c.x + off.y, c.y + off.x});
        ssd1351_draw_pixel(type, colour, (point_t){c.x - off.y, c.y + off.x});
        ssd1351_draw_pixel(type, colour, (point_t){c.x + off.y, c.y - off.x});
        ssd1351_draw_pixel(type, colour, (point_t){c.x - off.y, c.y - off.x});
    }
}

This may look intimidating, but this is drawing horizontal lines for filled and points for unfilled circles. Using exactly what we said before, +/- X, +/-Y and then the same swapping X and Y.

End Result

As I was coding these up I created a test screen that included lines as well as filled and unfilled versions of all the objects. Which looked like this:

And the code to get that scene was simply this:

static void line_test()
{
    uint16_t colour = ssd1351_make_colour(0x7f, 0x7f, 0xff);
    ssd1351_clear();
    ssd1351_draw_line(BUFFER_TYPE_DISPLAY, colour, (point_t){0, 0}, (point_t){128, 128});
    ssd1351_draw_line(BUFFER_TYPE_DISPLAY, colour, (point_t){128, 0}, (point_t){0, 128});
    ssd1351_draw_line(BUFFER_TYPE_DISPLAY, colour, (point_t){64, 0}, (point_t){64, 128});
    ssd1351_draw_line(BUFFER_TYPE_DISPLAY, colour, (point_t){0, 32}, (point_t){128, 96});
    ssd1351_draw_line(BUFFER_TYPE_DISPLAY, colour, (point_t){0, 96}, (point_t){128, 32});
    colour = ssd1351_make_colour(0x00, 0xff, 0x00);
    ssd1351_draw_rectangle(BUFFER_TYPE_DISPLAY, colour, (point_t){10, 10}, (point_t){90, 40}, false);
    ssd1351_draw_rectangle(BUFFER_TYPE_DISPLAY, colour, (point_t){90, 90}, (point_t){110, 120}, true);
    colour = ssd1351_make_colour(0xff, 0x7f, 0x7f);
    ssd1351_draw_circle(BUFFER_TYPE_DISPLAY, colour, (point_t){64, 64}, 20, true);
    colour = ssd1351_make_colour(0xff, 0xff, 0x00);
    ssd1351_draw_triangle(BUFFER_TYPE_DISPLAY, colour, (point_t){20, 70}, (point_t){30, 85}, (point_t){40, 60}, false);
    ssd1351_draw_triangle(BUFFER_TYPE_DISPLAY, colour, (point_t){80, 40}, (point_t){90, 90}, (point_t){120, 60}, true);
}

This post concludes where I am at with my SSD1351 library so far. There are lots of enhancements and more features I can add over time. If I add anything useful I will write a further blog post.

LinuxJedi

View Comments

  • My only question here really, is why does pixel plotting use memcpy ? You could do this:

    ```
    inline void ssd1351_draw_pixel(buffer_type type, uint16_t colour, point_t pos)
    {
    uint16_t *buffer = get_buffer(type);
    *(buffer + (pos.y*WIDTH) + pos.x) = colour;
    }

    • Due to endianess it would need a byteswap instruction on colour too that way. I *think* memcpy() compiles to one instruction here but I haven't confirmed it. But I have tried both ways and not noticed any performance difference.

Share
Published by
LinuxJedi

Recent Posts

Working with the AVR Dx family

In recent years, Microchip has launched a new range of AVR chips. They appear to…

4 days ago

My WordPress Slack ban

A couple of days ago, I got banned from the WordPress community Slack. As this…

5 days ago

Issues I found during WordPress.com to .org migration

Whilst migrating from wordpress.com to an installation of the open source WordPress, I hit some…

1 week ago

Why I moved my blog

Unfortunately, there is a war going on within the WordPress community. To stay as far…

1 week ago

Four new Amiga products for September 2024

Since the June Amiga Expo, I have been developing some new Amiga related products. I…

3 weeks ago

Repairing an Amiga that caught on fire!

Karl at Retro32 likes to challenge me, and this time he had an interesting one.…

1 month ago