#include <Wire.h> // Used for I2C communication (SDA/SCL lines)
#include <Adafruit_GFX.h> // Core graphics library for shapes, fonts
#include <Adafruit_SSD1306.h> // OLED driver for SSD1306 chip
#define SCREEN_WIDTH 128 // Width of OLED display in pixels
#define SCREEN_HEIGHT 64 // Height of OLED display in pixels
#define OLED_RESET -1 // No reset pin used; pass -1 to disable
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Create OLED display object with screen size and I2C bus
// === Eye Size and Center Position ===
int eyeX = 44; // X-position of eye (centered horizontally)
int eyeY = 12; // Y-position of eye (centered vertically)
int eyeWidth = 40; // Width of eye (in pixels)
int eyeHeight = 40; // Height of eye
// === Eye Movement Variables ===
int targetOffsetX = 0; // Where the eye *wants* to go horizontally
int targetOffsetY = 0; // Where the eye *wants* to go vertically
int moveSpeed = 3; // Controls smoothness of eye motion (lower = smoother)
// === Blinking Logic ===
int blinkState = 0; // 0 = eye open, 1 = eye closed
int blinkDelay = 4000; // Delay between blinks (in ms)
unsigned long lastBlinkTime = 0; // Timestamp of the last blink event
unsigned long moveTime = 0; // Timestamp of last eye movement trigger
void setup() {
display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // Start OLED with internal voltage pump at I2C addr 0x3C
display.display(); // Show splash screen (Adafruit logo) by default
delay(1000); // Wait for 1 second
display.clearDisplay(); // Clear splash screen to draw our own content
}
void loop() {
unsigned long currentTime = millis(); // Get the current time in milliseconds
// === Blink Start: Close the eye ===
if (currentTime - lastBlinkTime > blinkDelay && blinkState == 0) {
blinkState = 1; // Change state to 'eye closed'
lastBlinkTime = currentTime; // Update timestamp of blink start
}
// === Blink End: Open the eye after 150ms ===
if (currentTime - lastBlinkTime > 150 && blinkState == 1) {
// Explanation: 150ms is the time the eye stays closed. It looks realistic.
blinkState = 0; // Change state to 'eye open'
lastBlinkTime = currentTime; // Update timestamp to prevent repeated triggers
}
// === Random Eye Movement Trigger ===
if (currentTime - moveTime > random(1500, 3000) && blinkState == 0) {
// Explanation: Every 1.5 to 3 seconds, choose a new direction (if not blinking)
int direction = random(0, 6); // Pick random direction from 0 to 5
// Set new target offsets depending on direction
if (direction == 0) { // Move left
targetOffsetX = -8;
targetOffsetY = 0;
}
else if (direction == 1) { // Move right
targetOffsetX = 8;
targetOffsetY = 0;
}
else if (direction == 2) { // Move up
targetOffsetX = 0;
targetOffsetY = -6;
}
else if (direction == 3) { // Move down
targetOffsetX = 0;
targetOffsetY = 6;
}
else { // Center (default)
targetOffsetX = 0;
targetOffsetY = 0;
}
moveTime = currentTime; // Update movement timestamp
}
// === Smooth Eye Movement Logic ===
static int offsetX = 0; // Actual current X offset (for easing)
static int offsetY = 0; // Actual current Y offset (for easing)
// Easing formula: slowly move offset towards target
offsetX += (targetOffsetX - offsetX) / moveSpeed;
offsetY += (targetOffsetY - offsetY) / moveSpeed;
// === Clear the screen for redrawing ===
display.clearDisplay(); // Wipe display clean before drawing the next frame
// === Draw Eye (Open or Blinking) ===
if (blinkState == 0) {
// Eye is open → draw full rounded rectangle
drawEye(eyeX + offsetX, eyeY + offsetY, eyeWidth, eyeHeight);
} else {
// Eye is closed → draw a flat line (blink effect)
display.fillRect(eyeX + offsetX, eyeY + offsetY + eyeHeight / 2 - 2,
eyeWidth, 4, WHITE);
// Explanation: Draws a small white rectangle horizontally across the center
}
// === Refresh OLED to show what we just drew ===
display.display(); // Push all graphics to screen
delay(30); // Short delay to reduce screen flicker
}
// === Custom Function: Draw Rounded Eye ===
void drawEye(int x, int y, int w, int h) {
display.fillRoundRect(x, y, w, h, 8, WHITE); // Rounded white rectangle = eyeball
}