feat(lcd): animate fortune results with shared GFX layers
@@ -659,6 +659,20 @@ static void handle_command_line(char *line)
|
||||
handle_prepare_rotate(key_version);
|
||||
} else if (strcmp(cmd, "lcd_test") == 0) {
|
||||
handle_lcd_test();
|
||||
} else if (strcmp(cmd, "lcd_fortune_test") == 0) {
|
||||
/* 有界、仅显示测试;不调用云抽签、不写 NVS、不改变语义选择映射。 */
|
||||
if (argc != 2 || argv[1][0] < '0' || argv[1][0] > '4' || argv[1][1] != '\0') {
|
||||
cli_write_error("invalid_fortune_index");
|
||||
return;
|
||||
}
|
||||
const domain_expression_request_t request = {
|
||||
.id = DOMAIN_EXPRESSION_CHOUQIAN,
|
||||
.trigger = DOMAIN_EXPRESSION_TRIGGER_FACTORY_LCD_TEST,
|
||||
.chouqian_result_index_valid = true,
|
||||
.chouqian_result_index = (size_t)(argv[1][0] - '0'),
|
||||
};
|
||||
if (domain_runtime_request_expression(&request) == ESP_OK) cli_write_ok();
|
||||
else cli_write_error("lcd_fortune_test_failed");
|
||||
} else {
|
||||
cli_write_error("unknown_command");
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ if(CONFIG_TQ_LCD_EMOTE_GFX_TEST)
|
||||
list(APPEND platform_srcs
|
||||
"src/lcd_emote_assets.c"
|
||||
"src/lcd_emote_listening.c"
|
||||
"src/lcd_emote_fortune.c"
|
||||
"src/generated/emote_gfx/fortune_assets.c"
|
||||
"src/generated/emote_gfx/idle_base.c"
|
||||
"src/generated/emote_gfx/idle_eyes_open.c"
|
||||
"src/generated/emote_gfx/listening_base.c"
|
||||
|
||||
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
@@ -0,0 +1,114 @@
|
||||
{
|
||||
"canvas": [
|
||||
320,
|
||||
240
|
||||
],
|
||||
"window": [
|
||||
56,
|
||||
20,
|
||||
208,
|
||||
220
|
||||
],
|
||||
"layers": [
|
||||
{
|
||||
"name": "pile",
|
||||
"width": 208,
|
||||
"height": 76,
|
||||
"x": 56,
|
||||
"y": 164,
|
||||
"raw_bytes": 47424,
|
||||
"lz4_bytes": 9417
|
||||
},
|
||||
{
|
||||
"name": "back",
|
||||
"width": 63,
|
||||
"height": 113,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"raw_bytes": 21357,
|
||||
"lz4_bytes": 4179
|
||||
},
|
||||
{
|
||||
"name": "edge",
|
||||
"width": 41,
|
||||
"height": 110,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"raw_bytes": 13530,
|
||||
"lz4_bytes": 4389
|
||||
},
|
||||
{
|
||||
"name": "front",
|
||||
"width": 100,
|
||||
"height": 190,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"raw_bytes": 57000,
|
||||
"lz4_bytes": 10774
|
||||
},
|
||||
{
|
||||
"name": "question",
|
||||
"width": 27,
|
||||
"height": 50,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"raw_bytes": 4050,
|
||||
"lz4_bytes": 817
|
||||
},
|
||||
{
|
||||
"name": "rod",
|
||||
"width": 169,
|
||||
"height": 56,
|
||||
"x": 0,
|
||||
"y": 40,
|
||||
"raw_bytes": 28392,
|
||||
"lz4_bytes": 2094
|
||||
},
|
||||
{
|
||||
"name": "text_buyun",
|
||||
"width": 48,
|
||||
"height": 104,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"raw_bytes": 14976,
|
||||
"lz4_bytes": 2638
|
||||
},
|
||||
{
|
||||
"name": "text_heping",
|
||||
"width": 48,
|
||||
"height": 104,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"raw_bytes": 14976,
|
||||
"lz4_bytes": 2851
|
||||
},
|
||||
{
|
||||
"name": "text_shangshang",
|
||||
"width": 48,
|
||||
"height": 104,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"raw_bytes": 14976,
|
||||
"lz4_bytes": 2069
|
||||
},
|
||||
{
|
||||
"name": "text_xiaoji",
|
||||
"width": 48,
|
||||
"height": 104,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"raw_bytes": 14976,
|
||||
"lz4_bytes": 2210
|
||||
},
|
||||
{
|
||||
"name": "text_zhongshang",
|
||||
"width": 48,
|
||||
"height": 104,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"raw_bytes": 14976,
|
||||
"lz4_bytes": 2109
|
||||
}
|
||||
],
|
||||
"note": "黑背景归零;共享爪子,签文字形来自原图;旧 PNG 保留"
|
||||
}
|
||||
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 730 B |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
#include "lcd_emote_assets.h"
|
||||
|
||||
/* 平台私有图层与几何;领域仍选择资产序列,不引入业务状态机。 */
|
||||
enum { LCD_FORTUNE_PILE, LCD_FORTUNE_BACK, LCD_FORTUNE_EDGE,
|
||||
LCD_FORTUNE_FRONT, LCD_FORTUNE_QUESTION, LCD_FORTUNE_ROD,
|
||||
LCD_FORTUNE_SHARED_COUNT, LCD_FORTUNE_RESULT_COUNT = 5 };
|
||||
#define LCD_FORTUNE_X 56
|
||||
#define LCD_FORTUNE_Y 20
|
||||
#define LCD_FORTUNE_WIDTH 208U
|
||||
#define LCD_FORTUNE_HEIGHT 220U
|
||||
#define LCD_FORTUNE_MOTION_MS 2500U
|
||||
#define LCD_FORTUNE_TOTAL_MS 4000U
|
||||
#define LCD_FORTUNE_HOLD_MS 1500U
|
||||
#define LCD_FORTUNE_STEP_MS 34U
|
||||
typedef struct {
|
||||
uint16_t width, height, x, y;
|
||||
size_t compressed_size;
|
||||
const uint8_t *data;
|
||||
} lcd_fortune_asset_t;
|
||||
extern const lcd_fortune_asset_t lcd_fortune_shared[LCD_FORTUNE_SHARED_COUNT];
|
||||
extern const lcd_fortune_asset_t lcd_fortune_text[LCD_FORTUNE_RESULT_COUNT];
|
||||
/* 有界 LZ4 block 解码,无动态内存;拒绝截断、无效回指及长度不符。 */
|
||||
bool lcd_emote_fortune_decode(const uint8_t *src, size_t size, uint8_t *dst, size_t capacity);
|
||||
extern const char *const lcd_fortune_result_dirs[LCD_FORTUNE_RESULT_COUNT];
|
||||
|
||||
/* RGB565 高字节在前 + 独立 A8;所有像素由调用方拥有。 */
|
||||
typedef struct { uint16_t width, height; const uint8_t *pixels; } lcd_fortune_texture_t;
|
||||
/* 输出不透明 RGB565,完整重画固定局部窗口,确保旧轮廓恢复而不残影。 */
|
||||
bool lcd_emote_fortune_build_frame(uint8_t *dst, size_t capacity,
|
||||
const lcd_fortune_texture_t shared[LCD_FORTUNE_SHARED_COUNT],
|
||||
const lcd_fortune_texture_t *text, uint32_t time_ms);
|
||||
@@ -38,6 +38,7 @@
|
||||
#include "gfx.h"
|
||||
#include "lcd_emote_assets.h"
|
||||
#include "lcd_emote_listening.h"
|
||||
#include "lcd_emote_fortune.h"
|
||||
#endif
|
||||
|
||||
#define LCD_DIRECT_SPI_HOST SPI2_HOST
|
||||
@@ -970,6 +971,7 @@ static esp_err_t lcd_direct_listening_prepare(void)
|
||||
static esp_err_t lcd_direct_emote_select_face(bool listening)
|
||||
{
|
||||
const lcd_emote_rle_asset_t *base = listening ? &listening_base_rle_asset : &idle_base_rle_asset;
|
||||
ESP_RETURN_ON_ERROR(gfx_obj_set_visible(s_emote_background, true), TAG, "show face base failed");
|
||||
ESP_RETURN_ON_ERROR(gfx_img_set_src(s_emote_background,
|
||||
listening ? &s_listening_base_image : &s_emote_base_image), TAG, "switch face base failed");
|
||||
ESP_RETURN_ON_ERROR(gfx_obj_set_pos(s_emote_background, base->x, base->y), TAG, "switch face position failed");
|
||||
@@ -1166,6 +1168,203 @@ static esp_err_t lcd_direct_play_emote_face_until_pending(const lcd_direct_reque
|
||||
}
|
||||
return result != ESP_OK ? result : lock_err;
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
lcd_fortune_texture_t shared[LCD_FORTUNE_SHARED_COUNT], text;
|
||||
uint8_t *pixels[LCD_FORTUNE_SHARED_COUNT + 1], *frame;
|
||||
gfx_image_dsc_t image, rod_image;
|
||||
gfx_obj_t *object;
|
||||
} lcd_direct_fortune_scene_t;
|
||||
/* 极端锁/解绑故障保留已绑定资源到重启,不能释放给 GFX 的悬空指针。 */
|
||||
static lcd_direct_fortune_scene_t *s_fortune_orphan;
|
||||
|
||||
static int lcd_direct_fortune_sequence_index(const lcd_direct_request_t *request)
|
||||
{
|
||||
if (request == NULL || request->step_count != 6) return -1;
|
||||
for (size_t i = 0; i < 6; ++i) {
|
||||
const platform_lcd_presentation_step_t *step = &request->steps[i];
|
||||
if (step->asset_dir == NULL || step->frame_index != i % 3 ||
|
||||
step->icon_id != PLATFORM_LCD_PRESENTATION_NO_ICON ||
|
||||
step->delay_ms != (i == 5 ? 1500U : 500U)) return -1;
|
||||
if (i < 3 && strcmp(step->asset_dir, "chouqian_qianzhi") != 0) return -1;
|
||||
if (i >= 3 && strcmp(step->asset_dir, request->steps[3].asset_dir) != 0) return -1;
|
||||
}
|
||||
for (size_t i = 0; i < LCD_FORTUNE_RESULT_COUNT; ++i)
|
||||
if (strcmp(request->steps[3].asset_dir, lcd_fortune_result_dirs[i]) == 0) return (int)i;
|
||||
return -1; /* 不匹配的通用步骤仍使用原 PNG 播放,不偷换其他动作。 */
|
||||
}
|
||||
|
||||
static void lcd_direct_fortune_free(lcd_direct_fortune_scene_t *scene)
|
||||
{
|
||||
if (scene == NULL) return;
|
||||
for (size_t i = 0; i <= LCD_FORTUNE_SHARED_COUNT; ++i) free(scene->pixels[i]);
|
||||
free(scene->frame);
|
||||
free(scene);
|
||||
}
|
||||
|
||||
static lcd_direct_fortune_scene_t *lcd_direct_fortune_prepare(size_t index)
|
||||
{
|
||||
if (index >= LCD_FORTUNE_RESULT_COUNT) return NULL;
|
||||
lcd_direct_fortune_scene_t *scene = heap_caps_calloc(1, sizeof(*scene), MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (scene == NULL) return NULL;
|
||||
for (size_t i = 0; i <= LCD_FORTUNE_SHARED_COUNT; ++i) {
|
||||
const lcd_fortune_asset_t *asset = i < LCD_FORTUNE_SHARED_COUNT ?
|
||||
&lcd_fortune_shared[i] : &lcd_fortune_text[index];
|
||||
size_t bytes = (size_t)asset->width * asset->height * 3;
|
||||
scene->pixels[i] = heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (scene->pixels[i] == NULL || !lcd_emote_fortune_decode(asset->data,
|
||||
asset->compressed_size, scene->pixels[i], bytes)) {
|
||||
lcd_direct_fortune_free(scene);
|
||||
return NULL;
|
||||
}
|
||||
lcd_fortune_texture_t texture = {asset->width, asset->height, scene->pixels[i]};
|
||||
if (i < LCD_FORTUNE_SHARED_COUNT) scene->shared[i] = texture;
|
||||
else scene->text = texture;
|
||||
}
|
||||
const size_t bytes = LCD_FORTUNE_WIDTH * LCD_FORTUNE_HEIGHT * 2;
|
||||
scene->frame = heap_caps_malloc(bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
|
||||
if (scene->frame == NULL) {lcd_direct_fortune_free(scene); return NULL;}
|
||||
scene->image = (gfx_image_dsc_t) {
|
||||
.header.cf = GFX_COLOR_FORMAT_RGB565, .header.magic = C_ARRAY_HEADER_MAGIC,
|
||||
.header.w = LCD_FORTUNE_WIDTH, .header.h = LCD_FORTUNE_HEIGHT,
|
||||
.data_size = bytes, .data = scene->frame,
|
||||
};
|
||||
const lcd_fortune_asset_t *rod = &lcd_fortune_shared[LCD_FORTUNE_ROD];
|
||||
scene->rod_image = (gfx_image_dsc_t) {
|
||||
.header.cf = GFX_COLOR_FORMAT_RGB565A8, .header.magic = C_ARRAY_HEADER_MAGIC,
|
||||
.header.w = rod->width, .header.h = rod->height,
|
||||
.data_size = (size_t)rod->width * rod->height * 3,
|
||||
.data = scene->pixels[LCD_FORTUNE_ROD],
|
||||
};
|
||||
return scene;
|
||||
}
|
||||
|
||||
/* 同一任务持锁退出并解绑对象,然后才能释放一次性 PSRAM 图层。 */
|
||||
static esp_err_t lcd_direct_fortune_detach(lcd_direct_fortune_scene_t *scene)
|
||||
{
|
||||
esp_err_t err = gfx_emote_lock(s_emote_handle);
|
||||
if (err != ESP_OK) return err;
|
||||
portENTER_CRITICAL(&s_state_lock);
|
||||
s_emote_active = false;
|
||||
portEXIT_CRITICAL(&s_state_lock);
|
||||
err = lcd_direct_emote_select_face(false);
|
||||
if (scene->object != NULL) {
|
||||
esp_err_t deleted = gfx_obj_delete(scene->object);
|
||||
if (deleted == ESP_OK) scene->object = NULL;
|
||||
if (err == ESP_OK) err = deleted;
|
||||
}
|
||||
esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle);
|
||||
return err != ESP_OK ? err : unlock_err;
|
||||
}
|
||||
|
||||
static esp_err_t lcd_direct_play_fortune(size_t index)
|
||||
{
|
||||
if (s_fortune_orphan != NULL || s_emote_transfer_fault) return ESP_ERR_INVALID_STATE;
|
||||
if (lcd_direct_should_stop_idle()) return ESP_ERR_INVALID_STATE;
|
||||
const int64_t prepare_start_us = esp_timer_get_time();
|
||||
lcd_direct_fortune_scene_t *scene = lcd_direct_fortune_prepare(index);
|
||||
if (scene == NULL) return ESP_ERR_NOT_SUPPORTED;
|
||||
esp_err_t result = gfx_emote_lock(s_emote_handle);
|
||||
if (result != ESP_OK) {lcd_direct_fortune_free(scene); return result;}
|
||||
bool started = false;
|
||||
bool rod_hidden = false;
|
||||
if (lcd_direct_should_stop_idle()) result = ESP_ERR_INVALID_STATE;
|
||||
if (result == ESP_OK) {
|
||||
scene->object = gfx_img_create(s_emote_disp);
|
||||
result = scene->object != NULL ? ESP_OK : ESP_ERR_NO_MEM;
|
||||
}
|
||||
if (result == ESP_OK) result = gfx_obj_set_visible(s_emote_eyes_open, false);
|
||||
if (result == ESP_OK) result = gfx_obj_set_visible(s_emote_closed_eyes, false);
|
||||
if (result == ESP_OK && s_emote_cached_eyes != NULL) result = gfx_obj_set_visible(s_emote_cached_eyes, false);
|
||||
if (result == ESP_OK && s_listening_eyes != NULL) result = gfx_obj_set_visible(s_listening_eyes, false);
|
||||
if (result == ESP_OK) result = gfx_img_set_src(s_emote_background, &scene->rod_image);
|
||||
if (result == ESP_OK) result = gfx_obj_set_pos(s_emote_background, 0, 40);
|
||||
if (result == ESP_OK) result = gfx_obj_set_visible(s_emote_background, true);
|
||||
if (result == ESP_OK) result = gfx_obj_set_pos(scene->object, LCD_FORTUNE_X, LCD_FORTUNE_Y);
|
||||
if (result == ESP_OK) result = gfx_img_set_src(scene->object, &scene->image);
|
||||
if (result == ESP_OK) result = lcd_direct_emote_update_hud(true, NULL);
|
||||
esp_err_t unlock_err = gfx_emote_unlock(s_emote_handle);
|
||||
if (result == ESP_OK) result = unlock_err;
|
||||
size_t pixel_bytes = scene->image.data_size;
|
||||
for (size_t i = 0; i < LCD_FORTUNE_SHARED_COUNT; ++i)
|
||||
pixel_bytes += (size_t)scene->shared[i].width * scene->shared[i].height * 3;
|
||||
pixel_bytes += (size_t)scene->text.width * scene->text.height * 3;
|
||||
ESP_LOGI(TAG, "Fortune layers asset=%s pixels_psram=%u scene_bytes=%u prepare_us=%llu",
|
||||
lcd_fortune_result_dirs[index], (unsigned)pixel_bytes, (unsigned)sizeof(*scene),
|
||||
(unsigned long long)(esp_timer_get_time() - prepare_start_us));
|
||||
const int64_t start_us = esp_timer_get_time();
|
||||
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
|
||||
uint32_t frames = 0, draw_attempts = 0, late = 0, max_draw_us = 0;
|
||||
uint64_t total_draw_us = 0;
|
||||
#endif
|
||||
for (uint32_t ms = 0; result == ESP_OK;) {
|
||||
if (lcd_direct_should_stop_idle()) {result = ESP_ERR_INVALID_STATE; break;}
|
||||
const int64_t draw_start_us = esp_timer_get_time();
|
||||
result = gfx_emote_lock(s_emote_handle);
|
||||
if (result != ESP_OK) break;
|
||||
if (!lcd_emote_fortune_build_frame(scene->frame, scene->image.data_size,
|
||||
scene->shared, &scene->text, ms)) result = ESP_ERR_INVALID_SIZE;
|
||||
if (result == ESP_OK) result = gfx_img_set_src(scene->object, &scene->image);
|
||||
if (result == ESP_OK && ms >= 600 && !rod_hidden) {
|
||||
result = gfx_obj_set_visible(s_emote_background, false);
|
||||
rod_hidden = result == ESP_OK;
|
||||
}
|
||||
if (result == ESP_OK) result = lcd_direct_emote_update_hud(false, NULL);
|
||||
if (result == ESP_OK) {
|
||||
portENTER_CRITICAL(&s_state_lock);
|
||||
s_emote_active = true;
|
||||
portEXIT_CRITICAL(&s_state_lock);
|
||||
if (!started) gfx_disp_refresh_all(s_emote_disp);
|
||||
started = true;
|
||||
result = lcd_direct_emote_refresh();
|
||||
}
|
||||
unlock_err = gfx_emote_unlock(s_emote_handle);
|
||||
if (result == ESP_OK) result = unlock_err;
|
||||
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
|
||||
uint32_t draw_us = (uint32_t)(esp_timer_get_time() - draw_start_us);
|
||||
total_draw_us += draw_us;
|
||||
++draw_attempts;
|
||||
if (draw_us > max_draw_us) max_draw_us = draw_us;
|
||||
if (draw_us >= LCD_FORTUNE_STEP_MS * 1000) ++late;
|
||||
if (result == ESP_OK) ++frames;
|
||||
#else
|
||||
(void)draw_start_us;
|
||||
#endif
|
||||
if (result != ESP_OK) break;
|
||||
if (ms == LCD_FORTUNE_MOTION_MS) break;
|
||||
uint32_t next_ms = ms + LCD_FORTUNE_STEP_MS;
|
||||
if (next_ms > LCD_FORTUNE_MOTION_MS) next_ms = LCD_FORTUNE_MOTION_MS;
|
||||
result = lcd_direct_emote_wait_until(start_us + (int64_t)next_ms * 1000);
|
||||
ms = next_ms;
|
||||
}
|
||||
#if CONFIG_TQ_LCD_EMOTE_FPS_LOG
|
||||
if (started && frames > 0) {
|
||||
uint64_t motion_us = esp_timer_get_time() - start_us;
|
||||
uint64_t fps100 = lcd_direct_emote_fps_x100(frames, motion_us);
|
||||
ESP_LOGI(TAG, "fortune_fps result=%s frames=%u motion_us=%llu update_fps=%llu.%02llu "
|
||||
"draw_avg_us=%llu draw_max_us=%u late_frames=%u status=%s",
|
||||
lcd_fortune_result_dirs[index], (unsigned)frames, (unsigned long long)motion_us,
|
||||
(unsigned long long)(fps100 / 100), (unsigned long long)(fps100 % 100),
|
||||
(unsigned long long)(total_draw_us / draw_attempts), (unsigned)max_draw_us, (unsigned)late,
|
||||
esp_err_to_name(result));
|
||||
}
|
||||
#endif
|
||||
/* 运动仍锚定同一起点;迟到不能吞掉用户阅读签文所需的末帧停留。 */
|
||||
if (result == ESP_OK) result = lcd_direct_emote_wait_until(
|
||||
esp_timer_get_time() + (int64_t)LCD_FORTUNE_HOLD_MS * 1000);
|
||||
esp_err_t detach_err = lcd_direct_fortune_detach(scene);
|
||||
if (detach_err == ESP_OK) lcd_direct_fortune_free(scene);
|
||||
else {
|
||||
s_fortune_orphan = scene;
|
||||
s_emote_transfer_fault = true;
|
||||
ESP_LOGE(TAG, "Fortune detach failed; bound pixels retained, restart required");
|
||||
}
|
||||
if (result == ESP_OK) result = detach_err;
|
||||
if (result == ESP_OK && !lcd_direct_should_stop_idle()) lcd_direct_signal_completion();
|
||||
if (!started && detach_err == ESP_OK && result != ESP_ERR_INVALID_STATE)
|
||||
return ESP_ERR_NOT_SUPPORTED;
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
static uint16_t lcd_direct_swap_color(uint16_t color)
|
||||
@@ -2457,6 +2656,14 @@ static esp_err_t lcd_direct_play_step_sequence(const lcd_direct_request_t *reque
|
||||
ESP_ERR_INVALID_ARG,
|
||||
TAG,
|
||||
"invalid step sequence");
|
||||
#if CONFIG_TQ_LCD_EMOTE_GFX_TEST
|
||||
int fortune = lcd_direct_fortune_sequence_index(request);
|
||||
if (fortune >= 0 && s_emote_ready && LCD_DIRECT_WIDTH == 320 && LCD_DIRECT_HEIGHT == 240) {
|
||||
esp_err_t err = lcd_direct_play_fortune((size_t)fortune);
|
||||
if (err != ESP_ERR_NOT_SUPPORTED) return err;
|
||||
ESP_LOGW(TAG, "Fortune layers unavailable; using PNG fallback");
|
||||
}
|
||||
#endif
|
||||
|
||||
for (size_t i = 0; i < request->step_count; ++i) {
|
||||
(void)lcd_direct_service_preheat();
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
#include "lcd_emote_fortune.h"
|
||||
#include <math.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct { float x, y, u, v; } vertex_t;
|
||||
static bool read_length(const uint8_t *src,size_t size,size_t *pos,size_t *length)
|
||||
{
|
||||
if (*length!=15) return true;
|
||||
unsigned value;
|
||||
do {
|
||||
if (*pos>=size || *length>SIZE_MAX-255) return false;
|
||||
value=src[(*pos)++]; *length+=value;
|
||||
} while (value==255);
|
||||
return true;
|
||||
}
|
||||
bool lcd_emote_fortune_decode(const uint8_t *src,size_t size,uint8_t *dst,size_t capacity)
|
||||
{
|
||||
if (!src || !size || !dst || !capacity) return false;
|
||||
size_t pos=0,written=0;
|
||||
while (pos<size) {
|
||||
unsigned token=src[pos++]; size_t literals=token>>4;
|
||||
if (!read_length(src,size,&pos,&literals) || literals>size-pos || literals>capacity-written) return false;
|
||||
memcpy(dst+written,src+pos,literals);written+=literals;pos+=literals;
|
||||
if (pos==size) return written==capacity && (token&15)==0;
|
||||
if (size-pos<2) return false;
|
||||
size_t offset=src[pos]|((size_t)src[pos+1]<<8);pos+=2;
|
||||
size_t match=token&15;
|
||||
if (!offset || offset>written || !read_length(src,size,&pos,&match) ||
|
||||
match>SIZE_MAX-4 || match+4>capacity-written) return false;
|
||||
match+=4;
|
||||
for (size_t i=0;i<match;++i) {dst[written]=dst[written-offset];++written;}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
static float smooth(float v) { v = fmaxf(0, fminf(1, v)); return v*v*(3-2*v); }
|
||||
static uint16_t color_at(const lcd_fortune_texture_t *t, size_t i)
|
||||
{ return ((uint16_t)t->pixels[i*2]<<8) | t->pixels[i*2+1]; }
|
||||
static void blend(uint8_t *dst, int x, int y, uint16_t color, unsigned alpha)
|
||||
{
|
||||
x -= LCD_FORTUNE_X; y -= LCD_FORTUNE_Y;
|
||||
if (x < 0 || y < 0 || x >= LCD_FORTUNE_WIDTH || y >= LCD_FORTUNE_HEIGHT || !alpha) return;
|
||||
size_t i = ((size_t)y*LCD_FORTUNE_WIDTH+x)*2;
|
||||
uint16_t old = ((uint16_t)dst[i]<<8) | dst[i+1];
|
||||
unsigned r = (((color>>11)&31)*alpha + ((old>>11)&31)*(255-alpha)+127)/255;
|
||||
unsigned g = (((color>>5)&63)*alpha + ((old>>5)&63)*(255-alpha)+127)/255;
|
||||
unsigned b = ((color&31)*alpha + (old&31)*(255-alpha)+127)/255;
|
||||
uint16_t out = (r<<11)|(g<<5)|b;
|
||||
dst[i] = out>>8; dst[i+1] = out;
|
||||
}
|
||||
/* 双线性采样在预乘 Alpha 空间,避免黑色透明边缘造成轮廓暗边。 */
|
||||
static void sample(uint8_t *dst, int x, int y, const lcd_fortune_texture_t *t,
|
||||
float u, float v, unsigned opacity)
|
||||
{
|
||||
if (u < 0 || v < 0 || u > 1 || v > 1 || !opacity) return;
|
||||
float sx = u*(t->width-1), sy = v*(t->height-1);
|
||||
int x0 = (int)sx, y0 = (int)sy;
|
||||
int x1 = x0+1 < t->width ? x0+1 : x0, y1 = y0+1 < t->height ? y0+1 : y0;
|
||||
float fx = sx-x0, fy = sy-y0;
|
||||
const size_t indices[4] = {(size_t)y0*t->width+x0, (size_t)y0*t->width+x1,
|
||||
(size_t)y1*t->width+x0, (size_t)y1*t->width+x1};
|
||||
const float weights[4] = {(1-fx)*(1-fy), fx*(1-fy), (1-fx)*fy, fx*fy};
|
||||
float a=0, r=0, g=0, b=0;
|
||||
size_t count = (size_t)t->width*t->height;
|
||||
for (int i=0; i<4; ++i) {
|
||||
float aw = weights[i]*t->pixels[count*2+indices[i]];
|
||||
uint16_t c = color_at(t, indices[i]);
|
||||
a += aw; r += ((c>>11)&31)*aw; g += ((c>>5)&63)*aw; b += (c&31)*aw;
|
||||
}
|
||||
if (a < 0.5f) return;
|
||||
float inv_a = 1/a;
|
||||
uint16_t c = ((unsigned)(r*inv_a+0.5f)<<11)|((unsigned)(g*inv_a+0.5f)<<5)|(unsigned)(b*inv_a+0.5f);
|
||||
blend(dst,x,y,c,(unsigned)(a*opacity/255+0.5f));
|
||||
}
|
||||
static void blit(uint8_t *dst, const lcd_fortune_texture_t *t, int x, int y, unsigned opacity)
|
||||
{
|
||||
size_t count=(size_t)t->width*t->height;
|
||||
for (int sy=0; sy<t->height; ++sy) for (int sx=0; sx<t->width; ++sx) {
|
||||
size_t i=(size_t)sy*t->width+sx;
|
||||
unsigned a=t->pixels[count*2+i]*opacity/255;
|
||||
if (a) blend(dst,x+sx,y+sy,color_at(t,i),a);
|
||||
}
|
||||
}
|
||||
static void triangle(uint8_t *dst, const lcd_fortune_texture_t *t,
|
||||
vertex_t a, vertex_t b, vertex_t c, bool include_shared)
|
||||
{
|
||||
float det=(b.x-a.x)*(c.y-a.y)-(c.x-a.x)*(b.y-a.y);
|
||||
if (fabsf(det)<0.01f) return;
|
||||
int left=(int)floorf(fminf(a.x,fminf(b.x,c.x))), right=(int)ceilf(fmaxf(a.x,fmaxf(b.x,c.x)));
|
||||
int top=(int)floorf(fminf(a.y,fminf(b.y,c.y))), bottom=(int)ceilf(fmaxf(a.y,fmaxf(b.y,c.y)));
|
||||
if (left<LCD_FORTUNE_X) left=LCD_FORTUNE_X;
|
||||
if (top<LCD_FORTUNE_Y) top=LCD_FORTUNE_Y;
|
||||
if (right>LCD_FORTUNE_X+LCD_FORTUNE_WIDTH) right=LCD_FORTUNE_X+LCD_FORTUNE_WIDTH;
|
||||
if (bottom>LCD_FORTUNE_Y+LCD_FORTUNE_HEIGHT) bottom=LCD_FORTUNE_Y+LCD_FORTUNE_HEIGHT;
|
||||
float inv=1/det;
|
||||
for (int y=top; y<bottom; ++y) for (int x=left; x<right; ++x) {
|
||||
float px=x+0.5f-a.x, py=y+0.5f-a.y;
|
||||
float wb=(px*(c.y-a.y)-py*(c.x-a.x))*inv;
|
||||
float wc=((b.x-a.x)*py-(b.y-a.y)*px)*inv;
|
||||
/* 两个三角形共边只属于一方,避免 Alpha 轮廓重复混合。 */
|
||||
if (wb>=0 && wc>=0 && (include_shared ? wb+wc<=1 : wb+wc<1)) sample(dst,x,y,t,
|
||||
a.u+wb*(b.u-a.u)+wc*(c.u-a.u), a.v+wb*(b.v-a.v)+wc*(c.v-a.v),255);
|
||||
}
|
||||
}
|
||||
static vertex_t project(float u, float v, float width, float height, float theta,
|
||||
float cx, float cy, bool side, bool front)
|
||||
{
|
||||
float c=cosf(theta), s=sinf(theta), roll=0.025f*s;
|
||||
float x=side?0:(front?0.5f-u:u-0.5f)*width;
|
||||
float z=side?(u-0.5f)*10:0;
|
||||
float X=x*c-z*s, Z=x*s+z*c, k=320/(320+Z);
|
||||
float xx=X*k, yy=(v-0.5f)*height*k;
|
||||
return (vertex_t){cx+xx*cosf(roll)-yy*sinf(roll),cy+xx*sinf(roll)+yy*cosf(roll),u,v};
|
||||
}
|
||||
static void paw(uint8_t *dst, const lcd_fortune_texture_t *t, float width, float height,
|
||||
float theta, float cx, float cy, bool side, bool front)
|
||||
{
|
||||
/* 16 条纵向纹理带近似透视;每带仅两个三角形,非三维模型。 */
|
||||
for (int i=0; i<16; ++i) {
|
||||
float u0=i/16.0f,u1=(i+1)/16.0f;
|
||||
vertex_t a=project(u0,0,width,height,theta,cx,cy,side,front);
|
||||
vertex_t b=project(u1,0,width,height,theta,cx,cy,side,front);
|
||||
vertex_t c=project(u0,1,width,height,theta,cx,cy,side,front);
|
||||
vertex_t d=project(u1,1,width,height,theta,cx,cy,side,front);
|
||||
triangle(dst,t,a,b,c,true); triangle(dst,t,d,c,b,false);
|
||||
}
|
||||
}
|
||||
static void flip_question(uint8_t *dst,const lcd_fortune_texture_t *t,float theta,float cx,float cy)
|
||||
{
|
||||
/* 小问号与签的正面共用透视平面;正面展开时不突然贴出平面图案。 */
|
||||
vertex_t a=project(0.30f,0.43f,60,114,theta,cx,cy,false,true);
|
||||
vertex_t b=project(0.70f,0.43f,60,114,theta,cx,cy,false,true);
|
||||
vertex_t c=project(0.30f,0.816f,60,114,theta,cx,cy,false,true);
|
||||
vertex_t d=project(0.70f,0.816f,60,114,theta,cx,cy,false,true);
|
||||
/* 正面 local X 反向,因此按屏幕方向摆放 UV,签文不镜像。 */
|
||||
a.u=c.u=0; b.u=d.u=1; a.v=b.v=0; c.v=d.v=1;
|
||||
triangle(dst,t,a,b,c,true);triangle(dst,t,d,c,b,false);
|
||||
}
|
||||
static void overlay(uint8_t *dst,const lcd_fortune_texture_t *t,
|
||||
float left,float top,float width,float height,float reveal)
|
||||
{
|
||||
for (int y=(int)floorf(top); y<(int)ceilf(top+height*reveal); ++y)
|
||||
for (int x=(int)floorf(left); x<(int)ceilf(left+width); ++x)
|
||||
sample(dst,x,y,t,(x+0.5f-left)/width,(y+0.5f-top)/height,255);
|
||||
}
|
||||
static void stars(uint8_t *dst,float progress)
|
||||
{
|
||||
static const uint8_t points[][3]={{89,62,4},{232,91,6},{75,132,5},{251,145,4},
|
||||
{119,195,5},{229,211,6},{198,40,4},{79,191,4}};
|
||||
unsigned alpha=(unsigned)(255*progress);
|
||||
for (size_t i=0;i<sizeof(points)/sizeof(points[0]);++i) {
|
||||
int radius=points[i][2];
|
||||
for (int j=-radius;j<=radius;++j) {
|
||||
unsigned a=alpha*(radius+1-abs(j))/(radius+1);
|
||||
blend(dst,points[i][0]+j,points[i][1],0xffad,a);
|
||||
if (j) blend(dst,points[i][0],points[i][1]+j,0xffad,a);
|
||||
}
|
||||
}
|
||||
}
|
||||
bool lcd_emote_fortune_build_frame(uint8_t *dst,size_t capacity,
|
||||
const lcd_fortune_texture_t shared[LCD_FORTUNE_SHARED_COUNT],
|
||||
const lcd_fortune_texture_t *text,uint32_t time_ms)
|
||||
{
|
||||
const size_t bytes=LCD_FORTUNE_WIDTH*LCD_FORTUNE_HEIGHT*2;
|
||||
if (!dst || capacity<bytes || !shared || !text || !text->pixels ||
|
||||
!text->width || !text->height || text->width>320 || text->height>240) return false;
|
||||
for (int i=0;i<LCD_FORTUNE_SHARED_COUNT;++i)
|
||||
if (!shared[i].pixels || !shared[i].width || !shared[i].height ||
|
||||
shared[i].width>320 || shared[i].height>240) return false;
|
||||
memset(dst,0,bytes);
|
||||
if (time_ms>LCD_FORTUNE_MOTION_MS) time_ms=LCD_FORTUNE_MOTION_MS;
|
||||
float zoom=smooth(((float)time_ms-1900)/600);
|
||||
if (time_ms<600) blit(dst,&shared[LCD_FORTUNE_ROD],lcd_fortune_shared[LCD_FORTUNE_ROD].x,
|
||||
lcd_fortune_shared[LCD_FORTUNE_ROD].y,255);
|
||||
float width=60+40*zoom,height=114+76*zoom,cx=160,cy=94+25*zoom;
|
||||
if (time_ms<600) {
|
||||
cy=212-118*smooth(time_ms/600.0f);
|
||||
paw(dst,&shared[LCD_FORTUNE_BACK],60,110,0,cx,cy,false,false);
|
||||
} else if (time_ms<1200) {
|
||||
float theta=3.14159265f*smooth((time_ms-600)/600.0f), c=cosf(theta), s=sinf(theta);
|
||||
bool side=fabsf(c)<0.17f,front=c<0;
|
||||
const lcd_fortune_texture_t *t=&shared[side?LCD_FORTUNE_EDGE:front?LCD_FORTUNE_FRONT:LCD_FORTUNE_BACK];
|
||||
paw(dst,t,60,side?110:114,theta,cx+4*s,cy-3*s,side,front);
|
||||
if (front && !side) flip_question(dst,&shared[LCD_FORTUNE_QUESTION],theta,cx+4*s,cy-3*s);
|
||||
} else {
|
||||
paw(dst,&shared[LCD_FORTUNE_FRONT],width,height,3.14159265f,cx,cy,false,true);
|
||||
}
|
||||
if (time_ms>=1200 && time_ms<1600)
|
||||
overlay(dst,&shared[LCD_FORTUNE_QUESTION],cx-12,cy-8,24,44,1);
|
||||
if (time_ms>=1600) {
|
||||
float left=cx-width/2,top=cy-height/2;
|
||||
overlay(dst,text,left+width*0.26f,top+height*70/190,width*0.48f,height*104/190,
|
||||
smooth((time_ms-1600)/300.0f));
|
||||
}
|
||||
/* 下方爪子遮挡抽出时的签底;放大阶段淡出,最终背景纯黑。 */
|
||||
blit(dst,&shared[LCD_FORTUNE_PILE],lcd_fortune_shared[LCD_FORTUNE_PILE].x,
|
||||
lcd_fortune_shared[LCD_FORTUNE_PILE].y,(unsigned)(255*(1-zoom)));
|
||||
if (zoom>0) stars(dst,zoom);
|
||||
return true;
|
||||
}
|
||||