Refresh bottom nav with 0909 enlarged icons and aligned label art.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yuquanjun 2026-09-09 20:24:39 +08:00
parent 5bb403cd11
commit 24ffe87527
14 changed files with 1367 additions and 896 deletions

View File

@ -966,37 +966,32 @@ void UI_DrawSubPageHeader(const uint8_t *title_gbk)
void Draw_Bottom_Bar(uint8_t sel, uint8_t first_back)
{
/* 需求稿四键:万能 | 普通 | 专业 | 设置first_back 时设置位仍高亮(返回用触控逻辑) */
/* 四键合成图(图标+原稿文字);统一 32 高,文字底边对齐 */
const uint8_t *icons[4];
uint8_t ws[4], hs[4];
/* 0904四键中心约 40 / 100 / 160 / 210 */
uint8_t ws[4];
const uint16_t cx[4] = { 40, 100, 160, 210 };
const uint8_t icon_h = 32;
const uint16_t icon_y = 282;
uint8_t i;
(void)first_back;
/* 从段落钮底边之下清屏,勿切掉按钮底圆角(钮 y=231..264 */
LCD_FillByColor(0, (uint16_t)(UI0902_SECTION_Y + UI0902_SECTION_BTN_H),
240, 320, UI0902_BG_COLOR);
/* mode.png段落钮与底栏之间横线分隔与顶栏同色同厚 */
LCD_FillByColor(0, UI0902_SEP_LINE_Y, 240,
(uint16_t)(UI0902_SEP_LINE_Y + UI0902_SEP_LINE_THICK),
UI0902_SEP_LINE_COLOR);
if (sel == UI0902_NAV_UNIVERSAL) { icons[0] = gImage_Nav_Universal_Sel; ws[0] = 36; hs[0] = 28; }
else { icons[0] = gImage_Nav_Universal_Not; ws[0] = 36; hs[0] = 28; }
if (sel == UI0902_NAV_NORMAL) { icons[1] = gImage_Nav_Normal_Sel; ws[1] = 35; hs[1] = 29; }
else { icons[1] = gImage_Nav_Normal_Not; ws[1] = 35; hs[1] = 28; }
if (sel == UI0902_NAV_EXPERT) { icons[2] = gImage_Nav_Expert_Sel; ws[2] = 35; hs[2] = 27; }
else { icons[2] = gImage_Nav_Expert_Not; ws[2] = 35; hs[2] = 27; }
if (sel == UI0902_NAV_SETTING) { icons[3] = gImage_Nav_Setting_Sel; ws[3] = 18; hs[3] = 28; }
else { icons[3] = gImage_Nav_Setting_Not; ws[3] = 18; hs[3] = 28; }
icons[0] = (sel == UI0902_NAV_UNIVERSAL) ? gImage_Nav_Universal_Sel : gImage_Nav_Universal_Not;
icons[1] = (sel == UI0902_NAV_NORMAL) ? gImage_Nav_Normal_Sel : gImage_Nav_Normal_Not;
icons[2] = (sel == UI0902_NAV_EXPERT) ? gImage_Nav_Expert_Sel : gImage_Nav_Expert_Not;
icons[3] = (sel == UI0902_NAV_SETTING) ? gImage_Nav_Setting_Sel : gImage_Nav_Setting_Not;
ws[0] = ws[1] = ws[2] = 40;
ws[3] = 22;
for (i = 0; i < 4; i++)
{
LCD_WR_PIC_Trans(cx[i] - ws[i] / 2, 283, ws[i], hs[i], icons[i], BLACK);
LCD_WR_PIC_Trans((uint16_t)(cx[i] - ws[i] / 2), icon_y,
ws[i], icon_h, icons[i], BLACK);
}
}

View File

@ -183,15 +183,15 @@ void CallUI_RestoreSelect(void);
#define UI0902_NAV_MIXER UI0902_NAV_NORMAL
#define UI0902_NAV_MODE UI0902_NAV_EXPERT
/* 底部图标实际内容(生成脚本曾把 万能/普通/专业/设置 误命名为 Setting/Mixer/Mode */
#define gImage_Nav_Universal_Not gImage_UI0902_TabSetting_Sel_36x28 /* 底部-10 白 */
#define gImage_Nav_Universal_Sel gImage_UI0902_TabMode_Sel_36x28 /* 底部-14 蓝 */
#define gImage_Nav_Normal_Not gImage_UI0902_TabSetting_Not_35x28 /* 底部-11 白 */
#define gImage_Nav_Normal_Sel gImage_UI0902_TabMode_Not_35x29 /* 底部-15 蓝 */
#define gImage_Nav_Expert_Not gImage_UI0902_TabMixer_Sel_35x27 /* 底部-12 白 */
#define gImage_Nav_Expert_Sel gImage_UI0902_TabBack_Sel_35x27 /* 底部-16 蓝 */
#define gImage_Nav_Setting_Not gImage_UI0902_TabMixer_Not_18x28 /* 底部-13 白 */
#define gImage_Nav_Setting_Sel gImage_UI0902_TabBack_Not_18x28 /* 底部-17 蓝 */
/* 底部图标0909 加大图标 + 原稿文字条(合成图);生成脚本曾误命名 Setting/Mixer/Mode */
#define gImage_Nav_Universal_Not gImage_UI0902_TabSetting_Sel_40x32 /* 底部-10 白 */
#define gImage_Nav_Universal_Sel gImage_UI0902_TabMode_Sel_40x32 /* 底部-14 蓝 */
#define gImage_Nav_Normal_Not gImage_UI0902_TabSetting_Not_40x32 /* 底部-11 白 */
#define gImage_Nav_Normal_Sel gImage_UI0902_TabMode_Not_40x32 /* 底部-15 蓝 */
#define gImage_Nav_Expert_Not gImage_UI0902_TabMixer_Sel_40x32 /* 底部-12 白 */
#define gImage_Nav_Expert_Sel gImage_UI0902_TabBack_Sel_40x32 /* 底部-16 蓝 */
#define gImage_Nav_Setting_Not gImage_UI0902_TabMixer_Not_22x32 /* 底部-13 白 */
#define gImage_Nav_Setting_Sel gImage_UI0902_TabBack_Not_22x32 /* 底部-17 蓝 */
/* 旧几何电池Draw_Battery_Icon最左起点0902 图电池用 Draw_Status_Battery_Icon */
#define UI_STATUS_BATTERY_X 150
#define UI_STATUS_BATTERY_H 11

File diff suppressed because it is too large Load Diff

View File

@ -7,15 +7,15 @@ extern const unsigned char gImage_UI0902_BatteryOutline_29x12[];
extern const unsigned char gImage_UI0902_Volume_20x15[600];
extern const unsigned char gImage_UI0902_Bluetooth_11x17[374];
extern const unsigned char gImage_UI0902_BatteryFill_22x9[396];
extern const unsigned char gImage_UI0902_TabSetting_Sel_36x28[2016];
extern const unsigned char gImage_UI0902_TabSetting_Not_35x28[1960];
extern const unsigned char gImage_UI0902_TabMixer_Sel_35x27[1890];
extern const unsigned char gImage_UI0902_TabMixer_Not_18x28[1008];
extern const unsigned char gImage_UI0902_TabSetting_Sel_40x32[2560];
extern const unsigned char gImage_UI0902_TabSetting_Not_40x32[2560];
extern const unsigned char gImage_UI0902_TabMixer_Sel_40x32[2560];
extern const unsigned char gImage_UI0902_TabMixer_Not_22x32[1408];
extern const unsigned char gImage_UI0902_TabMixer_Not_35x28[1960];
extern const unsigned char gImage_UI0902_TabMode_Sel_36x28[2016];
extern const unsigned char gImage_UI0902_TabMode_Not_35x29[2030];
extern const unsigned char gImage_UI0902_TabBack_Sel_35x27[1890];
extern const unsigned char gImage_UI0902_TabBack_Not_18x28[1008];
extern const unsigned char gImage_UI0902_TabMode_Sel_40x32[2560];
extern const unsigned char gImage_UI0902_TabMode_Not_40x32[2560];
extern const unsigned char gImage_UI0902_TabBack_Sel_40x32[2560];
extern const unsigned char gImage_UI0902_TabBack_Not_22x32[1408];
extern const unsigned char gImage_UI0902_BtnMinus_23x22[1012];
extern const unsigned char gImage_UI0902_BtnPlus_23x22[1012];

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1017 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 902 B

After

Width:  |  Height:  |  Size: 845 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 982 B

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -137,14 +137,14 @@ INTERNAL = [
("gImage_UI0902_Volume_20x15", "02_顶部图标/音量图标-10.png"),
("gImage_UI0902_Bluetooth_11x17", "02_顶部图标/蓝牙图标-10.png"),
# BatteryFill generated by make_battery_fill() — not from mockup PNG
("gImage_UI0902_TabSetting_Sel_36x28", "03_底部图标/底部图标-10.png"),
("gImage_UI0902_TabSetting_Not_35x28", "03_底部图标/底部图标-11.png"),
("gImage_UI0902_TabMixer_Sel_35x27", "03_底部图标/底部图标-12.png"),
("gImage_UI0902_TabMixer_Not_18x28", "03_底部图标/底部图标-13.png"),
("gImage_UI0902_TabMode_Sel_36x28", "03_底部图标/底部图标-14.png"),
("gImage_UI0902_TabMode_Not_35x29", "03_底部图标/底部图标-15.png"),
("gImage_UI0902_TabBack_Sel_35x27", "03_底部图标/底部图标-16.png"),
("gImage_UI0902_TabBack_Not_18x28", "03_底部图标/底部图标-17.png"),
("gImage_UI0902_TabSetting_Sel_40x32", "03_底部图标/底部图标-10.png"), # 万能白
("gImage_UI0902_TabSetting_Not_40x32", "03_底部图标/底部图标-11.png"), # 普通白
("gImage_UI0902_TabMixer_Sel_40x32", "03_底部图标/底部图标-12.png"), # 专业白
("gImage_UI0902_TabMixer_Not_22x32", "03_底部图标/底部图标-13.png"), # 设置白
("gImage_UI0902_TabMode_Sel_40x32", "03_底部图标/底部图标-14.png"), # 万能蓝
("gImage_UI0902_TabMode_Not_40x32", "03_底部图标/底部图标-15.png"), # 普通蓝
("gImage_UI0902_TabBack_Sel_40x32", "03_底部图标/底部图标-16.png"), # 专业蓝
("gImage_UI0902_TabBack_Not_22x32", "03_底部图标/底部图标-17.png"), # 设置蓝
]
# ---------------- external flash (big images / text strips) ----------------

View File

@ -0,0 +1,206 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Bottom nav: 0909 enlarged icons + original design text strips, fixed baseline."""
from __future__ import annotations
import re
import subprocess
from io import BytesIO
from pathlib import Path
from PIL import Image
PROJ = Path(__file__).resolve().parents[1]
ROOT = PROJ.parent.parent
SRC_DIR = next(
p
for p in (ROOT / "Doc" / "UI" / "K1标准界面图 0904").iterdir()
if p.is_dir() and "0909" in p.name
)
ASSETS = PROJ / "tools" / "assets_0902" / "03_底部图标"
DOC_ASSETS = ROOT / "Doc" / "UI" / "k1_ui_png 0902" / "03_底部图标"
OUT_C = PROJ / "device" / "LCD_ILI9341" / "Drv_ILI9341_Lcd_Image_UI0902.c"
OUT_H = PROJ / "device" / "LCD_ILI9341" / "Drv_ILI9341_Lcd_Image_UI0902.h"
ROW_BG = (36, 43, 58)
CANVAS_H = 32
ICON_AREA_H = 20
TEXT_TOP = 22 # all labels share this top row
# png_idx, src_glyph_num, symbol_base, width
MAPPING = [
(10, 21, "gImage_UI0902_TabSetting_Sel", 40),
(11, 23, "gImage_UI0902_TabSetting_Not", 40),
(12, 25, "gImage_UI0902_TabMixer_Sel", 40),
(13, 27, "gImage_UI0902_TabMixer_Not", 22),
(14, 20, "gImage_UI0902_TabMode_Sel", 40),
(15, 22, "gImage_UI0902_TabMode_Not", 40),
(16, 24, "gImage_UI0902_TabBack_Sel", 40),
(17, 26, "gImage_UI0902_TabBack_Not", 22),
]
def git_png(rel: str) -> Image.Image:
data = subprocess.check_output(["git", "show", f"HEAD:{rel}"], cwd=PROJ)
return Image.open(BytesIO(data)).convert("RGBA")
def clear_bg(im: Image.Image) -> Image.Image:
im = im.convert("RGBA")
px = im.load()
for y in range(im.height):
for x in range(im.width):
r, g, b, a = px[x, y]
if a < 40 or (r < 12 and g < 12 and b < 12):
px[x, y] = (0, 0, 0, 0)
return im
def row_has_ink(im: Image.Image, y: int) -> bool:
w = im.width
for x in range(w):
r, g, b, a = im.getpixel((x, y))
if a > 40 and (r + g + b) > 40:
return True
return False
def extract_text(im: Image.Image) -> Image.Image:
"""Crop label band below the icon/gap from original composite."""
im = clear_bg(im)
h = im.height
# find last empty gap row in upper half, then text starts after
empty = [y for y in range(h) if not row_has_ink(im, y)]
# text region: from first non-empty after mid-gap
mid_empties = [y for y in empty if 10 <= y <= 20]
if mid_empties:
text_y0 = mid_empties[-1] + 1
else:
text_y0 = h // 2 + 2
while text_y0 < h and not row_has_ink(im, text_y0):
text_y0 += 1
text_y1 = h - 1
while text_y1 > text_y0 and not row_has_ink(im, text_y1):
text_y1 -= 1
band = im.crop((0, text_y0, im.width, text_y1 + 1))
# trim horizontal transparent
bbox = band.getbbox()
if bbox:
band = band.crop(bbox)
return clear_bg(band)
def fit_icon(im: Image.Image) -> Image.Image:
im = clear_bg(im)
nw, nh = im.size
if nw > 24 or nh > ICON_AREA_H:
scale = min(24 / nw, ICON_AREA_H / nh)
nw = max(1, int(round(nw * scale)))
nh = max(1, int(round(nh * scale)))
im = clear_bg(im.resize((nw, nh), Image.Resampling.NEAREST))
nw, nh = im.size
return im
def rgba_to_rgb565(im: Image.Image) -> tuple[bytes, int, int]:
im = im.convert("RGBA")
px = im.load()
w, h = im.size
out = bytearray(w * h * 2)
i = 0
for y in range(h):
for x in range(w):
r, g, b, a = px[x, y]
if a < 40 or (r < 12 and g < 12 and b < 12):
out[i] = out[i + 1] = 0
elif (
abs(r - ROW_BG[0]) <= 18
and abs(g - ROW_BG[1]) <= 18
and abs(b - ROW_BG[2]) <= 18
):
out[i] = out[i + 1] = 0
else:
r = (r * a) // 255
g = (g * a) // 255
b = (b * a) // 255
v = ((r & 0xF8) << 8) | ((g & 0xFC) << 3) | (b >> 3)
out[i] = (v >> 8) & 0xFF
out[i + 1] = v & 0xFF
i += 2
return bytes(out), w, h
def emit_c_array(name: str, data: bytes, w: int, h: int) -> str:
lines = [f"const unsigned char {name}[{len(data)}] = {{ /* {w}x{h} RGB565 BE */"]
for i in range(0, len(data), 16):
lines.append(",".join(f"0x{b:02X}" for b in data[i : i + 16]) + ",")
lines.append("};")
lines.append("")
return "\n".join(lines)
def replace_c_array(text: str, old_name: str, new_block: str) -> str:
pat = re.compile(
rf"const unsigned char {re.escape(old_name)}\[\d+\] = \{{.*?\n\}};\n?",
re.S,
)
if not pat.search(text):
raise SystemExit(f"array not found: {old_name}")
return pat.sub(new_block, text, count=1)
def replace_h_decl(text: str, old_name: str, new_name: str, nbytes: int) -> str:
pat = re.compile(rf"extern const unsigned char {re.escape(old_name)}\[\d+\];")
if not pat.search(text):
raise SystemExit(f"decl not found: {old_name}")
return pat.sub(
f"extern const unsigned char {new_name}[{nbytes}];", text, count=1
)
def main() -> None:
by_num = {int(f.stem.rsplit("-", 1)[-1]): f for f in SRC_DIR.glob("*.png")}
c_text = OUT_C.read_text(encoding="utf-8")
h_text = OUT_H.read_text(encoding="utf-8")
sizes: dict[int, tuple[int, int]] = {}
for out_idx, src_num, base, width in MAPPING:
icon = fit_icon(Image.open(by_num[src_num]))
old = git_png(f"tools/assets_0902/03_底部图标/底部图标-{out_idx}.png")
text = extract_text(old)
# widen canvas if text wider
cw = max(width, icon.width, text.width)
canvas = Image.new("RGBA", (cw, CANVAS_H), (0, 0, 0, 0))
iy = (ICON_AREA_H - icon.height) // 2
canvas.paste(icon, ((cw - icon.width) // 2, max(0, iy)), icon)
# pin text BOTTOM to same row so labels sit on one baseline
text_bottom = CANVAS_H - 1
ty = text_bottom - text.height + 1
if ty < TEXT_TOP:
ty = TEXT_TOP
canvas.paste(text, ((cw - text.width) // 2, ty), text)
out = ASSETS / f"底部图标-{out_idx}.png"
canvas.save(out)
if DOC_ASSETS.exists():
canvas.save(DOC_ASSETS / f"底部图标-{out_idx}.png")
data, w, h = rgba_to_rgb565(canvas)
new_name = f"{base}_{w}x{h}"
m = re.search(rf"{re.escape(base)}_\d+x\d+", c_text)
if not m:
raise SystemExit(f"cannot locate {base}")
old_name = m.group(0)
c_text = replace_c_array(c_text, old_name, emit_c_array(new_name, data, w, h))
h_text = replace_h_decl(h_text, old_name, new_name, len(data))
sizes[out_idx] = (w, h)
print(f"{out_idx}: {w}x{h} text={text.size} -> {new_name}")
OUT_C.write_text(c_text, encoding="utf-8")
OUT_H.write_text(h_text, encoding="utf-8")
print("SIZES", sizes)
if __name__ == "__main__":
main()