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

244 lines
10 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
"""recipe-four-step.op — 菜谱四步卡1080×13504:5 单卡·2×2 四宫格)
教程档里唯一一张**四宫格**。别的教程都是「一步一屏」,这张反着来:四步全
放在一张卡上,一眼看完、截图存相册就能照着做——菜谱的真实使用场景是站在
灶台前单手划手机,翻页是负担。
风格取「高级白底杂志风」:暖白纸、细线、字重对比撑层级,不靠色块堆。
### 配色推导(采样 → 收敛 → 论证)
- **采样**:从「食物本身」采——葱油面这类家常面食的记忆色是酱色与焦香
的暖褐红,不是餐饮 App 那种高饱和番茄红。取一档偏砖的赤陶 #B8452E。
- **收敛**:一个有彩色(赤陶)+ 一档极淡的同色相底(#F6E4DE只给序号
与提示条)+ 五档暖中性(纸 / 卡 / 线 / 次级 / 墨)。
- **论证**:菜谱卡上真正该有颜色的是**照片**。版面给到第二个有彩色,四
张成品图就会跟版面打架;所以有彩色收到只剩序号圆与那条提示带,其余全
部让位。暖中性而不是冷灰:冷灰底会把食物照片衬得发青。
### 负约束(本模板明令不做的事)
- **不给菜谱配「营养成分表」式的伪数据。** 卡路里 / 蛋白质克数没有可信
来源就是编的,编出来的数字比没有数字更糟。只给时间、份量、难度这三个
做饭的人自己就能验证的量。
- 不用第二个有彩色。四张照片就是这张卡的颜色。
- 不用阴影。卡片边界由 1px 暖灰 hairline 给。
- 不用 emoji🍜🔥当装饰不用手写体标题不做「厨房黑板」贴纸感。
- 步骤说明一行封顶。写不下说明这一步该拆,不缩字号。
- 不写「秘制 / 灵魂 / 绝了」这类种草黑话,每步都写成可执行的动作 + 一个
可验证的量(秒数、火力、水位)。
硬契约:
- 内容距边缘 ≥72px这里 72
- 配色全部走 color_vars换主色只需改 c-accent 与 c-accent-soft
- 正文与背景对比度 ≥2.0(实测表见文件末尾)
- **CJK 行高比西文全线高 0.2**display 1.2,标题 1.35,正文 1.7
- **CJK 负字距不超过 -0.02em**72px 标题 → -1.4px = -0.019em
- 汉字走 Noto Sans SC数字与拉丁走 Inter
- 顶层 frame 显式写 x/y
- 四张图位实测各 416×2351.77:1≈16:9——高度写死见 step_card
注释;改页头/指标条/页脚任何一处高度都要重算这个数
- 同一行的两张卡必须同为 fill_container否则 taffy 会让先声明的那张吃
掉整行宽度
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from oplib import (Ids, color_vars, frame, icon_font, rect, solid, stroke,
text, write_doc)
ids = Ids()
VARS = color_vars({
"c-bg": "#FBF7F0",
"c-card": "#FFFFFF",
"c-slot": "#F1EADF",
"c-line": "#E5DACA",
"c-ink": "#221C16",
"c-muted": "#6E6156",
"c-accent": "#B8452E",
"c-accent-soft": "#F7E5DF",
})
CJK = "Noto Sans SC"
NUM = "Inter"
W, H = 1080, 1350
EDGE = 72
LH_DISPLAY, LH_HEAD, LH_BODY = 1.2, 1.35, 1.7
# 四个图位的固定高度(见 step_card 注释)。
SLOT_H = 235
# 三个做饭的人自己能验证的量。刻意不给卡路里——见负约束。
META = [("时间", "10 分钟"), ("份量", "2 人"), ("难度", "新手可做")]
# (序号, 步骤名, 一行动作说明, 图位提示)
# 标题与说明都按「一行封顶」写:标题可用宽 322px / 27px ≈ 11 字,说明可用
# 宽 376px / 22px ≈ 17 字。超一个字就换行,四张卡的高度立刻不齐 —— 第一版
# 就是这么把页脚顶出画布的(说明写了 17 字,两行)。
STEPS = [
("1", "葱切段,白绿分开",
"葱白耐炸先下,葱绿后放不发苦。", "葱段特写"),
("2", "冷油下葱白熬 6 分钟",
"油面冒小泡是对的,冒烟就过了。", "熬油过程"),
("3", "两勺生抽一勺糖调汁",
"糖不为甜,是压住生抽的咸边。", "调汁碗"),
("4", "面煮 3 分钟,捞进碗拌",
"带一勺面汤,酱汁才挂得住面。", "成品面"),
]
def col(name, children, *, gap=16, width="fill_container", align="start",
height="fit_content", **props):
node = frame(ids, name, width=width, height=height,
layout="vertical", gap=gap, alignItems=align, fill=[],
**props)
node["children"] = children
return node
def row(name, children, *, gap=20, align="center", width="fill_container",
height="fit_content", **props):
node = frame(ids, name, width=width, height=height,
layout="horizontal", gap=gap, alignItems=align, fill=[],
**props)
node["children"] = children
return node
# ------------------------------------------------------------------ 页头
def header():
tag = frame(ids, "分类标签", width="fit_content", height="fit_content",
layout="horizontal", padding=[10, 20], cornerRadius=6,
alignItems="center", justifyContent="center",
fill=solid("$c-accent-soft"))
tag["children"] = [
text(ids, "分类文字", "家常面 · 四步", 24, 600, "$c-accent",
family=CJK, width="fit_content", growth="auto", line_height=1.4),
]
return col("页头", [
tag,
text(ids, "菜名", "十分钟葱油拌面", 72, 700, "$c-ink",
family=CJK, line_height=LH_DISPLAY, spacing=-1.4),
], gap=20)
# ------------------------------------------------------------------ 指标条
def meta_bar():
kids = []
for index, (label, value) in enumerate(META):
if index:
kids.append(rect(ids, "竖分隔", width=2, height=40,
fill=solid("$c-line")))
kids.append(col("指标", [
text(ids, "指标名", label, 22, 400, "$c-muted", family=CJK,
width="fit_content", growth="auto", line_height=1.4),
text(ids, "指标值", value, 28, 600, "$c-ink", family=CJK,
width="fit_content", growth="auto", line_height=1.35),
], gap=4, width="fit_content"))
bar = row("指标条", kids, gap=28, padding=[24, 28])
bar["fill"] = solid("$c-card")
bar["cornerRadius"] = 14
bar["stroke"] = stroke("$c-line", 2)
return bar
# ------------------------------------------------------------------ 四宫格
def step_card(no, title, desc, hint):
# 图位高度写死而不是 fill_container四张卡的标题/说明各自换行数不同,
# 交给 flex 分配余量会让四个图位高度互不相等(实测 186 / 149拼成
# 四宫格时那点差值一眼可见。写死 200 后余量落到卡片底部,看不出来。
slot = frame(ids, "图位", width="fill_container", height=SLOT_H,
layout="vertical", gap=10, alignItems="center",
justifyContent="center", cornerRadius=10,
fill=solid("$c-slot"))
slot["children"] = [
icon_font(ids, "图位图标", "camera", 34, "$c-muted"),
text(ids, "图位提示", hint, 22, 500, "$c-muted", family=CJK,
align="center", line_height=1.4),
]
badge = frame(ids, "序号圆", width=40, height=40, layout="horizontal",
alignItems="center", justifyContent="center",
cornerRadius=999, fill=solid("$c-accent"))
badge["children"] = [
text(ids, "序号", no, 22, 700, "$c-card", family=NUM,
width="fit_content", growth="auto", line_height=1.0),
]
card = col("步骤卡", [
slot,
row("步骤标题行", [
badge,
text(ids, "步骤名", title, 27, 600, "$c-ink", family=CJK,
line_height=LH_HEAD),
], gap=14, align="center"),
text(ids, "步骤说明", desc, 22, 400, "$c-muted", family=CJK,
line_height=LH_BODY),
], gap=16, height="fill_container", padding=[20, 20])
card["fill"] = solid("$c-card")
card["cornerRadius"] = 16
card["stroke"] = stroke("$c-line", 2)
return card
def grid():
cards = [step_card(*s) for s in STEPS]
top = row("上排", cards[:2], gap=24, align="stretch",
height="fill_container")
bottom = row("下排", cards[2:], gap=24, align="stretch",
height="fill_container")
node = col("四宫格", [top, bottom], gap=24, height="fill_container")
return node
# ------------------------------------------------------------------ 页脚
def footer():
left = row("提示左", [
icon_font(ids, "提示图标", "lightbulb", 26, "$c-accent"),
text(ids, "提示文字", "灰块是图片位,把过程图直接拖进去",
23, 500, "$c-ink", family=CJK, width="fit_content",
growth="auto", line_height=1.5),
], gap=12, align="center", width="fit_content")
# 账号名并进提示条而不是单独一行:单独一行要多吃 50px那 50px 直接从
# 四个图位里扣。
tip = row("页脚", [
left,
text(ids, "账号名", "@ 你的账号名", 22, 500, "$c-muted", family=CJK,
width="fit_content", growth="auto", line_height=1.4),
], gap=16, align="center", justifyContent="space_between",
padding=[16, 20])
tip["fill"] = solid("$c-accent-soft")
tip["cornerRadius"] = 12
return tip
def build():
page = frame(ids, "菜谱四步卡", width=W, height=H, layout="vertical",
padding=[64, EDGE], gap=28, fill=solid("$c-bg"),
clipContent=True)
page["children"] = [header(), meta_bar(), grid(), footer()]
page["x"], page["y"] = 0, 0
return [page]
# 对比度WCAG 相对亮度比op-design-lint 门槛 2.0;数值由脚本实测):
# c-ink on c-bg 15.79 c-muted on c-bg 5.61
# c-ink on c-card 16.86 c-muted on c-card 5.99
# c-ink on c-slot 14.11 c-muted on c-slot 5.01
# c-accent on c-bg 5.01 c-card on c-accent 5.35
# c-ink on c-accent-soft 13.83 c-accent on c-accent-soft 4.39
# 承载正文的最低一对是 5.01(图位提示压在 c-slot 上),高于 AA 正文门槛
# 4.5。4.39 那一对只出现在两处24px/600 的分类标签与 26px 的提示图标,
# 都落在 WCAG「大字 / 非文字图形」的 3.0 门槛下,且远高于 lint 的 2.0。
# c-line 只用于 hairline 描边,是非文字图形。
if __name__ == "__main__":
write_doc(sys.argv[1], VARS, build(), "菜谱四步卡 · 4:5 四宫格")