Project 3: Grid Explorer - 2D Rotation

Difficulty: Intermediate    Version: V8

Notice the version number: V8. This took 8 iterations to get right. That's normal! Don't expect your first version to be perfect.

What's New

This project introduces 2D rotation math - the foundation for all 3D graphics. You'll control a rotating grid that feels like looking through a window into another dimension.

Concept What It Means
2D Rotation Using sin() and cos() to spin points around a center.
Zoom Changing the grid spacing creates the illusion of moving closer/farther.
Split Axis Horizontal and vertical lines rotate independently - creates wild effects!
Combo Reset UP+DOWN together smoothly returns to default view.

Controls

Touch Action
LEFT (D0)Rotate horizontal lines clockwise
RIGHT (D3)Rotate vertical lines counter-clockwise
UP (D1)Zoom In (smaller grid = closer)
DOWN (D2)Zoom Out (larger grid = farther)
UP + DOWNSmooth reset to default view

The Code

/*
 * Grid Explorer V8.0 (Hold & Combo Reset)
 *
 * Controls:
 * - D0 (Left):  Rotate Horizontal CW
 * - D3 (Right): Rotate Vertical CCW
 * - D1 (Up):    Zoom In
 * - D2 (Down):  Zoom Out
 * - UP + DOWN:  Reset/Return to Default
 */

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

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

// Pins
#define PIN_LEFT 1
#define PIN_UP 2
#define PIN_DOWN 3
#define PIN_RIGHT 4

// State
float displayZoom = 10.0;
float angleH = 0.0;  // Horizontal lines rotation
float angleV = 0.0;  // Vertical lines rotation

// Targets (for reset)
float targetZoom = 10.0;
float targetAngle = 0.0;

// Config
float minZoom = 1.0;
float maxZoom = 60.0;
float twistSpeed = 0.05;
float zoomSpeed = 0.5;

struct Point2D {
  float x, y;
};

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

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

  display.begin(0x3C, true);
  display.clearDisplay();
  display.setTextSize(1);
  display.setTextColor(SH110X_WHITE);
  display.setCursor(30, 30);
  display.print("V8.0 LOADED");
  display.display();
  delay(1000);
}

// THE ROTATION FUNCTION - This is the magic!
Point2D rotate2D(Point2D p, float angle) {
  float c = cos(angle);
  float s = sin(angle);
  return {p.x * c - p.y * s, p.x * s + p.y * c};
}

void drawSplitGrid() {
  float limit = 100.0;
  Point2D start, end, rStart, rEnd;
  int cx = 64;  // Center X
  int cy = 32;  // Center Y

  // Vertical lines (using angleV)
  for (float i = -limit; i <= limit; i += displayZoom) {
    start = {i, -limit};
    end = {i, limit};
    rStart = rotate2D(start, angleV);
    rEnd = rotate2D(end, angleV);
    display.drawLine(cx + (int)rStart.x, cy + (int)rStart.y,
                     cx + (int)rEnd.x, cy + (int)rEnd.y, SH110X_WHITE);
  }

  // Horizontal lines (using angleH)
  for (float i = -limit; i <= limit; i += displayZoom) {
    start = {-limit, i};
    end = {limit, i};
    rStart = rotate2D(start, angleH);
    rEnd = rotate2D(end, angleH);
    display.drawLine(cx + (int)rStart.x, cy + (int)rStart.y,
                     cx + (int)rEnd.x, cy + (int)rEnd.y, SH110X_WHITE);
  }
}

void loop() {
  int thresh = 40000;
  bool tL = touchRead(PIN_LEFT) > thresh;
  bool tR = touchRead(PIN_RIGHT) > thresh;
  bool tU = touchRead(PIN_UP) > thresh;
  bool tD = touchRead(PIN_DOWN) > thresh;

  // Combo: UP + DOWN = Reset
  if (tU && tD) {
    // Smoothly return to defaults
    float zStep = zoomSpeed * 0.4;
    if (abs(displayZoom - targetZoom) > zStep) {
      if (displayZoom < targetZoom) displayZoom += zStep;
      else displayZoom -= zStep;
    } else {
      displayZoom = targetZoom;
    }

    float aStep = twistSpeed * 0.4;
    // Return angles to 0
    if (abs(angleH) > aStep) {
      if (angleH > 0) angleH -= aStep;
      else angleH += aStep;
    } else {
      angleH = 0;
    }
    if (abs(angleV) > aStep) {
      if (angleV > 0) angleV -= aStep;
      else angleV += aStep;
    } else {
      angleV = 0;
    }

  } else {
    // Normal Controls
    if (tU && !tD) displayZoom += zoomSpeed;
    if (tD && !tU) displayZoom -= zoomSpeed;
    if (tL) angleH += twistSpeed;
    if (tR) angleV -= twistSpeed;

    // Clamp zoom
    if (displayZoom < minZoom) displayZoom = minZoom;
    if (displayZoom > maxZoom) displayZoom = maxZoom;
  }

  display.clearDisplay();
  drawSplitGrid();

  // Status bar
  display.fillRect(0, 56, 128, 8, SH110X_BLACK);
  display.drawLine(0, 56, 128, 56, SH110X_WHITE);
  display.setTextSize(1);
  display.setTextColor(SH110X_WHITE);

  if (tU && tD) {
    display.setCursor(35, 57);
    display.print("<< RESET >>");
  } else {
    if (tL) { display.setCursor(2, 57); display.print("L"); }
    if (tU) { display.setCursor(30, 57); display.print("UP"); }
    if (tD) { display.setCursor(60, 57); display.print("DN"); }
    if (tR) { display.setCursor(90, 57); display.print("R"); }
  }

  display.setCursor(105, 57);
  display.print("v8");

  display.display();
  delay(16);
}

The Math: 2D Rotation

Every point (x, y) can be rotated around the origin using this formula:

newX = x * cos(angle) - y * sin(angle)
newY = x * sin(angle) + y * cos(angle)

This is the rotation matrix - the foundation of all computer graphics!

Angle cos(angle) sin(angle) Effect
010No change
PI/2 (90 deg)0190 deg rotation
PI (180 deg)-10Flip

Why Split Axes?

Standard grids rotate all lines together. This code rotates horizontal and vertical lines separately:

When they're different, you get impossible-looking geometric patterns!


Want to Build This Yourself?

Step Gemini Prompt
1. Draw a grid "Draw a grid of lines on SH1106 OLED with adjustable spacing"
2. Add zoom "Make the grid spacing change with UP/DOWN touch buttons"
3. Add rotation "Explain 2D rotation matrix and show how to rotate points with sin/cos"
4. Apply rotation "Rotate my grid lines around the screen center before drawing"
5. Split axes "Use separate rotation angles for horizontal vs vertical lines"

Key Concepts

Next Up: In Project 4, you'll take this math to the third dimension with a wireframe paper airplane!