|
| 1 | +""" |
| 2 | +Paint for PyPortal, PyBadge, PyGamer, and the like. |
| 3 | +
|
| 4 | +Adafruit invests time and resources providing this open source code. |
| 5 | +Please support Adafruit and open source hardware by purchasing |
| 6 | +products from Adafruit! |
| 7 | +
|
| 8 | +Written by Dave Astels for Adafruit Industries |
| 9 | +Copyright (c) 2019 Adafruit Industries |
| 10 | +Licensed under the MIT license. |
| 11 | +
|
| 12 | +All text above must be included in any redistribution. |
| 13 | +""" |
| 14 | + |
| 15 | +#pylint:disable=invalid-name, no-self-use |
| 16 | + |
| 17 | +import gc |
| 18 | +import time |
| 19 | +import board |
| 20 | +import displayio |
| 21 | +import adafruit_logging as logging |
| 22 | +try: |
| 23 | + import adafruit_touchscreen |
| 24 | +except ImportError: |
| 25 | + pass |
| 26 | +try: |
| 27 | + from adafruit_cursorcontrol.cursorcontrol import Cursor |
| 28 | + from adafruit_cursorcontrol.cursorcontrol_cursormanager import DebouncedCursorManager |
| 29 | +except ImportError: |
| 30 | + pass |
| 31 | + |
| 32 | +class Color(object): |
| 33 | + """Standard colors""" |
| 34 | + WHITE = 0xFFFFFF |
| 35 | + BLACK = 0x000000 |
| 36 | + RED = 0xFF0000 |
| 37 | + ORANGE = 0xFFA500 |
| 38 | + YELLOW = 0xFFFF00 |
| 39 | + GREEN = 0x00FF00 |
| 40 | + BLUE = 0x0000FF |
| 41 | + PURPLE = 0x800080 |
| 42 | + PINK = 0xFFC0CB |
| 43 | + |
| 44 | + colors = (BLACK, RED, ORANGE, YELLOW, GREEN, BLUE, PURPLE, WHITE) |
| 45 | + |
| 46 | + def __init__(self): |
| 47 | + pass |
| 48 | + |
| 49 | +################################################################################ |
| 50 | + |
| 51 | +class TouchscreenPoller(object): |
| 52 | + """Get 'pressed' and location updates from a touch screen device.""" |
| 53 | + |
| 54 | + def __init__(self, splash, cursor_bmp): |
| 55 | + logging.getLogger('Paint').debug('Creating a TouchscreenPoller') |
| 56 | + self._display_grp = splash |
| 57 | + self._touchscreen = adafruit_touchscreen.Touchscreen(board.TOUCH_XL, board.TOUCH_XR, |
| 58 | + board.TOUCH_YD, board.TOUCH_YU, |
| 59 | + calibration=((9000, 59000), |
| 60 | + (8000, 57000)), |
| 61 | + size=(320, 240)) |
| 62 | + self._cursor_grp = displayio.Group(max_size=1) |
| 63 | + self._cur_palette = displayio.Palette(3) |
| 64 | + self._cur_palette.make_transparent(0) |
| 65 | + self._cur_palette[1] = 0xFFFFFF |
| 66 | + self._cur_palette[2] = 0x0000 |
| 67 | + self._cur_sprite = displayio.TileGrid(cursor_bmp, |
| 68 | + pixel_shader=self._cur_palette) |
| 69 | + self._cursor_grp.append(self._cur_sprite) |
| 70 | + self._display_grp.append(self._cursor_grp) |
| 71 | + self._x_offset = cursor_bmp.width // 2 |
| 72 | + self._y_offset = cursor_bmp.height // 2 |
| 73 | + |
| 74 | + |
| 75 | + |
| 76 | + def poll(self): |
| 77 | + """Check for input. Returns contact (a bool) and it's location ((x,y) or None)""" |
| 78 | + |
| 79 | + p = self._touchscreen.touch_point |
| 80 | + if p is not None: |
| 81 | + self._cursor_grp.x = p[0] - self._x_offset |
| 82 | + self._cursor_grp.y = p[1] - self._y_offset |
| 83 | + return True, p |
| 84 | + else: |
| 85 | + return False, None |
| 86 | + |
| 87 | + def poke(self, location=None): |
| 88 | + """Force a bitmap refresh.""" |
| 89 | + self._display_grp.remove(self._cursor_grp) |
| 90 | + if location is not None: |
| 91 | + self._cursor_grp.x = location[0] - self._x_offset |
| 92 | + self._cursor_grp.y = location[1] - self._y_offset |
| 93 | + self._display_grp.append(self._cursor_grp) |
| 94 | + |
| 95 | +################################################################################ |
| 96 | + |
| 97 | +class CursorPoller(object): |
| 98 | + """Get 'pressed' and location updates from a D-Pad/joystick device.""" |
| 99 | + |
| 100 | + def __init__(self, splash, cursor_bmp): |
| 101 | + logging.getLogger('Paint').debug('Creating a CursorPoller') |
| 102 | + self._mouse_cursor = Cursor(board.DISPLAY, |
| 103 | + display_group=splash, |
| 104 | + bmp=cursor_bmp, |
| 105 | + cursor_speed=2) |
| 106 | + self._x_offset = cursor_bmp.width // 2 |
| 107 | + self._y_offset = cursor_bmp.height // 2 |
| 108 | + self._cursor = DebouncedCursorManager(self._mouse_cursor) |
| 109 | + self._logger = logging.getLogger('Paint') |
| 110 | + |
| 111 | + def poll(self): |
| 112 | + """Check for input. Returns press (a bool) and it's location ((x,y) or None)""" |
| 113 | + location = None |
| 114 | + self._cursor.update() |
| 115 | + button = self._cursor.held |
| 116 | + if button: |
| 117 | + location = (self._mouse_cursor.x + self._x_offset, |
| 118 | + self._mouse_cursor.y + self._y_offset) |
| 119 | + return button, location |
| 120 | + |
| 121 | + #pylint:disable=unused-argument |
| 122 | + def poke(self, x=None, y=None): |
| 123 | + """Force a bitmap refresh.""" |
| 124 | + self._mouse_cursor.hide() |
| 125 | + self._mouse_cursor.show() |
| 126 | + #pylint:enable=unused-argument |
| 127 | + |
| 128 | +################################################################################ |
| 129 | + |
| 130 | +class Paint(object): |
| 131 | + |
| 132 | + def __init__(self, display=board.DISPLAY): |
| 133 | + self._logger = logging.getLogger("Paint") |
| 134 | + self._logger.setLevel(logging.DEBUG) |
| 135 | + self._display = display |
| 136 | + self._w = self._display.width |
| 137 | + self._h = self._display.height |
| 138 | + self._x = self._w // 2 |
| 139 | + self._y = self._h // 2 |
| 140 | + |
| 141 | + self._splash = displayio.Group(max_size=5) |
| 142 | + |
| 143 | + self._bg_bitmap = displayio.Bitmap(self._w, self._h, 1) |
| 144 | + self._bg_palette = displayio.Palette(1) |
| 145 | + self._bg_palette[0] = Color.BLACK |
| 146 | + self._bg_sprite = displayio.TileGrid(self._bg_bitmap, |
| 147 | + pixel_shader=self._bg_palette, |
| 148 | + x=0, y=0) |
| 149 | + self._splash.append(self._bg_sprite) |
| 150 | + |
| 151 | + self._palette_bitmap = displayio.Bitmap(self._w, self._h, 5) |
| 152 | + self._palette_palette = displayio.Palette(len(Color.colors)) |
| 153 | + for i, c in enumerate(Color.colors): |
| 154 | + self._palette_palette[i] = c |
| 155 | + self._palette_sprite = displayio.TileGrid(self._palette_bitmap, |
| 156 | + pixel_shader=self._palette_palette, |
| 157 | + x=0, y=0) |
| 158 | + self._splash.append(self._palette_sprite) |
| 159 | + |
| 160 | + self._fg_bitmap = displayio.Bitmap(self._w, self._h, 5) |
| 161 | + self._fg_palette = displayio.Palette(len(Color.colors)) |
| 162 | + for i, c in enumerate(Color.colors): |
| 163 | + self._fg_palette[i] = c |
| 164 | + self._fg_sprite = displayio.TileGrid(self._fg_bitmap, |
| 165 | + pixel_shader=self._fg_palette, |
| 166 | + x=0, y=0) |
| 167 | + self._splash.append(self._fg_sprite) |
| 168 | + |
| 169 | + self._color_palette = self._make_color_palette() |
| 170 | + self._splash.append(self._color_palette) |
| 171 | + |
| 172 | + self._display.show(self._splash) |
| 173 | + self._display.refresh_soon() |
| 174 | + gc.collect() |
| 175 | + self._display.wait_for_frame() |
| 176 | + |
| 177 | + if hasattr(board, 'TOUCH_XL'): |
| 178 | + self._poller = TouchscreenPoller(self._splash, self._cursor_bitmap()) |
| 179 | + elif hasattr(board, 'BUTTON_CLOCK'): |
| 180 | + self._poller = CursorPoller(self._splash, self._cursor_bitmap()) |
| 181 | + else: |
| 182 | + raise AttributeError('PYOA requires a touchscreen or cursor.') |
| 183 | + |
| 184 | + self._pressed = False |
| 185 | + self._last_pressed = False |
| 186 | + self._location = None |
| 187 | + self._last_location = None |
| 188 | + |
| 189 | + self._pencolor = 7 |
| 190 | + |
| 191 | + def _make_color_palette(self): |
| 192 | + self._palette_bitmap = displayio.Bitmap(self._w // 10, self._h, 5) |
| 193 | + self._palette_palette = displayio.Palette(len(Color.colors)) |
| 194 | + swatch_height = self._h // len(Color.colors) |
| 195 | + for i, c in enumerate(Color.colors): |
| 196 | + self._palette_palette[i] = c |
| 197 | + for y in range(swatch_height): |
| 198 | + for x in range(self._w // 10): |
| 199 | + self._palette_bitmap[x, swatch_height * i + y] = i |
| 200 | + self._palette_bitmap[self._w // 10 - 1, swatch_height * i + y] = 7 |
| 201 | + |
| 202 | + |
| 203 | + return displayio.TileGrid(self._palette_bitmap, |
| 204 | + pixel_shader=self._palette_palette, |
| 205 | + x=0, y=0) |
| 206 | + |
| 207 | + def _cursor_bitmap(self): |
| 208 | + bmp = displayio.Bitmap(9, 9, 3) |
| 209 | + for i in range(9): |
| 210 | + bmp[4, i] = 1 |
| 211 | + bmp[i, 4] = 1 |
| 212 | + bmp[4, 4] = 0 |
| 213 | + return bmp |
| 214 | + |
| 215 | + def _plot(self, x, y, c): |
| 216 | + try: |
| 217 | + self._fg_bitmap[int(x), int(y)] = c |
| 218 | + except IndexError: |
| 219 | + pass |
| 220 | + |
| 221 | + #pylint:disable=too-many-branches,too-many-statements |
| 222 | + |
| 223 | + def _goto(self, start, end): |
| 224 | + """Draw a line from the previous position to the current one. |
| 225 | +
|
| 226 | + :param start: a tuple of (x, y) coordinatess to fram from |
| 227 | + :param end: a tuple of (x, y) coordinates to draw to |
| 228 | + """ |
| 229 | + x0 = start[0] |
| 230 | + y0 = start[1] |
| 231 | + x1 = end[0] |
| 232 | + y1 = end[1] |
| 233 | + self._logger.debug("* GoTo from (%d, %d) to (%d, %d)", x0, y0, x1, y1) |
| 234 | + steep = abs(y1 - y0) > abs(x1 - x0) |
| 235 | + rev = False |
| 236 | + dx = x1 - x0 |
| 237 | + |
| 238 | + if steep: |
| 239 | + x0, y0 = y0, x0 |
| 240 | + x1, y1 = y1, x1 |
| 241 | + dx = x1 - x0 |
| 242 | + |
| 243 | + if x0 > x1: |
| 244 | + rev = True |
| 245 | + dx = x0 - x1 |
| 246 | + |
| 247 | + dy = abs(y1 - y0) |
| 248 | + err = dx / 2 |
| 249 | + ystep = -1 |
| 250 | + if y0 < y1: |
| 251 | + ystep = 1 |
| 252 | + |
| 253 | + while (not rev and x0 <= x1) or (rev and x1 <= x0): |
| 254 | + if steep: |
| 255 | + try: |
| 256 | + self._plot(int(y0), int(x0), self._pencolor) |
| 257 | + except IndexError: |
| 258 | + pass |
| 259 | + self._x = y0 |
| 260 | + self._y = x0 |
| 261 | + self._poller.poke((int(y0), int(x0))) |
| 262 | + time.sleep(0.003) |
| 263 | + else: |
| 264 | + try: |
| 265 | + self._plot(int(x0), int(y0), self._pencolor) |
| 266 | + except IndexError: |
| 267 | + pass |
| 268 | + self._x = x0 |
| 269 | + self._y = y0 |
| 270 | + self._poller.poke((int(x0), int(y0))) |
| 271 | + time.sleep(0.003) |
| 272 | + err -= dy |
| 273 | + if err < 0: |
| 274 | + y0 += ystep |
| 275 | + err += dx |
| 276 | + if rev: |
| 277 | + x0 -= 1 |
| 278 | + else: |
| 279 | + x0 += 1 |
| 280 | + |
| 281 | + #pylint:enable=too-many-branches,too-many-statements |
| 282 | + |
| 283 | + |
| 284 | + def _pick_color(self, location): |
| 285 | + swatch_height = self._h // len(Color.colors) |
| 286 | + picked = location[1] // swatch_height |
| 287 | + self._pencolor = picked |
| 288 | + |
| 289 | + def _handle_motion(self, start, end): |
| 290 | + self._logger.debug('Moved: (%d, %d) -> (%d, %d)', start[0], start[1], end[0], end[1]) |
| 291 | + self._goto(start, end) |
| 292 | + |
| 293 | + def _handle_press(self, location): |
| 294 | + self._logger.debug('Pressed!') |
| 295 | + if location[0] < self._w // 10: # in color picker |
| 296 | + self._pick_color(location) |
| 297 | + else: |
| 298 | + self._plot(location[0], location[1], self._pencolor) |
| 299 | + self._poller.poke() |
| 300 | + |
| 301 | + #pylint:disable=unused-argument |
| 302 | + def _handle_release(self, location): |
| 303 | + self._logger.debug('Released!') |
| 304 | + #pylint:enable=unused-argument |
| 305 | + |
| 306 | + @property |
| 307 | + def _was_just_pressed(self): |
| 308 | + return self._pressed and not self._last_pressed |
| 309 | + |
| 310 | + @property |
| 311 | + def _was_just_released(self): |
| 312 | + return not self._pressed and self._last_pressed |
| 313 | + |
| 314 | + @property |
| 315 | + def _did_move(self): |
| 316 | + if self._location is not None and self._last_location is not None: |
| 317 | + x_changed = self._location[0] != self._last_location[0] |
| 318 | + y_changed = self._location[1] != self._last_location[1] |
| 319 | + return x_changed or y_changed |
| 320 | + else: |
| 321 | + return False |
| 322 | + |
| 323 | + def _update(self): |
| 324 | + self._last_pressed, self._last_location = self._pressed, self._location |
| 325 | + self._pressed, self._location = self._poller.poll() |
| 326 | + |
| 327 | + |
| 328 | + def run(self): |
| 329 | + """Run the painting program.""" |
| 330 | + while True: |
| 331 | + self._update() |
| 332 | + if self._was_just_pressed: |
| 333 | + self._handle_press(self._location) |
| 334 | + elif self._was_just_released: |
| 335 | + self._handle_release(self._location) |
| 336 | + if self._did_move and self._pressed: |
| 337 | + self._handle_motion(self._last_location, self._location) |
| 338 | + time.sleep(0.1) |
| 339 | + |
| 340 | +painter = Paint() |
| 341 | +painter.run() |
0 commit comments