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:
51
renderer.py
51
renderer.py
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user