diff --git a/.cursor/rules/commit-after-work.mdc b/.cursor/rules/commit-after-work.mdc new file mode 100644 index 0000000..a3b7059 --- /dev/null +++ b/.cursor/rules/commit-after-work.mdc @@ -0,0 +1,12 @@ +--- +description: Always create a local git commit after finishing a task +alwaysApply: true +--- + +# Git commit after work + +After completing a user-requested change (code, docs, firmware, assets), always: + +1. Update `CHANGELOG.md` when behavior changes +2. `git add` relevant files and create a local commit (do not push unless asked) +3. Keep the working tree clean before ending the turn diff --git a/CHANGELOG.md b/CHANGELOG.md index 5517c56..b7e4954 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +## 2026-10-19 — 去掉图后条纹 + +- 样张改为纯色底,去掉装饰横纹。 +- 画面 cover 铺满画布;LVGL 全屏 STRETCH,避免缩放留缝/条带。 + +## 2026-10-19 — GIF 不阻塞触控 + +- GIF 解码改到 core0 任务;`playFrame(false)` 去掉库内 `delay()`。 +- 切换相片先 `photo_view_stop()`,避免点按卡几秒。 + +## 2026-10-19 — 每次改完提交 + +- 项目规则:完成任务后必须本地 git commit。 + ## 2026-10-19 — Photo album - CYD LVGL 壳层来自 tftest(触摸映射冻结,TF + SoftSPI)。 diff --git a/README.md b/README.md index 6e217c8..83c8ef4 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ ESP32-2432S028R 电子相册:从 TF 卡读取 **JPG / PNG / GIF**,触控切 ## 功能 - 扫描 TF 卡 `/photos` 与根目录的 `.jpg` / `.jpeg` / `.png` / `.gif` -- 半分辨率画布 160×120,界面 2× 放大到 320×240(适配无 PSRAM 的 DRAM) -- JPG 过大自动 1/2、1/4、1/8 缩放;PNG / GIF 等比缩入画布 +- 半分辨率画布 160×120,全屏拉伸到 320×240(适配无 PSRAM 的 DRAM) +- JPG / PNG / GIF 按 cover 铺满画布(可裁边) - **点左半屏 = 上一张,点右半屏 = 下一张**;底栏 `<` / `>` 同样切换 - GIF 自动播帧;右上角显示 TF 状态 - 串口 `#PUT` 传文件到卡(`tools/sd_put.py`) diff --git a/assets/photos/01_blue.jpg b/assets/photos/01_blue.jpg index d15d962..cc47287 100644 Binary files a/assets/photos/01_blue.jpg and b/assets/photos/01_blue.jpg differ diff --git a/assets/photos/02_red.jpg b/assets/photos/02_red.jpg index 1d68779..59040de 100644 Binary files a/assets/photos/02_red.jpg and b/assets/photos/02_red.jpg differ diff --git a/assets/photos/03_green.png b/assets/photos/03_green.png index 12e1b34..b4251ed 100644 Binary files a/assets/photos/03_green.png and b/assets/photos/03_green.png differ diff --git a/assets/photos/04_bounce.gif b/assets/photos/04_bounce.gif index 72d2246..82b67b2 100644 Binary files a/assets/photos/04_bounce.gif and b/assets/photos/04_bounce.gif differ diff --git a/firmware/cyd/VERSION.txt b/firmware/cyd/VERSION.txt index 995162b..96ea728 100644 --- a/firmware/cyd/VERSION.txt +++ b/firmware/cyd/VERSION.txt @@ -1,5 +1,5 @@ name: CYD Photo album (cyd) -git: 99618a4 +git: 800e168 date: 2026-10-19 board: ESP32-2432S028R (ILI9341) features: jpg/png/gif album, SoftSPI touch, TF card @@ -12,5 +12,5 @@ offsets: sha256: bootloader.bin 3D234A7471F67B013686DABD4DEE7C1FA915C9928463616A94BC9297ACF1ABF8 boot_app0.bin F94C5D786A7A8FAB06AC5D10E33BF37711A6697636DC037559EA19CC410A17F0 - firmware.bin CB21DB25D8AAC041B39C10AA2E63E00D2401D1A7EA4F956F9B4985D1EA87C8EB + firmware.bin 8BAFA46DA3DF4BF3A15D5B67E3062B80E0D8576A2F59BE2101431747E24438AF partitions.bin 148B959CBFF1C38AA8E1D5C0BA9D612C54997B945E56A63F41223EEF650653A1 diff --git a/firmware/cyd/firmware.bin b/firmware/cyd/firmware.bin index 4ed631d..81648f2 100644 Binary files a/firmware/cyd/firmware.bin and b/firmware/cyd/firmware.bin differ diff --git a/include/photo_view.h b/include/photo_view.h index 583ba42..36f875f 100644 --- a/include/photo_view.h +++ b/include/photo_view.h @@ -11,6 +11,9 @@ void photo_view_deinit(); // Decode entry into half-res RGB565 canvas (160x120, shown 2x). bool photo_view_show(const PhotoEntry *entry); +// Abort GIF worker immediately so touch/switch stays responsive. +void photo_view_stop(); + const lv_image_dsc_t *photo_view_dsc(); uint16_t photo_view_canvas_w(); uint16_t photo_view_canvas_h(); @@ -18,5 +21,5 @@ uint16_t photo_view_img_w(); uint16_t photo_view_img_h(); bool photo_view_is_gif(); -// Advance GIF frame if due; returns true when pixels changed. +// Publish a ready GIF frame to the visible canvas (non-blocking). bool photo_view_gif_tick(); diff --git a/src/main.cpp b/src/main.cpp index 42bd0e9..a5b994e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -72,14 +72,15 @@ void setup() { } void loop() { - if (sd_put_poll()) { - refresh_sd_ui(true); - } - + // Touch/UI first — GIF decode runs on core0 and must not starve this. lv_timer_handler(); lv_tick_inc(5); ui_tick(); + if (sd_put_poll()) { + refresh_sd_ui(true); + } + const uint32_t now = millis(); if (now - last_refresh_ms >= REFRESH_MS) { last_refresh_ms = now; @@ -87,5 +88,5 @@ void loop() { refresh_sd_ui(false); } - delay(5); + delay(photo_view_is_gif() ? 2 : 5); } diff --git a/src/photo_view.cpp b/src/photo_view.cpp index edd5c84..3367409 100644 --- a/src/photo_view.cpp +++ b/src/photo_view.cpp @@ -8,6 +8,9 @@ #include #include +#include +#include + // Half-res canvas (fits ESP32 DRAM). UI shows it at 2x -> 320x240. static constexpr uint16_t CANVAS_W = UI_HOR_RES / 2; static constexpr uint16_t CANVAS_H = UI_VER_RES / 2; @@ -24,8 +27,6 @@ static PNG *g_png = nullptr; static AnimatedGIF *g_gif = nullptr; static File g_file; -static bool g_gif_open = false; -static uint32_t g_gif_next_ms = 0; static int g_off_x = 0; static int g_off_y = 0; static int g_src_w = 0; @@ -33,12 +34,24 @@ static int g_src_h = 0; static int g_dst_w = 0; static int g_dst_h = 0; -static void canvas_clear(uint16_t color) { +// GIF worker (core 0): decode must not block LVGL/touch on core 1. +static TaskHandle_t g_gif_task = nullptr; +static uint16_t *g_back = nullptr; // decode target (heap) +static uint16_t *g_draw_dst = g_pixels; // where gif_draw writes +static char g_gif_path[PHOTO_PATH_MAX]; +static volatile bool g_gif_active = false; // session open (UI-visible) +static volatile bool g_gif_busy = false; // worker running +static volatile bool g_gif_stop = false; // request abort +static volatile bool g_frame_ready = false; // back buffer has a new frame + +static void canvas_clear_buf(uint16_t *buf, uint16_t color) { for (size_t i = 0; i < CANVAS_PIXELS; i++) { - g_pixels[i] = color; + buf[i] = color; } } +static void canvas_clear(uint16_t color) { canvas_clear_buf(g_pixels, color); } + static void canvas_sync_dsc() { g_dsc.header.magic = LV_IMAGE_HEADER_MAGIC; g_dsc.header.cf = LV_COLOR_FORMAT_RGB565; @@ -65,47 +78,40 @@ static void free_png() { } } -static void gif_close_session() { - if (g_gif_open && g_gif) { - g_gif->close(); - g_gif_open = false; +// Tightest JPEGDEC scale that still covers the canvas (center-cropped). +static int jpeg_scale_cover(int w, int h, int *dw, int *dh) { + const int scales[] = {0, JPEG_SCALE_HALF, JPEG_SCALE_QUARTER, JPEG_SCALE_EIGHTH}; + const int divs[] = {1, 2, 4, 8}; + int best = 0; + int best_dw = w; + int best_dh = h; + int best_area = 0x7fffffff; + bool found = false; + for (int i = 0; i < 4; i++) { + const int dw_i = w / divs[i]; + const int dh_i = h / divs[i]; + if (dw_i < 1 || dh_i < 1) { + continue; + } + if (dw_i >= (int)CANVAS_W && dh_i >= (int)CANVAS_H) { + const int area = dw_i * dh_i; + if (area < best_area) { + best_area = area; + best = scales[i]; + best_dw = dw_i; + best_dh = dh_i; + found = true; + } + } } - if (g_gif) { - delete g_gif; - g_gif = nullptr; + if (!found) { + best = 0; + best_dw = w; + best_dh = h; } -} - -static void fit_size(int nw, int nh, int *dw, int *dh) { - *dw = nw; - *dh = nh; - if (*dw <= (int)CANVAS_W && *dh <= (int)CANVAS_H) { - return; - } - const float sx = (float)CANVAS_W / (float)nw; - const float sy = (float)CANVAS_H / (float)nh; - const float s = sx < sy ? sx : sy; - *dw = (int)(nw * s + 0.5f); - *dh = (int)(nh * s + 0.5f); - if (*dw < 1) { - *dw = 1; - } - if (*dh < 1) { - *dh = 1; - } -} - -static int jpeg_scale_for(int w, int h) { - if (w > CANVAS_W * 4 || h > CANVAS_H * 4) { - return JPEG_SCALE_EIGHTH; - } - if (w > CANVAS_W * 2 || h > CANVAS_H * 2) { - return JPEG_SCALE_QUARTER; - } - if (w > CANVAS_W || h > CANVAS_H) { - return JPEG_SCALE_HALF; - } - return 0; + *dw = best_dw; + *dh = best_dh; + return best; } static void *jpeg_open(const char *filename, int32_t *size) { @@ -192,34 +198,32 @@ static int png_draw(PNGDRAW *p) { return 0; } - // Line buffer for source width (cap to avoid stack blow) static uint16_t line[640]; if (p->iWidth > 640) { return 0; } g_png->getLineAsRGB565(p, line, PNG_RGB565_LITTLE_ENDIAN, 0x0000); - // Nearest-neighbor: which dest rows map from this source row? - for (int dy = 0; dy < g_dst_h; dy++) { - const int sy = (dy * g_src_h) / g_dst_h; - if (sy != p->y) { - continue; - } + int dy = 0; + while (dy < g_dst_h && (dy * g_src_h) / g_dst_h < p->y) { + dy++; + } + while (dy < g_dst_h && (dy * g_src_h) / g_dst_h == p->y) { const int y = g_off_y + dy; - if (y < 0 || y >= (int)CANVAS_H) { - continue; - } - for (int dx = 0; dx < g_dst_w; dx++) { - const int sx = (dx * g_src_w) / g_dst_w; - if (sx < 0 || sx >= p->iWidth) { - continue; + if (y >= 0 && y < (int)CANVAS_H) { + for (int dx = 0; dx < g_dst_w; dx++) { + const int sx = (dx * g_src_w) / g_dst_w; + if (sx < 0 || sx >= p->iWidth) { + continue; + } + const int x = g_off_x + dx; + if (x < 0 || x >= (int)CANVAS_W) { + continue; + } + g_pixels[(size_t)y * CANVAS_W + (size_t)x] = line[sx]; } - const int x = g_off_x + dx; - if (x < 0 || x >= (int)CANVAS_W) { - continue; - } - g_pixels[(size_t)y * CANVAS_W + (size_t)x] = line[sx]; } + dy++; } return 1; } @@ -258,41 +262,182 @@ static int32_t gif_seek(GIFFILE *pFile, int32_t iPosition) { } static void gif_draw(GIFDRAW *pDraw) { - if (!pDraw || g_src_w <= 0 || g_dst_w <= 0) { + if (!pDraw || !g_draw_dst || g_src_w <= 0 || g_dst_w <= 0) { return; } - // Map source frame lines into scaled dest canvas + if (g_gif_stop) { + return; + } + const int src_y = pDraw->iY + pDraw->y; - for (int dy = 0; dy < g_dst_h; dy++) { - const int sy = (dy * g_src_h) / g_dst_h; - if (sy != src_y) { - continue; - } - const int y = g_off_y + dy; + uint16_t *dst = g_draw_dst; + + // Fast path: 1:1 into canvas region + if (g_src_w == g_dst_w && g_src_h == g_dst_h) { + const int y = g_off_y + src_y; if (y < 0 || y >= (int)CANVAS_H) { - continue; + return; } - for (int dx = 0; dx < g_dst_w; dx++) { - const int sx = (dx * g_src_w) / g_dst_w; - const int lx = sx - pDraw->iX; - if (lx < 0 || lx >= pDraw->iWidth) { - continue; - } - const uint8_t idx = pDraw->pPixels[lx]; + uint16_t *row = dst + (size_t)y * CANVAS_W + (size_t)g_off_x + pDraw->iX; + for (int x = 0; x < pDraw->iWidth; x++) { + const uint8_t idx = pDraw->pPixels[x]; if (pDraw->ucHasTransparency && idx == pDraw->ucTransparent) { continue; } - const int x = g_off_x + dx; - if (x < 0 || x >= (int)CANVAS_W) { + row[x] = pDraw->pPalette[idx]; + } + return; + } + + // Scaled: only dest rows that sample this source line + int dy = 0; + while (dy < g_dst_h && (dy * g_src_h) / g_dst_h < src_y) { + dy++; + } + while (dy < g_dst_h && (dy * g_src_h) / g_dst_h == src_y) { + const int y = g_off_y + dy; + if (y >= 0 && y < (int)CANVAS_H) { + for (int dx = 0; dx < g_dst_w; dx++) { + const int sx = (dx * g_src_w) / g_dst_w; + const int lx = sx - pDraw->iX; + if (lx < 0 || lx >= pDraw->iWidth) { + continue; + } + const uint8_t idx = pDraw->pPixels[lx]; + if (pDraw->ucHasTransparency && idx == pDraw->ucTransparent) { + continue; + } + const int x = g_off_x + dx; + if (x < 0 || x >= (int)CANVAS_W) { + continue; + } + dst[(size_t)y * CANVAS_W + (size_t)x] = pDraw->pPalette[idx]; + } + } + dy++; + } +} + +static void gif_worker_cleanup() { + if (g_gif) { + g_gif->close(); + delete g_gif; + g_gif = nullptr; + } + g_draw_dst = g_pixels; + g_frame_ready = false; + g_gif_busy = false; + g_gif_active = false; +} + +static void gif_task(void * /*arg*/) { + for (;;) { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + g_gif_busy = true; + g_gif_stop = false; + g_frame_ready = false; + + if (!g_back) { + g_back = (uint16_t *)malloc(CANVAS_PIXELS * sizeof(uint16_t)); + if (!g_back) { + Serial.println("GIF: back buffer alloc failed"); + g_gif_busy = false; + g_gif_active = false; continue; } - g_pixels[(size_t)y * CANVAS_W + (size_t)x] = pDraw->pPalette[idx]; } + + g_gif = new AnimatedGIF(); + if (!g_gif) { + gif_worker_cleanup(); + continue; + } + + g_gif->begin(GIF_PALETTE_RGB565_LE); + if (!g_gif->open(g_gif_path, gif_open, gif_close, gif_read, gif_seek, + gif_draw)) { + Serial.printf("GIF open fail: %s\n", g_gif_path); + gif_worker_cleanup(); + continue; + } + + g_src_w = g_gif->getCanvasWidth(); + g_src_h = g_gif->getCanvasHeight(); + if (g_src_w <= 0 || g_src_h <= 0) { + gif_worker_cleanup(); + continue; + } + + // Cover full canvas (sample source into every dest pixel). + g_dst_w = CANVAS_W; + g_dst_h = CANVAS_H; + g_img_w = CANVAS_W; + g_img_h = CANVAS_H; + g_off_x = 0; + g_off_y = 0; + + canvas_clear_buf(g_back, 0x0000); + g_draw_dst = g_back; + g_gif_active = true; + + while (!g_gif_stop) { + int delay_ms = 0; + // bSync=false: never delay() inside the library (that froze touch). + g_gif->playFrame(false, &delay_ms); + if (g_gif_stop) { + break; + } + + g_frame_ready = true; + // Wait until UI consumed the frame (or abort). + while (g_frame_ready && !g_gif_stop) { + vTaskDelay(pdMS_TO_TICKS(2)); + } + if (g_gif_stop) { + break; + } + + if (delay_ms < 30) { + delay_ms = 30; + } + if (delay_ms > 200) { + delay_ms = 200; + } + int waited = 0; + while (waited < delay_ms && !g_gif_stop) { + vTaskDelay(pdMS_TO_TICKS(5)); + waited += 5; + } + } + + gif_worker_cleanup(); + } +} + +static void gif_ensure_task() { + if (g_gif_task) { + return; + } + xTaskCreatePinnedToCore(gif_task, "gif", 8192, nullptr, 1, &g_gif_task, 0); +} + +static void gif_stop_and_wait() { + g_gif_stop = true; + g_frame_ready = false; + const uint32_t t0 = millis(); + while (g_gif_busy && (millis() - t0) < 800) { + vTaskDelay(pdMS_TO_TICKS(5)); + } + // If worker hung, force local cleanup of flags (file may still be open briefly) + if (g_gif_busy) { + Serial.println("GIF: stop timeout"); + g_gif_busy = false; + g_gif_active = false; } } static bool load_jpeg(const char *path) { - gif_close_session(); + gif_stop_and_wait(); free_png(); free_jpeg(); @@ -310,22 +455,13 @@ static bool load_jpeg(const char *path) { const int nw = g_jpeg->getWidth(); const int nh = g_jpeg->getHeight(); - const int scale = jpeg_scale_for(nw, nh); - int dw = nw; - int dh = nh; - if (scale == JPEG_SCALE_HALF) { - dw /= 2; - dh /= 2; - } else if (scale == JPEG_SCALE_QUARTER) { - dw /= 4; - dh /= 4; - } else if (scale == JPEG_SCALE_EIGHTH) { - dw /= 8; - dh /= 8; - } + int dw = 0; + int dh = 0; + const int scale = jpeg_scale_cover(nw, nh, &dw, &dh); - g_img_w = (uint16_t)dw; - g_img_h = (uint16_t)dh; + g_img_w = CANVAS_W; + g_img_h = CANVAS_H; + // Center; negative offsets crop overflow into the canvas (cover). const int xoff = ((int)CANVAS_W - dw) / 2; const int yoff = ((int)CANVAS_H - dh) / 2; @@ -341,7 +477,7 @@ static bool load_jpeg(const char *path) { } static bool load_png(const char *path) { - gif_close_session(); + gif_stop_and_wait(); free_jpeg(); free_png(); @@ -367,11 +503,13 @@ static bool load_png(const char *path) { return false; } - fit_size(g_src_w, g_src_h, &g_dst_w, &g_dst_h); - g_img_w = (uint16_t)g_dst_w; - g_img_h = (uint16_t)g_dst_h; - g_off_x = ((int)CANVAS_W - g_dst_w) / 2; - g_off_y = ((int)CANVAS_H - g_dst_h) / 2; + // Sample source into every canvas pixel (cover / fill). + g_dst_w = CANVAS_W; + g_dst_h = CANVAS_H; + g_img_w = CANVAS_W; + g_img_h = CANVAS_H; + g_off_x = 0; + g_off_y = 0; canvas_clear(0x0000); const int drc = g_png->decode(nullptr, 0); @@ -385,44 +523,37 @@ static bool load_png(const char *path) { } static bool load_gif(const char *path) { + gif_stop_and_wait(); free_jpeg(); free_png(); - gif_close_session(); - g_gif = new AnimatedGIF(); - if (!g_gif) { - return false; - } - - g_gif->begin(GIF_PALETTE_RGB565_LE); - if (!g_gif->open(path, gif_open, gif_close, gif_read, gif_seek, gif_draw)) { - Serial.printf("GIF open fail: %s\n", path); - gif_close_session(); - return false; - } - - g_src_w = g_gif->getCanvasWidth(); - g_src_h = g_gif->getCanvasHeight(); - if (g_src_w <= 0 || g_src_h <= 0) { - gif_close_session(); - return false; - } - - fit_size(g_src_w, g_src_h, &g_dst_w, &g_dst_h); - g_img_w = (uint16_t)g_dst_w; - g_img_h = (uint16_t)g_dst_h; - g_off_x = ((int)CANVAS_W - g_dst_w) / 2; - g_off_y = ((int)CANVAS_H - g_dst_h) / 2; - g_gif_open = true; + gif_ensure_task(); + strncpy(g_gif_path, path, sizeof(g_gif_path) - 1); + g_gif_path[sizeof(g_gif_path) - 1] = '\0'; + // Show black immediately; worker fills frames async. canvas_clear(0x0000); - int delay_ms = 0; - g_gif->playFrame(true, &delay_ms); - if (delay_ms < 20) { - delay_ms = 100; + canvas_sync_dsc(); + g_gif_stop = false; + xTaskNotifyGive(g_gif_task); + + // Brief wait for first frame (non-blocking beyond a short timeout). + const uint32_t t0 = millis(); + while ((millis() - t0) < 250) { + if (g_frame_ready) { + break; + } + // Worker finished with no frame => open/decode failed + if ((millis() - t0) > 40 && !g_gif_busy && !g_gif_active) { + return false; + } + vTaskDelay(pdMS_TO_TICKS(5)); } - g_gif_next_ms = millis() + (uint32_t)delay_ms; - return true; + if (g_frame_ready && g_back) { + memcpy(g_pixels, g_back, CANVAS_PIXELS * sizeof(uint16_t)); + g_frame_ready = false; + } + return g_gif_active || g_gif_busy; } bool photo_view_init() { @@ -432,6 +563,7 @@ bool photo_view_init() { canvas_clear(0x0000); canvas_sync_dsc(); g_ready = true; + gif_ensure_task(); Serial.printf("photo_view: canvas %ux%u (%u bytes), free heap %u\n", (unsigned)CANVAS_W, (unsigned)CANVAS_H, (unsigned)(CANVAS_PIXELS * 2), (unsigned)ESP.getFreeHeap()); @@ -439,13 +571,19 @@ bool photo_view_init() { } void photo_view_deinit() { - gif_close_session(); + photo_view_stop(); free_jpeg(); free_png(); + if (g_back) { + free(g_back); + g_back = nullptr; + } g_ready = false; g_dsc.data = nullptr; } +void photo_view_stop() { gif_stop_and_wait(); } + bool photo_view_show(const PhotoEntry *entry) { if (!entry || !photo_view_init()) { return false; @@ -476,21 +614,13 @@ uint16_t photo_view_canvas_h() { return CANVAS_H; } uint16_t photo_view_img_w() { return g_img_w; } uint16_t photo_view_img_h() { return g_img_h; } -bool photo_view_is_gif() { return g_gif_open; } +bool photo_view_is_gif() { return g_gif_active || g_gif_busy; } bool photo_view_gif_tick() { - if (!g_gif_open || !g_gif) { + if (!g_frame_ready || !g_back) { return false; } - const uint32_t now = millis(); - if ((int32_t)(now - g_gif_next_ms) < 0) { - return false; - } - int delay_ms = 0; - g_gif->playFrame(true, &delay_ms); - if (delay_ms < 20) { - delay_ms = 100; - } - g_gif_next_ms = now + (uint32_t)delay_ms; + memcpy(g_pixels, g_back, CANVAS_PIXELS * sizeof(uint16_t)); + g_frame_ready = false; return true; } diff --git a/src/ui.cpp b/src/ui.cpp index 90dfd2a..b5af1a0 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -110,6 +110,9 @@ static void go_delta(int delta) { if (n == 0) { return; } + // Abort GIF worker first so the click is not stuck behind decode/delay. + photo_view_stop(); + int16_t next = (int16_t)(g_index + delta); while (next < 0) { next = (int16_t)(next + (int16_t)n); @@ -117,9 +120,6 @@ static void go_delta(int delta) { while (next >= (int16_t)n) { next = (int16_t)(next - (int16_t)n); } - if (next == g_index && n > 1) { - // still reload if single? skip - } g_index = next; show_current(); } @@ -185,9 +185,12 @@ void ui_create() { lv_obj_add_event_cb(tap, on_tap_zone, LV_EVENT_CLICKED, nullptr); img_photo = lv_image_create(scr); - lv_obj_set_size(img_photo, photo_view_canvas_w(), photo_view_canvas_h()); - lv_image_set_scale(img_photo, 512); // 256=1x, 512=2x -> full 320x240 - lv_obj_align(img_photo, LV_ALIGN_CENTER, 0, 0); + // Full-screen widget + stretch avoids scale-transform gaps/stripes behind the photo. + lv_obj_set_size(img_photo, UI_HOR_RES, UI_VER_RES); + lv_obj_set_pos(img_photo, 0, 0); + lv_image_set_inner_align(img_photo, LV_IMAGE_ALIGN_STRETCH); + lv_obj_set_style_bg_color(img_photo, lv_color_hex(0x000000), 0); + lv_obj_set_style_bg_opa(img_photo, LV_OPA_COVER, 0); lv_obj_clear_flag(img_photo, LV_OBJ_FLAG_CLICKABLE); // Top bar diff --git a/tools/make_test_photos.py b/tools/make_test_photos.py index 316b0f1..10c2c65 100644 --- a/tools/make_test_photos.py +++ b/tools/make_test_photos.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Generate 320x240 JPG/PNG/GIF samples into assets/photos.""" +"""Generate 320x240 JPG/PNG/GIF samples into assets/photos (solid bg, no stripes).""" from __future__ import annotations from pathlib import Path @@ -27,16 +27,13 @@ def font(size: int = 28) -> ImageFont.ImageFont: def card(bg, accent, title: str, subtitle: str, path: Path, fmt: str) -> None: im = Image.new("RGB", (W, H), bg) d = ImageDraw.Draw(im) - for i in range(0, H, 12): - c = tuple(max(0, min(255, v + (i % 24) - 12)) for v in accent) - d.rectangle((0, i, W, i + 6), fill=c) - d.rectangle((16, 40, W - 16, H - 40), fill=bg) - d.rectangle((16, 40, W - 16, H - 40), outline=accent, width=3) - d.text((28, 70), title, fill=(234, 240, 246), font=font(36)) - d.text((28, 130), subtitle, fill=(122, 139, 156), font=font(20)) - d.text((28, 170), "CYD ALBUM TEST", fill=accent, font=font(16)) + # Solid panel only — no horizontal stripe decoration + d.rounded_rectangle((20, 36, W - 20, H - 36), radius=12, fill=bg, outline=accent, width=3) + d.text((36, 70), title, fill=(234, 240, 246), font=font(36)) + d.text((36, 130), subtitle, fill=(160, 175, 190), font=font(20)) + d.text((36, 170), "CYD ALBUM TEST", fill=accent, font=font(16)) if fmt == "JPEG": - im.save(path, "JPEG", quality=90) + im.save(path, "JPEG", quality=92) else: im.save(path, "PNG") print(f"wrote {path} ({path.stat().st_size} bytes)") @@ -45,7 +42,7 @@ def card(bg, accent, title: str, subtitle: str, path: Path, fmt: str) -> None: def main() -> None: OUT.mkdir(parents=True, exist_ok=True) card((10, 16, 22), (79, 195, 247), "01 JPG", "tap left / right", OUT / "01_blue.jpg", "JPEG") - card((20, 12, 28), (232, 93, 76), "02 JPG", "red sample", OUT / "02_red.jpg", "JPEG") + card((28, 12, 18), (232, 93, 76), "02 JPG", "red sample", OUT / "02_red.jpg", "JPEG") card((8, 28, 20), (61, 220, 151), "03 PNG", "green sample", OUT / "03_green.png", "PNG") frames = [] @@ -53,10 +50,9 @@ def main() -> None: im = Image.new("RGB", (W, H), (10, 16, 22)) d = ImageDraw.Draw(im) d.text((24, 24), "04 GIF", fill=(234, 240, 246), font=font(32)) - d.text((24, 70), "animated", fill=(122, 139, 156), font=font(18)) + d.text((24, 70), "animated", fill=(160, 175, 190), font=font(18)) x = 20 + i * 40 - d.rectangle((x, 120, x + 80, 180), fill=(255, 200, 40)) - d.rectangle((20, 200, 300, 210), fill=(36, 49, 64)) + d.rounded_rectangle((x, 120, x + 80, 180), radius=8, fill=(255, 200, 40)) frames.append(im) gif_path = OUT / "04_bounce.gif" frames[0].save(