Class: Denko::Display::Canvas
- Inherits:
-
Object
- Object
- Denko::Display::Canvas
- Defined in:
- lib/denko/display/canvas.rb
Constant Summary collapse
- DEFAULT_TEXT_CURSOR =
[0, 7]
Instance Attribute Summary collapse
-
#colors ⇒ Object
readonly
Returns the value of attribute colors.
-
#columns ⇒ Object
readonly
Returns the value of attribute columns.
-
#current_color ⇒ Object
Returns the value of attribute current_color.
-
#font ⇒ Object
Returns the value of attribute font.
-
#font_scale ⇒ Object
Returns the value of attribute font_scale.
-
#framebuffer ⇒ Object
readonly
Returns the value of attribute framebuffer.
-
#framebuffers ⇒ Object
readonly
Returns the value of attribute framebuffers.
-
#rows ⇒ Object
readonly
Returns the value of attribute rows.
-
#x_max ⇒ Object
readonly
Returns the value of attribute x_max.
-
#y_max ⇒ Object
readonly
Returns the value of attribute y_max.
Instance Method Summary collapse
- #_draw_char(byte_array, x, y, width, scale, color = current_color) ⇒ Object
- #_ellipse(x_center, y_center, a, b, filled = false, color = current_color) ⇒ Object
-
#_get_pixel(x, y) ⇒ Object
PIXEL OPERATIONS.
-
#_line(x1, y1, x2, y2, color = current_color) ⇒ Object
LINE.
-
#_path(points, color = current_color) ⇒ Object
PATH, POLYGON & TRIANGLE.
- #_polygon(points, filled = false, color = current_color) ⇒ Object
-
#_rectangle(x1, y1, x2, y2, filled = false, color = current_color) ⇒ Object
RECTANGLE & SQUARE.
- #_set_pixel(x, y, color = current_color) ⇒ Object
- #_triangle(x1, y1, x2, y2, x3, y3, filled = false, color = current_color) ⇒ Object
- #calculate_bounds ⇒ Object
- #circle(x:, y:, r:, filled: false, color: current_color) ⇒ Object
- #clear ⇒ Object
- #clear_pixel(x:, y:) ⇒ Object
- #draw_char(char, color: current_color) ⇒ Object
- #ellipse(x:, y:, a:, b:, filled: false, color: current_color) ⇒ Object
- #fill ⇒ Object
- #get_pixel(x:, y:) ⇒ Object
-
#initialize(columns, rows, colors: 1) ⇒ Canvas
constructor
A new instance of Canvas.
- #line(x1:, y1:, x2:, y2:, color: current_color) ⇒ Object
- #path(points, color: current_color) ⇒ Object
- #polygon(points, filled: false, color: current_color) ⇒ Object
- #rectangle(x1: nil, y1: nil, x2: nil, y2: nil, x: nil, y: nil, w: nil, h: nil, filled: false, color: current_color) ⇒ Object
- #reflect(axis) ⇒ Object
- #reflect_x ⇒ Object
- #reflect_y ⇒ Object
-
#rotate(degrees = 180) ⇒ Object
TRANSFORMATION.
- #set_pixel(x:, y:, color: current_color) ⇒ Object
- #square(x:, y:, size:, filled: false, color: current_color) ⇒ Object
-
#text(str, color: current_color) ⇒ Object
BITMAP TEXT.
- #text_cursor ⇒ Object
- #text_cursor=(array = DEFAULT_TEXT_CURSOR) ⇒ Object
- #triangle(x1:, y1:, x2:, y2:, x3:, y3:, filled: false, color: current_color) ⇒ Object
Constructor Details
#initialize(columns, rows, colors: 1) ⇒ Canvas
Returns a new instance of Canvas.
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/denko/display/canvas.rb', line 6 def initialize(columns, rows, colors: 1) @columns = columns @rows = rows @rows = ((rows / 8.0).ceil * 8) if (rows % 8 != 0) # 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 setup. 1-bit framebuffer for each color. # Works for mono LCDs and OLEDs, or mono/multi-color e-paper. @colors = colors @framebuffers = [] @colors.times { @framebuffers << Array.new(@bytes) { 0x00 } } # Only first framebuffer is used for mono displays. @framebuffer = @framebuffers.first # Default drawing state self.font = Denko::Display::Font::BMP_6X8 @font_scale = 1 @current_color = 1 # Transformation state @swap_xy = false @invert_x = false @invert_y = false @rotation = 0 calculate_bounds end |
Instance Attribute Details
#colors ⇒ Object (readonly)
Returns the value of attribute colors.
4 5 6 |
# File 'lib/denko/display/canvas.rb', line 4 def colors @colors end |
#columns ⇒ Object (readonly)
Returns the value of attribute columns.
4 5 6 |
# File 'lib/denko/display/canvas.rb', line 4 def columns @columns end |
#current_color ⇒ Object
Returns the value of attribute current_color.
388 389 390 |
# File 'lib/denko/display/canvas.rb', line 388 def current_color @current_color end |
#font ⇒ Object
Returns the value of attribute font.
418 419 420 |
# File 'lib/denko/display/canvas.rb', line 418 def font @font end |
#font_scale ⇒ Object
Returns the value of attribute font_scale.
418 419 420 |
# File 'lib/denko/display/canvas.rb', line 418 def font_scale @font_scale end |
#framebuffer ⇒ Object (readonly)
Returns the value of attribute framebuffer.
4 5 6 |
# File 'lib/denko/display/canvas.rb', line 4 def framebuffer @framebuffer end |
#framebuffers ⇒ Object (readonly)
Returns the value of attribute framebuffers.
4 5 6 |
# File 'lib/denko/display/canvas.rb', line 4 def framebuffers @framebuffers end |
#rows ⇒ Object (readonly)
Returns the value of attribute rows.
4 5 6 |
# File 'lib/denko/display/canvas.rb', line 4 def rows @rows end |
#x_max ⇒ Object (readonly)
Returns the value of attribute x_max.
472 473 474 |
# File 'lib/denko/display/canvas.rb', line 472 def x_max @x_max end |
#y_max ⇒ Object (readonly)
Returns the value of attribute y_max.
472 473 474 |
# File 'lib/denko/display/canvas.rb', line 472 def y_max @y_max end |
Instance Method Details
#_draw_char(byte_array, x, y, width, scale, color = current_color) ⇒ Object
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 |
# File 'lib/denko/display/canvas.rb', line 362 def _draw_char(byte_array, x, y, width, scale, color=current_color) byte_array.each_slice(width) do |slice| slice.each_with_index do |byte, col_offset| 8.times do |bit| # Don't do anything if this bit isn't set in the font. if (((byte >> bit) & 0b1) == 1) scale.times do |x_offset| scale.times do |y_offset| _set_pixel(x + (col_offset * scale) + x_offset, y + (bit * scale) + y_offset, color) end end end end end y = y + (8 * scale) end end |
#_ellipse(x_center, y_center, a, b, filled = false, color = current_color) ⇒ Object
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 |
# File 'lib/denko/display/canvas.rb', line 279 def _ellipse(x_center, y_center, a, b, filled=false, color=current_color) # Midpoint ellipse / circle based on Bresenham's circle algorithm. # 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 left and right quadrants in single line, alternating +ve and -ve y. _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) else # Stroke quadrants in order as if y-axis is reversed and going counter-clockwise from +ve X. _set_pixel(x_center - x, y_center - y, color) _set_pixel(x_center + x, y_center - y, color) _set_pixel(x_center + x, y_center + y, color) _set_pixel(x_center - x, y_center + 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 _set_pixel(x_center, y_center + y, color) _set_pixel(x_center, y_center - y, color) end end |
#_get_pixel(x, y) ⇒ Object
PIXEL OPERATIONS
49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/denko/display/canvas.rb', line 49 def _get_pixel(x, y) byte = ((y / 8) * @columns) + x bit = y % 8 # Go through framebuffers and return that color when bit is set. @framebuffers.each_with_index do |fb, index| return index+1 if ((fb[byte] >> bit) & 0b1 == 1) end # If bit wasn't set in any framebuffer, color is 0. return 0 end |
#_line(x1, y1, x2, y2, color = current_color) ⇒ Object
LINE
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 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 |
# File 'lib/denko/display/canvas.rb', line 104 def _line(x1, y1, x2, y2, color=current_color) # Deltas in each axis. dy = y2 - y1 dx = x2 - x1 # Optimize vertical lines, and avoid division by 0. if (dx == 0) # Ensure y1 < y2. y1, y2 = y2, y1 if (y2 < y1) (y1..y2).each { |y| _set_pixel(x1, y, color) } return end # Optimize horizontal lines. if (dy == 0) # Ensure x1 < x2. x1, x2 = x2, x1 if (x2 < x1) (x1..x2).each { |x| _set_pixel(x, y1, color) } return end # Based on Bresenham's line algorithm # Slope calculations step_axis = (dx.abs > dy.abs) ? :x : :y step_count = (step_axis == :x) ? dx.abs : dy.abs x_step = (dx > 0) ? 1 : -1 y_step = (dy > 0) ? 1 : -1 # Error calculations error_step = (step_axis == :x) ? dy.abs : dx.abs error_threshold = (step_axis == :x) ? dx.abs : dy.abs x = x1 y = y1 error = 0 (0..step_count).each do |i| _set_pixel(x, y, color) if (step_axis == :x) x += x_step error += error_step if (error >= error_threshold) y += y_step error -= error_threshold end else y += y_step error += error_step if (error >= error_threshold) x += x_step error -= error_threshold end end end end |
#_path(points, color = current_color) ⇒ Object
PATH, POLYGON & TRIANGLE
206 207 208 209 210 211 212 213 |
# File 'lib/denko/display/canvas.rb', line 206 def _path(points, color=current_color) start = points[0] (1..points.length-1).each do |i| finish = points[i] _line(start[0], start[1], finish[0], finish[1], color) start = finish end end |
#_polygon(points, filled = false, color = current_color) ⇒ Object
219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 |
# File 'lib/denko/display/canvas.rb', line 219 def _polygon(points, filled=false, color=current_color) if filled # 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 # First condition excludes horizontal edges. # Second and third check for +ve and -ve intersection respectively. if (coords_y[i] != coords_y[j]) && ((coords_y[i] < y && coords_y[j] >= y) || (coords_y[j] < y && coords_y[i] >= y)) # Interoplate to find the intersection point (node). 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 ignores the spaces between odd then even nodes (eg. 1->2), which are outside the polygon. nodes.each_slice(2) do |pair| _line(pair.first, y, pair.last, y, color) if pair.length == 2 end end end # Stroke regardless, since floating point math misses thin areas of fill. _path(points, color) # Close open path by adding a line between the first and last points. _line(points[-1][0], points[-1][1], points[0][0], points[0][1], color) end |
#_rectangle(x1, y1, x2, y2, filled = false, color = current_color) ⇒ Object
RECTANGLE & SQUARE
167 168 169 170 171 172 173 174 175 176 177 |
# File 'lib/denko/display/canvas.rb', line 167 def _rectangle(x1, y1, x2, y2, filled=false, color=current_color) if filled y1, y2 = y2, y1 if (y2 < y1) (y1..y2).each { |y| _line(x1, y, x2, y, color) } else _line(x1, y1, x2, y1, color) _line(x2, y1, x2, y2, color) _line(x2, y2, x1, y2, color) _line(x1, y2, x1, y1, color) end end |
#_set_pixel(x, y, color = current_color) ⇒ Object
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 |
# File 'lib/denko/display/canvas.rb', line 66 def _set_pixel(x, y, color=current_color) xt = (@invert_x) ? @x_max - x : x yt = (@invert_y) ? @y_max - y : y if (@swap_xy) tt = xt xt = yt yt = tt end # Bounds check return nil if (xt < 0 || yt < 0 || xt > @columns-1 || yt > @rows -1) return nil if (color < 0) || (color > colors) byte = ((yt / 8) * @columns) + xt bit = yt % 8 # Set pixel bit in that color's buffer. Clear in others. # When color == 0, clears in all buffers and sets in none. for i in 1..colors if (color == i) @framebuffers[i-1][byte] |= (0b1 << bit) else @framebuffers[i-1][byte] &= ~(0b1 << bit) end end end |
#_triangle(x1, y1, x2, y2, x3, y3, filled = false, color = current_color) ⇒ Object
265 266 267 268 269 270 271 272 273 |
# File 'lib/denko/display/canvas.rb', line 265 def _triangle(x1, y1, x2, y2, x3, y3, filled=false, color=current_color) if filled _polygon([[x1,y1], [x2,y2], [x3,y3]], filled, color) else _line(x1, y1, x2, y2, color) _line(x2, y2, x3, y3, color) _line(x3, y3, x1, y1, color) end end |
#calculate_bounds ⇒ Object
462 463 464 465 466 467 468 469 470 |
# File 'lib/denko/display/canvas.rb', line 462 def calculate_bounds if @swap_xy @x_max = @rows - 1 @y_max = @columns - 1 else @x_max = @columns - 1 @y_max = @rows - 1 end end |
#circle(x:, y:, r:, filled: false, color: current_color) ⇒ Object
334 335 336 |
# File 'lib/denko/display/canvas.rb', line 334 def circle(x:, y:, r:, filled:false, color:current_color) _ellipse(x, y, r, r, filled, color) end |
#clear ⇒ Object
35 36 37 |
# File 'lib/denko/display/canvas.rb', line 35 def clear @framebuffers.each { |fb| fb.fill 0x00 } end |
#clear_pixel(x:, y:) ⇒ Object
97 98 99 |
# File 'lib/denko/display/canvas.rb', line 97 def clear_pixel(x:, y:) _set_pixel(x, y, 0) end |
#draw_char(char, color: current_color) ⇒ Object
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 |
# File 'lib/denko/display/canvas.rb', line 345 def draw_char(char, color:current_color) # 0th character in font is SPACE. Offset ASCII code and show ? if character doesn't exist in font. index = char.ord - 32 index = 31 if (index < 0 || index > @font_last_character) char_map = @font_characters[index] # Offset by scaled height, since bottom left of char starts at text cursor. x = text_cursor[0] y = text_cursor[1] + 1 - (@font_height * @font_scale) # Draw it _draw_char(char_map, x, y, @font_width, @font_scale, color) # Increment the text cursor, scaling width. self.text_cursor[0] += @font_width * @font_scale end |
#ellipse(x:, y:, a:, b:, filled: false, color: current_color) ⇒ Object
330 331 332 |
# File 'lib/denko/display/canvas.rb', line 330 def ellipse(x:, y:, a:, b:, filled:false, color:current_color) _ellipse(x, y, a, b, filled, color) end |
#fill ⇒ Object
39 40 41 42 43 44 |
# File 'lib/denko/display/canvas.rb', line 39 def fill # Clear all buffers, then fill the first one, which is the only # one for mono displays, black for multi-color e-paper. clear @framebuffers.first.fill 0xFF end |
#get_pixel(x:, y:) ⇒ Object
62 63 64 |
# File 'lib/denko/display/canvas.rb', line 62 def get_pixel(x:, y:) _get_pixel(x, y) end |
#line(x1:, y1:, x2:, y2:, color: current_color) ⇒ Object
160 161 162 |
# File 'lib/denko/display/canvas.rb', line 160 def line(x1:, y1:, x2:, y2:, color:current_color) _line(x1, y1, x2, y2, color) end |
#path(points, color: current_color) ⇒ Object
215 216 217 |
# File 'lib/denko/display/canvas.rb', line 215 def path(points, color:current_color) _path(points, color) end |
#polygon(points, filled: false, color: current_color) ⇒ Object
261 262 263 |
# File 'lib/denko/display/canvas.rb', line 261 def polygon(points, filled:false, color:current_color) _polygon(points, filled, color) end |
#rectangle(x1: nil, y1: nil, x2: nil, y2: nil, x: nil, y: nil, w: nil, h: nil, filled: false, color: current_color) ⇒ Object
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/denko/display/canvas.rb', line 179 def rectangle(x1:nil, y1:nil, x2:nil, y2:nil, x:nil, y:nil, w:nil, h:nil, filled:false, color:current_color) if (x || y || w || h) && (x1 || x2 || y1 || y2) raise ArgumentError, "#rectangle accepts x:, y:, w:, h: OR x1:, y1:, x2:, y2: not a combination of both" end if (w && h) x1 = x x2 = x1 + w x2 += 1 if (x2 < x1) x2 -= 1 if (x1 < x2) y1 = y y2 = y1 + h y2 += 1 if (y2 < y1) y2 -= 1 if (y1 < y2) end _rectangle(x1, y1, x2, y2, filled, color) end |
#reflect(axis) ⇒ Object
448 449 450 451 452 |
# File 'lib/denko/display/canvas.rb', line 448 def reflect(axis) raise ArgumentError "Canvas can only be reflected in :x or :y axis" unless [:x, :y].include? (axis) (axis == :x) ? @invert_x = !@invert_x : @invert_y = !@invert_y calculate_bounds end |
#reflect_x ⇒ Object
454 455 456 |
# File 'lib/denko/display/canvas.rb', line 454 def reflect_x reflect(:x) end |
#reflect_y ⇒ Object
458 459 460 |
# File 'lib/denko/display/canvas.rb', line 458 def reflect_y reflect(:y) end |
#rotate(degrees = 180) ⇒ Object
TRANSFORMATION
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 |
# File 'lib/denko/display/canvas.rb', line 423 def rotate(degrees=180) raise ArgumentError, "Canvas can only be rotated in multiples of 90 degrees" unless (degrees % 90 == 0) old_rotation = @rotation @rotation = (old_rotation + degrees) % 360 change = (@rotation - old_rotation) % 360 (change / 90).times do @swap_xy = !@swap_xy if (!@invert_x && !@invert_y) @invert_x = true @invert_y = false elsif (@invert_x && !@invert_y) @invert_x = true @invert_y = true elsif (@invert_x && @invert_y) @invert_x = false @invert_y = true elsif (!@invert_x && @invert_y) @invert_x = false @invert_y = false end end calculate_bounds end |
#set_pixel(x:, y:, color: current_color) ⇒ Object
93 94 95 |
# File 'lib/denko/display/canvas.rb', line 93 def set_pixel(x:, y:, color:current_color) _set_pixel(x, y, color) end |
#square(x:, y:, size:, filled: false, color: current_color) ⇒ Object
199 200 201 |
# File 'lib/denko/display/canvas.rb', line 199 def square(x:, y:, size:, filled:false, color:current_color) rectangle(x: x, y: y, w: size, h: size, filled: filled, color: color) end |
#text(str, color: current_color) ⇒ Object
BITMAP TEXT
341 342 343 |
# File 'lib/denko/display/canvas.rb', line 341 def text(str, color:current_color) str.to_s.split("").each { |char| draw_char(char, color: color) } end |
#text_cursor ⇒ Object
392 393 394 |
# File 'lib/denko/display/canvas.rb', line 392 def text_cursor @text_cursor ||= DEFAULT_TEXT_CURSOR end |
#text_cursor=(array = DEFAULT_TEXT_CURSOR) ⇒ Object
396 397 398 |
# File 'lib/denko/display/canvas.rb', line 396 def text_cursor=(array=DEFAULT_TEXT_CURSOR) @text_cursor = array end |
#triangle(x1:, y1:, x2:, y2:, x3:, y3:, filled: false, color: current_color) ⇒ Object
275 276 277 |
# File 'lib/denko/display/canvas.rb', line 275 def triangle(x1:, y1:, x2:, y2:, x3:, y3:, filled:false, color:current_color) _triangle(x1, y1, x2, y2, x3, y3, filled, color) end |