Project 2: Capacitive Touch Etch-A-Sketch

Difficulty: Intermediate    Version: V2


What's New

Building on the D-Pad from Project 1, we now use touch input to control a cursor that draws on screen. Key new concepts:

Concept What It Means
Persistent Drawing We DON'T clear the whole screen each frame - pixels stay where we draw them.
Float Cursor Using float (decimal) for position allows smooth, slow movement.
UI Overlay A small controller display that updates without erasing the drawing.
Combo Gesture Hold all 4 buttons for 2 seconds to erase - like shaking an Etch-A-Sketch!

Controls

Touch Action
LEFT (D0)Move cursor left
RIGHT (D3)Move cursor right
UP (D1)Move cursor up
DOWN (D2)Move cursor down
ALL 4 (hold 2s)Erase everything!

The Code

Same pin mapping as Project 1. Copy, paste, upload:

/*
 * EtchASketch_CapTouch.ino
 * V2: Erase Gesture (Hold All 4 Buttons for 2s)
 *
 * Logic:
 * - NO display.clearDisplay() in loop - pixels persist!
 * - Draw pixels at cursor position.
 * - Hold L+U+R+D > 2000ms -> Clear Screen & Reset
 */

#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>
#include <Wire.h>

Adafruit_SH1106G display = Adafruit_SH1106G(128, 64, &Wire, -1);

// Touch Pins (same as Project 1)
const int PIN_LEFT = 1;
const int PIN_UP = 2;
const int PIN_DOWN = 3;
const int PIN_RIGHT = 4;

float cursorX = 64;
float cursorY = 32;

void setup() {
  Serial.begin(115200);

  Wire.begin(5, 6);
  Wire.setClock(400000);

  display.begin(0x3C, true);
  display.clearDisplay();

  // Draw frame
  display.drawRect(0, 0, 128, 64, SH110X_WHITE);
  display.setTextSize(1);
  display.setTextColor(SH110X_WHITE);
  display.setCursor(2, 2);
  display.print("Etch-A-Sketch");

  display.display();
}

void drawMiniArrow(int x, int y, int dir, bool active) {
  int color = SH110X_WHITE;
  if (active) {
    display.fillCircle(x, y, 4, SH110X_WHITE);
    color = SH110X_BLACK;
  } else {
    display.drawCircle(x, y, 4, SH110X_WHITE);
  }

  // Mini Arrows
  if (dir == 0) { // Left
    display.drawLine(x + 2, y, x - 2, y, color);
    display.drawLine(x - 2, y, x, y - 2, color);
    display.drawLine(x - 2, y, x, y + 2, color);
  } else if (dir == 1) { // Up
    display.drawLine(x, y + 2, x, y - 2, color);
    display.drawLine(x, y - 2, x - 2, y, color);
    display.drawLine(x, y - 2, x + 2, y, color);
  } else if (dir == 2) { // Down
    display.drawLine(x, y - 2, x, y + 2, color);
    display.drawLine(x, y + 2, x - 2, y, color);
    display.drawLine(x, y + 2, x + 2, y, color);
  } else if (dir == 3) { // Right
    display.drawLine(x - 2, y, x + 2, y, color);
    display.drawLine(x + 2, y, x, y - 2, color);
    display.drawLine(x + 2, y, x, y + 2, color);
  }
}

void loop() {
  int vL = touchRead(PIN_LEFT);
  int vU = touchRead(PIN_UP);
  int vD = touchRead(PIN_DOWN);
  int vR = touchRead(PIN_RIGHT);

  bool tL = vL > 80000;
  bool tU = vU > 80000;
  bool tD = vD > 80000;
  bool tR = vR > 80000;

  // Move cursor (0.8 pixels per frame for smooth movement)
  if (tL) cursorX -= 0.8;
  if (tR) cursorX += 0.8;
  if (tU) cursorY -= 0.8;
  if (tD) cursorY += 0.8;

  // Keep cursor inside border
  if (cursorX < 1) cursorX = 1;
  if (cursorX > 126) cursorX = 126;
  if (cursorY < 1) cursorY = 1;
  if (cursorY > 62) cursorY = 62;

  // ERASE GESTURE: Hold all 4 for 2 seconds
  static unsigned long holdStart = 0;

  if (tL && tU && tD && tR) {
    if (holdStart == 0) {
      holdStart = millis();
    } else {
      if (millis() - holdStart > 2000) {
        // ERASE!
        display.clearDisplay();
        display.drawRect(0, 0, 128, 64, SH110X_WHITE);
        display.setTextSize(1);
        display.setTextColor(SH110X_WHITE);
        display.setCursor(2, 2);
        display.print("Etch-A-Sketch");

        cursorX = 64;
        cursorY = 32;

        display.display();
        delay(500);
        holdStart = 0;
      }
    }
  } else {
    holdStart = 0;
  }

  // 1. Draw pixel at cursor (this STAYS on screen)
  display.drawPixel((int)cursorX, (int)cursorY, SH110X_WHITE);

  // 2. Erase controller area (bottom right corner)
  display.fillRect(88, 36, 40, 28, SH110X_BLACK);

  // 3. Redraw controller overlay
  display.drawRect(88, 36, 40, 28, SH110X_WHITE);

  int cx = 88 + 20;
  int cy = 36 + 14;
  int sp = 9;

  drawMiniArrow(cx - sp, cy, 0, tL);
  drawMiniArrow(cx, cy - sp, 1, tU);
  drawMiniArrow(cx, cy + sp, 2, tD);
  drawMiniArrow(cx + sp, cy, 3, tR);

  // Show erase progress bar
  if (holdStart > 0) {
    int progress = map(millis() - holdStart, 0, 2000, 0, 36);
    if (progress > 36) progress = 36;
    display.fillRect(90, 60, progress, 2, SH110X_WHITE);
  }

  display.display();
  delay(10);
}

Why Float Instead of Int?

Compare these two approaches:

Integer Cursor Float Cursor
int cursorX = 64; float cursorX = 64;
cursorX += 1; cursorX += 0.8;
Jumps 1 pixel per frame = 60 pixels/second Moves 0.8 pixels per frame = 48 pixels/second
Fast, jerky Smooth, controllable

The (int)cursorX cast converts back to integer only when drawing.


Understanding Persistent Drawing

Most animations call display.clearDisplay() every frame. This sketch does NOT - that's what makes pixels "stick" like an Etch-A-Sketch.

The Trick: We selectively erase only the controller area (fillRect with BLACK), then redraw the controller. The drawing area is never touched.

Want to Build This Yourself?

Build piece by piece:

Step Gemini Prompt
1. Moving cursor "Modify my D-Pad code to move a cursor position instead of just showing button states"
2. Persistent pixel "How do I draw pixels that stay on screen without clearing display each frame?"
3. UI overlay "How do I update just part of an OLED screen while keeping the rest unchanged?"
4. Hold gesture "How do I detect when multiple buttons are held for 2 seconds using millis()?"
5. Progress bar "Add a progress bar that fills up while holding buttons"

Key Concepts

Next Up: In Project 3, you'll learn 2D rotation math to create an interactive grid that spins and zooms!