#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <math.h>
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1
#define SCREEN_ADDRESS 0x3C
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
// Eye & face settings
const int eyeRadius = 22;
const int pupilRadius = 6;
const int eyeBaseLeftX = 32;
const int eyeBaseRightX = 96;
const int eyeBaseY = 24;
const int mouthBaseX = 45;
const int mouthBaseY = 50;
// Pupil behavior
int pupilOffsetX = 0;
int pupilOffsetY = 0;
int currentState = 0; // 0 = default, 1 = look, 2 = second look, 3 = return to center
unsigned long stateStartTime = 0;
unsigned long stateDuration = 0;
int yOffset = 0;
int sideSway = 0;
void drawEyes(int yOffset, int sideSway, int eyeHeight) {
display.clearDisplay();
for (int y = -eyeHeight; y <= eyeHeight; y++) {
int h = sqrt((float)(eyeRadius * eyeRadius) - ((float)(eyeRadius * y) / eyeHeight) * ((float)(eyeRadius * y) / eyeHeight));
display.drawFastHLine(eyeBaseLeftX + sideSway - h, eyeBaseY + yOffset + y, h * 2, WHITE);
display.drawFastHLine(eyeBaseRightX + sideSway - h, eyeBaseY + yOffset + y, h * 2, WHITE);
}
if (eyeHeight > pupilRadius) {
display.fillCircle(eyeBaseLeftX + sideSway + pupilOffsetX, eyeBaseY + yOffset + pupilOffsetY, pupilRadius, BLACK);
display.fillCircle(eyeBaseRightX + sideSway + pupilOffsetX, eyeBaseY + yOffset + pupilOffsetY, pupilRadius, BLACK);
}
display.fillRect(mouthBaseX + sideSway, mouthBaseY + yOffset, 40, 5, WHITE);
display.display();
}
void setPupilOffset(int x, int y) {
pupilOffsetX = x;
pupilOffsetY = y;
}
void enterState(int newState, unsigned long duration) {
currentState = newState;
stateStartTime = millis();
stateDuration = duration;
}
void setup() {
Serial.begin(9600);
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
while (true);
}
display.clearDisplay();
display.display();
randomSeed(analogRead(A0));
enterState(0, random(3000, 4000)); // Start with default face
}
void loop() {
// You can add gentle motion here later if you want
yOffset = 0;
sideSway = 0;
drawEyes(yOffset, sideSway, eyeRadius);
// State timing logic
if (millis() - stateStartTime >= stateDuration) {
switch (currentState) {
case 0: // Default face → dart
setPupilOffset(random(-6, 7), random(-3, 4));
enterState(1, random(300, 600));
break;
case 1: // First dart → maybe second dart or center
if (random(0, 100) < 10) {
setPupilOffset(random(-6, 7), random(-3, 4));
enterState(2, random(300, 500));
} else {
setPupilOffset(0, 0);
enterState(3, 200);
}
break;
case 2: // Second dart → center
setPupilOffset(0, 0);
enterState(3, 200);
break;
case 3: // Back to default
setPupilOffset(0, 0);
enterState(0, random(3000, 4000));
break;
}
}
// 👁 Faster blinking ONLY in default state
if (currentState == 0 && random(0, 100) < 1) { // 1% chance
int blinkSteps = 4;
int blinkDelay = 8;
for (int h = eyeRadius; h >= 0; h -= (eyeRadius / blinkSteps)) {
drawEyes(yOffset, sideSway, h);
delay(blinkDelay);
}
delay(80); // Eyes closed pause
for (int h = 0; h <= eyeRadius; h += (eyeRadius / blinkSteps)) {
drawEyes(yOffset, sideSway, h);
delay(blinkDelay);
}
}
delay(20); // Main loop pace (50 FPS-ish)
}