feat: 版式重构为三段 + 接入 Claude Usage(只读本地) + 定位江宁
将仪表盘从「四区(时钟/天气/今日实时/30天趋势)」重构为上中下三段: - A 顶部:大时钟+日期 / 天气(定位改南京·江宁) - B 中部主角 Claude Usage:5h/7d 用量进度条 + 大号百分比 + 时间维度对比 pace (pace=用量%−时间%,超前↑红/节余↓黑,参考 claude-statusline);用量≥阈值该条转红预警 - C 底部 橘喵今日实时:三列版式对齐主分支(名F12/值F24粗/环比同比偏移 +18/+34/+64/+82) - 近30天趋势图隐藏(_trend_chart 及依赖保留、未调用,可挂回) 用量只读本地、不发网络请求:新增 usage_local.py 读取 claude-statusline 落盘的 /tmp/claude/statusline-usage-cache.json(utilization + resets_at)。utilization 可能滞后, 但 resets_at 为绝对时间,故 pace 每次按当前时间现算、始终准确;文件缺失回退 "--"。 其他: - 天气定位 秦淮→江宁(config 默认 118.840,31.953) - 进度条边框随条色(红条即红框);行内元素按墨迹竖直中心对齐(_mid) - 新增 F13/F16 字号(非整数倍,fontmode=1 保持纯色) - 全图仍严格三色(黑/白/红)、无灰阶不抖动;底部留 ~11px 下边距 - 同步更新 CLAUDE.md / README.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
138
renderer.py
138
renderer.py
@@ -4,12 +4,20 @@
|
||||
渲染层 —— 将数据绘制成 400x300 三色(黑/白/红)墨水屏仪表盘。
|
||||
|
||||
设计要点:
|
||||
- 三色 BWR:仍无真灰阶(灰阶只能抖动成网点,弃用),底色黑白;红作为稀缺强调色点缀。
|
||||
红只承载语义——方向(涨)、警示、寻路锚点——占墨极小,靠构图 + 字号 + 留白建立层级。
|
||||
- 三色 BWR:仍无真灰阶(灰阶只能抖动成网点,弃用);底色黑白,靠留白 + 大字号 + 一处大红块建层级。
|
||||
克制而有分量:全图只一个「大红」焦点(用量超阈值的整条红进度条),其余红为小面积语义点缀。
|
||||
- 关键墨水屏配色铁律:红与黑同属「深色」,红叠黑对比极低 → 红只出现在**白底**(红字/红三角/红块),
|
||||
**绝不红字压黑底**;白底红块 + 白字最稳。
|
||||
- Fusion Pixel 点阵字体,按整数倍尺寸(12/24/48)渲染,fontmode='1' 关闭抗锯齿 → 像素锐利。
|
||||
- 重点数据用「伪粗体」(偏移叠绘)加粗,仍对齐像素网格。
|
||||
布局:上=大时钟+日期 / 天气;中=橘喵今日经营三列;下=近30天三指标趋势折线图。
|
||||
红点缀清单:时钟冒号 / 定位图标 / 天气图标的太阳与闪电 / 两处标题方块■ / 环比同比“涨”三角 / 趋势图主指标(单量)折线。
|
||||
布局(自上而下三段,两道横规分隔):
|
||||
A 顶部:大时钟+日期(左)/ 天气(右),白底黑字 + 红点缀(定位红水滴、太阳/闪电红);
|
||||
B 中部主角:Claude Usage——每条单行 = 左标签(5h/7d,F13) + 进度条 + 百分比(F16,主数字) + 时间维度对比 pace(F13);
|
||||
pct ≥ 阈值(warn) 时该条进度条(含边框)与百分比整体转红;
|
||||
pace = 用量% − 时间% ,用上/下三角(替代 +/−)表示超前/节余,超前(↑)红、节余(↓)黑;
|
||||
C 底部:橘喵今日实时三列,环比/同比「涨=红·跌=黑」。
|
||||
近30天趋势图暂隐藏(`_trend_chart` 及其依赖 `_styled_polyline`/`_style_sample` 保留、未调用,随时可再挂回)。
|
||||
红出现处:① 定位水滴 ② 天气太阳/闪电 ③ 用量进度条与百分比(≥warn) ④ pace 超前(↑) ⑤ 环比/同比「涨」的三角与数值。
|
||||
"""
|
||||
import math
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
@@ -26,6 +34,8 @@ class DashboardRenderer:
|
||||
def __init__(self, font_path=config.FONT_PATH, size=(config.WIDTH, config.HEIGHT)):
|
||||
self.W, self.H = size
|
||||
self.F12 = ImageFont.truetype(font_path, 12)
|
||||
self.F13 = ImageFont.truetype(font_path, 13) # 5h/7d 与 pace 用;接近原生 12px,畸变很小
|
||||
self.F16 = ImageFont.truetype(font_path, 16) # 非整数倍(1.33x):靠 fontmode='1' 保持纯色,笔画略不均可接受
|
||||
self.F24 = ImageFont.truetype(font_path, 24)
|
||||
self.F48 = ImageFont.truetype(font_path, 48)
|
||||
|
||||
@@ -83,12 +93,66 @@ class DashboardRenderer:
|
||||
self.tri(x + trw / 2, y + 8.5, 7, up=up, fill=val_color); x += trw + tgap
|
||||
self.text(x, y, val, self.F12, fill=val_color)
|
||||
|
||||
def _title_bar(self, y, title, upd):
|
||||
"""标题栏:■ 标题(左) + 上次更新 HH:mm(右)。"""
|
||||
sq, sq_y = 6, y + 4
|
||||
self.d.rectangle([10, sq_y, 10 + sq, sq_y + sq], fill=BLACK)
|
||||
self.text(10 + sq + 6, y, title, self.F12)
|
||||
self.rtext(self.W - 10, y, f"上次更新 {upd}", self.F12)
|
||||
def _label(self, y, title, right=None):
|
||||
"""区块标题(两区块统一,对齐主分支 _title_bar 风格):
|
||||
■ 方块装饰(8px,与 F12 标题墨迹竖直居中) + 标题(F12,常规字重) + 可选右侧信息。"""
|
||||
SQ = 8
|
||||
self.d.rectangle([10, y + 4, 10 + SQ, y + 4 + SQ], fill=BLACK) # 中心≈y+8,对齐 F12 墨迹中心(≈y+8.5)
|
||||
self.text(10 + SQ + 6, y, title, self.F12)
|
||||
if right:
|
||||
self.rtext(self.W - 10, y, right, self.F12)
|
||||
|
||||
def _mid(self, x, cy, s, ft, fill=BLACK, bold=0, align="l"):
|
||||
"""按字符串「墨迹竖直中心」对齐到 cy 绘制——跨字号(F12/F24)也能真正居中,
|
||||
避免不同字号按顶部对齐时的视觉错位。align: l 左对齐 / r 右对齐 / c 居中于 x。"""
|
||||
_, t, _, b = ft.getbbox(s) # 该串墨迹在字身框内的上/下沿
|
||||
y = cy - (t + b) / 2.0
|
||||
w = self._tw(s, ft)
|
||||
if align == "r":
|
||||
x -= w + bold
|
||||
elif align == "c":
|
||||
x -= (w + bold) / 2.0
|
||||
self._draw(x, y, s, ft, fill, bold)
|
||||
|
||||
def _pace(self, xr, cy, diff):
|
||||
"""时间维度对比(pace = 用量% − 时间%):>0 超前↑红;<0 节余↓黑;=0 持平。
|
||||
数值(F13)两位补零、右端对齐 xr,小三角(6)紧贴其左;均竖直居中 cy。返回整体宽度供左侧定位。"""
|
||||
up = diff > 0
|
||||
color = RED if up else BLACK
|
||||
txt = f"{abs(int(diff)):02d}"
|
||||
tw = self._tw(txt, self.F13)
|
||||
self._mid(xr, cy, txt, self.F13, fill=color, bold=1, align="r")
|
||||
if diff == 0:
|
||||
return tw
|
||||
TRI_GAP, TRI = 4, 6
|
||||
self.tri(xr - tw - TRI_GAP - TRI / 2, cy, TRI, up=up, fill=color)
|
||||
return tw + TRI_GAP + TRI
|
||||
|
||||
def _usage_row(self, x0, x1, y, bar, warn):
|
||||
"""一条用量(单行):左标签(5h/7d, F13) + 进度条 + 百分比(F16, 主数字) + 时间维度对比 pace(F13)。
|
||||
右侧从右往左排布,显式控制两处横向间距:G1 进度条↔百分比、G2 百分比↔pace;全部竖直居中 cy。
|
||||
pct ≥ warn → 进度条(含整条边框)与百分比转红;边框随条色,红条即红框,无黑白错位。"""
|
||||
raw = bar.get("pct")
|
||||
ok = isinstance(raw, (int, float)) # None/缺失 → 显示 "--"
|
||||
pct = max(0, min(100, int(raw))) if ok else 0
|
||||
color = RED if (ok and pct >= warn) else BLACK
|
||||
LABEL_W, G1, G2, H = 26, 13, 8, 15 # G1 进度条↔百分比、G2 百分比↔pace(视觉约 12/11)
|
||||
cy = y + 10 # 行竖直中心
|
||||
self._mid(x0, cy, bar["k"], self.F13, bold=1) # 左标签 F13
|
||||
# 右侧从右往左:pace(最右) → 百分比 → 进度条
|
||||
pct_r = x1
|
||||
if ok and "time_pct" in bar:
|
||||
pace_w = self._pace(x1, cy, pct - int(bar["time_pct"]))
|
||||
pct_r = x1 - pace_w - G2
|
||||
pct_txt = f"{pct}%" if ok else "--"
|
||||
self._mid(pct_r, cy, pct_txt, self.F16, fill=color, bold=1, align="r") # 百分比 F16(主)
|
||||
bx0 = x0 + LABEL_W
|
||||
bx1 = pct_r - self._tw(pct_txt, self.F16) - G1
|
||||
by = cy - H // 2
|
||||
self.d.rectangle([bx0, by, bx1, by + H], outline=color, width=1) # 轨道边框随条色
|
||||
fw = (bx1 - bx0) * pct / 100.0
|
||||
if fw >= 2:
|
||||
self.d.rectangle([bx0, by, bx0 + fw, by + H], fill=color)
|
||||
|
||||
def _loc_pin(self, x, y, w=8, fg=BLACK, bg=WHITE):
|
||||
"""定位图标:实心水滴(圆头+尖尾) + 白色挖孔,1-bit 下轮廓清晰。
|
||||
@@ -285,55 +349,63 @@ class DashboardRenderer:
|
||||
|
||||
# ---------- 入口 ----------
|
||||
def render(self, data):
|
||||
"""根据 data 渲染并返回 PIL.Image('RGB' 模式,仅黑/白/红三色)。"""
|
||||
"""根据 data 渲染并返回 PIL.Image('RGB' 模式,仅黑/白/红三色)。
|
||||
|
||||
版式(自上而下三段,两道横规分隔):
|
||||
A 顶部:大时钟+日期(左)/ 天气(右)——白底黑字,红点缀(定位/太阳闪电);
|
||||
B 中部主角:Claude Code 用量——每条=标题行 + 一条粗进度条,pct≥阈值转红预警;
|
||||
C 底部:橘喵今日实时三列(涨=红·跌=黑)。近30天趋势图暂隐藏(_trend_chart 保留)。
|
||||
"""
|
||||
self.img = Image.new("RGB", (self.W, self.H), WHITE)
|
||||
self.d = ImageDraw.Draw(self.img)
|
||||
self.d.fontmode = "1" # 关闭抗锯齿,点阵锐利(RGB 上仍生效,纯色描边不发糊)
|
||||
|
||||
dt, wt, mao = data["date"], data["weather"], data["mao"]
|
||||
dt, wt, mao, usage = data["date"], data["weather"], data["mao"], data["usage"]
|
||||
|
||||
# A. Hero:大时钟 + 日期(时钟上移使「顶部留白 ≈ 与日期间距」对称)
|
||||
# ---- A. 顶部:大时钟+日期(左)/ 天气(右)。白底黑字,红点缀 ----
|
||||
self.text(10, 0, dt["time"], self.F48, bold=2)
|
||||
self.text(12, 64, f'{dt["week"]} {dt["greg"]}', self.F12)
|
||||
|
||||
# B. 天气:右对齐文字 + 居中的 28° + 放大右靠的图标
|
||||
DIV_Y = 90 # 天气区底部分割线
|
||||
DIV_Y = 90
|
||||
loc, temp, cr = wt["loc"], wt["temp"], f'{wt["cond"]} {wt["range"]}'
|
||||
self.rtext(390, 8, loc, self.F12)
|
||||
self.rtext(390, 30, temp, self.F24, bold=1)
|
||||
self.rtext(390, 64, cr, self.F12)
|
||||
# 定位图标:置于 loc 文字左侧,留间距并与文字竖直居中
|
||||
PIN_W, PIN_GAP = 8, 6
|
||||
loc_pin_x = 390 - self._tw(loc, self.F12) - PIN_GAP - PIN_W
|
||||
self._loc_pin(loc_pin_x, 10, PIN_W, fg=RED) # 定位图标:红水滴,地图别针的通用语汇
|
||||
self._loc_pin(loc_pin_x, 10, PIN_W, fg=RED) # 定位红水滴
|
||||
s = 1.5
|
||||
iw, ih = 36 * s, 38 * s # 图标原生约 36x38
|
||||
text_left = min(loc_pin_x,
|
||||
390 - self._tw(temp, self.F24) - 1,
|
||||
390 - self._tw(cr, self.F12))
|
||||
icon_y = (DIV_Y - ih) / 2 # 顶部(0)与分割线正中:上下留白相等
|
||||
self._weather_icon(text_left - 12 - iw, icon_y, self._weather_category(wt.get("icon")), s=s, accent=RED)
|
||||
|
||||
self._weather_icon(text_left - 12 - iw, (DIV_Y - ih) / 2,
|
||||
self._weather_category(wt.get("icon")), s=s, accent=RED)
|
||||
self.hdash(8, 392, DIV_Y)
|
||||
|
||||
# C. 橘喵今日经营:三列(数值 + 环比/同比)
|
||||
self._title_bar(96, "橘喵·今日实时", mao["upd"])
|
||||
# ---- B. 中部主角:Claude Usage(单行:标签 + 进度条 + 百分比 + pace,≥阈值转红)----
|
||||
# 标题贴近上分割线(与今日实时一致的 6px 间距);两条进度条在余下空间内等距(≈19px)
|
||||
self._label(96, usage.get("title", "Claude Usage"))
|
||||
warn = usage.get("warn", 80)
|
||||
ry = 123
|
||||
for b in usage.get("bars", []):
|
||||
self._usage_row(10, 390, ry, b, warn)
|
||||
ry += 31 # 略压缩两条间距,给底部让出下边距
|
||||
|
||||
self.hdash(8, 392, 187) # B/C 分割线上移,最终给底部今日实时留出与整图下边的间距
|
||||
|
||||
# ---- C. 底部:橘喵·今日实时(三列,版式对齐主分支:标题+18=名/+34=值/+64=环比/+82=同比)----
|
||||
self._label(193, "橘喵·今日实时", right=f'上次更新 {mao["upd"]}')
|
||||
cols = mao["cols"]; n = len(cols)
|
||||
ax0, ax1 = 8, 392
|
||||
cw = (ax1 - ax0) / n
|
||||
for i in range(1, n):
|
||||
self.vdash(ax0 + cw * i, 128, 192)
|
||||
self.vdash(ax0 + cw * i, 225, 289)
|
||||
for i, c in enumerate(cols):
|
||||
cx = ax0 + cw * (i + 0.5)
|
||||
self.ctext(cx, 114, c["k"], self.F12)
|
||||
self.ctext(cx, 130, c["v"], self.F24, bold=1)
|
||||
self._cmp_line(cx, 160, "环比", c["hb"])
|
||||
self._cmp_line(cx, 178, "同比", c["tb"])
|
||||
|
||||
self.hdash(8, 392, 200)
|
||||
|
||||
# D. 近30天三指标趋势折线图
|
||||
self._trend_chart(204, mao["trend"])
|
||||
self.ctext(cx, 211, c["k"], self.F12) # 指标名(标题+18)
|
||||
self.ctext(cx, 227, c["v"], self.F24, bold=1) # 数值 伪粗体(+34)
|
||||
self._cmp_line(cx, 257, "环比", c["hb"]) # +64
|
||||
self._cmp_line(cx, 275, "同比", c["tb"]) # +82,墨迹止于≈289,底部留≈10px
|
||||
|
||||
return self.img
|
||||
|
||||
|
||||
Reference in New Issue
Block a user