Raspberry Pi, Python and TFT 2.4″ – drawing a line

We can draw pixels so let move further and write an algorithm for drawing a line. We could use loop and draw_pixel for it but this would be very inefficient. Remember that we operate in selected area so using pixel would first select a 1×1 area and then set a color. Two operations for each point in the line.

Part 1

GitHub

Line

Drawing a line is not as simple as it looks. I’m no graphics engineer so I’m gonna take the simplest approach.
We can use the area to our advantage. Lets look at two cases, horizontal line and vertical line. To draw it we can select the area and just write color data. Functions for it:

  def _draw_horizontal_line(self, x, y, length):
      """draw horizontal line"""
      self._set_area(x, y, x + length, y)
      for _ in itertools.repeat(None, length):
          self.data(self._color())

Why we are using itertools and nor range? Because I read that it is slightly faster.
A similar function for a vertical line:

  def _draw_vertical_line(self, x, y, length):
      """draw vertical line"""
      self._set_area(x, y, x, y + length)
      for _ in itertools.repeat(None, length):
          self.data(self._color())

Those functions have a flaw, they always draw from point to right side or bottom. But if we want to draw up? There is a solution, we will have one function to transform data and use private functions to draw.

But for now those functions are enough, we can display a grid, back to main.py with code:

s.color = {'R': 200, 'G': 200, 'B': 200}

for i in range(1, 11):
  s._draw_horizontal_line(0, i * 30, 239)

for i in range(1, 9):
  s._draw_vertical_line(i * 30, 0, 319)

And we have a nice grid 🙂
Time for a hard part, diagonal lines. If we look closely at them we may see that they are in fact group of vertical or horizontal lines. We can calculate the number of such lines and their length. It is very naive but it must be.
But again it is not so simple, we can draw only from left to right and from top to bottom.

def _draw_diagonal_line(self, x1, y1, x2, y2):
  """draw diagonal line"""
  width = abs(x2 - x1)
  height = abs(y2 - y1)
  if width > height:
      if x2 < x1: x1, x2 = x2, x1 y1, y2 = y2, y1 offset_y = 1 if y2 > y1 else -1
      offset_x = 1 if x2 > x1 else -1
      horizontal = True
      step = height
      length = width / step
      steps = [length for _ in range(0, step)]
      if step * length < width:
          steps.append(width - step * length)
  else:
      if y2 < y1: x1, x2 = x2, x1 y1, y2 = y2, y1 offset_y = 1 if y2 > y1 else -1
      offset_x = 1 if x2 > x1 else -1
      horizontal = False
      step = width
      length = height / step
      steps = [length for _ in range(0, step)]
      if step * length < height:
          steps.append(height - step * length)

Lots of things are happening there. We calculate a width, height and direction, horizontal or vertical. Direction defines if we draw horizontal small lines or vertical one. To keep correct vector we swap start point with end point if necessary.
Next, we calculate the length of each sub-line and add filler if necessary. This is the code:

  steps = [length for _ in range(0, step)]
  if step * length < width:
      steps.append(width - step * length)

I’m showing it because we need to upgrade this at the proper time. Why? Imagine that after setting sub-lines length for 5, we have a 12 appendix. It looks very bad (see the yellow line at the bottom):

Why?
We want to draw a line from (182,292) to (60,270). Its length is 122 and width 22. From our calculation step is 5 pixels. But 5*22 is less than line length 122. To compensate for it, we add an appendix that is 12 pixels. And it looks bad.

At this point we are ready to draw something:

  dy = 0
  dx = 0
  for idx, step in enumerate(steps):
      if horizontal:
          self._draw_horizontal_line(
              x1 + dx,
              y1 + (idx * offset_y),
              step
          )
          dx += step * offset_x
      else:
          self._draw_vertical_line(
              x1 + (idx * offset_x),
              y1 + dy,
             step
          )
          dy += step * offset_y

We iterate over steps and just draw them.

Lets see how all this works. New shiny main.py:

import RPi.GPIO as GPIO
from screen import TFT
GPIO.setmode(GPIO.BCM)


LED = 6
GPIO.setup(LED, GPIO.OUT)
GPIO.output(LED, 1)


s = TFT()
s.init()

for i in range(0, 11):
  s.draw_pixel(100 + i, 100)
  s.draw_pixel(160 + i, 100)


s.color = {'R': 200, 'G': 200, 'B': 200}

for i in range(1, 11):
  s._draw_horizontal_line(0, i * 30, 239)

for i in range(1, 9):
  s._draw_vertical_line(i * 30, 0, 319)


s.color = {'R': 255, 'G': 0, 'B': 0}
s._draw_diagonal_line(60, 60, 47, 202)
s.color = {'R': 0, 'G': 255, 'B': 0}
s._draw_diagonal_line(60, 60, 73, 202)

s.color = {'R': 0, 'G': 0, 'B': 255}
s._draw_diagonal_line(180, 240, 167, 88)
s.color = {'R': 255, 'G': 255, 'B': 0}
s._draw_diagonal_line(180, 240, 193, 88)


s.color = {'R': 255, 'G': 0, 'B': 0}
s._draw_diagonal_line(60, 50, 150, 57)
s.color = {'R': 0, 'G': 255, 'B': 0}
s._draw_diagonal_line(150, 43, 60, 50)

s.color = {'R': 0, 'G': 0, 'B': 255}
s._draw_diagonal_line(60, 270, 182, 241)
s.color = {'R': 255, 'G': 255, 'B': 0}
s._draw_diagonal_line(182, 292, 60, 270)

The effect is stunning:

Calculating steps

I’m talking about this part: (and similar part in vertical)

  steps = [length for _ in range(0, step)]
  if step * length < width:
      steps.append(width - step * length)

it looks bad and we will fix it. First extract function from code in both horizontal:

steps = self._calculate_steps(length, step, width)

and vertical calculations:

steps = self._calculate_steps(length, step, height)

I think the best way of keeping line smooth is to spread appendix over existing steps. So we won’t have the last step:)

   def _calculate_steps(self, length, step, required_length):
        """calculate lineparts - helper"""
        steps = [length for _ in range(0, step)]
        if step * length < required_length:
            for idx in range(0, required_length - step * length):
                steps[idx] += 1

        return steps

It looks very good:

Draw a line

We can now encapsulate our different drawing line functions into one:

    def draw_line(self, x1, y1, x2, y2):
        """draw a line"""
        if x1 == x2:
            self._draw_vertical_line(x1, min(y1, y2), abs(y2 - y1))
        elif y1 == y2:
            self._draw_horizontal_line(min(x1, x2), y1, abs(x2 - x1))
        else:
            self._draw_diagonal_line(x1, y1, x2, y2)

Draw rectangle

If we combine line power we can create a new function to draw a rectangle:

   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)

Summary

I never thought that drawing a line can be so difficult 🙂 But we made it! Another little step in endless struggle to display something somewhere 🙂

Advertisements

One comment

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