Skip to content

Commit 3dd1a22

Browse files
authored
Merge pull request #744 from dastels/pypaint
PyPaint code
2 parents 0ec11a7 + ed03d7b commit 3dd1a22

File tree

1 file changed

+341
-0
lines changed

1 file changed

+341
-0
lines changed

CircuitPython_PyPaint/code.py

Lines changed: 341 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,341 @@
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

Comments
 (0)