Author: Curt Werline, Firia Design Engineer
Birth of an idea
During Pathfinders summer 2022 week I decided to go through the Python with CodeX Mission 11: Spirit Level to get my feet wet with the latest CodeX firmware and the next generation CodeSpace Development Environment, in order to better assist our teacher students as they experienced the joys of coding in python. I chose this mission because it focused on the accelerometer sensor (which I think is the coolest sensor) and I wanted to build something on top of what was taught in the existing curriculum. But what should I build? I know! How about a 2-axis level? In mission 11 I learned how to find the CodeX tilt angle in the x direction and so it was very easy to include the y direction as well. Okay, now what? The new 2-axis level worked just as I expected but the “bubble” was just an empty circle and hard to see. So to fill in the circle with some color, I went to the CodeX Python module application programming interface (or more simply called the API) help documentation located here - bitmap- Core Graphics Rendering. Hmmmm, rectangles have a color fill function called fill_rect
(x: int, y: int, width: int, height: int, color: int), but to my chagrin, circles do not have a similar color fill function. No problem! I’ll just write the python code myself. Here it is…
for r in range(1, brush_size):
display.fill_circle(x, y, r, color_fill)
By simply drawing concentric circles from 1 to the brush size radius I’m able to fill in a nice looking circle. But there’s a catch! When this bubble circle moves on the screen I need to erase its original position and redraw in its new position. The refresh rate of this erase and draw code is very slow and it doesn’t look very good. It pulses in a very disturbing way so don’t look too long.
I wonder what would happen if I don’t erase the filled bubble circle?
Wow! Now this is a cool effect. I’m actually drawing on this tiny little CodeX monitor! What about writing a simple paint program instead? BINGO! That’s what I’ll do!
Painting needs color
Add a color picker control to display all available colors and a clever way to select a fill color.
# U button increases brush size
if buttons.was_pressed(BTN_U):
brush_size += 1
if brush_size > MAX_BRUSH_SIZE:
brush_size = MAX_BRUSH_SIZE
# D button decreases brush size
elif buttons.was_pressed(BTN_D):
brush_size -= 1
if brush_size < MIN_BRUSH_SIZE:
brush_size = MIN_BRUSH_SIZE
Painting needs erasing
Well you don't really "erase" a painting, you just paint over your masterpiece with the background color.
# A button erases with a filled white circle
if (buttons.is_pressed(BTN_A)):
for r in range(1, brush_size):
display.fill_circle(x, y, r, WHITE)
Painting needs accents
A simple white or black outline around the brush will add the perfect subtle lighter or darker accents.
# B button draws with a filled color circle
elif (buttons.is_pressed(BTN_B)):
for r in range(1, brush_size):
display.fill_circle(x, y, r, color_fill)
display.draw_circle(x, y, brush_size, color_rim)
Commands:
A button erases
B button draws
A+B buttons toggle black and white outline
L button moves color selector to the left
R button moves color selector to the right
U button increases brush size (max 25)
D button decreases brush size (min 1)
L+R+U+D buttons erase all
Creations:
Python Code:
from codex import *
from time import sleep
# Constants
CENTER = 120
BK_COLOR = WHITE
MIN_BRUSH_SIZE = 1
MAX_BRUSH_SIZE = 50
SWATCH_LIST = [BLACK, WHITE, RED, GREEN, BLUE, YELLOW, CYAN, MAGENTA,
BROWN, PINK, LIGHT_GRAY, GRAY, ORANGE, DARK_GREEN, DARK_BLUE, PURPLE]
SWATCH_SIZE = int(240 / len(SWATCH_LIST))
FIRST_COLOR = 0
LAST_COLOR = len(SWATCH_LIST)
# Initialize variables
x = CENTER
y = CENTER
brush_size = 15
color_rim = BLACK
color_fill = 0
# Start with a clean white canvas
display.fill(WHITE)
# Function to draw the color picker
def draw_color_picker():
for color in range(FIRST_COLOR, LAST_COLOR):
display.fill_rect(color * SWATCH_SIZE, 0, SWATCH_SIZE, SWATCH_SIZE, color)
display.draw_rect(color * SWATCH_SIZE, 0, SWATCH_SIZE, SWATCH_SIZE, BLACK)
display.fill_rect(color_fill * SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, color_fill)
display.draw_rect(color * SWATCH_SIZE, 0, SWATCH_SIZE, SWATCH_SIZE, BLACK)
draw_color_picker()
while True:
# L+R+U+D buttons erase all drawing
if (buttons.is_pressed(BTN_U) and buttons.is_pressed(BTN_D) and
buttons.is_pressed(BTN_L) and buttons.is_pressed(BTN_R)):
display.fill(WHITE)
draw_color_picker()
continue
# U button increases brush size
if buttons.is_pressed(BTN_U):
brush_size += 1
if brush_size > MAX_BRUSH_SIZE:
brush_size = MAX_BRUSH_SIZE
# D button decreases brush size
elif buttons.is_pressed(BTN_D):
brush_size -= 1
if brush_size < MIN_BRUSH_SIZE:
brush_size = MIN_BRUSH_SIZE
# L button moves color selector to the left
if buttons.was_pressed(BTN_L):
# Erase the previous color selector
display.fill_rect(color_fill * SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, WHITE)
display.draw_rect(color_fill * SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, WHITE)
# Move color selector one position to the left
color_fill = color_fill - 1
# Wrap around to last position if on the far left
if color_fill < FIRST_COLOR:
color_fill = LAST_COLOR - 1
# Draw the filled color rectangle with a black border
display.fill_rect(color_fill * SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, color_fill)
display.draw_rect(color_fill * SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, BLACK)
# R button moves color selector to the right
if buttons.was_pressed(BTN_R):
# Erase the previous color selector
display.fill_rect(color_fill * SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, WHITE)
display.draw_rect(color_fill * SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, WHITE)
# Move color selector one position to the left
color_fill = color_fill + 1
# Wrap around to the first position if on the far right
if color_fill > LAST_COLOR - 1:
color_fill = FIRST_COLOR
# Draw the filled color rectangle with a black border
display.fill_rect(color_fill * SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, color_fill)
display.draw_rect(color_fill * SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, SWATCH_SIZE, BLACK)
# Read the accelerometer sensor and get its raw values and convert to degrees in X and Y
# From Python with CodeX Mission 11: Spirit Level
val = accel.read()
tilt_x = val[0]
tilt_y = val[1]
scaled_x = (tilt_x / 16384) * 90
scaled_y = (tilt_y / 16384) * 90
degrees_x = int(scaled_x)
degrees_y = int(scaled_y)
# Clamp the X angle to -90 an +90
if degrees_x < -90:
degrees_x = -90
elif degrees_x > 90:
degrees_x = 90
# Clamp the Y angle to -90 an +90
if degrees_y < -90:
degrees_y = -90
elif degrees_y > 90:
degrees_y = 90
# Set the new brush center with X/Y tilt angles
x = CENTER + degrees_x
y = CENTER + degrees_y
# A+B buttons toggle black and white outline
if (buttons.is_pressed(BTN_A) and buttons.is_pressed(BTN_B)):
if color_rim == WHITE:
color_rim = BLACK
else:
color_rim = WHITE
# A button erases with a filled white circle
if (buttons.is_pressed(BTN_A)):
for r in range(1, brush_size):
display.fill_circle(x, y, r, WHITE)
# B button draws with a filled color circle
elif (buttons.is_pressed(BTN_B)):
for r in range(1, brush_size):
display.fill_circle(x, y, r, color_fill)
display.draw_circle(x, y, brush_size, color_rim)
Now what?
Upload the code to your CodeX and make some of your own amazing art! If you have an idea of how to improve the Wet Paint app, then step through the code with CodeSpace’s built-in debugger to better understand what’s going on and write your Python code into reality! Be sure to share your new found creativity in the arts and sciences, and most importantly, have fun!
You can download the complete code from our public repository at https://bitbucket.org/firia/labs-demos/src/master/codex/wet_paint_app/.