Difficulty: Intermediate Version: V2
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! |
| 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! |
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); }
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.
Most animations call display.clearDisplay() every frame. This sketch does NOT - that's what makes pixels "stick" like an Etch-A-Sketch.
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" |
static unsigned long holdStart = 0; keeps its value between loop() calls.