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

116 lines
4.8 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.

"""卡片体系的共享契约 —— 画幅、安全区、网格、字阶、字族。
来源是 `openpencil-docs/openpencil/generation/card-system-0808.md` 的 §4.0
(全局网格与字阶)与 §5(平台规格)。六套卡片模板都从这里取值,spec 改了
只需要改这一个文件 —— 把 §4.0 抄六遍,第七套模板就会和前六套对不齐。
oplib 是所有生成器共用的**节点构造**层;这一层是**卡片体系专用**的度量层,
只被 tpl_*_card.py 引用。
"""
# ---------------------------------------------------------------- 字族
#
# spec §3 给每套主题指定了具体的 CJK 字族(汇文明朝体 / 霞鹜文楷 / 得意黑
# / 源流明体 / 京华老宋体 …),本机一个都没有装。实测(fontprobe):
#
# "Noto Serif SC" → 落到系统默认无衬线,**衬线意图完全丢失**
# "Songti SC" → 真宋体 ✓
# "STSong" → 真宋体 ✓
# "Noto Sans SC" → 无衬线(实际是 PingFang SC)✓
#
# 所以凡是 spec 要「明朝 / 宋 / 楷」气质的地方一律写 `Songti SC`,而不是照
# 抄 spec 的字族名 —— 照抄的结果是全部退化成同一个黑体,主题之间的字体差
# 异归零。spec 自己也写明字体名是「示例锚点」,与 huashu 的口径一致。
#
# 这条替身关系必须留在注释里:将来真装上了那些字族,改这三个常量即可,
# 不必回头翻六个模板。
SERIF = "Songti SC" # 承接 spec 的 明朝体 / 老宋 / 源流明 / 文楷 意图
SANS = "Noto Sans SC" # 承接 思源黑 / 霞鹜新晰黑 / 未来荧黑 意图
NUM = "Inter" # 数字与拉丁一律走西文族(中文排印规范的 fallback 链)
# ---------------------------------------------------------------- 画幅
class Canvas:
"""一个发布规格:画幅 + 安全区 + 12 列网格。
安全区来自 spec §5 的平台规格表。**下边距故意大于上边距** —— 信息流
详情页底部会叠互动浮层,多留的那一截是给它的。
"""
def __init__(self, name, width, height, pad_x, pad_top, pad_bottom):
self.name = name
self.width = width
self.height = height
self.pad_x = pad_x
self.pad_top = pad_top
self.pad_bottom = pad_bottom
@property
def inner(self):
"""内容宽 = 12 列 + 11 沟槽。"""
return self.width - self.pad_x * 2
@property
def padding(self):
"""oplib 的 [top, right, bottom, left]。"""
return [self.pad_top, self.pad_x, self.pad_bottom, self.pad_x]
def cols(self, n):
"""n 列的像素宽(含中间的沟槽)。12 列 = 满栏。"""
col = (self.inner - GUTTER * 11) / 12
return round(col * n + GUTTER * (n - 1))
GUTTER = 16
# spec §5 平台规格表。4:5 是本仓在 spec 之外补的一档(小红书封面推荐比例),
# 安全区按 3:4 与 1:1 的中间值取,理由写在用到它的模板里。
VERTICAL = Canvas("3:4", 1080, 1440, 80, 96, 128)
SQUARE = Canvas("1:1", 1080, 1080, 80, 88, 112)
PORTRAIT45 = Canvas("4:5", 1080, 1350, 80, 92, 120)
# ---------------------------------------------------------------- 字阶
#
# spec §4.0 的 8 档,(fontSize, lineHeight, letterSpacing)。
#
# **任何小于 32px 的文字在本体系里是错误**,不是「小字」—— 1080 宽的卡片在
# 390pt 的手机上缩 2.77 倍,32px 落到约 11.6pt,那已经是注释可读的下限。
#
# 行高遵循「中文行高比西文高 0.2」:西文正文 1.5 → 中文 1.7。
# 字距遵循「CJK 负字距不超过 -0.02em」:只有 ≥120px 的展示档给 -0.01em,
# 48px 以下一律 0 或微正(正文加 0.02em 提透气度)。
SCALE = {
"display-xl": (168, 1.05, -1.7),
"display-l": (120, 1.10, -1.2),
"display": (88, 1.15, 0),
"title-1": (64, 1.25, 0),
"title-2": (48, 1.30, 0),
"body-l": (40, 1.70, 0.8),
"body": (36, 1.70, 0.7),
"caption": (32, 1.50, 0.6),
}
MIN_FONT_SIZE = 32
def step(name):
"""取一档字阶,返回 (size, line_height, spacing)。"""
if name not in SCALE:
raise KeyError(f"{name} 不是 spec §4.0 的字阶档;单页最多用 4 档")
return SCALE[name]
def typed(ids, text_fn, name, content, scale_name, weight, color, *,
family=SANS, width="fill_container", growth="fixed-width",
align=None):
"""按字阶档建一个文本节点。
比直接调 oplib.text 多一层,是为了让「用了哪一档」在代码里显式可数 ——
spec 规定单页最多 4 档,数不出来就守不住。
"""
size, line_height, spacing = step(scale_name)
return text_fn(ids, name, content, size, weight, color, family=family,
line_height=line_height, width=width, growth=growth,
align=align, spacing=spacing)