Desk Pal touch doodle

A round finger-painting pad in a 3D-printed case. Draw with your fingertip, hold still to change ink, shake to erase. Print two parts, click in an ESP32-S3 touch screen, and learn how pixels, colours and timers work.

DifficultyBeginner
Build time40 min
Est. cost$30
Parts4
Files9
Steps13

Things you'll need

QtyPartType
1
Waveshare ESP32-S3-Touch-LCD-1.28 board
Waveshare ESP32-S3-Touch-LCD-1.28 board The brain, a round 1.28-inch touch screen and a motion sensor in one board. No soldering.
electronics
2
Printed parts
Printed parts Back cup and front ring. Both are in the 3MF below on one plate, no supports.
printed
1
USB-C data cable Powers the board and loads the program. It must be a data cable, not a charge-only one.
electronics
11 g
PLA filament Any colour. White looks like a little gadget; black makes the neon inks pop.
consumable

Tools & software

1
Chrome or Edge browser Loads the program onto the board straight from this page. Nothing to install.
1
VS Code + PlatformIO (optional) Free. Only needed if you want to change the code later. See 'Level up' at the end.

Files & downloads

Download all

Flash writes a precompiled image over USB-C in Chrome or Edge. The .ino below is the source — you do not need Arduino IDE unless you want to change the sketch.

What you're building

A tiny round drawing pad. Drag your fingertip across the glass and a glowing line follows it. Hold your finger still and the ink changes colour. Shake the puck and your drawing crumbles into dust and disappears, like a magic sketch toy.

No soldering. No wires. You print two parts, drop in one board, push the halves together, plug in one cable, and click one button on this page to load the program.

Note

The glass knows where your finger is. A tiny touch chip under the glass reports your finger's position as two numbers, x and y, many times a second. The program joins those dots into a line. That is all drawing on a screen ever is.

What you'll learn

  • How a screen is a grid of pixels, and every pixel has an address (x, y)
  • How to keep a drawing inside a circle with one line of Pythagoras
  • How connect-the-dots turns jumpy finger readings into a smooth line
  • How a colour is just three numbers: red, green and blue
  • How a timer tells a tap from a hold, and how a motion sensor feels a shake

Meet the parts

The white printed back cup and front ring next to each other
Two printed parts: the back cup (left) and the front ring (right). Add one board and you're done.
  1. Back cup: holds everything. It has a window in the side for the USB-C plug and a slot in the back, so you can push the board out again later.
  2. Front ring: the bezel around the screen. It has a small notch where the USB-C port goes.
  3. The board: a Waveshare ESP32-S3 with a round 1.28-inch touch screen and a motion sensor built in. Brain, screen and sensor in one.
Tip

Same case, different toy. This round case is shared with the fortune 8-ball. Print one in white for doodling and one in black for the 8-ball, or keep one case and just flash a different program.

Note

Before you start. The printer works on its own for about 25 minutes. You do about 5 minutes of clicking parts together and 2 minutes of loading the program. No tools needed.


Part 1: Print

One plate, two parts, one colour. Start the printer, then read Part 2 while it works.

1

Download the 3MF

Grab Print-ready 3MF from Files & downloads above and open it in Bambu Studio. Using another slicer? Open the STL instead; both parts are in it, already lying flat.

Download the 3MF
2

Pick a colour and print

Settings are already in the file: 0.2 mm layers, 2 walls, 15% infill, no supports. The cup prints with its back on the plate, open side up, and the ring prints flat. About 25 minutes on a Bambu P1S.

Pick a colour and print
3

Check the parts

Run a fingertip around the inside of the cup and the ring. Pick off any strings or blobs, especially around the USB-C window in the side of the cup and the notch in the ring. The board has to slide in without catching.


Part 2: Put it together

No glue, no screws. Three moves: board into the ring, ring into the cup, check the port. If a part feels tight, wiggle it; never force it.

1

Drop the board into the ring

Lay the front ring on the table. Put the board in screen-down, so you are looking at the back of the board with all the little chips. Turn it until the USB-C port sits in the notch of the ring.

Drop the board into the ring
2

Slide it into the cup, USB-C first

Pick up the ring with the board in it. Tilt it and slide the USB-C end in first, so the port pokes into the window in the side of the cup. Then lower the other side down.

Slide it into the cup, USB-C first
3

Press it flat and check the port

Press around the ring until it sits flush with the cup all the way round. Look at the side window: the USB-C port should sit right in the middle, so the cable can plug in straight.

Press it flat and check the port

Part 3: Load the program

New boards come with a factory demo. You are going to replace it with the doodle program, straight from this web page. That takes about a minute.

Note

Word to know: flash. Flashing means copying a program onto a chip. The chip keeps it even when unplugged, like saving a game.

1

Plug in

Connect the USB-C cable from the board to a computer (Windows, Mac, Chromebook or Linux). It must be a data cable, not a charge-only one.

2

Click Flash to board

Scroll up to Files & downloads and click Flash to board. Use Chrome or Edge. Pick the port that appears, click Install, and wait for the green tick.

3

Watch it wake up

The screen goes dark with a thin cyan ring around the edge. That ring is the border of your canvas, and its colour always shows which ink you are holding. Time to draw!

Watch it wake up
Tip

No port in the list? The BOOT button is hidden inside the case. Push the board out through the slot in the back, hold BOOT while you plug the cable in, let go, then click Flash again. Phones and Safari can't flash; use a computer.


Play it

Four things to try, in order.

1

Drag = draw

Touch the glass and drag. A line follows your fingertip. Lift and start again anywhere. The ink stops at the ring, so you can scribble right up to the edge without making a mess.

Drag = draw
2

Hold still = new ink

Put your finger down and don't move it for about 1.5 seconds. The ring changes colour: that's your new ink. Keep holding and it steps again about every second. Six inks: neon red, amber, lemon, lime, cyan and violet.

Hold still = new ink
3

Double-tap = next ink, fast

Two quick taps also step to the next ink. Handy when you're in a hurry.

4

Shake = erase

Lift your finger, hold the puck and its cable together, and give it a few short, sharp shakes. The drawing crumbles into dust and sparks fly. Keep shaking until the canvas is clean. Pressing on the glass never counts as a shake, so you can't wipe your art by accident.


How it works: five tiny lessons

You don't need to read the whole program (it's in Files & downloads if you're curious). Each lesson below shows the one small piece of code that does the job, and gives you something to try.

Lesson 1: A screen is a grid

The screen is a square of 240 × 240 tiny lights called pixels. Every pixel has an address: x counts across from the left, y counts down from the top. The middle is (120, 120).

The screen is round, so the program has to know whether a pixel is inside the circle. It uses Pythagoras: measure how far the pixel is from the middle across (dx) and down (dy). If dx² + dy² is smaller than the radius², the pixel is inside.

touch-doodle.ino
static const int CX = 120;      // middle of the screen, across
static const int CY = 120;      // middle of the screen, down
static const int CLIP_R = 118;  // radius of the drawing circle

static bool inCircle(int x, int y) {
  const int dx = x - CX;                        // how far right of the middle
  const int dy = y - CY;                        // how far below the middle
  return dx * dx + dy * dy <= CLIP_R * CLIP_R;  // Pythagoras: inside the circle?
}

Simplified from inCircle(). Every brush dab asks this question before it paints a pixel.

Tip

Try it on paper: is the pixel (200, 200) inside? dx = 80 and dy = 80, so 80² + 80² = 12,800. The radius² is 118² = 13,924. Inside! Now try (220, 220). (Answer: 100² + 100² = 20,000, so it's outside, on the black corner behind the case.)

Lesson 2: Connect the dots

The touch chip doesn't report a smooth line. It reports separate dots, and when your finger moves fast, the dots land far apart. So for every new dot, the program walks from the last dot to the new one in tiny steps and presses a round brush at each step. No gaps, however fast you flick.

touch-doodle.ino
static void drawStroke(int x0, int y0, int x1, int y1, uint16_t c) {
  int steps = max(abs(x1 - x0), abs(y1 - y0));  // how many pixels apart are the dots?
  if (steps < 1) steps = 1;
  for (int i = 0; i <= steps; i++) {
    const int x = x0 + (x1 - x0) * i / steps;   // walk from the old dot...
    const int y = y0 + (y1 - y0) * i / steps;   // ...to the new one
    stamp(x, y, c, BRUSH);                      // press a round brush, 3 pixels across each way
  }
}

Simplified from drawStroke(). This trick is called linear interpolation.

Tip

Try it: draw one line very slowly and one with a super-fast flick. Both come out joined up. Without drawStroke, the fast one would be a dotted trail.

Lesson 3: A colour is three numbers

Every ink is a mix of red, green and blue light, each from 0 (off) to 255 (full). Lots of red and a bit of blue makes neon red. Red and green together make yellow, because that's how light mixes (paint is different!).

The screen squeezes each colour into 16 bits: 5 for red, 6 for green, 5 for blue. That still gives 65,536 colours. Green gets the extra bit because our eyes are best at telling greens apart.

touch-doodle.ino
static const Ink INKS[] = {
  {"neon-red", {255,  40,  80}},   // {red, green, blue}
  {"amber",    {255, 160,  24}},
  {"lemon",    {255, 230,  40}},   // red + green light = yellow
  {"lime",     { 40, 255, 100}},
  {"cyan",     { 40, 200, 255}},   // the ink it starts with
  {"violet",   {220,  80, 255}},
};

The six inks, exactly as they are in the program.

Tip

Try it: open the Serial Monitor (see Secret commands below) and type color list, then color violet. The ring changes straight away.

Lesson 4: Tap or hold? Ask a stopwatch

Your finger on the glass could mean three things, so the program watches the clock and the distance:

  • Moved more than 10 pixels: you're drawing.
  • Stayed within a small spot for 1.5 seconds: you want a new ink.
  • Lifted again within 0.6 seconds: a tap. Two taps within 0.35 seconds: a double-tap.

millis() is the board's stopwatch: the number of milliseconds since it was switched on.

touch-doodle.ino
// The finger is down, it hasn't drawn anything, and it has barely moved...
if (!longPressFired && now - fingerDownAt > 1500) {  // ...for 1.5 seconds?
  longPressFired = true;
  nextColorAt = now + 900;                           // next step in 0.9 seconds
  cycleInk();                                        // new ink!
} else if (longPressFired && now >= nextColorAt) {   // still holding?
  nextColorAt = now + 900;
  cycleInk();                                        // and another
}

Simplified from holdMaybeCycle().

Tip

Try it: hold your finger still and count "one Mississippi, two Mississippi". The ink should change halfway through the second Mississippi.

Lesson 5: Feeling a shake

The board has a motion sensor that measures pushes in three directions (an accelerometer) and spinning (a gyroscope). Lying still, it feels exactly 1 g: gravity pulling down. A shake pushes it far above or below 1 g.

One bump shouldn't wipe your picture, so the program uses a leaky bucket. Every reading, the bucket loses 16% of what's in it and each jolt pours a little in. A single bump leaks away. Steady shaking fills it past the line, and the eraser starts.

touch-doodle.ino
const float dev = fabsf(mag - 1.0f);                             // how far from plain gravity (1 g)?
const float accTerm = dev > 0.28f ? (dev - 0.28f) * 0.48f : 0;   // ignore tiny wobbles
const float gyroTerm = gyro > 130 ? (gyro - 130) / 1300.0f : 0;  // spinning counts too
shakeEnergy = shakeEnergy * 0.84f + accTerm + gyroTerm;          // the leaky bucket
if (shakeEnergy > 2.35f) shaking = true;                         // full enough: erase!

Simplified from updateMotion(). While your finger is on the glass, the bucket is emptied, so drawing never erases.

Tip

Try it: type status in the Serial Monitor right after a shake and look at energy=. Then put the puck on the table and tap the table hard. Does the energy get past 2.35?

Note

Why doesn't it flicker? The drawing lives in a sprite: a 240 × 240 picture kept in memory (about 115 KB). The program paints into the sprite, then copies the whole thing to the screen in one go. Like finishing a drawing before you hold it up.


Secret commands

Your computer can chat with the board through the same USB-C cable. That chat is called serial. Open the Serial Monitor in VS Code (the plug icon in the PlatformIO bar) or any serial app at 115200, type a word, press Enter.

Type this What happens
help lists the commands and the six inks
color steps to the next ink
color list names every ink with its number
color lime or color 3 picks an ink by name or number
clear wipes the canvas instantly, no shaking needed
status shows the ink, the shake energy and how many strokes you've drawn
Tip

It keeps score. Every 5 seconds the board prints its status by itself, including strokes=: how many lines you've drawn since it was switched on. Challenge a friend: who can draw a cat in the fewest strokes?


Level up: change the code

Ready to make it yours? The easiest way to change the code is PlatformIO, a free add-on for VS Code. It downloads the right libraries and settings for this board by itself.

  1. Install VS Code, then open Extensions and install PlatformIO IDE.
  2. Download platformio.zip from Files & downloads, unzip it, and open the touch-doodle-platformio folder in VS Code (File → Open Folder).
  3. Open touch-doodle/touch-doodle.ino. Change one of the numbers below, then save.
  4. Plug in the board and click the → (Upload) arrow in the blue bar at the bottom of VS Code. The first build takes a few minutes while it downloads everything; after that it takes seconds.

Ideas, easiest first:

  • Fatter brush: change BRUSH = 3 to BRUSH = 6.
  • Faster ink change: set LONG_PRESS_MS to 800 (0.8 seconds).
  • Start with a different ink: inkIndex = 4 is cyan. Try 0 for neon red.
  • Add a seventh ink: add one line to INKS, like the pink one below. The program counts the inks by itself.
touch-doodle.ino
static const Ink INKS[] = {
  {"neon-red", {255,  40,  80}},
  ...
  {"violet",   {220,  80, 255}},
  {"pink",     {255, 120, 200}},   // your new ink: {red, green, blue}
};

One ink is one line. Keep each number between 0 and 255.


If something goes wrong

  • Screen stays black after flashing. Unplug and plug back in. Still black? Try a different USB-C cable; many are charge-only.
  • No port shows up. Push the board out of the case, hold BOOT while you plug it in, let go, and click Flash again.
  • The ink keeps changing instead of drawing. You're holding still too long at the start of a line. Start moving within a second.
  • Shaking does nothing. Lift your finger off the glass first. Short, sharp shakes work better than slow wobbles.
  • Lines appear in the wrong place, like a mirror image. In the code, set TOUCH_FLIP_X or TOUCH_FLIP_Y to true and upload again.
  • The cable won't plug in. The board is a bit crooked. Press the ring down flat and check that the port sits in the middle of the side window.

Taking it apart

The back of the white cup with its slot, held in a hand
**Need to take it apart?** Unplug the cable. Push through the slot in the back of the cup with a fingertip or the blunt end of a pencil. The ring and the board pop out of the front.

Build photos

Licensed under CC BY-SA 4.0. Check with the author before commercial use.

Community makes

Built this? Share a photo of your print so others can see how it turned out.

No makes yet. Be the first.