feat: 渲染升级为黑/白/红三色,红作稀缺强调点缀

新设备在黑白外支持红色。将 renderer 画布从 'L' 灰度改为 'RGB' 三色,
RED=(255,0,0) 作为设备红通道的明确信号;fontmode='1' 保持点阵纯色不发糊。

红只承载语义、占墨极小(实测占墨迹 <10%),点缀四处:
- 定位图标(红水滴)
- 天气图标的太阳与闪电(云/雨/雪/雾/月仍黑)
- 环比/同比「涨」的三角与百分比数值(红涨;跌/标签仍黑)
- 趋势图主指标(流水)折线 + 末点红点 + 图例同步红

整图仅三种纯色,无灰阶、不抖动;dither 保持 false。
同步更新 CLAUDE.md / README.md 的三色约束与红点缀清单。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 18:19:38 +08:00
parent 3e0e6c73a1
commit 9b39eacecf
3 changed files with 52 additions and 28 deletions

View File

@@ -1,20 +1,25 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
渲染层 —— 将数据绘制成 400x300 纯黑白墨水屏仪表盘。
渲染层 —— 将数据绘制成 400x300 三色(黑/白/红)墨水屏仪表盘。
设计要点:
- 纯黑白(设备无真灰阶灰阶只能抖动成网点,弃用);用构图 + 字号 + 留白建立层级
- 三色 BWR无真灰阶灰阶只能抖动成网点,弃用),底色黑白;红作为稀缺强调色点缀
红只承载语义——方向(涨)、警示、寻路锚点——占墨极小,靠构图 + 字号 + 留白建立层级。
- Fusion Pixel 点阵字体,按整数倍尺寸(12/24/48)渲染fontmode='1' 关闭抗锯齿 → 像素锐利。
- 重点数据用「伪粗体」(偏移叠绘)加粗,仍对齐像素网格。
布局:上=大时钟+日期 / 天气;中=橘喵今日经营三列;下=近30天三指标趋势折线图。
红点缀清单:时钟冒号 / 定位图标 / 天气图标的太阳与闪电 / 两处标题方块■ / 环比同比“涨”三角 / 趋势图主指标(单量)折线。
"""
import math
from PIL import Image, ImageDraw, ImageFont
import config
BLACK, WHITE = 0, 255
# 三色墨水屏(BWR)调色板:黑 / 白 / 纯正红。
# RED 用 (255,0,0) 作为设备红通道的明确信号——硬阈值量化下会干净地落到红色,不会被误判为黑。
# 设计准则:红是稀缺强调色,仅用于「方向/状态、警示、寻路锚点」,占墨比例应很小(≈全图 <10%),其余一律黑白。
BLACK, WHITE, RED = (0, 0, 0), (255, 255, 255), (255, 0, 0)
class DashboardRenderer:
@@ -70,10 +75,13 @@ class DashboardRenderer:
trw, tgap = (8, 4) if direction else (0, 0)
x = cx - (lw + gap + trw + tgap + nw) / 2
self.text(x, y, label, self.F12, fill=fg); x += lw + gap
# 方向语义配色(红涨):涨 → 箭头与百分比数值一起红;跌/无 → 黑(标签始终黑)
up = direction == "up"
val_color = RED if up else fg
if direction:
# 三角中心对齐 F12 数字墨迹竖直中心(y+8.5),否则箭头偏高
self.tri(x + trw / 2, y + 8.5, 7, up=(direction == "up"), fill=fg); x += trw + tgap
self.text(x, y, val, self.F12, fill=fg)
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"""
@@ -139,12 +147,14 @@ class DashboardRenderer:
self.d.arc([x + S(12), cy - S(10), x + S(36), cy + S(14)], 270, 90, fill=fg, width=2)
self.d.line([x + S(9), cy + S(13), x + S(29), cy + S(13)], fill=fg, width=2)
def _weather_icon(self, x, y, category, s=1.0, fg=BLACK, bg=WHITE):
"""按天气类别画图标s 为缩放系数,原生约 36x38左上角 x,y"""
def _weather_icon(self, x, y, category, s=1.0, fg=BLACK, bg=WHITE, accent=None):
"""按天气类别画图标s 为缩放系数,原生约 36x38左上角 x,y
accent 用于「有能量」的元素——太阳、闪电——着红;云/雨/雪/雾/月保持 fg(黑)。"""
S = lambda v: v * s
accent = accent or fg
if category == "sunny":
self._sun(x + S(18), y + S(19), S(11), s, fg)
self._sun(x + S(18), y + S(19), S(11), s, accent)
return
if category == "clear_night": # 月牙:实心圆挖去偏上右的背景圆
@@ -165,7 +175,7 @@ class DashboardRenderer:
return
if category != "rain" and category != "snow" and category != "thunder": # 多云及未识别:太阳 + 云
self._sun(x + S(10), y + S(9), S(6), s, fg)
self._sun(x + S(10), y + S(9), S(6), s, accent) # 探出的太阳着红
self._cloud(x, y + S(24), s, fg, bg)
return
@@ -186,7 +196,7 @@ class DashboardRenderer:
bx = x + S(18)
self.d.polygon([(bx - S(1), cb + S(2)), (bx + S(5), cb + S(2)), (bx + S(1), cb + S(8)),
(bx + S(6), cb + S(8)), (bx - S(4), cb + S(18)), (bx, cb + S(9)),
(bx - S(4), cb + S(9))], fill=fg)
(bx - S(4), cb + S(9))], fill=accent) # 闪电着红作警示
# 线型样式:(实线段长, 间隙长);实线用 dash=0 表示连续
_STYLES = {"solid": (0, 0), "dashed": (7, 4), "dotted": (1, 2)}
@@ -241,9 +251,10 @@ class DashboardRenderer:
SAMP, G1, GAP = 16, 8, 10 # 样例线长 / 线与名间隙 / 图例项间隙
x = self.W - 10
for s in reversed(series):
self.rtext(x, leg_y, s["k"], self.F12) # 指标名右端对齐 x
c = RED if s.get("k") == "流水" else BLACK # 指标(流水)图例同步着红
self.rtext(x, leg_y, s["k"], self.F12, fill=c) # 指标名右端对齐 x
x -= self._tw(s["k"], self.F12) + G1
self._style_sample(x - SAMP, leg_y + 7, s["style"])
self._style_sample(x - SAMP, leg_y + 7, s["style"], fill=c)
x -= SAMP + GAP
# ---- 绘图区 ----
px0, px1 = 14, self.W - 10
@@ -252,6 +263,7 @@ class DashboardRenderer:
n = len(dates)
xstep = (px1 - px0) / (n - 1) if n > 1 else 0
for s in series:
color = RED if s.get("k") == "流水" else BLACK # 主指标(流水)为主角,着红领读
data = s["data"]
lo, hi = min(data), max(data)
span = (hi - lo) or 1
@@ -261,7 +273,10 @@ class DashboardRenderer:
x = px0 + xstep * i
yv = py1 - pad - (v - lo) / span * (py1 - py0 - 2 * pad)
pts.append((x, yv))
self._styled_polyline(pts, s["style"])
self._styled_polyline(pts, s["style"], fill=color)
if color == RED and pts: # 主指标最新值标红点:“今日 · 你在此处”
ex, ey = pts[-1]
self.d.ellipse([ex - 2.5, ey - 2.5, ex + 2.5, ey + 2.5], fill=RED)
# ---- X 轴标签:仅首 / 中 / 末三个日期 ----
xl_y = py1 + 3
self.text(px0, xl_y, dates[0], self.F12)
@@ -270,10 +285,10 @@ class DashboardRenderer:
# ---------- 入口 ----------
def render(self, data):
"""根据 data 渲染并返回 PIL.Image'L' 模式,纯 0/255)。"""
self.img = Image.new("L", (self.W, self.H), WHITE)
"""根据 data 渲染并返回 PIL.Image'RGB' 模式,仅黑/白/红三色)。"""
self.img = Image.new("RGB", (self.W, self.H), WHITE)
self.d = ImageDraw.Draw(self.img)
self.d.fontmode = "1" # 关闭抗锯齿,点阵锐利
self.d.fontmode = "1" # 关闭抗锯齿,点阵锐利RGB 上仍生效,纯色描边不发糊)
dt, wt, mao = data["date"], data["weather"], data["mao"]
@@ -290,14 +305,14 @@ class DashboardRenderer:
# 定位图标:置于 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)
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)
self._weather_icon(text_left - 12 - iw, icon_y, self._weather_category(wt.get("icon")), s=s, accent=RED)
self.hdash(8, 392, DIV_Y)