openpencil/templates/step0/_generators/tpl_compare_spec_table.py
Fini c998d551eb feat(editor): grow the template library to fifty-eight across six scenes
Every non-deck, non-web scene now holds ten templates: cards (spec
themes as living samples), infographics, tutorials, comparisons and
carousels (nine spec themes with per-set master-board rules), plus
two web landings whose 'generate from this' pins their measured
style guides — a basis the document swap used to drop before the
turn ever saw it. All fifty-eight ship generator-reproduced (sha256-
matched), audit-clean, with baked preview cards, and the scene tab
row pins Slides second-from-last and Web last. Two prompt-center
entries carry the web pair with re-rendered previews and honest
render provenance.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-08-09 02:24:43 +08:00

344 lines
15 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""spec-table-comparison.op — 参数表对比长图1080×N 竖版)
对比档的「密集数据」一张:两个候选放在一张真表里逐行比,赢的一格用**深底
反白**顶起来,一眼扫下来就知道各自赢在哪几行。
### 最近邻论证(为什么它不是已有的哪一张)
- **before-after1600×900 横版)**:那张比的是**同一个东西的两个时刻**
主体是两张截图,文字只做注解。这张比的是**两个不同的东西**,主体是
一张参数表,没有图片位。
- **pitfall-list-infographic避坑排行**:那张是「一列条目按严重度排
序」,单位是**一条**;这张的单位是**一行两格**,同一指标必须在同一行
上被两次回答——这正是对比档和清单档的分界。
- **本批 03 三方案横评**:那张是三张并列卡片(各自讲自己的定位),这张是
行优先的表(同一指标横着比)。卡片比气质,表比数值。
### 配色推导(采样 → 收敛 → 论证)
- **采样**:从「参数表」这件事本身采——它属于说明书、规格页、比价页,那
类版面的记忆色是纸白配深墨蓝,不是任何品牌色。
- **收敛**单色相靛蓝H≈232°的一条七级明度序列 L 0.11 / 0.20 / 0.38 /
0.63 / 0.88 / 0.92 / 1.0chroma 随明度升高而降低(深处 0.05、浅处
0.01),只有一个色相。
- **论证**:表格的层级全部由「行/列/格」承担,颜色一旦引入第二个色相,
读者会立刻把它当成语义(红=差、绿=好),而参数表恰恰不该替读者下这个
判断——1.24kg 和 1.62kg 谁「好」取决于他背不背着走。所以赢的一格只用
**明度**顶起来(深底反白 = 更重),不用色相说好坏。
### 负约束(本模板明令不做的事)
- **不用第二个色相。** 整张图只有靛蓝一族 + 中性白,破了它「不替读者判
断好坏」这条论证就没了。
- 不用红绿对错配色。参数表最容易滑向「A 列绿 B 列红」,那是价值判断,不
是参数。
- 不给赢的一格打勾/星/奖杯图标。图标是结论,表格只出示事实。
- 不画分隔线节点。格线由「表体底色从单元格缝隙透出」实现,多一层描边就
会和缝隙对不齐。
- 不用蓝紫渐变、霓虹线条、伪 3D不用 emoji 当图标,不用阴影。
- 一格文案不超过 10 个字:装不下就说明该拆成两行指标,不缩字号。
- 结尾不硬凑「A 胜出」。四比四就写四比四,再给两句「什么情况选谁」。
硬契约:
- 内容距边缘 ≥64px这里 64
- 配色全部走 color_vars单色序列改一处色相即整张换肤
- 正文与背景对比度 ≥2.0(最低一对见文件末尾实测表)
- **CJK 行高**:页头大标题 1.2,区块标题 1.3,正文 1.7
- **CJK 负字距不超过 -0.02em**;数字与拉丁走 Inter
- 三列宽度写死并且必须加起来等于 INNER否则表尾会溢出
- 顶层 frame 必须显式写 x/y否则多帧会全部堆在原点
- 根高固定ROOT_H 是量出来的(见文件末尾),改内容后要重量一次
"""
import os
import sys
sys.path.insert(0, os.path.join(
os.path.dirname(os.path.dirname(os.path.dirname(
os.path.dirname(os.path.abspath(__file__))))),
"templates", "step0", "_generators"))
from oplib import (Ids, color_vars, frame, rect, solid, text, write_doc)
ids = Ids()
VARS = color_vars({
"c-bg": "#F1F2F7",
"c-card": "#FFFFFF",
"c-tint": "#E6E8F1",
"c-line": "#D5D8E4",
"c-ink": "#12162A",
"c-deep": "#242B4C",
"c-muted": "#4E5573",
# 反白页面上的次级文字:同一个灰压在近黑上会掉到 4 以下,深浅两套次级色
# 是必要的(做法同 pitfall-list
"c-inv-muted": "#A9AEC2",
})
CJK = "Noto Sans SC"
NUM = "Inter"
W = 1080
EDGE = 64
INNER = W - EDGE * 2 # 952
# 三列宽度 + 两条纵向格线必须正好等于 INNER。表格没有自动列宽写错就溢出。
#
# GRID 是格线宽度,同时兼两个职责:视觉上它是表格的分隔线(表体底色 c-line
# 从单元格之间透出来),几何上它让相邻单元格拉开 4px —— 几何检测器
# geometry_spill_diagnostics 的 SIBLING_JAM_GAP=3把 0px 相邻的两个
# 文本单元格判成「读起来连成一个词」的 jam。真格线正好一并解决两件事。
# 列宽 = INNER 扣掉表体两侧的 GRID 内边距,再扣掉两条纵向格线。
GRID = 4
COL_LABEL, COL_A, COL_B = 276, 330, 330
assert COL_LABEL + COL_A + COL_B + GRID * 4 == INNER
LH_DISPLAY, LH_HEAD, LH_BODY = 1.2, 1.3, 1.7
# 量出来的根高(根设 fit_content 渲一次读 PNG 高度,见文件末尾)。
ROOT_H = 2405
A_NAME, A_TAG = "轻装派", "13.6 英寸 · 1.24kg"
B_NAME, B_TAG = "生产力", "16 英寸 · 1.62kg"
# (指标, A 值, B 值, 谁更强)。"-" 表示这一行不判胜负。
# 每格 ≤10 字:三列都是定宽,超了会换行把行高顶起来,表就不齐了。
SPECS = [
("整机重量", "1.24 kg", "1.62 kg", "a"),
("实测续航", "11 小时", "7 小时", "a"),
("屏幕", "60Hz 标准屏", "120Hz 高刷", "b"),
("视频剪辑", "1080p 顺畅", "4K 不掉帧", "b"),
("接口", "2 个 USB-C", "3 口带读卡器", "b"),
("满载噪音", "几乎听不见", "能听见风扇", "a"),
("到手价", "8,999 元", "13,499 元", "a"),
("保修", "一年质保", "两年含上门", "b"),
]
# 两句「什么情况选谁」。四比四的表必须给它,否则读者拿不走结论。
# 每句 ≤14 字:建议卡内容宽 410px26px 中文一行约 15 字,写到 16 字就会把
# 句号单独甩到第二行变成孤字。
PICKS = [
("" + A_NAME, "常在路上,写字和开会为主。"),
("" + B_NAME, "固定工位,剪片调色,外设多。"),
]
def band(name, *, fill, pad, gap, children, align="start"):
node = frame(ids, name, width="fill_container", height="fit_content",
layout="vertical", padding=pad, gap=gap, alignItems=align,
fill=fill)
node["children"] = children
return node
def col(name, children, *, gap=16, width="fill_container", align="start",
**props):
node = frame(ids, name, width=width, height="fit_content",
layout="vertical", gap=gap, alignItems=align, fill=[],
**props)
node["children"] = children
return node
def row(name, children, *, gap=0, align="stretch", width="fill_container",
**props):
node = frame(ids, name, width=width, height="fit_content",
layout="horizontal", gap=gap, alignItems=align, fill=[],
**props)
node["children"] = children
return node
def tag(label, *, bg, fg):
node = frame(ids, "标签", width="fit_content", height="fit_content",
layout="horizontal", padding=[10, 22], cornerRadius=999,
alignItems="center", justifyContent="center", fill=solid(bg))
node["children"] = [
text(ids, "标签文字", label, 24, 600, fg, family=CJK,
width="fit_content", growth="auto", line_height=1.4),
]
return node
def section_head(title, note):
return col("区块头", [
rect(ids, "强调短线", width=72, height=8, fill=solid("$c-ink")),
text(ids, "区块标题", title, 44, 700, "$c-ink", family=CJK,
line_height=LH_HEAD),
text(ids, "区块说明", note, 26, 400, "$c-muted", family=CJK,
line_height=LH_BODY),
], gap=16)
# ------------------------------------------------------------------ 01 页头
def header():
return band("01 页头", fill=solid("$c-ink"), pad=[76, EDGE, 64, EDGE],
gap=26, children=[
tag("参数对比 · 选购向", bg="$c-card", fg="$c-ink"),
text(ids, "主标题", "两台机器\n到底差在哪", 76, 700, "$c-card",
family=CJK, line_height=LH_DISPLAY, spacing=-1.4),
text(ids, "副标题",
"八项常用指标逐行摆开,深色那一格是这一行里更强的一方。",
27, 400, "$c-inv-muted", family=CJK, line_height=LH_BODY),
])
# ---------------------------------------------------------------- 02 候选者
def candidate(name, spec, inverted):
"""候选者名片。两张一模一样,只有明度不同——不给任何一方配色相。"""
ink = "$c-card" if inverted else "$c-ink"
sub = "$c-inv-muted" if inverted else "$c-muted"
card = col("候选者", [
text(ids, "候选者名", name, 40, 700, ink, family=CJK,
line_height=LH_HEAD),
text(ids, "候选者规格", spec, 25, 400, sub, family=NUM,
line_height=1.5),
], gap=10, width=(INNER - 20) // 2, padding=[28, 30])
card["fill"] = solid("$c-deep" if inverted else "$c-card")
if not inverted:
card["stroke"] = {"thickness": 2, "fill": solid("$c-line")}
return card
def candidates():
return band("02 候选者", fill=[], pad=[56, EDGE, 0, EDGE], gap=0,
children=[
row("候选者行", [
candidate(A_NAME, A_TAG, False),
candidate(B_NAME, B_TAG, True),
], gap=20, align="stretch"),
])
# ------------------------------------------------------------------ 03 参数表
def cell(content, width_px, *, weight, color, fill, family=CJK, first=False):
box = frame(ids, "单元格", width=width_px, height="fit_content",
layout="vertical", padding=[20, 22], gap=0,
justifyContent="center", fill=fill)
box["children"] = [
text(ids, "单元格文字", content, 27 if first else 28,
weight, color, family=family, line_height=1.45),
]
return box
def head_row():
cells = [
cell("指标", COL_LABEL, weight=600, color="$c-inv-muted",
fill=solid("$c-deep"), first=True),
cell(A_NAME, COL_A, weight=700, color="$c-card",
fill=solid("$c-deep")),
cell(B_NAME, COL_B, weight=700, color="$c-card",
fill=solid("$c-deep")),
]
return row("表头", cells, gap=GRID, align="stretch")
def spec_row(label, va, vb, winner, zebra):
"""一行两格。赢的一格换成深底反白——层级由明度给,不由色相给。"""
base = solid("$c-tint") if zebra else solid("$c-card")
win_fill = solid("$c-deep")
cells = [
cell(label, COL_LABEL, weight=600, color="$c-muted", fill=base,
first=True),
cell(va, COL_A, weight=700 if winner == "a" else 400,
color="$c-card" if winner == "a" else "$c-ink",
fill=win_fill if winner == "a" else base, family=NUM),
cell(vb, COL_B, weight=700 if winner == "b" else 400,
color="$c-card" if winner == "b" else "$c-ink",
fill=win_fill if winner == "b" else base, family=NUM),
]
return row("参数行", cells, gap=GRID, align="stretch")
def table():
rows = [head_row()]
rows += [spec_row(label, va, vb, winner, index % 2 == 1)
for index, (label, va, vb, winner) in enumerate(SPECS)]
# 表体底色就是格线色:单元格之间留出的 GRID 缝隙让它透出来,于是格线不是
# 画上去的,而是「没被单元格盖住的底」——少一层节点,也永远对得齐。
grid = col("表体", rows, gap=GRID)
grid["fill"] = solid("$c-line")
grid["padding"] = [GRID, GRID]
grid["clipContent"] = True
grid["cornerRadius"] = 4
return band("03 参数表", fill=[], pad=[56, EDGE, 0, EDGE], gap=28,
children=[
section_head("八行看完", "深色格 = 这一行里更强的那一台,只说事实。"),
grid,
])
# ------------------------------------------------------------------ 04 结论
def tally():
wins_a = sum(1 for _, _, _, w in SPECS if w == "a")
wins_b = sum(1 for _, _, _, w in SPECS if w == "b")
score = row("比分行", [
text(ids, "比分", f"{wins_a} : {wins_b}", 88, 700, "$c-ink",
family=NUM, width="fit_content", growth="auto", line_height=1.0,
spacing=-2),
text(ids, "比分说明",
f"{A_NAME}赢四行,{B_NAME}赢四行——这张表不替你选。",
27, 400, "$c-muted", family=CJK, line_height=LH_BODY),
], gap=28, align="center")
picks = []
for title, why in PICKS:
head = text(ids, "选谁", title, 32, 700, "$c-ink", family=CJK,
width="fit_content", growth="auto", line_height=LH_HEAD)
item = col("建议", [
head,
text(ids, "理由", why, 26, 400, "$c-muted", family=CJK,
line_height=LH_BODY),
], gap=10, width="fill_container", padding=[26, 28])
item["fill"] = solid("$c-card")
item["stroke"] = {"thickness": 2, "fill": solid("$c-line")}
picks.append(item)
return band("04 结论", fill=[], pad=[56, EDGE, 64, EDGE], gap=28,
children=[
section_head("四比四", "指标数不是答案,你的使用方式才是。"),
score,
row("建议行", picks, gap=20, align="stretch"),
])
# ------------------------------------------------------------------ 05 页脚
def footer():
return band("05 页脚", fill=solid("$c-ink"), pad=[44, EDGE], gap=12,
children=[
text(ids, "结语", "比参数之前,先写下你每天真正在做的三件事。",
30, 600, "$c-card", family=CJK, line_height=LH_HEAD),
row("署名行", [
text(ids, "账号名", "@ 你的账号名", 26, 600, "$c-card",
family=CJK, width="fit_content", growth="auto",
line_height=1.4),
text(ids, "更新说明", "每周一张能照着抄的对比表", 24, 400,
"$c-inv-muted", family=CJK, width="fit_content",
growth="auto", line_height=1.4),
], gap=16, width="fill_container"),
])
def build():
page = frame(ids, "参数表对比长图", width=W, height=ROOT_H,
layout="vertical", gap=0, fill=solid("$c-bg"),
clipContent=True)
page["children"] = [header(), candidates(), table(), tally(), footer()]
page["x"], page["y"] = 0, 0
return [page]
# 对比度WCAG 相对亮度比op-design-lint 门槛 2.0;数值由 contrast.py 实测):
# c-ink on c-bg 16.00 c-muted on c-bg 6.54
# c-ink on c-card 17.89 c-muted on c-card 7.31
# c-ink on c-tint 14.63 c-muted on c-tint 5.98
# c-card on c-deep 13.77 c-inv-muted on c-deep 6.24
# c-card on c-ink 17.89 c-inv-muted on c-ink 8.11
# 承载正文的最低一对是 5.98(斑马行里的指标名)。整张图只有靛蓝
# 一个色相,赢的格靠明度顶起来——换肤只需整体旋转这一族的色相。
if __name__ == "__main__":
write_doc(sys.argv[1], VARS, build(), "参数表对比长图")