// 动漫角色界面 - LVGL图形基元绘制 + 序列帧动画 // 默认静态显示,BOOT按键触发眨眼+说话动画 // 零图片资源,纯代码绘制 #include "../ui.h" #include "ui_ScreenChar.h" #include "esp_log.h" static const char *TAG = "CHAR"; lv_obj_t *ui_ScreenChar = NULL; // === 角色配色 === #define COL_SKIN 0xFFD5B8 // 肤色 #define COL_HAIR 0xFFD54F // 金黄色头发 #define COL_EYE_WHITE 0xFFFFFF // 眼白 #define COL_PUPIL 0x1565C0 // 蓝色瞳孔 #define COL_MOUTH 0xE57373 // 粉红嘴巴 #define COL_BLUSH 0xFFAB91 // 腮红 #define COL_SHIRT 0x42A5F5 // 蓝色衣服 #define COL_COLLAR 0xFFFFFF // 白色衣领 #define COL_BG 0x1A1A2E // 深蓝背景 // === 角色布局坐标(基于360x360屏幕)=== // 角色居中偏上,留出底部空间给提示文字 #define CHAR_CX 180 // 角色中心X #define FACE_CY 120 // 脸部中心Y #define FACE_R 72 // 脸部半径 // 眼睛 #define EYE_Y 110 // 眼睛Y #define EYE_L_X 152 // 左眼X #define EYE_R_X 208 // 右眼X #define EYE_W 26 // 眼白宽度 #define EYE_H 26 // 眼白高度(眨眼时变小) #define PUPIL_R 7 // 瞳孔半径 // 嘴巴 #define MOUTH_Y 155 // 嘴巴Y #define MOUTH_W 24 // 嘴巴宽度 #define MOUTH_H_IDLE 6 // 静态嘴巴高度(微笑线) #define MOUTH_H_TALK 18 // 说话时嘴巴高度 // 身体 #define NECK_Y 185 // 脖子顶部Y #define NECK_W 28 // 脖子宽度 #define NECK_H 18 // 脖子高度 #define BODY_Y 203 // 身体顶部Y #define BODY_W 160 // 肩宽 #define BODY_H 160 // 身体高度(延伸到屏幕外) #define SHOULDER_R 25 // 肩部圆角 // 胳膊 #define ARM_W 28 // 胳膊宽度 #define ARM_H 100 // 胳膊长度 #define ARM_Y 215 // 胳膊顶部Y(肩膀下方) #define ARM_L_X (CHAR_CX - BODY_W / 2 - ARM_W / 2 + 8) // 左胳膊X #define ARM_R_X (CHAR_CX + BODY_W / 2 + ARM_W / 2 - 8) // 右胳膊X #define HAND_R 16 // 手掌半径 // === 角色控件指针 === static lv_obj_t *hair_obj = NULL; // 头发 static lv_obj_t *face_obj = NULL; // 脸 static lv_obj_t *eye_l_white = NULL; // 左眼白 static lv_obj_t *eye_r_white = NULL; // 右眼白 static lv_obj_t *pupil_l = NULL; // 左瞳孔 static lv_obj_t *pupil_r = NULL; // 右瞳孔 static lv_obj_t *blush_l = NULL; // 左腮红 static lv_obj_t *blush_r = NULL; // 右腮红 static lv_obj_t *mouth_obj = NULL; // 嘴巴 static lv_obj_t *neck_obj = NULL; // 脖子 static lv_obj_t *body_obj = NULL; // 身体/衣服 static lv_obj_t *collar_l = NULL; // 左衣领 static lv_obj_t *collar_r = NULL; // 右衣领 static lv_obj_t *arm_l = NULL; // 左胳膊 static lv_obj_t *arm_r = NULL; // 右胳膊 static lv_obj_t *hand_l = NULL; // 左手 static lv_obj_t *hand_r = NULL; // 右手 static lv_obj_t *hint_label = NULL; // 提示文字 // === 动画状态 === static lv_timer_t *blink_timer = NULL; // 眨眼定时器 static lv_timer_t *mouth_timer = NULL; // 说话定时器 static bool anim_playing = false; // 动画是否在播放 // 眨眼动画状态 static uint8_t blink_phase = 0; // 0=睁眼 1=半闭 2=闭眼 3=半闭 static uint32_t next_blink_ms = 0; // 下次眨眼时间 // 说话动画状态 static uint8_t mouth_phase = 0; // 嘴巴帧索引 static const lv_coord_t mouth_heights[] = {6, 12, 18, 14, 8, 14, 18, 12}; // 嘴巴高度序列 #define MOUTH_FRAMES (sizeof(mouth_heights) / sizeof(mouth_heights[0])) // === 创建圆形对象的辅助函数 === static lv_obj_t *create_circle(lv_obj_t *parent, lv_coord_t cx, lv_coord_t cy, lv_coord_t w, lv_coord_t h, uint32_t color) { lv_obj_t *obj = lv_obj_create(parent); lv_obj_remove_style_all(obj); lv_obj_set_size(obj, w, h); lv_obj_set_pos(obj, cx - w / 2, cy - h / 2); lv_obj_set_style_bg_color(obj, lv_color_hex(color), 0); lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, 0); lv_obj_set_style_radius(obj, LV_RADIUS_CIRCLE, 0); lv_obj_set_style_border_width(obj, 0, 0); lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE); return obj; } // 创建矩形/圆角矩形 static lv_obj_t *create_rect(lv_obj_t *parent, lv_coord_t x, lv_coord_t y, lv_coord_t w, lv_coord_t h, uint32_t color, lv_coord_t radius) { lv_obj_t *obj = lv_obj_create(parent); lv_obj_remove_style_all(obj); lv_obj_set_size(obj, w, h); lv_obj_set_pos(obj, x, y); lv_obj_set_style_bg_color(obj, lv_color_hex(color), 0); lv_obj_set_style_bg_opa(obj, LV_OPA_COVER, 0); lv_obj_set_style_radius(obj, radius, 0); lv_obj_set_style_border_width(obj, 0, 0); lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE | LV_OBJ_FLAG_CLICKABLE); return obj; } // === 绘制角色 === static void draw_character(lv_obj_t *screen) { // 1. 胳膊(最底层,被身体部分遮挡) // 左胳膊(衣袖) arm_l = create_rect(screen, ARM_L_X - ARM_W / 2, ARM_Y, ARM_W, ARM_H, COL_SHIRT, ARM_W / 2); // 右胳膊(衣袖) arm_r = create_rect(screen, ARM_R_X - ARM_W / 2, ARM_Y, ARM_W, ARM_H, COL_SHIRT, ARM_W / 2); // 左手(肤色圆形) hand_l = create_circle(screen, ARM_L_X, ARM_Y + ARM_H - 5, HAND_R * 2, HAND_R * 2, COL_SKIN); // 右手(肤色圆形) hand_r = create_circle(screen, ARM_R_X, ARM_Y + ARM_H - 5, HAND_R * 2, HAND_R * 2, COL_SKIN); // 2. 身体/衣服 body_obj = create_rect(screen, CHAR_CX - BODY_W / 2, BODY_Y, BODY_W, BODY_H, COL_SHIRT, SHOULDER_R); // 3. 衣领(V形,用两个斜矩形模拟) collar_l = create_rect(screen, CHAR_CX - 18, BODY_Y, 20, 28, COL_COLLAR, 3); lv_obj_set_style_transform_angle(collar_l, 200, 0); // 旋转20度 collar_r = create_rect(screen, CHAR_CX - 2, BODY_Y, 20, 28, COL_COLLAR, 3); lv_obj_set_style_transform_angle(collar_r, -200, 0); // 旋转-20度 // 3. 脖子 neck_obj = create_rect(screen, CHAR_CX - NECK_W / 2, NECK_Y, NECK_W, NECK_H, COL_SKIN, 5); // 4. 头发(脸后面的半圆,稍大于脸) hair_obj = create_circle(screen, CHAR_CX, FACE_CY - 5, FACE_R * 2 + 16, FACE_R * 2 + 16, COL_HAIR); // 头发刘海(额头上方的矩形) create_rect(screen, CHAR_CX - FACE_R - 2, FACE_CY - FACE_R - 10, FACE_R * 2 + 4, 35, COL_HAIR, 8); // 5. 脸(圆形) face_obj = create_circle(screen, CHAR_CX, FACE_CY, FACE_R * 2, FACE_R * 2, COL_SKIN); // 6. 腮红 blush_l = create_circle(screen, EYE_L_X - 5, EYE_Y + 18, 22, 12, COL_BLUSH); lv_obj_set_style_bg_opa(blush_l, LV_OPA_60, 0); blush_r = create_circle(screen, EYE_R_X + 5, EYE_Y + 18, 22, 12, COL_BLUSH); lv_obj_set_style_bg_opa(blush_r, LV_OPA_60, 0); // 7. 眼睛(眼白 + 瞳孔) eye_l_white = create_circle(screen, EYE_L_X, EYE_Y, EYE_W, EYE_H, COL_EYE_WHITE); eye_r_white = create_circle(screen, EYE_R_X, EYE_Y, EYE_W, EYE_H, COL_EYE_WHITE); pupil_l = create_circle(screen, EYE_L_X, EYE_Y + 2, PUPIL_R * 2, PUPIL_R * 2, COL_PUPIL); pupil_r = create_circle(screen, EYE_R_X, EYE_Y + 2, PUPIL_R * 2, PUPIL_R * 2, COL_PUPIL); // 眼睛高光(小白点) create_circle(screen, EYE_L_X + 3, EYE_Y - 1, 5, 5, COL_EYE_WHITE); create_circle(screen, EYE_R_X + 3, EYE_Y - 1, 5, 5, COL_EYE_WHITE); // 8. 眉毛(深棕色,黄毛配深眉更协调) lv_obj_t *brow_l = create_rect(screen, EYE_L_X - 14, EYE_Y - 20, 22, 4, 0x5D4037, 2); lv_obj_set_style_transform_angle(brow_l, -50, 0); // 微微倾斜 lv_obj_t *brow_r = create_rect(screen, EYE_R_X - 8, EYE_Y - 20, 22, 4, 0x5D4037, 2); lv_obj_set_style_transform_angle(brow_r, 50, 0); // 9. 鼻子(小三角,用一个很小的圆角矩形) create_circle(screen, CHAR_CX, FACE_CY + 12, 6, 6, 0xFFBCA0); // 10. 嘴巴(椭圆形,高度可变实现说话效果) mouth_obj = create_circle(screen, CHAR_CX, MOUTH_Y, MOUTH_W, MOUTH_H_IDLE, COL_MOUTH); } // === 眨眼动画回调 === static void blink_timer_cb(lv_timer_t *t) { if (!anim_playing || !eye_l_white || !eye_r_white) return; uint32_t now = lv_tick_get(); if (blink_phase == 0) { // 睁眼状态,等待下次眨眼 if (now < next_blink_ms) return; blink_phase = 1; } // 眨眼帧序列:睁眼26 → 半闭12 → 闭眼3 → 半闭12 → 睁眼26 static const lv_coord_t eye_h[] = {12, 3, 12, EYE_H}; lv_coord_t h = eye_h[blink_phase - 1]; // 更新眼睛高度(模拟眨眼) lv_obj_set_height(eye_l_white, h); lv_obj_set_y(eye_l_white, EYE_Y - h / 2); lv_obj_set_height(eye_r_white, h); lv_obj_set_y(eye_r_white, EYE_Y - h / 2); // 瞳孔跟随(闭眼时隐藏) if (h <= 5) { lv_obj_add_flag(pupil_l, LV_OBJ_FLAG_HIDDEN); lv_obj_add_flag(pupil_r, LV_OBJ_FLAG_HIDDEN); } else { lv_obj_clear_flag(pupil_l, LV_OBJ_FLAG_HIDDEN); lv_obj_clear_flag(pupil_r, LV_OBJ_FLAG_HIDDEN); } blink_phase++; if (blink_phase > 4) { blink_phase = 0; // 随机间隔2-4秒后下次眨眼 next_blink_ms = now + 2000 + (lv_tick_get() % 2000); } } // === 说话动画回调 === static void mouth_timer_cb(lv_timer_t *t) { if (!anim_playing || !mouth_obj) return; lv_coord_t h = mouth_heights[mouth_phase]; lv_obj_set_height(mouth_obj, h); lv_obj_set_y(mouth_obj, MOUTH_Y - h / 2); mouth_phase = (mouth_phase + 1) % MOUTH_FRAMES; } // === 手势回调 === static void ui_event_ScreenChar(lv_event_t *e) { lv_event_code_t code = lv_event_get_code(e); if (code == LV_EVENT_GESTURE) { lv_dir_t dir = lv_indev_get_gesture_dir(lv_indev_get_act()); // 下滑返回ScreenImg if (dir == LV_DIR_BOTTOM) { lv_indev_wait_release(lv_indev_get_act()); char_anim_stop(); _ui_screen_change(&ui_ScreenImg, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenImg_screen_init); } // 左滑/右滑返回Home if (dir == LV_DIR_LEFT || dir == LV_DIR_RIGHT) { lv_indev_wait_release(lv_indev_get_act()); char_anim_stop(); _ui_screen_change(&ui_ScreenHome, LV_SCR_LOAD_ANIM_NONE, 0, 0, &ui_ScreenHome_screen_init); } } } // === 界面初始化 === void ui_ScreenChar_screen_init(void) { ui_ScreenChar = lv_obj_create(NULL); lv_obj_clear_flag(ui_ScreenChar, LV_OBJ_FLAG_SCROLLABLE); lv_obj_set_style_bg_color(ui_ScreenChar, lv_color_hex(COL_BG), LV_PART_MAIN); lv_obj_set_style_bg_opa(ui_ScreenChar, 255, LV_PART_MAIN); // 绘制角色 draw_character(ui_ScreenChar); // 提示标签 hint_label = lv_label_create(ui_ScreenChar); lv_label_set_text(hint_label, "BOOT: Talk"); lv_obj_set_align(hint_label, LV_ALIGN_BOTTOM_MID); lv_obj_set_y(hint_label, -15); lv_obj_set_style_text_color(hint_label, lv_color_hex(0x666688), LV_PART_MAIN); lv_obj_set_style_text_font(hint_label, &lv_font_montserrat_14, LV_PART_MAIN); // 手势:下滑返回Img,左右滑返回Home lv_obj_add_flag(ui_ScreenChar, LV_OBJ_FLAG_CLICKABLE); lv_obj_add_flag(ui_ScreenChar, LV_OBJ_FLAG_SCROLLABLE); lv_obj_add_event_cb(ui_ScreenChar, ui_event_ScreenChar, LV_EVENT_ALL, NULL); ESP_LOGI(TAG, "ScreenChar界面初始化完成"); } void ui_ScreenChar_screen_destroy(void) { char_anim_stop(); if (ui_ScreenChar) lv_obj_del(ui_ScreenChar); ui_ScreenChar = NULL; // 所有子控件随屏幕一起销毁,只需清空指针 face_obj = hair_obj = NULL; eye_l_white = eye_r_white = NULL; pupil_l = pupil_r = NULL; blush_l = blush_r = NULL; mouth_obj = neck_obj = body_obj = NULL; collar_l = collar_r = NULL; arm_l = arm_r = hand_l = hand_r = NULL; hint_label = NULL; } // === 动画控制接口 === void char_anim_start(void) { if (anim_playing) return; anim_playing = true; blink_phase = 0; mouth_phase = 0; next_blink_ms = lv_tick_get() + 500; // 0.5秒后第一次眨眼 // 眨眼定时器:50ms间隔(眨眼帧切换速度) blink_timer = lv_timer_create(blink_timer_cb, 50, NULL); // 说话定时器:80ms间隔(嘴巴变化速度) mouth_timer = lv_timer_create(mouth_timer_cb, 80, NULL); // 更新提示 if (hint_label) { lv_label_set_text(hint_label, "Speaking..."); lv_obj_set_style_text_color(hint_label, lv_color_hex(0x42A5F5), LV_PART_MAIN); } ESP_LOGI(TAG, "角色动画启动(序列帧)"); } void char_anim_stop(void) { if (!anim_playing) return; anim_playing = false; // 删除定时器 if (blink_timer) { lv_timer_del(blink_timer); blink_timer = NULL; } if (mouth_timer) { lv_timer_del(mouth_timer); mouth_timer = NULL; } // 恢复静态状态 if (eye_l_white) { lv_obj_set_height(eye_l_white, EYE_H); lv_obj_set_y(eye_l_white, EYE_Y - EYE_H / 2); } if (eye_r_white) { lv_obj_set_height(eye_r_white, EYE_H); lv_obj_set_y(eye_r_white, EYE_Y - EYE_H / 2); } if (pupil_l) lv_obj_clear_flag(pupil_l, LV_OBJ_FLAG_HIDDEN); if (pupil_r) lv_obj_clear_flag(pupil_r, LV_OBJ_FLAG_HIDDEN); if (mouth_obj) { lv_obj_set_height(mouth_obj, MOUTH_H_IDLE); lv_obj_set_y(mouth_obj, MOUTH_Y - MOUTH_H_IDLE / 2); } // 恢复提示 if (hint_label) { lv_label_set_text(hint_label, "BOOT: Talk"); lv_obj_set_style_text_color(hint_label, lv_color_hex(0x666688), LV_PART_MAIN); } ESP_LOGI(TAG, "角色动画停止"); } bool char_anim_is_playing(void) { return anim_playing; }