#include <Arduino.h>
#include <U8g2lib.h>
#include <Wire.h>
// Choose your display type (uncomment one)
#define USING_SH1106
//#define USING_SSD1306
// Pin Definitions for ESP32
#define OLED_SDA 21
#define OLED_SCL 22
#define OLED_RST -1 // -1 if not used
#define OLED_ADDR 0x3C
#ifdef USING_SH1106
U8G2_SH1106_128X64_NONAME_F_HW_I2C display(U8G2_R0, OLED_RST, OLED_SCL, OLED_SDA);
#else
U8G2_SSD1306_128X64_NONAME_F_HW_I2C display(U8G2_R0, OLED_RST, OLED_SCL, OLED_SDA);
#endif
enum Mood {
HAPPY,
ANGRY,
CRYING,
SLEEPING,
CURIOUS
};
class KawaiBot {
private:
struct EyePosition {
float velocityX = 0;
float velocityY = 0;
float leftX = 0;
float leftY = 0;
float rightX = 0;
float rightY = 0;
};
// Constants
const float GRAVITY = 0.2;
const float DAMPING = 0.95;
const float MAX_VELOCITY = 3.0;
const unsigned long MIN_BLINK_INTERVAL = 2000;
const unsigned long MAX_BLINK_INTERVAL = 6000;
const unsigned long BLINK_DURATION = 150;
const int LEFT_EYE_X = 40;
const int RIGHT_EYE_X = 88;
const int EYE_Y = 25;
// State variables
Mood currentMood;
EyePosition eyePos;
unsigned long lastBlink;
unsigned long blinkStartTime;
bool isBlinking;
float blinkProgress;
unsigned long lastFrameUpdate;
unsigned long lastMoodChange;
void updateGooglyEyes() {
// Apply gravity to shared velocity
eyePos.velocityY += GRAVITY;
// Random momentum changes
if (random(100) < 10) {
float randX = random(-100, 100) / 100.0;
float randY = random(-100, 100) / 100.0;
eyePos.velocityX += randX;
eyePos.velocityY += randY;
}
// Apply shared velocity
eyePos.leftX += eyePos.velocityX;
eyePos.leftY += eyePos.velocityY;
eyePos.rightX += eyePos.velocityX;
eyePos.rightY += eyePos.velocityY;
// Constrain eyes
const float maxRadius = 5.0;
// Left eye boundary
float leftDist = sqrt(eyePos.leftX * eyePos.leftX + eyePos.leftY * eyePos.leftY);
if (leftDist > maxRadius) {
float angle = atan2(eyePos.leftY, eyePos.leftX);
eyePos.leftX = cos(angle) * maxRadius;
eyePos.leftY = sin(angle) * maxRadius;
float normalX = eyePos.leftX / leftDist;
float normalY = eyePos.leftY / leftDist;
float dotProduct = eyePos.velocityX * normalX + eyePos.velocityY * normalY;
eyePos.velocityX = (-2 * dotProduct * normalX + eyePos.velocityX) * DAMPING;
eyePos.velocityY = (-2 * dotProduct * normalY + eyePos.velocityY) * DAMPING;
}
// Right eye boundary
float rightDist = sqrt(eyePos.rightX * eyePos.rightX + eyePos.rightY * eyePos.rightY);
if (rightDist > maxRadius) {
float angle = atan2(eyePos.rightY, eyePos.rightX);
eyePos.rightX = cos(angle) * maxRadius;
eyePos.rightY = sin(angle) * maxRadius;
float normalX = eyePos.rightX / rightDist;
float normalY = eyePos.rightY / rightDist;
float dotProduct = eyePos.velocityX * normalX + eyePos.velocityY * normalY;
eyePos.velocityX = (-2 * dotProduct * normalX + eyePos.velocityX) * DAMPING;
eyePos.velocityY = (-2 * dotProduct * normalY + eyePos.velocityY) * DAMPING;
}
// Apply damping and constraints
eyePos.velocityX *= DAMPING;
eyePos.velocityY *= DAMPING;
eyePos.velocityX = constrain(eyePos.velocityX, -MAX_VELOCITY, MAX_VELOCITY);
eyePos.velocityY = constrain(eyePos.velocityY, -MAX_VELOCITY, MAX_VELOCITY);
}
void handleBlinking() {
unsigned long currentTime = millis();
if (!isBlinking) {
if (currentTime - lastBlink >= random(MIN_BLINK_INTERVAL, MAX_BLINK_INTERVAL)) {
isBlinking = true;
blinkStartTime = currentTime;
}
} else {
unsigned long blinkTime = currentTime - blinkStartTime;
if (blinkTime >= BLINK_DURATION) {
isBlinking = false;
lastBlink = currentTime;
}
blinkProgress = (float)blinkTime / BLINK_DURATION;
blinkProgress = sin(blinkProgress * PI);
}
}
void drawEyes(int baseSize) {
if (isBlinking) {
float eyeHeight = baseSize * (1.0 - sin(blinkProgress * PI));
if (eyeHeight < 1) eyeHeight = 1;
display.drawEllipse(LEFT_EYE_X, EYE_Y, baseSize, eyeHeight/2);
display.drawEllipse(RIGHT_EYE_X, EYE_Y, baseSize, eyeHeight/2);
} else {
display.drawCircle(LEFT_EYE_X, EYE_Y, baseSize);
display.drawCircle(RIGHT_EYE_X, EYE_Y, baseSize);
display.drawDisc(
LEFT_EYE_X + eyePos.leftX,
EYE_Y + eyePos.leftY,
3
);
display.drawDisc(
RIGHT_EYE_X + eyePos.rightX,
EYE_Y + eyePos.rightY,
3
);
}
}
public:
KawaiBot() : currentMood(HAPPY), lastBlink(0), blinkStartTime(0),
isBlinking(false), blinkProgress(0), lastFrameUpdate(0),
lastMoodChange(0) {}
void begin() {
Wire.begin(OLED_SDA, OLED_SCL);
Serial.begin(115200);
if (OLED_RST != -1) {
pinMode(OLED_RST, OUTPUT);
digitalWrite(OLED_RST, HIGH);
delay(100);
digitalWrite(OLED_RST, LOW);
delay(100);
digitalWrite(OLED_RST, HIGH);
}
display.begin();
display.setFont(u8g2_font_6x10_tr);
display.setFontRefHeightExtendedText();
display.setDrawColor(1);
display.setFontPosTop();
display.setFontDirection(0);
}
void setMood(Mood mood) {
if (mood != currentMood) {
currentMood = mood;
Serial.print("Mood changed to: ");
Serial.println(getMoodString());
}
}
void setRandomMood() {
setMood((Mood)random(5));
}
Mood getMood() {
return currentMood;
}
const char* getMoodString() {
switch (currentMood) {
case HAPPY: return "HAPPY";
case ANGRY: return "ANGRY";
case CRYING: return "CRYING";
case SLEEPING: return "SLEEPING";
case CURIOUS: return "CURIOUS";
default: return "UNKNOWN";
}
}
void update() {
unsigned long currentTime = millis();
// Handle blinking
handleBlinking();
// Update googly eyes
if (currentTime - lastFrameUpdate >= 16) { // ~60fps
updateGooglyEyes();
lastFrameUpdate = currentTime;
}
// Draw everything
display.clearBuffer();
drawEyes(8);
drawMouth(currentMood);
display.sendBuffer();
}
private:
void drawMouth(Mood mood) {
switch (mood) {
case HAPPY: {
int mouthY = 45 + sin(millis() * 0.003) * 2;
display.drawEllipse(64, mouthY, 15, 8);
display.drawBox(49, mouthY - 8, 31, 8);
break;
}
case ANGRY: {
int mouthY = 45 + sin(millis() * 0.003) * 2;
display.drawTriangle(
54, mouthY,
74, mouthY,
64, mouthY + 10
);
break;
}
case CRYING: {
display.drawEllipse(64, 55, 15, 8);
int tearOffset = (millis() / 100) % 20;
display.drawBox(35, 35 + tearOffset, 3, 8);
display.drawBox(83, 35 + tearOffset, 3, 8);
break;
}
case SLEEPING: {
int zOffset = (millis() / 250) % 3;
display.drawGlyph(60 + zOffset, 45 - zOffset, 'z');
display.drawGlyph(67 + zOffset, 40 - zOffset, 'z');
display.drawGlyph(74 + zOffset, 35 - zOffset, 'Z');
break;
}
case CURIOUS: {
int mouthSize = 3 + sin(millis() * 0.003) * 2;
display.drawCircle(64, 45, mouthSize);
break;
}
}
}
};
// Global bot instance
KawaiBot bot;
void setup() {
bot.begin();
}
void loop() {
// Example of how to change moods:
// bot.setMood(HAPPY);
// bot.setMood(ANGRY);
// etc...
// For now, change mood randomly every 8 seconds
static unsigned long lastMoodChange = 0;
if (millis() - lastMoodChange >= 8000) {
bot.setRandomMood();
lastMoodChange = millis();
}
bot.update();
}