openpencil/templates/step0/_generators/tpl_compare_time_shift.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

250 lines
11 KiB
Python
Raw 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
"""time-shift-comparison.op — 时间对比 一年前 / 现在1080×1440 竖版 3:4
对比档的「中轴镜像」那一张:一条居中的标签脊柱,左边是一年前,右边是现
在,同一项的两个取值在同一行上左右分开。读者的视线沿脊柱往下走,每停一
次就同时读到两侧。
### 最近邻论证(为什么它不是已有的哪一张)
- **本批 01 参数表**:那张的指标名在**最左列**,两个取值都在它右边——
读法是「从左往右」。这张的指标名在**正中**,两个取值分居两侧——读法
是「从中间往两边」。同样三列,重心完全不同:那张比的是两个东西,这张
比的是同一个人的两个时刻,中轴就是时间本身。
- **before-after1600×900**:那张也是同一主体的两个时刻,但主体是两
张截图、只有一组。这张是**五项并行**的状态变化,没有图片位。
- **本批 04 版本变化**:那张是产品的**逐条变更**(客观、可核对);这张是
人的**习惯变化**(主观、成序列)。所以那张一行一行读,这张必须两侧同
时读——「六小时 → 九十分钟」只有并置才有说服力。
### 配色推导(采样 → 收敛 → 论证)
- **采样**:从时间本身采——「过去」在版面语言里是褪色的(旧照片、存档、
灰度),「现在」是有颜色的。
- **收敛**:一年前那一侧**完全无彩**(中性灰阶 L 0.22 / 0.42 / 0.62
现在那一侧只加**一个**低饱和赤陶 #A94E24chroma≈0.11),页面其余部分
共用同一条中性序列。
- **论证**:这张图唯一要传达的判断是「有变化,而且方向是对的」。把有彩
色全部押在「现在」这一侧,色相本身就成了那个判断,不需要任何箭头或
「↑」符号来补充。反过来说,如果两侧都上色,读者就得先分辨哪个色代表
好——又回到了红绿那套需要图例的编码。
### 负约束(本模板明令不做的事)
- **只允许一个有彩色,且只出现在「现在」这一侧。** 一年前那一侧必须是
纯灰阶——它的褪色就是论证。
- 不用红绿。这里没有对错,只有变化。
- 不画「↑ 提升 300%」这类增长箭头。并置本身已经说完了。
- 不写「蜕变 / 逆袭 / 脱胎换骨」。每一项只写它变成了什么。
- 不用蓝紫渐变、霓虹线条、伪 3D、阴影、emoji 图标。
- 每个取值 ≤8 字:两侧列宽固定 336px写长了两侧会一高一低脊柱就歪了。
- 中轴标签必须等宽SPINE_W它是这张图的对齐基准。
硬契约:
- 内容距边缘 ≥64px这里 64
- 固定 3:4 画幅:根高写死 1440靠 space_between 分配三块之间的空隙
- 三列宽度 + 两条间距必须正好等于 INNER
- 左列文字右对齐、右列文字左对齐——镜像是靠 textAlign 做的
- 配色全部走 color_vars换主色 = 改 c-now 一处
- 正文与背景对比度 ≥2.0(最低一对见文件末尾实测表)
- **CJK 行高**:大标题 1.2,标签 1.3,正文 1.5
- **CJK 负字距不超过 -0.02em**;数字走 Inter
"""
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": "#F4F3F1",
"c-card": "#FFFFFF",
"c-panel": "#E7E5E1",
"c-ink": "#14120F",
"c-muted": "#57534C",
"c-faint": "#6B665E",
"c-inv-muted": "#A9A49B",
# 唯一的有彩色,只出现在「现在」那一侧。
"c-now": "#A94E24",
})
CJK = "Noto Sans SC"
NUM = "Inter"
W, H = 1080, 1440
EDGE = 64
INNER = W - EDGE * 2 # 952
SPINE_W = 200
SIDE_W = 336
SPINE_GAP = 40
assert SIDE_W * 2 + SPINE_W + SPINE_GAP * 2 == INNER
LH_DISPLAY, LH_HEAD = 1.2, 1.3
THEN_LABEL, NOW_LABEL = "一年前", "现在"
# (中轴标签, 一年前, 现在)。每个取值 ≤8 字,否则两侧行高会不一致。
SHIFTS = [
("更新节奏", "想起来才发", "每周三固定"),
("单条耗时", "六个小时", "九十分钟"),
("选题来源", "刷到什么写什么", "读者的提问"),
("封面", "每次重做", "一套模板"),
("收藏率", "1.2%", "6.8%"),
("读者留言", "偶尔一两条", "每条都有人问"),
]
# 收尾那句。六行并置容易读成「样样都进步了」,这一句把因果关系点破。
SPINE_NOTE = "六项里只有第一项是主动改的,其余五项都是它带来的。"
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=16, align="center", width="fill_container",
justify="start", **props):
node = frame(ids, name, width=width, height="fit_content",
layout="horizontal", gap=gap, alignItems=align,
justifyContent=justify, fill=[], **props)
node["children"] = children
return node
def tag(label, *, bg, fg, size=24):
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, size, 600, fg, family=CJK,
width="fit_content", growth="auto", line_height=1.4),
]
return node
# --------------------------------------------------------------------- 页头
def head():
return col("页头", [
tag("时间对比 · 一年", bg="$c-ink", fg="$c-card"),
text(ids, "主标题", "一年时间\n到底变了什么", 70, 700, "$c-ink",
family=CJK, line_height=LH_DISPLAY, spacing=-1.4),
text(ids, "副标题", "左边是去年,右边是现在。有颜色的那一侧是当下。",
26, 400, "$c-muted", family=CJK, line_height=1.7),
], gap=20)
# --------------------------------------------------------------------- 中轴
def era_header():
"""两侧的时代标签。左侧纯灰、右侧上色——整张图的色彩规则在这里声明。"""
return row("时代行", [
col("左时代", [
text(ids, "去年标签", THEN_LABEL, 30, 700, "$c-faint", family=CJK,
align="right", line_height=LH_HEAD),
], gap=0, width=SIDE_W),
col("轴头", [], gap=0, width=SPINE_W),
col("右时代", [
text(ids, "现在标签", NOW_LABEL, 30, 700, "$c-now", family=CJK,
align="left", line_height=LH_HEAD),
], gap=0, width=SIDE_W),
], gap=SPINE_GAP, align="center")
def spine_label(label):
"""中轴上的一格。等宽是整张图的对齐基准,所以它写死 SPINE_W。"""
node = frame(ids, "轴标签", width=SPINE_W, height=64,
layout="horizontal", alignItems="center",
justifyContent="center", cornerRadius=6,
fill=solid("$c-panel"))
node["children"] = [
text(ids, "轴标签文字", label, 24, 600, "$c-muted", family=CJK,
width="fit_content", growth="auto", align="center",
line_height=1.4),
]
return node
def shift_row(label, then, now):
return row("变化行", [
col("去年值", [
text(ids, "去年文字", then, 30, 400, "$c-faint", family=NUM,
align="right", line_height=1.5),
], gap=0, width=SIDE_W, align="end"),
spine_label(label),
col("现在值", [
text(ids, "现在文字", now, 30, 700, "$c-now", family=NUM,
align="left", line_height=1.5),
], gap=0, width=SIDE_W, align="start"),
], gap=SPINE_GAP, align="center")
def spine_note():
node = frame(ids, "中轴收尾", width="fill_container", height="fit_content",
layout="horizontal", padding=[22, 26], cornerRadius=6,
alignItems="center", justifyContent="center",
fill=solid("$c-panel"))
node["children"] = [
text(ids, "收尾文字", SPINE_NOTE, 25, 500, "$c-muted", family=CJK,
width="fit_content", growth="auto", align="center",
line_height=1.5),
]
return node
def spine():
rows = [shift_row(*shift) for shift in SHIFTS]
return col("中轴", [era_header(), col("变化列表", rows, gap=14),
spine_note()], gap=22)
# --------------------------------------------------------------------- 页脚
def tail():
band = col("页脚", [
text(ids, "结语", "变的不是能力,是把哪件事固定了下来。", 29, 600,
"$c-card", family=CJK, line_height=LH_HEAD),
row("署名行", [
text(ids, "账号名", "@ 你的账号名", 25, 600, "$c-card",
family=CJK, width="fit_content", growth="auto",
line_height=1.4),
text(ids, "更新说明", "每年这个时候复盘一次", 23, 400,
"$c-inv-muted", family=CJK, width="fit_content",
growth="auto", line_height=1.4),
], gap=14),
], gap=12, padding=[32, 32])
band["fill"] = solid("$c-ink")
return band
def build():
page = frame(ids, "时间对比 一年前与现在", width=W, height=H,
layout="vertical", padding=[68, EDGE], gap=40,
justifyContent="space_between", alignItems="start",
fill=solid("$c-bg"), clipContent=True)
page["children"] = [head(), spine(), tail()]
page["x"], page["y"] = 0, 0
return [page]
# 对比度WCAG 相对亮度比op-design-lint 门槛 2.0;数值由 contrast.py 实测):
# c-ink on c-bg 16.86 c-muted on c-bg 6.89
# c-faint on c-bg 5.14 c-now on c-bg 4.98
# c-muted on c-panel 6.08 c-card on c-ink 18.70
# c-inv-muted on c-ink 7.54
# 承载正文的最低一对是 4.98(「现在」那一侧的取值),刚好过 AA 正文门槛。
# 「一年前」那一侧最初用的是更浅的一档灰3.26 on c-bg——褪色的观感对
# 了,但 30px 常规字够不到 4.5,于是压到 5.14:仍然明显轻于 16.86 的正文
# 黑与有色的当下,褪色靠的是「比谁都浅」,不是「浅到读不出来」。
if __name__ == "__main__":
write_doc(sys.argv[1], VARS, build(), "时间对比 一年前与现在")