Class: Denko::Display::Canvas

Inherits:
Object
  • Object
show all
Includes:
Fonts
Defined in:
lib/denko/display/canvas.rb

Constant Summary

Constants included from Fonts

Fonts::FONT_6x8

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(columns, rows) ⇒ Canvas

Returns a new instance of Canvas.

Raises:

  • (ArgumentError)


8
9
10
11
12
13
14
15
16
17
18
# File 'lib/denko/display/canvas.rb', line 8

def initialize(columns, rows)
  raise ArgumentError, "bitmap height must be divisible by 8" unless (rows % 8 == 0)

  @columns     = columns
  @rows        = rows

  # Use a byte array for the framebuffer. Each byte is 8 pixels arranged vertically.
  # Each slice @columns long represents an area @columns wide * 8 pixels tall.
  @bytes       = @columns * (@rows / 8)
  @framebuffer = Array.new(@bytes) { 0x00 }
end

Instance Attribute Details

#columnsObject (readonly)

Returns the value of attribute columns.



6
7
8
# File 'lib/denko/display/canvas.rb', line 6

def columns
  @columns
end

#framebufferObject (readonly)

Returns the value of attribute framebuffer.



6
7
8
# File 'lib/denko/display/canvas.rb', line 6

def framebuffer
  @framebuffer
end

#rowsObject (readonly)

Returns the value of attribute rows.



6
7
8
# File 'lib/denko/display/canvas.rb', line 6

def rows
  @rows
end

Instance Method Details

#circle(x_center, y_center, radius, color = 1, filled = false) ⇒ Object



235
236
237
# File 'lib/denko/display/canvas.rb', line 235

def circle(x_center, y_center, radius, color=1, filled=false)
  ellipse(x_center, y_center, radius, radius, color, filled)
end

#clearObject



24
25
26
# File 'lib/denko/display/canvas.rb', line 24

def clear
  @framebuffer.fill(0x00)
end

#clear_pixel(x, y) ⇒ Object



40
41
42
43
44
# File 'lib/denko/display/canvas.rb', line 40

def clear_pixel(x, y)
  byte = ((y / 8) * @columns) + x
  bit  = y % 8
  @framebuffer[byte] &= ~(0b1 << bit)
end

#ellipse(x_center, y_center, a, b, color = 1, filled = false) ⇒ Object

Midpoint ellipse / circle based on Bresenham’s circle algorithm.



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/denko/display/canvas.rb', line 178

def ellipse(x_center, y_center, a, b, color=1, filled=false)
  # Start position
  x = -a
  y = 0

  # Precompute x and y increments for each step.
  x_increment = 2 * b * b
  y_increment = 2 * a * a

  # Start errors
  dx = (1 + (2 * x)) * b * b
  dy = x * x
  e1 = dx + dy
  e2 = dx

  # Since starting at max negative X, continue until x is 0.
  while (x <= 0)
    if filled
      fill_quadrants(x_center, y_center, x, y, color)
    else
      stroke_quadrants(x_center, y_center, x, y, color)
    end

    e2 = 2 * e1
    if (e2 >= dx)
      x  += 1
      dx += x_increment
      e1 += dx
    end
    if (e2 <= dy)
      y  += 1
      dy += y_increment
      e1 += dy
    end
  end

  # Continue if y hasn't reached the vertical size.
  while (y < b)
    y += 1
    pixel(x_center, y_center + y, color)
    pixel(x_center, y_center - y, color)
  end
end

#fillObject



20
21
22
# File 'lib/denko/display/canvas.rb', line 20

def fill
  @framebuffer.fill(0xFF)
end

#fill_quadrants(x_center, y_center, x, y, color) ⇒ Object



230
231
232
233
# File 'lib/denko/display/canvas.rb', line 230

def fill_quadrants(x_center, y_center, x, y, color)
  line(x_center - x, y_center + y, x_center + x, y_center + y, color)
  line(x_center - x, y_center - y, x_center + x, y_center - y, color)
end

#filled_circle(x_center, y_center, radius, color = 1) ⇒ Object



239
240
241
# File 'lib/denko/display/canvas.rb', line 239

def filled_circle(x_center, y_center, radius, color=1)
  ellipse(x_center, y_center, radius, radius, color, true)
end

#filled_polygon(points = [], color = 1) ⇒ Object

Filled polygon using horizontal ray casting + stroked polygon.



131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/denko/display/canvas.rb', line 131

def filled_polygon(points=[], color=1)
  # Get all the X and Y coordinates from the points as floats.
  coords_x = points.map { |point| point.first.to_f }
  coords_y = points.map { |point| point.last.to_f  }

  # Get Y bounds of the polygon to limit rows.
  y_min = coords_y.min.to_i
  y_max = coords_y.max.to_i

  # Cast horizontal ray on each row, storing nodes where it intersects polygon edges.
  (y_min..y_max).each do |y|
    nodes = []
    i = 0
    j = points.count - 1

    while (i < points.count) do
      if (coords_y[i] < y && coords_y[j] >= y || coords_y[j] < y && coords_y[i] >= y)
        nodes << (coords_x[i] + (y - coords_y[i]) / (coords_y[j] - coords_y[i]) *(coords_x[j] - coords_x[i])).round
      end
      j  = i
      i += 1
    end
    nodes = nodes.sort

    # Take pairs of nodes and fill between them. This automatically ignores the spaces
    # between even then odd nodes, which are outside the polygon.
    nodes.each_slice(2) do |pair|
      next if pair.length < 2
      line(pair.first, y,  pair.last, y, color)
    end
  end

  # Stroke the polygon anyway. Floating point math misses thin areas.
  polygon(points, color)
end

#filled_rectangle(x, y_start, width, height, color = 1) ⇒ Object

Draw a vertical line for every x value to get a filled rectangle.



105
106
107
108
109
110
111
# File 'lib/denko/display/canvas.rb', line 105

def filled_rectangle(x, y_start, width, height, color=1)
  y_end = y_start + height
  y_start, y_end = y_end, y_start if (y_end < y_start)
  (y_start..y_end).each do |y|
    line(x, y, x+width, y, color)
  end
end

#filled_triangle(x1, y1, x2, y2, x3, y3, color = 1) ⇒ Object

Filled triangle with 3 points as 6 flat args.



173
174
175
# File 'lib/denko/display/canvas.rb', line 173

def filled_triangle(x1, y1, x2, y2, x3, y3, color=1)
  filled_polygon([[x1,y1], [x2,y2], [x3,y3]], color)
end

#get_pixel(x, y) ⇒ Object



28
29
30
31
32
# File 'lib/denko/display/canvas.rb', line 28

def get_pixel(x, y)
  byte = ((y / 8) * @columns) + x
  bit  = y % 8
  (@framebuffer[byte] >> bit) & 0b00000001
end

#line(x1, y1, x2, y2, color = 1) ⇒ Object

Draw a line based on Bresenham’s line algorithm.



51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
# File 'lib/denko/display/canvas.rb', line 51

def line(x1, y1, x2, y2, color=1)
  # Deltas in each axis.
  dy = y2 - y1
  dx = x2 - x1

  # Catch vertical lines to avoid division by 0.
  if (dx == 0)
    # Ensure y1 < y2.
    y1, y2 = y2, y1 if (y2 < y1)
    (y1..y2).each do |y|
      pixel(x1, y, color)
    end
    return
  end

  gradient = dy.to_f / dx

  # Gradient magnitude <= 45 degrees: find y for each x.
  if (gradient >= -1) && (gradient <= 1)
    # Ensure x1 < x2.
    x1, y1, x2, y2 = x2, y2, x1, y1 if (x2 < x1)

    # When x increments, add gradient to y.
    y = y1
    y_step = gradient
    (x1..x2).each do |x|
      pixel(x, y.round, color)
      y = y + y_step
    end

  # Gradient magnitude > 45 degrees: find x for each y.
  else
    # Ensure y1 < y2.
    x1, y1, x2, y2 = x2, y2, x1, y1 if (y2 < y1)

    # When y increments, add inverse of gradient to x.
    x = x1
    x_step = 1 / gradient
    (y1..y2).each do |y|
      pixel(x.round, y, color)
      x = x + x_step
    end
  end
end

#path(points = [], color = 1) ⇒ Object

Open ended path



114
115
116
117
118
119
120
121
122
# File 'lib/denko/display/canvas.rb', line 114

def path(points=[], color=1)
  return unless points
  start = points[0]
  (1..points.length-1).each do |i|
    finish = points[i]
    line(start[0], start[1], finish[0], finish[1])
    start = finish
  end
end

#pixel(x, y, state = 0) ⇒ Object



46
47
48
# File 'lib/denko/display/canvas.rb', line 46

def pixel(x, y, state=0)
  (state == 0) ? clear_pixel(x, y) : set_pixel(x, y)
end

#polygon(points = [], color = 1) ⇒ Object

Close paths by repeating the start value at the end.



125
126
127
128
# File 'lib/denko/display/canvas.rb', line 125

def polygon(points=[], color=1)
  points << points[0]
  path(points)
end


251
252
253
254
255
# File 'lib/denko/display/canvas.rb', line 251

def print(str)
  str.to_s.split("").each do |char|
    print_char(char)
  end
end


257
258
259
260
261
262
263
264
# File 'lib/denko/display/canvas.rb', line 257

def print_char(char)
  # 0th character in font is SPACE. Offset and limit to printable chars.
  index = char.ord - 32
  index = 0 if (index < 0 || index > 94)
  char_map = FONT_6x8[index]

  raw_char(char_map)
end

#raw_char(byte_array) ⇒ Object



266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/denko/display/canvas.rb', line 266

def raw_char(byte_array)
  # Get the starting byte index.
  page = text_cursor[1] / 8
  byte_index = (@columns * page) + text_cursor[0]

  # Replace those bytes in the framebuffer with the character.
  byte_array.each do |byte| 
    @framebuffer[byte_index] = byte
    byte_index += 1
  end

  # Increment the text cursor.
  self.text_cursor[0] += byte_array.length
end

#rectangle(x, y, width, height, color = 1) ⇒ Object

Rectangles and squares as a combination of lines.



97
98
99
100
101
102
# File 'lib/denko/display/canvas.rb', line 97

def rectangle(x, y, width, height, color=1)
  line(x,       y,        x+width, y,        color)
  line(x+width, y,        x+width, y+height, color)
  line(x+width, y+height, x,       y+height, color)
  line(x,       y+height, x,       y,        color)
end

#set_pixel(x, y) ⇒ Object



34
35
36
37
38
# File 'lib/denko/display/canvas.rb', line 34

def set_pixel(x, y)
  byte = ((y / 8) * @columns) + x
  bit  = y % 8
  @framebuffer[byte] |= (0b1 << bit)
end

#stroke_quadrants(x_center, y_center, x, y, color) ⇒ Object



222
223
224
225
226
227
228
# File 'lib/denko/display/canvas.rb', line 222

def stroke_quadrants(x_center, y_center, x, y, color)
  # Quadrants in order as if y-axis is reversed and going counter-clockwise from +ve X.
  pixel(x_center - x, y_center - y, color)
  pixel(x_center + x, y_center - y, color)
  pixel(x_center + x, y_center + y, color)
  pixel(x_center - x, y_center + y, color)
end

#text_cursorObject



247
248
249
# File 'lib/denko/display/canvas.rb', line 247

def text_cursor
  @text_cursor ||= [0, 7]
end

#text_cursor=(array = []) ⇒ Object



243
244
245
# File 'lib/denko/display/canvas.rb', line 243

def text_cursor=(array=[])
  @text_cursor = array
end

#triangle(x1, y1, x2, y2, x3, y3, color = 1) ⇒ Object

Triangle with 3 points as 6 flat args.



168
169
170
# File 'lib/denko/display/canvas.rb', line 168

def triangle(x1, y1, x2, y2, x3, y3, color=1)
  polygon([[x1,y1], [x2,y2], [x3,y3]], color)
end