Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- /********* Pleasedontcode.com **********
- Pleasedontcode thanks you for automatic code generation! Enjoy your code!
- - Terms and Conditions:
- You have a non-exclusive, revocable, worldwide, royalty-free license
- for personal and commercial use. Attribution is optional; modifications
- are allowed, but you're responsible for code maintenance. We're not
- liable for any loss or damage. For full terms,
- please visit pleasedontcode.com/termsandconditions.
- - Project: # Emotion Display
- - Version: 002
- - Source Code NOT compiled for: ESP32S3 Dev Module
- - Source Code created on: 2026-03-09 21:11:32
- ********* Pleasedontcode.com **********/
- /****** SYSTEM REQUIREMENTS *****/
- /****** SYSTEM REQUIREMENT 1 *****/
- /* Procedural face animation system: render eyes with */
- /* physics-based blinking, pupil tracking with */
- /* inertia damping, iris glow effects, eyelids, */
- /* eyebrows, mouth expressions, cheeks, blush, nose */
- /* on 128x64 OLED at 45FPS */
- /****** SYSTEM REQUIREMENT 2 *****/
- /* Emotion system with 8 states: neutral, happy, sad, */
- /* excited, sleepy, curious, shy, playful. Each */
- /* emotion modifies facial features, animation speed, */
- /* and micro-movement patterns for lifelike */
- /* expression */
- /****** SYSTEM REQUIREMENT 3 *****/
- /* Touch button input cycles through emotions */
- /* sequentially. When idle (no touch for 5 seconds), */
- /* enter autonomous baby-like behavior mode with */
- /* random gestures, exploration, micro-movements, and */
- /* periodic state changes */
- /****** SYSTEM REQUIREMENT 4 *****/
- /* Modular architecture with separate animation */
- /* modules, physics engine, emotion state machine. */
- /* Code fully commented, optimized for ESP32-S3 */
- /* performance, minimal memory footprint */
- /****** END SYSTEM REQUIREMENTS *****/
- /* START CODE */
- /****** DEFINITION OF LIBRARIES *****/
- #include <Adafruit_SSD1306.h> // https://github.com/adafruit/Adafruit_SSD1306
- #include <Adafruit_GFX.h> // https://github.com/adafruit/Adafruit-GFX-Library
- #include <EasyButton.h> // https://github.com/evert-arias/EasyButton
- /****** SYSTEM REQUIREMENTS *****/
- // SR1: Procedural face animation system: render eyes with physics-based blinking,
- // pupil tracking with inertia damping, iris glow effects, eyelids, eyebrows,
- // mouth expressions, cheeks, blush, nose on 128x64 OLED at 45FPS
- // SR2: Emotion system with 8 states: neutral, happy, sad, excited, sleepy, curious,
- // shy, playful. Each emotion modifies facial features, animation speed, and
- // micro-movement patterns for lifelike expression
- // SR3: Touch button input cycles through emotions sequentially. When idle (no touch
- // for 5 seconds), enter autonomous baby-like behavior mode with random gestures,
- // exploration, micro-movements, and periodic state changes
- // SR4: Modular architecture with separate animation modules, physics engine, emotion
- // state machine. Code fully commented, optimized for ESP32-S3 performance,
- // minimal memory footprint
- /****** FUNCTION PROTOTYPES *****/
- void setup(void);
- void loop(void);
- /***** DEFINITION OF DIGITAL INPUT PINS *****/
- const uint8_t faceDisplay_PushButton_PIN_D1 = 1;
- /***** DEFINITION OF DIGITAL OUTPUT PINS *****/
- const uint8_t emotionButton_LED_PIN_D2 = 2;
- /****** OLED DISPLAY CONFIGURATION *****/
- #define SCREEN_WIDTH 128
- #define SCREEN_HEIGHT 64
- #define OLED_RESET -1
- #define SCREEN_ADDRESS 0x3C
- // Create display instance using I2C
- Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
- /****** EASY BUTTON CONFIGURATION *****/
- EasyButton button(faceDisplay_PushButton_PIN_D1);
- /****** EMOTION ENUMERATION *****/
- enum Emotion {
- NEUTRAL = 0,
- HAPPY = 1,
- SAD = 2,
- EXCITED = 3,
- SLEEPY = 4,
- CURIOUS = 5,
- SHY = 6,
- PLAYFUL = 7
- };
- /****** PHYSICS ENGINE STRUCTURE *****/
- struct PhysicsState {
- // Eye pupil tracking physics
- float pupilX; // Current pupil X position (-1.0 to 1.0)
- float pupilY; // Current pupil Y position (-1.0 to 1.0)
- float pupilVelX; // Pupil velocity X (inertia)
- float pupilVelY; // Pupil velocity Y (inertia)
- float targetPupilX; // Target pupil X position
- float targetPupilY; // Target pupil Y position
- // Blinking physics
- float blinkValue; // 0.0 (open) to 1.0 (closed)
- float blinkVel; // Blinking velocity for smooth animation
- bool isBlinking; // Currently blinking
- unsigned long blinkStartTime;
- // Eyelid animation
- float eyelidOffset; // Additional eyelid movement beyond blink
- // Eyebrow animation
- float eyebrowAngle; // Eyebrow rotation in degrees
- float eyebrowTargetAngle;
- // Iris glow
- float irisGlow; // 0.0 to 1.0 glow intensity
- // Mouth expression
- float mouthHeight; // Mouth curvature
- float mouthAngle; // Mouth angle/tilt
- // Cheek blush
- float cheekBlush; // Cheek color intensity
- // Nose animation
- float noseWiggle; // Nose movement micro-animation
- };
- /****** ANIMATION STATE STRUCTURE *****/
- struct AnimationState {
- Emotion currentEmotion;
- Emotion targetEmotion;
- // Autonomous behavior
- bool autonomousMode;
- unsigned long lastInputTime;
- unsigned long idleThreshold; // 5000ms = 5 seconds
- // Animation timing
- unsigned long frameTime; // Current frame timestamp
- unsigned long lastFrameTime;
- float deltaTime; // Time since last frame in seconds
- uint32_t frameCount; // Total frames rendered
- // Animation phase for cyclic behaviors
- float animationPhase; // 0.0 to 1.0, wraps around
- float phaseVelocity; // Controls animation speed based on emotion
- // Micro-movement - FIXED: moved from PhysicsState to AnimationState
- float microMoveX; // Subtle position jitter
- float microMoveY; // Subtle position jitter
- // Gaze direction
- float gazeX; // Where the eyes are looking
- float gazeY;
- };
- /****** EMOTION CONFIGURATION STRUCTURE *****/
- struct EmotionConfig {
- float animationSpeed; // How fast animations play
- float blinkFrequency; // How often to blink (blinks per second)
- float pupilTrackingIntensity; // How responsive pupil is to gaze
- float microMoveAmount; // Amount of micro-movement jitter
- int8_t eyebrowAngle; // Base eyebrow angle in degrees
- float mouthHeight; // Base mouth curve height
- float cheekColor; // Cheek blush intensity (0.0-1.0)
- float irisGlowIntensity; // Iris glow brightness
- };
- /****** GLOBAL STATE VARIABLES *****/
- PhysicsState physics;
- AnimationState animation;
- EmotionConfig emotionConfigs[8];
- /****** INITIALIZE EMOTION CONFIGURATIONS *****/
- void initializeEmotionConfigs() {
- // NEUTRAL: calm, baseline state
- emotionConfigs[NEUTRAL] = {
- 1.0f, // animationSpeed
- 1.5f, // blinkFrequency (blinks per second)
- 0.7f, // pupilTrackingIntensity
- 0.5f, // microMoveAmount
- 0, // eyebrowAngle
- 0.3f, // mouthHeight
- 0.1f, // cheekColor
- 0.3f // irisGlowIntensity
- };
- // HAPPY: cheerful, animated
- emotionConfigs[HAPPY] = {
- 1.5f, // animationSpeed (faster)
- 2.0f, // blinkFrequency
- 0.8f, // pupilTrackingIntensity
- 1.0f, // microMoveAmount (more movement)
- 15, // eyebrowAngle (raised)
- 0.7f, // mouthHeight (big smile)
- 0.6f, // cheekColor (strong blush)
- 0.5f // irisGlowIntensity
- };
- // SAD: depressed, slower movements
- emotionConfigs[SAD] = {
- 0.5f, // animationSpeed (slower)
- 0.8f, // blinkFrequency
- 0.5f, // pupilTrackingIntensity
- 0.2f, // microMoveAmount
- -15, // eyebrowAngle (lowered)
- -0.5f, // mouthHeight (frown)
- 0.2f, // cheekColor
- 0.1f // irisGlowIntensity
- };
- // EXCITED: very animated, rapid blinks
- emotionConfigs[EXCITED] = {
- 2.0f, // animationSpeed (very fast)
- 3.0f, // blinkFrequency
- 0.9f, // pupilTrackingIntensity
- 1.5f, // microMoveAmount
- 20, // eyebrowAngle
- 0.8f, // mouthHeight
- 0.7f, // cheekColor
- 0.7f // irisGlowIntensity
- };
- // SLEEPY: slow, droopy eyes
- emotionConfigs[SLEEPY] = {
- 0.3f, // animationSpeed
- 0.3f, // blinkFrequency (frequent half-blinks)
- 0.3f, // pupilTrackingIntensity
- 0.1f, // microMoveAmount
- -20, // eyebrowAngle (relaxed)
- 0.2f, // mouthHeight
- 0.05f, // cheekColor
- 0.05f // irisGlowIntensity
- };
- // CURIOUS: searching movements, alert
- emotionConfigs[CURIOUS] = {
- 1.3f, // animationSpeed
- 1.2f, // blinkFrequency
- 1.0f, // pupilTrackingIntensity (maximum)
- 0.8f, // microMoveAmount
- 10, // eyebrowAngle
- 0.4f, // mouthHeight
- 0.2f, // cheekColor
- 0.6f // irisGlowIntensity
- };
- // SHY: minimal movements, down-looking
- emotionConfigs[SHY] = {
- 0.6f, // animationSpeed
- 1.0f, // blinkFrequency
- 0.4f, // pupilTrackingIntensity
- 0.3f, // microMoveAmount
- -10, // eyebrowAngle
- 0.1f, // mouthHeight
- 0.4f, // cheekColor (blushing)
- 0.2f // irisGlowIntensity
- };
- // PLAYFUL: bouncy, dynamic movements
- emotionConfigs[PLAYFUL] = {
- 1.8f, // animationSpeed
- 2.5f, // blinkFrequency
- 0.85f, // pupilTrackingIntensity
- 1.3f, // microMoveAmount
- 12, // eyebrowAngle
- 0.6f, // mouthHeight
- 0.5f, // cheekColor
- 0.6f // irisGlowIntensity
- };
- }
- /****** PHYSICS ENGINE: UPDATE PUPIL TRACKING *****/
- void updatePupilTracking(float deltaTime) {
- EmotionConfig& config = emotionConfigs[animation.currentEmotion];
- // Calculate target pupil position based on gaze
- physics.targetPupilX = animation.gazeX * config.pupilTrackingIntensity;
- physics.targetPupilY = animation.gazeY * config.pupilTrackingIntensity;
- // Inertia damping: smooth acceleration towards target
- float inertiaFactor = 5.0f * config.pupilTrackingIntensity;
- float maxVelocity = 2.0f;
- // Update velocity with acceleration towards target
- physics.pupilVelX += (physics.targetPupilX - physics.pupilX) * inertiaFactor * deltaTime;
- physics.pupilVelY += (physics.targetPupilY - physics.pupilY) * inertiaFactor * deltaTime;
- // Clamp velocity
- physics.pupilVelX = constrain(physics.pupilVelX, -maxVelocity, maxVelocity);
- physics.pupilVelY = constrain(physics.pupilVelY, -maxVelocity, maxVelocity);
- // Apply damping (friction)
- float damping = 0.92f;
- physics.pupilVelX *= damping;
- physics.pupilVelY *= damping;
- // Update position
- physics.pupilX += physics.pupilVelX * deltaTime;
- physics.pupilY += physics.pupilVelY * deltaTime;
- // Clamp pupil position within eye bounds (-1.0 to 1.0)
- physics.pupilX = constrain(physics.pupilX, -1.0f, 1.0f);
- physics.pupilY = constrain(physics.pupilY, -1.0f, 1.0f);
- }
- /****** PHYSICS ENGINE: UPDATE BLINKING *****/
- void updateBlinking(float deltaTime) {
- EmotionConfig& config = emotionConfigs[animation.currentEmotion];
- // Calculate blink duration based on emotion
- float blinkDuration = 0.15f; // 150ms standard blink
- float blinkInterval = 1.0f / config.blinkFrequency;
- if (!physics.isBlinking) {
- // Check if it's time to blink
- float timeSinceBlink = (millis() - physics.blinkStartTime) / 1000.0f;
- // Add some randomness to blink timing
- float randomBlink = 0.8f + (random(40) / 100.0f); // 0.8 to 1.2 variation
- if (timeSinceBlink > blinkInterval * randomBlink) {
- physics.isBlinking = true;
- physics.blinkStartTime = millis();
- physics.blinkVel = 4.0f / blinkDuration; // Velocity to close eyes in blinkDuration
- }
- } else {
- // Animate blinking
- float timeSinceBlink = (millis() - physics.blinkStartTime) / 1000.0f;
- if (timeSinceBlink < 0.075f) {
- // Closing phase (first 75ms)
- physics.blinkValue += physics.blinkVel * deltaTime;
- physics.blinkValue = min(physics.blinkValue, 1.0f);
- } else if (timeSinceBlink < 0.15f) {
- // Opening phase (next 75ms)
- physics.blinkValue -= physics.blinkVel * deltaTime;
- physics.blinkValue = max(physics.blinkValue, 0.0f);
- } else {
- // Blink complete
- physics.isBlinking = false;
- physics.blinkValue = 0.0f;
- physics.blinkStartTime = millis();
- }
- }
- }
- /****** PHYSICS ENGINE: UPDATE EYEBROW ANIMATION *****/
- void updateEyebrows(float deltaTime) {
- EmotionConfig& config = emotionConfigs[animation.currentEmotion];
- // Base eyebrow angle from emotion config
- physics.eyebrowTargetAngle = config.eyebrowAngle;
- // Add micro-animations based on emotion phase
- float emotionInfluence = sin(animation.animationPhase * M_PI * 2.0f);
- switch(animation.currentEmotion) {
- case HAPPY:
- case EXCITED:
- physics.eyebrowTargetAngle += emotionInfluence * 5.0f;
- break;
- case CURIOUS:
- physics.eyebrowTargetAngle += abs(emotionInfluence) * 3.0f;
- break;
- case SHY:
- physics.eyebrowTargetAngle -= abs(emotionInfluence) * 2.0f;
- break;
- default:
- break;
- }
- // Smoothly interpolate to target angle
- float lerpFactor = 8.0f * deltaTime;
- physics.eyebrowAngle += (physics.eyebrowTargetAngle - physics.eyebrowAngle) * lerpFactor;
- }
- /****** PHYSICS ENGINE: UPDATE MOUTH EXPRESSION *****/
- void updateMouthExpression(float deltaTime) {
- EmotionConfig& config = emotionConfigs[animation.currentEmotion];
- // Base mouth height from emotion
- float targetMouthHeight = config.mouthHeight;
- // Add animation variation
- float animVariation = sin(animation.animationPhase * M_PI * 2.0f);
- targetMouthHeight += animVariation * 0.1f * config.animationSpeed;
- // Add mouth angle (smile tilt)
- physics.mouthAngle = config.eyebrowAngle * 0.3f;
- // Smooth interpolation
- float lerpFactor = 6.0f * deltaTime;
- physics.mouthHeight += (targetMouthHeight - physics.mouthHeight) * lerpFactor;
- }
- /****** PHYSICS ENGINE: UPDATE CHEEK BLUSH *****/
- void updateCheeks(float deltaTime) {
- EmotionConfig& config = emotionConfigs[animation.currentEmotion];
- // Base cheek blush from emotion
- float targetCheekBlush = config.cheekColor;
- // Pulse the blush slightly based on animation phase
- float pulseInfluence = (sin(animation.animationPhase * M_PI * 2.0f) + 1.0f) * 0.5f;
- targetCheekBlush += pulseInfluence * 0.15f * config.animationSpeed;
- // Smooth interpolation
- float lerpFactor = 3.0f * deltaTime;
- physics.cheekBlush += (targetCheekBlush - physics.cheekBlush) * lerpFactor;
- physics.cheekBlush = constrain(physics.cheekBlush, 0.0f, 1.0f);
- }
- /****** PHYSICS ENGINE: UPDATE IRIS GLOW *****/
- void updateIrisGlow(float deltaTime) {
- EmotionConfig& config = emotionConfigs[animation.currentEmotion];
- // Base glow intensity from emotion
- float targetGlow = config.irisGlowIntensity;
- // Pulse glow subtly
- float glowPulse = (sin(animation.animationPhase * M_PI * 4.0f) + 1.0f) * 0.5f;
- targetGlow += glowPulse * 0.1f;
- // Smooth interpolation
- float lerpFactor = 4.0f * deltaTime;
- physics.irisGlow += (targetGlow - physics.irisGlow) * lerpFactor;
- physics.irisGlow = constrain(physics.irisGlow, 0.0f, 1.0f);
- }
- /****** PHYSICS ENGINE: UPDATE NOSE WIGGLE *****/
- void updateNose(float deltaTime) {
- // Nose wiggles subtly with high-frequency animation
- physics.noseWiggle = sin(animation.animationPhase * M_PI * 8.0f) * 0.5f;
- }
- /****** PHYSICS ENGINE: UPDATE MICRO-MOVEMENTS *****/
- void updateMicroMovements(float deltaTime) {
- EmotionConfig& config = emotionConfigs[animation.currentEmotion];
- // Generate micro-movement using noise-like pattern
- float time = millis() / 1000.0f;
- // Use sine waves at different frequencies to create natural micro-jitter
- // FIXED: Now correctly assigning to animation.microMoveX and animation.microMoveY
- animation.microMoveX = sin(time * 2.3f) * 0.5f + sin(time * 3.7f) * 0.3f;
- animation.microMoveY = cos(time * 1.9f) * 0.5f + cos(time * 4.1f) * 0.3f;
- // Scale by emotion's micro-move amount
- animation.microMoveX *= config.microMoveAmount;
- animation.microMoveY *= config.microMoveAmount;
- }
- /****** ANIMATION ENGINE: UPDATE GAZE *****/
- void updateGaze(float deltaTime) {
- // Generate gaze pattern based on emotion
- float time = millis() / 1000.0f;
- switch(animation.currentEmotion) {
- case NEUTRAL:
- // Gentle scanning
- animation.gazeX = sin(time * 0.5f) * 0.7f;
- animation.gazeY = cos(time * 0.7f) * 0.5f;
- break;
- case HAPPY:
- // Looking upward and around
- animation.gazeX = sin(time * 0.8f) * 0.8f;
- animation.gazeY = 0.5f + cos(time * 0.6f) * 0.3f;
- break;
- case SAD:
- // Looking downward
- animation.gazeX = sin(time * 0.3f) * 0.4f;
- animation.gazeY = -0.6f + sin(time * 0.5f) * 0.2f;
- break;
- case EXCITED:
- // Rapid, wide scanning
- animation.gazeX = sin(time * 1.5f);
- animation.gazeY = cos(time * 1.8f);
- break;
- case SLEEPY:
- // Slowly drooping gaze
- animation.gazeX = sin(time * 0.2f) * 0.3f;
- animation.gazeY = -0.5f - abs(sin(time * 0.3f)) * 0.3f;
- break;
- case CURIOUS:
- // Focused searching
- animation.gazeX = sin(time * 1.2f) * 0.9f;
- animation.gazeY = cos(time * 1.0f) * 0.8f;
- break;
- case SHY:
- // Shy glancing down
- animation.gazeX = sin(time * 0.4f) * 0.3f;
- animation.gazeY = -0.7f + sin(time * 0.6f) * 0.2f;
- break;
- case PLAYFUL:
- // Bouncy, playful eye movement
- animation.gazeX = sin(time * 1.3f) * 0.85f;
- animation.gazeY = abs(sin(time * 1.6f)) * 0.7f;
- break;
- }
- // Clamp gaze to valid range
- animation.gazeX = constrain(animation.gazeX, -1.0f, 1.0f);
- animation.gazeY = constrain(animation.gazeY, -1.0f, 1.0f);
- }
- /****** ANIMATION ENGINE: UPDATE AUTONOMOUS BEHAVIOR *****/
- void updateAutonomousBehavior(float deltaTime) {
- // Generate random gestures and state changes in autonomous mode
- static float autonomousPhase = 0.0f;
- // Slowly accumulate phase
- autonomousPhase += deltaTime * 0.2f;
- if (autonomousPhase > 1.0f) {
- autonomousPhase -= 1.0f;
- // Randomly change emotion during autonomous mode
- if (random(100) < 30) { // 30% chance each cycle
- animation.targetEmotion = (Emotion)(random(8));
- }
- }
- // Add more pronounced micro-movements in autonomous mode
- float autonomousJitter = sin(millis() / 500.0f) * 0.3f;
- animation.microMoveX += autonomousJitter;
- animation.microMoveY += cos(millis() / 700.0f) * 0.3f;
- }
- /****** ANIMATION ENGINE: UPDATE ANIMATION PHASE *****/
- void updateAnimationPhase(float deltaTime) {
- EmotionConfig& config = emotionConfigs[animation.currentEmotion];
- // Update animation phase (cycles 0.0 to 1.0)
- animation.animationPhase += deltaTime * config.animationSpeed;
- if (animation.animationPhase > 1.0f) {
- animation.animationPhase -= 1.0f;
- }
- }
- /****** ANIMATION ENGINE: UPDATE EMOTION TRANSITION *****/
- void updateEmotionTransition(float deltaTime) {
- // Smooth transition between emotions
- if (animation.currentEmotion != animation.targetEmotion) {
- // Immediate emotion change (can be made smoother if needed)
- animation.currentEmotion = animation.targetEmotion;
- }
- }
- /****** ANIMATION ENGINE: MAIN UPDATE LOOP *****/
- void updateAnimation() {
- // Calculate delta time
- unsigned long currentTime = millis();
- animation.frameTime = currentTime;
- if (animation.lastFrameTime == 0) {
- animation.lastFrameTime = currentTime;
- }
- animation.deltaTime = (currentTime - animation.lastFrameTime) / 1000.0f;
- animation.lastFrameTime = currentTime;
- // Cap delta time to prevent physics instability
- if (animation.deltaTime > 0.033f) {
- animation.deltaTime = 0.033f;
- }
- // Update physics systems
- updatePupilTracking(animation.deltaTime);
- updateBlinking(animation.deltaTime);
- updateEyebrows(animation.deltaTime);
- updateMouthExpression(animation.deltaTime);
- updateCheeks(animation.deltaTime);
- updateIrisGlow(animation.deltaTime);
- updateNose(animation.deltaTime);
- updateMicroMovements(animation.deltaTime);
- updateGaze(animation.deltaTime);
- // Update animation control
- updateAnimationPhase(animation.deltaTime);
- updateEmotionTransition(animation.deltaTime);
- // Update autonomous behavior if active
- if (animation.autonomousMode) {
- updateAutonomousBehavior(animation.deltaTime);
- }
- // Increment frame count
- animation.frameCount++;
- }
- /****** RENDERING: DRAW LEFT EYE *****/
- void drawLeftEye(int16_t centerX, int16_t centerY, int16_t radius) {
- // Eye white (sclera)
- display.fillCircle(centerX, centerY, radius, SSD1306_WHITE);
- // Apply eyelid closure (blink effect)
- if (physics.blinkValue > 0.1f) {
- int16_t eyelidHeight = (int16_t)(radius * 2.0f * physics.blinkValue);
- // Top eyelid
- display.fillRect(centerX - radius, centerY - radius, radius * 2, eyelidHeight, SSD1306_BLACK);
- }
- // Calculate pupil position with physics
- int16_t pupilX = centerX + (int16_t)(physics.pupilX * (radius - 5));
- int16_t pupilY = centerY + (int16_t)(physics.pupilY * (radius - 5));
- // Iris (darker circle)
- display.fillCircle(pupilX, pupilY, radius / 2, SSD1306_BLACK);
- // Pupil (very dark center)
- display.fillCircle(pupilX, pupilY, radius / 3, SSD1306_BLACK);
- // Iris glow effect (small highlights)
- if (physics.irisGlow > 0.0f) {
- int8_t glowIntensity = (int8_t)(physics.irisGlow * 255);
- if (glowIntensity > 50) {
- display.drawCircle(pupilX - 2, pupilY - 2, 2, SSD1306_WHITE);
- }
- }
- }
- /****** RENDERING: DRAW RIGHT EYE *****/
- void drawRightEye(int16_t centerX, int16_t centerY, int16_t radius) {
- // Eye white (sclera)
- display.fillCircle(centerX, centerY, radius, SSD1306_WHITE);
- // Apply eyelid closure (blink effect)
- if (physics.blinkValue > 0.1f) {
- int16_t eyelidHeight = (int16_t)(radius * 2.0f * physics.blinkValue);
- // Top eyelid
- display.fillRect(centerX - radius, centerY - radius, radius * 2, eyelidHeight, SSD1306_BLACK);
- }
- // Calculate pupil position with physics
- int16_t pupilX = centerX + (int16_t)(physics.pupilX * (radius - 5));
- int16_t pupilY = centerY + (int16_t)(physics.pupilY * (radius - 5));
- // Iris (darker circle)
- display.fillCircle(pupilX, pupilY, radius / 2, SSD1306_BLACK);
- // Pupil (very dark center)
- display.fillCircle(pupilX, pupilY, radius / 3, SSD1306_BLACK);
- // Iris glow effect (small highlights)
- if (physics.irisGlow > 0.0f) {
- int8_t glowIntensity = (int8_t)(physics.irisGlow * 255);
- if (glowIntensity > 50) {
- display.drawCircle(pupilX - 2, pupilY - 2, 2, SSD1306_WHITE);
- }
- }
- }
- /****** RENDERING: DRAW EYEBROWS *****/
- void drawEyebrows(int16_t leftEyeX, int16_t rightEyeX, int16_t eyeY, int16_t eyeRadius) {
- int16_t eyebrowY = eyeY - eyeRadius - 8;
- int16_t eyebrowWidth = eyeRadius + 5;
- int16_t eyebrowHeight = 3;
- // Convert angle to slope for drawing
- int8_t angleOffset = (int8_t)(physics.eyebrowAngle * 0.3f);
- // Left eyebrow (with angle)
- display.drawLine(leftEyeX - eyebrowWidth, eyebrowY + angleOffset,
- leftEyeX + eyebrowWidth, eyebrowY - angleOffset, SSD1306_WHITE);
- display.drawLine(leftEyeX - eyebrowWidth, eyebrowY + angleOffset + 1,
- leftEyeX + eyebrowWidth, eyebrowY - angleOffset + 1, SSD1306_WHITE);
- // Right eyebrow (with angle)
- display.drawLine(rightEyeX - eyebrowWidth, eyebrowY - angleOffset,
- rightEyeX + eyebrowWidth, eyebrowY + angleOffset, SSD1306_WHITE);
- display.drawLine(rightEyeX - eyebrowWidth, eyebrowY - angleOffset + 1,
- rightEyeX + eyebrowWidth, eyebrowY + angleOffset + 1, SSD1306_WHITE);
- }
- /****** RENDERING: DRAW MOUTH *****/
- void drawMouth(int16_t centerX, int16_t centerY) {
- int16_t mouthWidth = 20;
- int16_t mouthY = centerY + 10;
- // Mouth curve based on expression
- int16_t curveHeight = (int16_t)(physics.mouthHeight * 8.0f);
- if (curveHeight > 0) {
- // Happy/smiling mouth (arc)
- for (int16_t x = -mouthWidth; x <= mouthWidth; x += 2) {
- int16_t y = (int16_t)(curveHeight - (x * x) / (float)(mouthWidth * mouthWidth) * curveHeight);
- display.drawPixel(centerX + x, mouthY + y, SSD1306_WHITE);
- }
- } else if (curveHeight < 0) {
- // Sad mouth (inverted arc)
- for (int16_t x = -mouthWidth; x <= mouthWidth; x += 2) {
- int16_t y = (int16_t)(curveHeight + (x * x) / (float)(mouthWidth * mouthWidth) * abs(curveHeight));
- display.drawPixel(centerX + x, mouthY + y, SSD1306_WHITE);
- }
- } else {
- // Neutral mouth (straight line)
- display.drawLine(centerX - mouthWidth, mouthY, centerX + mouthWidth, mouthY, SSD1306_WHITE);
- }
- }
- /****** RENDERING: DRAW NOSE *****/
- void drawNose(int16_t centerX, int16_t centerY) {
- int16_t noseX = centerX + (int16_t)(physics.noseWiggle * 2.0f);
- int16_t noseY = centerY - 2;
- // Simple nose (small triangle)
- display.drawLine(noseX, noseY, noseX - 2, noseY + 3, SSD1306_WHITE);
- display.drawLine(noseX, noseY, noseX + 2, noseY + 3, SSD1306_WHITE);
- display.drawLine(noseX - 2, noseY + 3, noseX + 2, noseY + 3, SSD1306_WHITE);
- }
- /****** RENDERING: DRAW CHEEKS *****/
- void drawCheeks(int16_t leftEyeX, int16_t rightEyeX, int16_t eyeY) {
- int16_t cheekRadius = 5;
- int16_t cheekY = eyeY + 10;
- // Cheek blush intensity determines if we draw
- if (physics.cheekBlush > 0.1f) {
- // Left cheek
- for (int16_t i = 0; i < cheekRadius; i++) {
- display.drawCircle(leftEyeX - 20, cheekY, i, SSD1306_WHITE);
- }
- // Right cheek
- for (int16_t i = 0; i < cheekRadius; i++) {
- display.drawCircle(rightEyeX + 20, cheekY, i, SSD1306_WHITE);
- }
- }
- }
- /****** RENDERING: MAIN DRAW FUNCTION *****/
- void drawFace() {
- // Clear display
- display.clearDisplay();
- // Calculate face position with micro-movements
- // FIXED: Now correctly using animation.microMoveX and animation.microMoveY
- int16_t faceCenterX = SCREEN_WIDTH / 2 + (int16_t)(animation.microMoveX * 2.0f);
- int16_t faceCenterY = SCREEN_HEIGHT / 2 + (int16_t)(animation.microMoveY * 2.0f);
- int16_t eyeRadius = 8;
- int16_t eyeSeparation = 30;
- int16_t eyeY = faceCenterY - 10;
- // Draw eyes
- drawLeftEye(faceCenterX - eyeSeparation / 2, eyeY, eyeRadius);
- drawRightEye(faceCenterX + eyeSeparation / 2, eyeY, eyeRadius);
- // Draw eyebrows
- drawEyebrows(faceCenterX - eyeSeparation / 2, faceCenterX + eyeSeparation / 2, eyeY, eyeRadius);
- // Draw nose
- drawNose(faceCenterX, faceCenterY);
- // Draw mouth
- drawMouth(faceCenterX, faceCenterY);
- // Draw cheeks
- drawCheeks(faceCenterX - eyeSeparation / 2, faceCenterX + eyeSeparation / 2, eyeY);
- // Display status text (optional, comment out if needed for more room)
- display.setTextSize(1);
- display.setTextColor(SSD1306_WHITE);
- display.setCursor(0, 0);
- display.println(animation.frameCount);
- // Push to display
- display.display();
- }
- /****** BUTTON EVENT HANDLER *****/
- void handleButtonPress() {
- // Cycle to next emotion
- animation.targetEmotion = (Emotion)((animation.currentEmotion + 1) % 8);
- animation.lastInputTime = millis();
- animation.autonomousMode = false;
- // Blink indicator LED
- digitalWrite(emotionButton_LED_PIN_D2, HIGH);
- delay(100);
- digitalWrite(emotionButton_LED_PIN_D2, LOW);
- }
- /****** SETUP FUNCTION *****/
- void setup(void) {
- // Initialize serial for debugging
- Serial.begin(115200);
- delay(100);
- // Configure GPIO pins
- pinMode(faceDisplay_PushButton_PIN_D1, INPUT_PULLUP);
- pinMode(emotionButton_LED_PIN_D2, OUTPUT);
- digitalWrite(emotionButton_LED_PIN_D2, LOW);
- // Initialize OLED display
- if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
- Serial.println(F("SSD1306 allocation failed"));
- for (;;); // Halt if display fails
- }
- // Clear and show startup message
- display.clearDisplay();
- display.setTextSize(1);
- display.setTextColor(SSD1306_WHITE);
- display.setCursor(32, 28);
- display.println(F("Initializing..."));
- display.display();
- delay(1000);
- // Initialize emotion configurations
- initializeEmotionConfigs();
- // Initialize animation state
- animation.currentEmotion = NEUTRAL;
- animation.targetEmotion = NEUTRAL;
- animation.autonomousMode = false;
- animation.lastInputTime = millis();
- animation.idleThreshold = 5000; // 5 seconds
- animation.frameTime = 0;
- animation.lastFrameTime = 0;
- animation.frameCount = 0;
- animation.animationPhase = 0.0f;
- animation.phaseVelocity = 0.0f;
- // Initialize micro-movement variables in AnimationState
- animation.microMoveX = 0.0f;
- animation.microMoveY = 0.0f;
- animation.gazeX = 0.0f;
- animation.gazeY = 0.0f;
- // Initialize physics state
- physics.pupilX = 0.0f;
- physics.pupilY = 0.0f;
- physics.pupilVelX = 0.0f;
- physics.pupilVelY = 0.0f;
- physics.targetPupilX = 0.0f;
- physics.targetPupilY = 0.0f;
- physics.blinkValue = 0.0f;
- physics.blinkVel = 0.0f;
- physics.isBlinking = false;
- physics.blinkStartTime = millis();
- physics.eyelidOffset = 0.0f;
- physics.eyebrowAngle = 0.0f;
- physics.eyebrowTargetAngle = 0.0f;
- physics.irisGlow = 0.3f;
- physics.mouthHeight = 0.3f;
- physics.mouthAngle = 0.0f;
- physics.cheekBlush = 0.0f;
- physics.noseWiggle = 0.0f;
- // Setup EasyButton
- button.begin();
- button.onPressed(handleButtonPress);
- Serial.println(F("Face animation system initialized!"));
- Serial.println(F("Press button to cycle emotions."));
- Serial.println(F("System idle for 5 seconds to enter autonomous mode."));
- }
- /****** MAIN LOOP *****/
- void loop(void) {
- // Read button input
- button.read();
- // Check for idle timeout (autonomous mode)
- unsigned long timeSinceLastInput = millis() - animation.lastInputTime;
- if (timeSinceLastInput > animation.idleThreshold && !animation.autonomousMode) {
- animation.autonomousMode = true;
- Serial.println(F("Entering autonomous mode..."));
- }
- // Update animation physics and state
- updateAnimation();
- // Render face to OLED
- drawFace();
- // Target 45 FPS (approximately 22ms per frame)
- // delayMicroseconds(22222); // Optional frame rate limiting
- }
- /* END CODE */
Advertisement
Add Comment
Please, Sign In to add comment