Dzbj_ESP32-S3/main/ui/screens/ui_ScreenChar.c
Rdzleo 6711a24a68 feat: 新增ScreenChar角色动画界面 + 启用GIF解码器
- 新增ui_ScreenChar:LVGL基元绘制动漫角色(金黄头发/脸/眼/嘴/身体/胳膊)
- 序列帧动画:BOOT按键触发眨眼+说话动画,再按停止
- ScreenImg上滑导航至ScreenChar(保留原Home路径可切换)
- BOOT按键逻辑:ScreenChar界面切换动画,其他界面返回Home
- 启用CONFIG_LV_USE_GIF=y支持GIF动图显示

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-24 18:05:54 +08:00

382 lines
14 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 动漫角色界面 - 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;
}