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

267 lines
12 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
"""pricing-tiers-comparison.op — 价格档位对比1080×1440 竖版 3:4
对比档的「递增序列」那一张:免费 / Pro / 团队三档并排,越往右买得越多。
它和其它多方对照的根本差别是——**三列之间有序**,右边那列包含左边那列,所
以每一列的第一行不是定位而是**价格**,读者是拿价格当锚点往下读的。
### 最近邻论证(为什么它不是已有的哪一张)
- **本批 03 三方案横评**:那张的三列是三条**互不兼容**的路,谁也不是谁的
升级版,所以推荐理由只能是「大多数人在这」。这张的三列是**一条递增的
线**,右边严格包含左边,所以中间那列可以正当地打「最多人选」——多花的
钱换到了什么,是能逐条数出来的。
- **本批 01 参数表**:那张按指标横着比两台机器,指标之间没有包含关系;
这张按档位竖着列,每一档的清单是**累加**的(下面一档默认包含上面)。
- **本批 09 场景选择指南**:那张回答「什么情况选哪个」,单位是处境;这张
回答「多少钱买到什么」,单位是价格。
### 配色推导(采样 → 收敛 → 论证)
- **采样**:从「定价页」这件事采——纸质价目表、菜单、票价牌,那一族的记
忆色是米黄纸配深棕墨,不是 SaaS 官网那种蓝。
- **收敛**单色相暖棕H≈40°的一条七级明度序列 L 0.10 / 0.22 / 0.40 /
0.62 / 0.88 / 0.93 / 1.0chroma 深处 0.04、浅处 0.012,只有一个色相。
- **论证**:定价页最容易犯的错是给推荐档配一个高饱和色,读者会把那当成
促销标签而不是信息。这里推荐档改用**明度反转**(唯一一块深色),它在
三列里天然最重,却不带任何「打折 / 限时」的暗示;剩下的层级由字号(价
格数字最大)和字重承担。
### 负约束(本模板明令不做的事)
- **不用第二个色相。** 整张图只有暖棕一族 + 中性白。
- 不用红绿。价格没有对错。
- 不写「限时 / 立省 / 原价 ¥199」这类促销话术也不画划掉的原价。这张图
的任务是让人看懂差别,不是催人下单。
- 不给推荐档加皇冠、火焰、星星。它凭「唯一一块深色」被认出来就够了。
- 不用蓝紫渐变、霓虹线条、伪 3D、阴影、emoji 图标。
- 每条包含项 ≤9 字:列内容宽 262px23px 中文一行约 11 字,留两字余量。
- 三档的条目数必须相同(这里各 4 条),否则三列一高一低,「递增」看起来
像「随便列了列」。
硬契约:
- 内容距边缘 ≥64px这里 64
- 固定 3:4 画幅:根高写死 1440靠 space_between 分配三块之间的空隙
- 三列必须全部 fill_container + stretch
- 配色全部走 color_vars单色序列改一处色相即整张换肤
- 正文与背景对比度 ≥2.0(最低一对见文件末尾实测表)
- **CJK 行高**:大标题 1.2,档名 1.3,正文 1.6
- **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, icon_font, rect, solid, text,
write_doc)
ids = Ids()
VARS = color_vars({
"c-bg": "#F7F3EC",
"c-card": "#FFFFFF",
"c-line": "#DCD1BF",
"c-ink": "#1E1710",
"c-deep": "#3B2E1E",
"c-muted": "#6A5945",
"c-faint": "#9C8B75",
# 深卡上的正文与次级文字要分两档。只有一档时,推荐列的清单会比左右两列
# 的黑字看起来更"轻",视觉上反而像被淘汰的那一列——正好和它的身份相反。
"c-inv-body": "#E4DAC9",
"c-inv-muted": "#B6A78F",
})
CJK = "Noto Sans SC"
NUM = "Inter"
W, H = 1080, 1440
EDGE = 64
GUTTER = 16
LH_DISPLAY, LH_HEAD, LH_BODY = 1.2, 1.3, 1.6
# (档名, 价格, 适合谁, [包含项 ≤9 字 ×4], 累加标, 是否推荐)
# 三档的条目数必须一样多,否则三列不等高。
TIERS = [
("免费", "0", "先试试水",
["每月 10 张", "带小水印", "标准清晰度", "只存七天"],
"从这里开始", False),
("Pro", "29", "每周都要发",
["每月 300 张", "去掉水印", "高清导出", "永久保存"],
"含免费档全部", True),
("团队", "99", "三个人以上",
["张数不限", "共享素材库", "成员权限", "优先答复"],
"含 Pro 全部", False),
]
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",
**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, size=22):
node = frame(ids, "标签", width="fit_content", height="fit_content",
layout="horizontal", padding=[8, 18], 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", size=24),
text(ids, "主标题", "多花的钱\n买到了什么", 72, 700, "$c-ink",
family=CJK, line_height=LH_DISPLAY, spacing=-1.4),
text(ids, "副标题", "右边一档默认包含左边一档,只看新增的那几行就够。",
26, 400, "$c-muted", family=CJK, line_height=LH_BODY),
], gap=20)
# --------------------------------------------------------------------- 三档
def price_line(amount, featured):
"""价格行。数字最大、单位最小——层级完全由字号给。"""
return row("价格", [
text(ids, "货币", "¥", 28, 600,
"$c-inv-muted" if featured else "$c-faint", family=NUM,
width="fit_content", growth="auto", line_height=1.1),
text(ids, "金额", amount, 62, 700,
"$c-card" if featured else "$c-ink", family=NUM,
width="fit_content", growth="auto", line_height=1.0,
spacing=-1.5),
text(ids, "单位", "/月", 24, 400,
"$c-inv-muted" if featured else "$c-muted", family=CJK,
width="fit_content", growth="auto", line_height=1.1),
], gap=6, align="end", width="fit_content")
def feature(line, featured):
mark = icon_font(ids, "", "check", 20,
"$c-inv-body" if featured else "$c-ink")
wrap = frame(ids, "标位", width=20, height=37, layout="vertical",
justifyContent="center", fill=[])
wrap["children"] = [mark]
return row("包含项", [
wrap,
text(ids, "包含项文字", line, 23, 400,
"$c-inv-body" if featured else "$c-muted", family=CJK,
line_height=LH_BODY),
], gap=10, align="start")
def carry_pill(label, featured):
"""「含上一档全部」。这一行是本模板的论点本身——三列是一条递增的线,
右边严格包含左边。没有它,三列就退化成三个互不相干的选项。"""
node = frame(ids, "累加标", width="fit_content", height="fit_content",
layout="horizontal", padding=[7, 16], cornerRadius=6,
alignItems="center", justifyContent="center", fill=[],
stroke={"thickness": 2,
"fill": solid("$c-inv-muted" if featured
else "$c-line")})
node["children"] = [
text(ids, "累加标文字", label, 21, 600,
"$c-inv-body" if featured else "$c-muted", family=CJK,
width="fit_content", growth="auto", line_height=1.4),
]
return node
def tier_card(name, amount, who, features, carry, featured):
head_block = col("档头", [
text(ids, "档名", name, 34, 700,
"$c-card" if featured else "$c-ink", family=CJK,
line_height=LH_HEAD),
price_line(amount, featured),
text(ids, "适合谁", who, 23, 400,
"$c-inv-muted" if featured else "$c-muted", family=CJK,
line_height=1.45),
], gap=12)
body = col("档体", [
head_block,
rect(ids, "分线", width="fill_container", height=2,
fill=solid("$c-inv-muted" if featured else "$c-line")),
carry_pill(carry, featured),
col("包含组", [feature(line, featured) for line in features], gap=10),
], gap=22, padding=[32, 22, 34, 22])
card = col("档位", [body], gap=0, width="fill_container")
card["fill"] = solid("$c-deep" if featured else "$c-card")
card["cornerRadius"] = 6
if not featured:
card["stroke"] = {"thickness": 2, "fill": solid("$c-line")}
return card
def tiers():
cards = [tier_card(*tier) for tier in TIERS]
return col("三档", [
row("档位行", cards, gap=GUTTER, align="stretch"),
row("推荐说明", [
tag("最多人选", bg="$c-ink", fg="$c-card"),
text(ids, "推荐理由", "中间那档是唯一一块深色——不是促销,是位置。",
24, 400, "$c-muted", family=CJK, line_height=LH_BODY),
], gap=14, align="center"),
], 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=[76, EDGE], gap=40, justifyContent="space_between",
alignItems="start", fill=solid("$c-bg"), clipContent=True)
page["children"] = [head(), tiers(), tail()]
page["x"], page["y"] = 0, 0
return [page]
# 对比度WCAG 相对亮度比op-design-lint 门槛 2.0;数值由 contrast.py 实测):
# c-ink on c-bg 16.02 c-muted on c-bg 6.07
# c-ink on c-card 17.72 c-muted on c-card 6.71
# c-card on c-deep 13.16 c-inv-body on c-deep 9.51
# c-inv-muted on c-deep 5.59 c-card on c-ink 17.72
# c-inv-muted on c-ink 7.52 c-faint on c-card 3.30
# 承载正文的最低一对是 5.59(推荐档的「适合谁」与货币符号)。清单正文走
# c-inv-body 的 9.51 —— 推荐列必须比左右两列更实,否则唯一的深色反而读成
# 「被划掉的那一列」。c-faint 只画货币符号旁的非推荐档小字与卡片描边。
if __name__ == "__main__":
write_doc(sys.argv[1], VARS, build(), "价格档位对比")