Skip to content

Lab 17: Tilt-a-Sketch

Pixel says...

Pixel waves hello You've drawn pictures by typing letters. Now let's draw by tilting! A blinking dot is your pen. Tip the kit and it rolls, leaving a trail of color behind. Let's light this up!

Program files: 17-tilt-a-sketch.py and tilt_a_sketch.py

What you'll learn

  • How a list can remember a whole picture while you draw it
  • How a few variables hold the state of the pen
  • How the word not flips True to False and back
  • How to spot a shake with the tilt sensor
  • How to draw in layers: the picture first, then the pen on top

What you'll need

  • Your whole kit: the matrix, the accelerometer, and both buttons, wired as shown in the Kit Guide
  • These files saved on the Pico: config.py, font.py, kit.py, tilt_a_sketch.py, and 17-tilt-a-sketch.py
  • Thonny open and connected to your Pico
  • The rolling ball from Lab 16 and the pictures from Lab 9

The program

This program turns the kit into a drawing toy. You tilt to move the pen, and the pen paints every pixel it rolls over.

This lab has two files, like Lab 15. The short one, 17-tilt-a-sketch.py, is the one you run. It starts the module.

17-tilt-a-sketch.py
# Lab 17: Tilt-a-Sketch
# Filename: 17-tilt-a-sketch.py
# Version: 1.1.0
#
# Draw your own picture by tilting the kit. A blinking dot is your pen.
# Tilt the board and the pen rolls toward the low side, leaving a line of
# color behind it.
#   Button 1 (GPIO 14): change the pen color
#   Button 2 (GPIO 15): lift the pen up, or put it back down
#   Shake the kit hard: erase the whole picture
# The code lives in the module tilt_a_sketch.py (with the shared helpers in
# kit.py) so that program 19 can load it as a mode too. Upload all the .py
# files with ./upload-code.sh before you run this.
# Not yet tried by hand: the tilt and buttons need a person to draw with them.

import tilt_a_sketch

print("Lab 17: Tilt-a-Sketch (version 1.1.0)")

tilt_a_sketch.run()

The real work lives in the module tilt_a_sketch.py. Keeping the sketch in its own module lets the demo program 19-template-main.py load it as one of its modes.

tilt_a_sketch.py
# Module: Tilt-a-Sketch (program 17, and a mode of program 19)
# Filename: tilt_a_sketch.py
# Version: 1.0.0
#
# Draw your own picture by tilting the kit. A blinking dot is your pen.
# Tilt the board and the pen rolls toward the low side, leaving a line of
# color behind it.
#   Button 1 (GPIO 14): change the pen color
#   Button 2 (GPIO 15): lift the pen up, or put it back down
#   Shake the kit hard: erase the whole picture
# Run it on its own with 17-tilt-a-sketch.py.
# In program 19 the two buttons change modes, so the sketch leaves them
# alone. run() takes this setting:
#   auto_color - True to move to the next pen color with every step, so the
#                line comes out in many colors without pressing a button

from utime import ticks_ms, ticks_diff
import kit

# the pen colors, in the order button 1 steps through them. The picture can
# fill all 64 pixels, so these numbers stay small.
PEN_COLORS = [
    ("red", (24, 0, 0)),
    ("orange", (24, 8, 0)),
    ("yellow", (20, 16, 0)),
    ("green", (0, 24, 0)),
    ("blue", (0, 0, 24)),
    ("violet", (14, 0, 22)),
    ("white", (14, 14, 14)),
]
PEN_UP_COLOR = (30, 30, 30)     # the pen blinks white while it is lifted

# the rolling pen
TILT_MIN = 0.25         # tilts smaller than this (in g) do nothing
TILT_FULL = 0.8         # tipping this far rolls the pen at full speed
STEP_SLOW_MS = 420      # time per pixel at the gentlest tilt
STEP_FAST_MS = 140      # time per pixel when tipped all the way

SHAKE_G = 1.7           # a pull along the board stronger than this is a shake
BLINK_MS = 250          # how long the pen stays on, then off
LOOP_MS = 10            # the program looks at the tilt 100 times a second

# the picture so far: one color for every spot, in (x, y) order
canvas = [kit.OFF] * kit.NUMBER_PIXELS

pen_x = kit.WIDTH // 2
pen_y = kit.HEIGHT // 2
pen_down = True
color_number = 0

def spot(x, y):
    # where the color for column x, row y is kept in the canvas list
    return y * kit.WIDTH + x

def bright(color):
    # the pen shows a brighter copy of its color, so you can find it
    return (color[0] * 2, color[1] * 2, color[2] * 2)

def show(pen_lit):
    # draw the picture, then the pen on top of it
    for y in range(kit.HEIGHT):
        for x in range(kit.WIDTH):
            kit.strip[kit.xy(x, y)] = canvas[spot(x, y)]
    if pen_down:
        if pen_lit:
            kit.strip[kit.xy(pen_x, pen_y)] = bright(PEN_COLORS[color_number][1])
        else:
            kit.strip[kit.xy(pen_x, pen_y)] = kit.OFF
    elif pen_lit:
        kit.strip[kit.xy(pen_x, pen_y)] = PEN_UP_COLOR
    kit.strip.write()

def tilt_direction(g):
    # -1, 0 or 1: which way a tilt pushes, or 0 if it is too small
    if g >= TILT_MIN:
        return 1
    if g <= -TILT_MIN:
        return -1
    return 0

def run(settings=None):
    # run() changes the pen's state, so it says these are the variables
    # at the top of the file, not new ones of its own
    global pen_x, pen_y, pen_down, color_number
    auto_color = settings is not None and settings["auto_color"]
    # in program 19 the buttons belong to the mode changer
    use_buttons = not kit.mode_buttons_on

    canvas[spot(pen_x, pen_y)] = PEN_COLORS[color_number][1]
    if use_buttons:
        print("Button 1: next color. Button 2: pen up or down. Shake: erase.")
    else:
        print("Tilt to draw. Shake: erase.")
    print("Pen color:", PEN_COLORS[color_number][0])

    last1 = 1
    last2 = 1
    last_step = ticks_ms()
    last_blink = ticks_ms()
    pen_lit = True
    show(pen_lit)

    while True:
        changed = False

        # the buttons: a press is a change from 1 to 0
        if use_buttons:
            now1 = kit.button1.value()
            now2 = kit.button2.value()
            if now1 == 0 and last1 == 1:
                color_number = (color_number + 1) % len(PEN_COLORS)
                print("Pen color:", PEN_COLORS[color_number][0])
                if pen_down:
                    canvas[spot(pen_x, pen_y)] = PEN_COLORS[color_number][1]
                changed = True
            if now2 == 0 and last2 == 1:
                pen_down = not pen_down
                print("Pen down" if pen_down else "Pen up")
                if pen_down:
                    canvas[spot(pen_x, pen_y)] = PEN_COLORS[color_number][1]
                changed = True
            last1 = now1
            last2 = now2

        gx, gy = kit.read_tilt()
        now = ticks_ms()

        # a hard shake erases the picture
        if (gx * gx + gy * gy) ** 0.5 > SHAKE_G:
            print("Shake! The picture is erased.")
            for i in range(kit.NUMBER_PIXELS):
                canvas[i] = kit.OFF
            if pen_down:
                canvas[spot(pen_x, pen_y)] = PEN_COLORS[color_number][1]
            last_step = now + 500       # hold the pen still for half a second
            changed = True

        # the tilt rolls the pen, faster when you tip farther
        size = max(abs(gx), abs(gy))
        if size >= TILT_MIN:
            speed = min(1, (size - TILT_MIN) / (TILT_FULL - TILT_MIN))
            interval = STEP_SLOW_MS - (STEP_SLOW_MS - STEP_FAST_MS) * speed
            if ticks_diff(now, last_step) >= interval:
                # move one pixel along the direction tipped hardest
                if abs(gx) >= abs(gy):
                    new_x = pen_x + tilt_direction(gx)
                    new_y = pen_y
                else:
                    new_x = pen_x
                    new_y = pen_y + tilt_direction(gy)
                # the pen stops at the edges of the matrix
                if 0 <= new_x < kit.WIDTH and 0 <= new_y < kit.HEIGHT:
                    pen_x = new_x
                    pen_y = new_y
                    if auto_color:
                        color_number = (color_number + 1) % len(PEN_COLORS)
                    if pen_down:
                        canvas[spot(pen_x, pen_y)] = PEN_COLORS[color_number][1]
                    last_step = now
                    changed = True

        # blink the pen
        if ticks_diff(now, last_blink) >= BLINK_MS:
            pen_lit = not pen_lit
            last_blink = now
            changed = True

        if changed:
            show(pen_lit)
        kit.wait(LOOP_MS)

Run 17-tilt-a-sketch.py and hold the kit flat. A red dot blinks near the middle. Tip the kit a little and the dot rolls toward the low side, leaving a red line. Hold the kit level to stop.

To do this Do this
Move the pen Tilt the kit. A bigger tilt moves it faster
Change the pen color Press Button 1
Lift the pen, or put it back down Press Button 2
Erase the whole picture Shake the kit hard

When the pen is lifted, it blinks white and does not paint. That is how you move to a new spot without leaving a line. The Shell tells you what is happening:

Lab 17: Tilt-a-Sketch (version 1.1.0)
Button 1: next color. Button 2: pen up or down. Shake: erase.
Pen color: red
Pen color: orange
Pen up
Pen down
Shake! The picture is erased.

A simulated 8x8 LED matrix showing a square spiral drawn in one line. The line starts red in the middle and changes to orange, yellow, green, blue, violet, and white as it winds outward. One bright pixel at the end of the line is the pen.

This picture was drawn by a computer simulator. A script tilted a pretend kit to draw the spiral, so your real drawing will look different.

How it works

A list that remembers the picture

tilt_a_sketch.py (lines 43 to 53)
# the picture so far: one color for every spot, in (x, y) order
canvas = [kit.OFF] * kit.NUMBER_PIXELS

pen_x = kit.WIDTH // 2
pen_y = kit.HEIGHT // 2
pen_down = True
color_number = 0

def spot(x, y):
    # where the color for column x, row y is kept in the canvas list
    return y * kit.WIDTH + x

The list canvas holds 64 colors, one for every pixel. It starts with every spot set to off. When the pen rolls over a spot, the program stores the pen's color there. A real canvas is the cloth a painter paints on.

Why not paint straight onto the matrix? Because the pen blinks. The blinking pen would wipe out the pixel under it. So the picture stays safe in canvas, and the matrix is redrawn from it each time something changes.

The four variables after it are the state of the pen. State means everything the program must remember right now. That is where the pen is, whether it is down, and which color it holds.

The function run changes the state as you draw. Its first line is global pen_x, pen_y, pen_down, color_number. The word global tells Python to change the variables at the top of the file. Without it, run would make new variables of its own, and show would never see the pen move.

The function spot(x, y) turns a column and a row into a place in the list. It uses the same math as xy from Lab 7: row times 8, plus column.

Draw in layers

tilt_a_sketch.py (lines 59 to 71)
def show(pen_lit):
    # draw the picture, then the pen on top of it
    for y in range(kit.HEIGHT):
        for x in range(kit.WIDTH):
            kit.strip[kit.xy(x, y)] = canvas[spot(x, y)]
    if pen_down:
        if pen_lit:
            kit.strip[kit.xy(pen_x, pen_y)] = bright(PEN_COLORS[color_number][1])
        else:
            kit.strip[kit.xy(pen_x, pen_y)] = kit.OFF
    elif pen_lit:
        kit.strip[kit.xy(pen_x, pen_y)] = PEN_UP_COLOR
    kit.strip.write()

The function show draws in two layers. First the nested loop copies the whole canvas to the matrix. Then the pen is drawn on top.

The pen blinks so that you can find it. When the pen is down, it flips between a bright copy of its color and off. When the pen is up, it flips between white and the picture underneath.

Buttons change the state

tilt_a_sketch.py (lines 116 to 121)
            if now2 == 0 and last2 == 1:
                pen_down = not pen_down
                print("Pen down" if pen_down else "Pen up")
                if pen_down:
                    canvas[spot(pen_x, pen_y)] = PEN_COLORS[color_number][1]
                changed = True

Look at the line pen_down = not pen_down. The word not flips a value. If pen_down was True, it becomes False. If it was False, it becomes True. One button can switch something on and off this way.

Button 1 works like the picture show in Lab 10. It adds 1 to color_number and uses % to wrap around after the last color.

All the button code sits inside if use_buttons:. In the demo program 19-template-main.py, the buttons switch between modes. There, the sketch leaves the buttons alone.

Shake to erase

tilt_a_sketch.py (lines 128 to 136)
        # a hard shake erases the picture
        if (gx * gx + gy * gy) ** 0.5 > SHAKE_G:
            print("Shake! The picture is erased.")
            for i in range(kit.NUMBER_PIXELS):
                canvas[i] = kit.OFF
            if pen_down:
                canvas[spot(pen_x, pen_y)] = PEN_COLORS[color_number][1]
            last_step = now + 500       # hold the pen still for half a second
            changed = True

The numbers gx and gy are the pull along the board, left to right and top to bottom. The if line joins them into one number: the total pull along the board. It squares each one, adds them, and takes the square root. The ** 0.5 part is the square root.

Gravity alone can never pull harder than about 1 g. A shake can. So if the total goes above SHAKE_G, which is 1.7, the program knows you shook the kit.

Here is one example. A shake gives gx = 1.5 and gy = 1.0.

Step Math Answer
Square each one 1.5 × 1.5 and 1.0 × 1.0 2.25 and 1.00
Add them 2.25 + 1.00 3.25
Take the square root the square root of 3.25 about 1.80

1.80 is bigger than 1.7, so the picture is erased. The loop sets every spot in canvas back to off.

Stay on the matrix

tilt_a_sketch.py (lines 151 to 160)
                # the pen stops at the edges of the matrix
                if 0 <= new_x < kit.WIDTH and 0 <= new_y < kit.HEIGHT:
                    pen_x = new_x
                    pen_y = new_y
                    if auto_color:
                        color_number = (color_number + 1) % len(PEN_COLORS)
                    if pen_down:
                        canvas[spot(pen_x, pen_y)] = PEN_COLORS[color_number][1]
                    last_step = now
                    changed = True

The pen takes one step at a time, the same way the ball did in the maze. Before it moves, this check makes sure the new spot is on the matrix. Columns and rows both go from 0 to 7. If the new spot would be off the edge, the pen stays where it is.

The two auto_color lines are for the demo program. Its buttons are busy changing modes, so the pen moves to the next color with every step instead.

Key idea

A drawing program keeps two things apart: the picture it remembers and the picture it shows. The pen, the blinking, and the erasing all change what is shown without losing what is remembered.

Try it yourself

For questions 2 to 5, open tilt_a_sketch.py in Thonny, change it, and save it onto the Pico. Then run 17-tilt-a-sketch.py again.

  1. Draw the first letter of your name. Use Button 2 to lift the pen between lines.
  2. Add a pen color. Add the line ("pink", (24, 4, 12)), to the PEN_COLORS list. How many presses of Button 1 does it take to reach pink?
  3. Start in a corner. Change pen_x = kit.WIDTH // 2 to pen_x = 0, and do the same for pen_y. Where does the pen start now?
  4. Make the pen slower. Raise STEP_FAST_MS from 140 to 300. Is it easier to draw neat lines?
  5. Change the shake. Set SHAKE_G = 1.2. Draw something, then stand the kit up on its edge and tap it. What happens? Why is 1.7 a safer number?

Check your understanding

  1. What does the list canvas remember? How many items does it hold?
  2. What does the word not do to pen_down?
  3. Why can a slow tilt never erase the picture?
  4. The pen is at column 7 and you tilt to the right. What does the pen do, and which line of code decides that?
  5. Why does the program draw the pen after it draws the canvas?

Lab complete!

Pixel celebrates You built your own drawing toy! You are the artist now, and gravity is your paintbrush.

What's next: In Lab 18: Modes, you will load twelve light shows from one menu with your two buttons.