Raspberry Pi and drawing with 8 bit page – part three

More drawing! Hurray! Our endless efforts for displaying anything anywhere bring the results 😀
What now?
The rectangle, the circle, the arc and as a bonus filled rectangle.

part one
part two

Code @ GitGub

Rectangle

To draw a rectangle, we need to draw four lines. So this one is easy, replace stub with:

    def draw_rect(self, x1, y1, x2, y2):
        """draw a rectangle"""
        self.draw_line(x1, y1, x2, y1)
        self.draw_line(x1, y2, x2, y2)
        self.draw_line(x1, y1, x1, y2)
        self.draw_line(x2, y1, x2, y2)

And add code to test script:

lcd_nju.draw_rect(10, 10, 40, 30)
lcd_oled.draw_rect(10, 10, 40, 40)

Now look closely at the picture:

Yes, we have a bug. Lines are not as they should be.

When you look at the line_drawing you will see lines:

steps = [height-2]

and

steps = [width-1]

Why we subtract 2 and 1? No idea 🙂 It is something that left from checks. We need to leave only height/width.
This looks much better:

Circle

Like with the lines we already have written this function for TFT. We are using a midpoint circle algorithm.
Let’s copy the code:

    def draw_circle(self, x, y, radius):
        """draw a circle"""
        err = 0
        offset_x = radius
        offset_y = 0
        while offset_x >= offset_y:
            self.draw_pixel(x + offset_x, y + offset_y)
            self.draw_pixel(x + offset_y, y + offset_x)
            self.draw_pixel(x - offset_y, y + offset_x)
            self.draw_pixel(x - offset_x, y + offset_y)
            self.draw_pixel(x - offset_x, y - offset_y)
            self.draw_pixel(x - offset_y, y - offset_x)
            self.draw_pixel(x + offset_y, y - offset_x)
            self.draw_pixel(x + offset_x, y - offset_y)
            if err <= 0:
                offset_y += 1
                err += 2*offset_y + 1
            else:
                offset_x -= 1
                err -= 2*offset_x + 1

And check how it works:

lcd_nju.draw_circle(60, 15, 15)
lcd_oled.draw_circle(60, 32, 31)

And result:

Yey. No changes and it is ok!.

Arc

If the circle was ok, let’s try the same with an arc, copy and paste the code:

    def draw_arc(self, x, y, radius, start, end):
        """draw an arc"""
        start = start * math.pi / 180
        end = end * math.pi / 180

        err = 0
        offset_x = radius
        offset_y = 0
        while offset_x >= offset_y:
            if start <= math.atan2(offset_y, offset_x) <= end:
                self.draw_pixel(x + offset_x, y + offset_y)
            if start <= math.atan2(offset_x, offset_y) <= end:
                self.draw_pixel(x + offset_y, y + offset_x)
            if start <= math.atan2(offset_x, -offset_y) <= end:
                self.draw_pixel(x - offset_y, y + offset_x)
            if start <= math.atan2(offset_y, -offset_x) <= end:
                self.draw_pixel(x - offset_x, y + offset_y)

            if start <= math.atan2(-offset_y, -offset_x) + 2*math.pi <= end:
                self.draw_pixel(x - offset_x, y - offset_y)
            if start <= math.atan2(-offset_x, -offset_y) + 2*math.pi <= end:
                self.draw_pixel(x - offset_y, y - offset_x)
            if start <= math.atan2(-offset_x, offset_y) + 2*math.pi <= end:
                self.draw_pixel(x + offset_y, y - offset_x)
            if start <= math.atan2(-offset_y, offset_x) + 2*math.pi <= end:
                self.draw_pixel(x + offset_x, y - offset_y)

            if err <= 0:
                offset_y += 1
                err += 2*offset_y + 1
            else:
                offset_x -= 1
                err -= 2*offset_x + 1

We can use this function and see the result:

lcd_nju.draw_circle(60, 15, 15)
lcd_nju.draw_arc(60, 15, 10, 45, 135)

lcd_oled.draw_circle(60, 32, 31)
lcd_oled.draw_arc(60, 32, 20, 45, 135)

It is so nice that everything goes quickly and with the positive result.

Filled rectangle

We sill have one stub left. So let’s implement it. The simplest way would be to write pixel at each position. But maybe we can improve it a little?
We may try and operate on the page. It is more complicated but is much more efficient.
How it should go?
Lets imagine that we want to fill area: (2, 2) -> (42, 29).
If we look from the page perspective then start page is 0 and bit is 2 (because 2 // 8 => 0 and 2 mod 8 = 2).
Now we have an 8-bit page and we know that we need to fill ‘1’ from 3’rd bit to achieve: 0b1111 1100.
The last page is 3 and bit is 5. Here we need to achieve 0b0011 1111. Everything between pages is 0xFF.

Sounds complicated? Let’s look at diagrams, one column (first page, (…) middle pages, and last page):

0         0
0         0
0         1
0         1
0         1
0         1
0         1
0         1
(...)     0xFF
0         1        
0         1
0         1
0         1
0         1
0         1
0         0
0         0

This is for y1 = 2 and y2 = 29. Hopefully, this shows what I have in mind.
Back to code and Python. Before we do any calculations we need to normalise coordinates, so we have a top-left and bottom-right point. Then we can calculate pages:

    def fill_rect(self, x1, y1, x2, y2):
        """draw a filled rectangle"""
        if y2 < y1:
            y1, y2 = y2, y1
        if x2 < x1:
            x1, x2 = x2, x1
        start_page = y1 // 8
        start_bit = y1 % 8
        end_page = y2 // 8
        end_bit = y2 % 8
        rows = []
        first_page = int(('0' * start_bit).rjust(8, '1'), 2)
        last_page = int('1' * (end_bit+1), 2)

I had the problem with bitwise operations so I used strings… sorry 😀 But it was much easier to visualise and recalculate.
There are two cases. One, when the start and the end page are different. Second when they are the same. With second case we need to just merge two results.

        if start_page != end_page:
            rows.append(first_page)
            for _ in range(end_page - start_page - 1):
                rows.append(255)
            rows.append(last_page)
        else:
            rows.append(first_page & last_page)

Calculated page values we store in the array. Why? Because it is easier to merge data with buffer:

        page = start_page
        for v in rows:
            for x in range(x2-x1+1):
                self.buffer[x1+x][page] |= v
            page += 1

The code could be easier with pixel drawing but this algorithm is much, much faster.
With this function we can draw a filled rectangle:

lcd_nju.fill_rect(2, 2, 42, 29)

lcd_nju.fill_rect(119, 2, 109, 12)
lcd_nju.fill_rect(119, 17, 109, 19)

Smily

As a proof that our code works on different LCDs we will create a smiling face:

lcd_nju.draw_circle(60, 15, 15)
lcd_nju.draw_circle(53, 10, 3)
lcd_nju.draw_circle(67, 10, 3)
lcd_nju.draw_arc(60, 15, 10, 45, 135)
lcd_nju.draw_line(60, 12, 57, 17)
lcd_nju.draw_line(60, 12, 63, 17)
lcd_nju.draw_arc(60, 15, 3, 45, 135)

lcd_oled.draw_circle(60, 32, 31)
lcd_oled.draw_circle(48, 22, 7)
lcd_oled.draw_circle(72, 22, 7)
lcd_oled.draw_arc(60, 32, 20, 45, 135)
lcd_oled.draw_line(60, 27, 56, 38)
lcd_oled.draw_line(60, 27, 64, 38)
lcd_oled.draw_arc(60, 35, 5, 45, 135)

lcd_oled.flush(True)
lcd_nju.flush(True)

Summary

Without much work, we reimplemented drawing of the circle, the arc and the rectangle. With little more work, we created the fill_rect function.

Advertisements

3 comments

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s