/* * face_animation.c — Face Emotion Renderer for LCD Display * * Implements expressive face animations with smooth transitions between emotions. * Supports idle blinking and parameterized eye/mouth shapes for each emotion. */ #include "face_animation.h" #include "face_lcd.h" #include #include /* === Configuration === */ #define TRANSITION_FRAMES 15 /* ~0.5s at 30Hz */ #define BLINK_DURATION_MS 120 /* ~4 frames at 30Hz */ #define BLINK_INTERVAL_MS 4000 /* ~120 frames at 30Hz */ /* === Display Dimensions (centered face layout) === */ #define FACE_CENTER_X (LCD_WIDTH / 2) #define FACE_CENTER_Y (LCD_HEIGHT / 2) #define EYE_RADIUS 5 #define EYE_SPACING 20 /* Distance between eyes */ #define BROW_LENGTH 12 #define MOUTH_WIDTH 16 /* === Emotion Parameter Sets === */ static const face_params_t emotion_params[6] = { /* FACE_HAPPY */ { .eye_x = -EYE_SPACING/2, .eye_y = -10, .eye_open_y = 5, .eye_close_y = 0, .brow_angle = 15, .brow_y_offset = -6, .mouth_x = 0, .mouth_y = 10, .mouth_width = MOUTH_WIDTH, .mouth_curve = 4, /* Upturned smile */ .blink_interval_ms = 120, }, /* FACE_SAD */ { .eye_x = -EYE_SPACING/2, .eye_y = -8, .eye_open_y = 5, .eye_close_y = 0, .brow_angle = -15, .brow_y_offset = -8, .mouth_x = 0, .mouth_y = 12, .mouth_width = MOUTH_WIDTH, .mouth_curve = -3, /* Downturned frown */ .blink_interval_ms = 180, /* Slower blink when sad */ }, /* FACE_CURIOUS */ { .eye_x = -EYE_SPACING/2, .eye_y = -12, .eye_open_y = 7, .eye_close_y = 0, /* Wide eyes */ .brow_angle = 20, .brow_y_offset = -10, /* Raised brows */ .mouth_x = 2, .mouth_y = 10, .mouth_width = 12, .mouth_curve = 1, /* Slight smile */ .blink_interval_ms = 150, }, /* FACE_ANGRY */ { .eye_x = -EYE_SPACING/2, .eye_y = -6, .eye_open_y = 3, .eye_close_y = 0, /* Narrowed eyes */ .brow_angle = -20, .brow_y_offset = -5, .mouth_x = 0, .mouth_y = 11, .mouth_width = 14, .mouth_curve = -5, /* Strong frown */ .blink_interval_ms = 90, /* Angry blinks faster */ }, /* FACE_SLEEPING */ { .eye_x = -EYE_SPACING/2, .eye_y = -8, .eye_open_y = 0, .eye_close_y = -2, /* Closed/squinted */ .brow_angle = 5, .brow_y_offset = -4, .mouth_x = 0, .mouth_y = 10, .mouth_width = 10, .mouth_curve = 2, /* Peaceful smile */ .blink_interval_ms = 60, /* Not used when sleeping */ }, /* FACE_NEUTRAL */ { .eye_x = -EYE_SPACING/2, .eye_y = -8, .eye_open_y = 5, .eye_close_y = 0, .brow_angle = 0, .brow_y_offset = -6, .mouth_x = 0, .mouth_y = 10, .mouth_width = 12, .mouth_curve = 0, /* Straight line */ .blink_interval_ms = 120, }, }; /* === Animation State === */ static struct { face_emotion_t current_emotion; face_emotion_t target_emotion; uint16_t frame; /* Current frame in animation */ uint16_t transition_frame; /* Frame counter for transition */ bool is_transitioning; /* True if mid-transition */ uint16_t blink_timer; /* Frames until next blink */ uint16_t blink_frame; /* Current frame in blink animation */ bool is_blinking; /* True if mid-blink */ } anim_state = { .current_emotion = FACE_NEUTRAL, .target_emotion = FACE_NEUTRAL, .frame = 0, .transition_frame = 0, .is_transitioning = false, .blink_timer = BLINK_INTERVAL_MS / 33, /* ~120 frames */ .blink_frame = 0, .is_blinking = false, }; /* === Easing Functions === */ /** * Ease-in-out cubic interpolation [0, 1]. * Smooth acceleration/deceleration for transitions. */ static float ease_in_out_cubic(float t) { if (t < 0.5f) return 4.0f * t * t * t; else { float f = 2.0f * t - 2.0f; return 0.5f * f * f * f + 1.0f; } } /** * Interpolate two emotion parameters by factor [0, 1]. */ static face_params_t interpolate_params(const face_params_t *a, const face_params_t *b, float t) { face_params_t result; result.eye_x = (int16_t)(a->eye_x + (b->eye_x - a->eye_x) * t); result.eye_y = (int16_t)(a->eye_y + (b->eye_y - a->eye_y) * t); result.eye_open_y = (int16_t)(a->eye_open_y + (b->eye_open_y - a->eye_open_y) * t); result.eye_close_y = (int16_t)(a->eye_close_y + (b->eye_close_y - a->eye_close_y) * t); result.brow_angle = (int16_t)(a->brow_angle + (b->brow_angle - a->brow_angle) * t); result.brow_y_offset = (int16_t)(a->brow_y_offset + (b->brow_y_offset - a->brow_y_offset) * t); result.mouth_x = (int16_t)(a->mouth_x + (b->mouth_x - a->mouth_x) * t); result.mouth_y = (int16_t)(a->mouth_y + (b->mouth_y - a->mouth_y) * t); result.mouth_width = (int16_t)(a->mouth_width + (b->mouth_width - a->mouth_width) * t); result.mouth_curve = (int16_t)(a->mouth_curve + (b->mouth_curve - a->mouth_curve) * t); return result; } /* === Drawing Functions === */ /** * Draw an eye (circle) with optional closure (eyelid). */ static void draw_eye(int16_t x, int16_t y, int16_t open_y, int16_t close_y, bool is_blinking) { lcd_color_t color = LCD_WHITE; /* Eye position accounts for blink closure */ int16_t eye_h = is_blinking ? close_y : open_y; if (eye_h <= 0) { /* Closed: draw horizontal line instead */ face_lcd_line(x - EYE_RADIUS, y, x + EYE_RADIUS, y, color); } else { /* Open: draw circle (simplified ellipse) */ face_lcd_circle(x, y, EYE_RADIUS, color); /* Fill iris pupil */ face_lcd_fill_rect(x - 2, y - 1, 4, 2, color); } } /** * Draw an eyebrow with angle and offset. */ static void draw_brow(int16_t x, int16_t y, int16_t angle, int16_t y_offset) { /* Approximate angled line by adjusting endpoints */ int16_t brow_y = y + y_offset; int16_t angle_offset = (angle * BROW_LENGTH) / 45; /* ~1 pixel per 45 degrees */ face_lcd_line(x - BROW_LENGTH/2 - angle_offset, brow_y, x + BROW_LENGTH/2 + angle_offset, brow_y, LCD_WHITE); } /** * Draw mouth (curved line or bezier approximation). */ static void draw_mouth(int16_t x, int16_t y, int16_t width, int16_t curve) { /* Simplified mouth: two diagonal lines forming a V or inverted V */ int16_t mouth_left = x - width / 2; int16_t mouth_right = x + width / 2; int16_t mouth_bottom = y + (curve > 0 ? 3 : 0); if (curve > 0) { /* Smile: V shape upturned */ face_lcd_line(mouth_left, y + 2, x, mouth_bottom, LCD_WHITE); face_lcd_line(x, mouth_bottom, mouth_right, y + 2, LCD_WHITE); } else if (curve < 0) { /* Frown: ^ shape downturned */ face_lcd_line(mouth_left, y - 2, x, y + 2, LCD_WHITE); face_lcd_line(x, y + 2, mouth_right, y - 2, LCD_WHITE); } else { /* Neutral: straight line */ face_lcd_line(mouth_left, y, mouth_right, y, LCD_WHITE); } } /* === Public API Implementation === */ void face_animation_init(void) { anim_state.current_emotion = FACE_NEUTRAL; anim_state.target_emotion = FACE_NEUTRAL; anim_state.frame = 0; anim_state.transition_frame = 0; anim_state.is_transitioning = false; anim_state.blink_timer = BLINK_INTERVAL_MS / 33; anim_state.blink_frame = 0; anim_state.is_blinking = false; } void face_animation_set_emotion(face_emotion_t emotion) { if (emotion < 6) { anim_state.target_emotion = emotion; anim_state.is_transitioning = true; anim_state.transition_frame = 0; } } void face_animation_tick(void) { anim_state.frame++; /* Handle transition */ if (anim_state.is_transitioning) { anim_state.transition_frame++; if (anim_state.transition_frame >= TRANSITION_FRAMES) { /* Transition complete */ anim_state.current_emotion = anim_state.target_emotion; anim_state.is_transitioning = false; } } /* Handle idle blink */ if (!anim_state.is_blinking) { anim_state.blink_timer--; if (anim_state.blink_timer == 0) { anim_state.is_blinking = true; anim_state.blink_frame = 0; /* Reset timer for next blink */ anim_state.blink_timer = BLINK_INTERVAL_MS / 33; } } else { /* In blink */ anim_state.blink_frame++; if (anim_state.blink_frame >= BLINK_DURATION_MS / 33) { /* Blink complete */ anim_state.is_blinking = false; anim_state.blink_frame = 0; } } } void face_animation_render(void) { /* Clear display */ face_lcd_clear(); /* Get current emotion parameters (interpolated if transitioning) */ face_params_t params; if (anim_state.is_transitioning) { float t = ease_in_out_cubic((float)anim_state.transition_frame / TRANSITION_FRAMES); params = interpolate_params( &emotion_params[anim_state.current_emotion], &emotion_params[anim_state.target_emotion], t); } else { params = emotion_params[anim_state.current_emotion]; } /* Draw left eye */ draw_eye(FACE_CENTER_X + params.eye_x, FACE_CENTER_Y + params.eye_y, params.eye_open_y, params.eye_close_y, anim_state.is_blinking); /* Draw right eye */ draw_eye(FACE_CENTER_X - params.eye_x, FACE_CENTER_Y + params.eye_y, params.eye_open_y, params.eye_close_y, anim_state.is_blinking); /* Draw left brow */ draw_brow(FACE_CENTER_X + params.eye_x, FACE_CENTER_Y + params.brow_y_offset, params.brow_angle, 0); /* Draw right brow (mirrored) */ draw_brow(FACE_CENTER_X - params.eye_x, FACE_CENTER_Y + params.brow_y_offset, -params.brow_angle, 0); /* Draw mouth */ draw_mouth(FACE_CENTER_X + params.mouth_x, FACE_CENTER_Y + params.mouth_y, params.mouth_width, params.mouth_curve); /* Push framebuffer to display */ face_lcd_flush(); } face_emotion_t face_animation_get_emotion(void) { return anim_state.is_transitioning ? anim_state.target_emotion : anim_state.current_emotion; } void face_animation_blink_now(void) { anim_state.is_blinking = true; anim_state.blink_frame = 0; anim_state.blink_timer = BLINK_INTERVAL_MS / 33; } bool face_animation_is_idle(void) { return !anim_state.is_transitioning && !anim_state.is_blinking; }