/ CircuitPython_PyPaint / code.py
code.py
1 # SPDX-FileCopyrightText: 2019 Dave Astels for Adafruit Industries 2 # 3 # SPDX-License-Identifier: MIT 4 5 """ 6 Paint for PyPortal, PyBadge, PyGamer, and the like. 7 8 Adafruit invests time and resources providing this open source code. 9 Please support Adafruit and open source hardware by purchasing 10 products from Adafruit! 11 12 Written by Dave Astels for Adafruit Industries 13 Copyright (c) 2019 Adafruit Industries 14 Licensed under the MIT license. 15 16 All text above must be included in any redistribution. 17 """ 18 19 import gc 20 import time 21 import board 22 import displayio 23 import adafruit_logging as logging 24 25 try: 26 import adafruit_touchscreen 27 except ImportError: 28 pass 29 try: 30 from adafruit_cursorcontrol.cursorcontrol import Cursor 31 from adafruit_cursorcontrol.cursorcontrol_cursormanager import DebouncedCursorManager 32 except ImportError: 33 pass 34 35 36 class Color(object): 37 """Standard colors""" 38 39 WHITE = 0xFFFFFF 40 BLACK = 0x000000 41 RED = 0xFF0000 42 ORANGE = 0xFFA500 43 YELLOW = 0xFFFF00 44 GREEN = 0x00FF00 45 BLUE = 0x0000FF 46 PURPLE = 0x800080 47 PINK = 0xFFC0CB 48 49 colors = (BLACK, RED, ORANGE, YELLOW, GREEN, BLUE, PURPLE, WHITE) 50 51 def __init__(self): 52 pass 53 54 55 ################################################################################ 56 57 58 class TouchscreenPoller(object): 59 """Get 'pressed' and location updates from a touch screen device.""" 60 61 def __init__(self, splash, cursor_bmp): 62 logger = logging.getLogger("Paint") 63 if not logger.hasHandlers(): 64 logger.addHandler(logging.StreamHandler()) 65 logger.debug("Creating a TouchscreenPoller") 66 self._display_grp = splash 67 self._touchscreen = adafruit_touchscreen.Touchscreen( 68 board.TOUCH_XL, board.TOUCH_XR, 69 board.TOUCH_YD, board.TOUCH_YU, 70 calibration=((9000, 59000), (8000, 57000)), 71 size=(320, 240), 72 ) 73 self._cursor_grp = displayio.Group() 74 self._cur_palette = displayio.Palette(3) 75 self._cur_palette.make_transparent(0) 76 self._cur_palette[1] = 0xFFFFFF 77 self._cur_palette[2] = 0x0000 78 self._cur_sprite = displayio.TileGrid( 79 cursor_bmp, pixel_shader=self._cur_palette 80 ) 81 self._cursor_grp.append(self._cur_sprite) 82 self._display_grp.append(self._cursor_grp) 83 self._x_offset = cursor_bmp.width // 2 84 self._y_offset = cursor_bmp.height // 2 85 86 def poll(self): 87 """Check for input. Returns contact (a bool), False (no button B), 88 and it's location ((x,y) or None)""" 89 90 p = self._touchscreen.touch_point 91 if p is not None: 92 self._cursor_grp.x = p[0] - self._x_offset 93 self._cursor_grp.y = p[1] - self._y_offset 94 return True, p 95 else: 96 return False, None 97 98 def poke(self, location=None): 99 """Force a bitmap refresh.""" 100 self._display_grp.remove(self._cursor_grp) 101 if location is not None: 102 self._cursor_grp.x = location[0] - self._x_offset 103 self._cursor_grp.y = location[1] - self._y_offset 104 self._display_grp.append(self._cursor_grp) 105 106 def set_cursor_bitmap(self, bmp): 107 """Update the cursor bitmap. 108 109 :param bmp: the new cursor bitmap 110 """ 111 self._cursor_grp.remove(self._cur_sprite) 112 self._cur_sprite = displayio.TileGrid(bmp, pixel_shader=self._cur_palette) 113 self._cursor_grp.append(self._cur_sprite) 114 self.poke() 115 116 117 ################################################################################ 118 119 120 class CursorPoller(object): 121 """Get 'pressed' and location updates from a D-Pad/joystick device.""" 122 123 def __init__(self, splash, cursor_bmp): 124 self._logger = logging.getLogger("Paint") 125 if not self._logger.hasHandlers(): 126 self._logger.addHandler(logging.StreamHandler()) 127 self._logger.debug("Creating a CursorPoller") 128 self._mouse_cursor = Cursor( 129 board.DISPLAY, display_group=splash, bmp=cursor_bmp, cursor_speed=2 130 ) 131 self._x_offset = cursor_bmp.width // 2 132 self._y_offset = cursor_bmp.height // 2 133 self._cursor = DebouncedCursorManager(self._mouse_cursor) 134 135 def poll(self): 136 """Check for input. Returns press of A (a bool), B, 137 and the cursor location ((x,y) or None)""" 138 location = None 139 self._cursor.update() 140 a_button = self._cursor.held 141 if a_button: 142 location = ( 143 self._mouse_cursor.x + self._x_offset, 144 self._mouse_cursor.y + self._y_offset, 145 ) 146 return a_button, location 147 148 def poke(self, x=None, y=None): 149 """Force a bitmap refresh.""" 150 self._mouse_cursor.hide() 151 self._mouse_cursor.show() 152 153 def set_cursor_bitmap(self, bmp): 154 """Update the cursor bitmap. 155 156 :param bmp: the new cursor bitmap 157 """ 158 self._mouse_cursor.cursor_bitmap = bmp 159 self.poke() 160 161 162 ################################################################################ 163 164 165 class Paint(object): 166 def __init__(self, display=board.DISPLAY): 167 self._logger = logging.getLogger("Paint") 168 if not self._logger.hasHandlers(): 169 self._logger.addHandler(logging.StreamHandler()) 170 self._logger.setLevel(logging.DEBUG) 171 self._display = display 172 self._w = self._display.width 173 self._h = self._display.height 174 self._x = self._w // 2 175 self._y = self._h // 2 176 177 self._splash = displayio.Group() 178 179 self._bg_bitmap = displayio.Bitmap(self._w, self._h, 1) 180 self._bg_palette = displayio.Palette(1) 181 self._bg_palette[0] = Color.BLACK 182 self._bg_sprite = displayio.TileGrid( 183 self._bg_bitmap, pixel_shader=self._bg_palette, x=0, y=0 184 ) 185 self._splash.append(self._bg_sprite) 186 187 self._palette_bitmap = displayio.Bitmap(self._w, self._h, 5) 188 self._palette_palette = displayio.Palette(len(Color.colors)) 189 for i, c in enumerate(Color.colors): 190 self._palette_palette[i] = c 191 self._palette_sprite = displayio.TileGrid( 192 self._palette_bitmap, pixel_shader=self._palette_palette, x=0, y=0 193 ) 194 self._splash.append(self._palette_sprite) 195 196 self._fg_bitmap = displayio.Bitmap(self._w, self._h, 5) 197 self._fg_palette = displayio.Palette(len(Color.colors)) 198 for i, c in enumerate(Color.colors): 199 self._fg_palette[i] = c 200 self._fg_sprite = displayio.TileGrid( 201 self._fg_bitmap, pixel_shader=self._fg_palette, x=0, y=0 202 ) 203 self._splash.append(self._fg_sprite) 204 205 self._number_of_palette_options = len(Color.colors) + 2 206 self._swatch_height = self._h // self._number_of_palette_options 207 self._swatch_width = self._w // 10 208 self._logger.debug("Height: %d", self._h) 209 self._logger.debug("Swatch height: %d", self._swatch_height) 210 211 self._palette = self._make_palette() 212 self._splash.append(self._palette) 213 214 self._display.show(self._splash) 215 try: 216 gc.collect() 217 self._display.refresh(target_frames_per_second=60) 218 except AttributeError: 219 self._display.refresh_soon() 220 gc.collect() 221 self._display.wait_for_frame() 222 223 self._brush = 0 224 self._cursor_bitmaps = [self._cursor_bitmap_1(), self._cursor_bitmap_3()] 225 if hasattr(board, "TOUCH_XL"): 226 self._poller = TouchscreenPoller(self._splash, self._cursor_bitmaps[0]) 227 elif hasattr(board, "BUTTON_CLOCK"): 228 self._poller = CursorPoller(self._splash, self._cursor_bitmaps[0]) 229 else: 230 raise AttributeError("PyPaint requires a touchscreen or cursor.") 231 232 self._a_pressed = False 233 self._last_a_pressed = False 234 self._location = None 235 self._last_location = None 236 237 self._pencolor = 7 238 239 def _make_palette(self): 240 self._palette_bitmap = displayio.Bitmap(self._w // 10, self._h, 5) 241 self._palette_palette = displayio.Palette(len(Color.colors)) 242 for i, c in enumerate(Color.colors): 243 self._palette_palette[i] = c 244 for y in range(self._swatch_height): 245 for x in range(self._swatch_width): 246 self._palette_bitmap[x, self._swatch_height * i + y] = i 247 248 swatch_x_offset = (self._swatch_width - 9) // 2 249 swatch_y_offset = (self._swatch_height - 9) // 2 250 swatch_y = self._swatch_height * len(Color.colors) + swatch_y_offset 251 for i in range(9): 252 self._palette_bitmap[swatch_x_offset + 4, swatch_y + i] = 1 253 self._palette_bitmap[swatch_x_offset + i, swatch_y + 4] = 1 254 self._palette_bitmap[swatch_x_offset + 4, swatch_y + 4] = 0 255 256 swatch_y += self._swatch_height 257 for i in range(9): 258 self._palette_bitmap[swatch_x_offset + 3, swatch_y + i] = 1 259 self._palette_bitmap[swatch_x_offset + 4, swatch_y + i] = 1 260 self._palette_bitmap[swatch_x_offset + 5, swatch_y + i] = 1 261 self._palette_bitmap[swatch_x_offset + i, swatch_y + 3] = 1 262 self._palette_bitmap[swatch_x_offset + i, swatch_y + 4] = 1 263 self._palette_bitmap[swatch_x_offset + i, swatch_y + 5] = 1 264 for i in range(swatch_x_offset + 3, swatch_x_offset + 6): 265 for j in range(swatch_y + 3, swatch_y + 6): 266 self._palette_bitmap[i, j] = 0 267 268 for i in range(self._h): 269 self._palette_bitmap[self._swatch_width - 1, i] = 7 270 271 return displayio.TileGrid( 272 self._palette_bitmap, pixel_shader=self._palette_palette, x=0, y=0 273 ) 274 275 def _cursor_bitmap_1(self): 276 bmp = displayio.Bitmap(9, 9, 3) 277 for i in range(9): 278 bmp[4, i] = 1 279 bmp[i, 4] = 1 280 bmp[4, 4] = 0 281 return bmp 282 283 def _cursor_bitmap_3(self): 284 bmp = displayio.Bitmap(9, 9, 3) 285 for i in range(9): 286 bmp[3, i] = 1 287 bmp[4, i] = 1 288 bmp[5, i] = 1 289 bmp[i, 3] = 1 290 bmp[i, 4] = 1 291 bmp[i, 5] = 1 292 for i in range(3, 6): 293 for j in range(3, 6): 294 bmp[i, j] = 0 295 return bmp 296 297 def _plot(self, x, y, c): 298 if self._brush == 0: 299 r = [0] 300 else: 301 r = [-1, 0, 1] 302 for i in r: 303 for j in r: 304 try: 305 self._fg_bitmap[int(x + i), int(y + j)] = c 306 except IndexError: 307 pass 308 309 def _draw_line(self, start, end): 310 """Draw a line from the previous position to the current one. 311 312 :param start: a tuple of (x, y) coordinatess to fram from 313 :param end: a tuple of (x, y) coordinates to draw to 314 """ 315 x0 = start[0] 316 y0 = start[1] 317 x1 = end[0] 318 y1 = end[1] 319 self._logger.debug("* GoTo from (%d, %d) to (%d, %d)", x0, y0, x1, y1) 320 steep = abs(y1 - y0) > abs(x1 - x0) 321 rev = False 322 dx = x1 - x0 323 324 if steep: 325 x0, y0 = y0, x0 326 x1, y1 = y1, x1 327 dx = x1 - x0 328 329 if x0 > x1: 330 rev = True 331 dx = x0 - x1 332 333 dy = abs(y1 - y0) 334 err = dx / 2 335 ystep = -1 336 if y0 < y1: 337 ystep = 1 338 339 while (not rev and x0 <= x1) or (rev and x1 <= x0): 340 if steep: 341 try: 342 self._plot(int(y0), int(x0), self._pencolor) 343 except IndexError: 344 pass 345 self._x = y0 346 self._y = x0 347 self._poller.poke((int(y0), int(x0))) 348 time.sleep(0.003) 349 else: 350 try: 351 self._plot(int(x0), int(y0), self._pencolor) 352 except IndexError: 353 pass 354 self._x = x0 355 self._y = y0 356 self._poller.poke((int(x0), int(y0))) 357 time.sleep(0.003) 358 err -= dy 359 if err < 0: 360 y0 += ystep 361 err += dx 362 if rev: 363 x0 -= 1 364 else: 365 x0 += 1 366 367 def _handle_palette_selection(self, location): 368 selected = location[1] // self._swatch_height 369 if selected >= self._number_of_palette_options: 370 return 371 self._logger.debug("Palette selection: %d", selected) 372 if selected < len(Color.colors): 373 self._pencolor = selected 374 else: 375 self._brush = selected - len(Color.colors) 376 self._poller.set_cursor_bitmap(self._cursor_bitmaps[self._brush]) 377 378 def _handle_motion(self, start, end): 379 self._logger.debug( 380 "Moved: (%d, %d) -> (%d, %d)", start[0], start[1], end[0], end[1] 381 ) 382 self._draw_line(start, end) 383 384 def _handle_a_press(self, location): 385 self._logger.debug("A Pressed!") 386 if location[0] < self._w // 10: # in color picker 387 self._handle_palette_selection(location) 388 else: 389 self._plot(location[0], location[1], self._pencolor) 390 self._poller.poke() 391 392 def _handle_a_release(self, location): 393 self._logger.debug("A Released!") 394 395 @property 396 def _was_a_just_pressed(self): 397 return self._a_pressed and not self._last_a_pressed 398 399 @property 400 def _was_a_just_released(self): 401 return not self._a_pressed and self._last_a_pressed 402 403 @property 404 def _did_move(self): 405 if self._location is not None and self._last_location is not None: 406 x_changed = self._location[0] != self._last_location[0] 407 y_changed = self._location[1] != self._last_location[1] 408 return x_changed or y_changed 409 else: 410 return False 411 412 def _update(self): 413 self._last_a_pressed, self._last_location = self._a_pressed, self._location 414 self._a_pressed, self._location = self._poller.poll() 415 416 def run(self): 417 """Run the painting program.""" 418 while True: 419 self._update() 420 if self._was_a_just_pressed: 421 self._handle_a_press(self._location) 422 elif self._was_a_just_released: 423 self._handle_a_release(self._location) 424 if self._did_move and self._a_pressed: 425 self._handle_motion(self._last_location, self._location) 426 time.sleep(0.1) 427 428 429 painter = Paint() 430 painter.run()