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

240 lines
11 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
"""software-step-tutorial.op — 软件操作步骤卡1080×13504:5 单卡)
教程档九张里的「工具/软件」那一张,也是唯一一张**深色**教程。
### 和既有 screenshot-tutorial 的区别(为什么不是重复选题)
仓里已有 `screenshot-tutorial`:暖米底 3:4、五帧轮播、三个整页截图位
的是「怎么做一套教程图」。这一张换了三个维度,不是同一张的改色版:
- **画幅与帧数**4:5 **单卡**。一张图讲完一件事,适合评论区直接甩链接
的场景;轮播那张适合当系列首图。
- **信息主元素**:主元素是**菜单路径条**(文件 导出为 PNG 2x
是截图。软件教程真正难的是「那个按钮在哪」,路径条把它一行说清;截图
退成佐证,只留一个。
- **色温**:深色 UI 风。软件截图九成是浅色界面,压在深底上边界自己就出
来了,不需要再给它画边框——暖米底那张必须靠 3px 描边才框得住截图。
### 配色推导(采样 → 收敛 → 论证)
- **采样**:从「代码编辑器 / 设计工具的深色主题」这一族取色:底不是纯
黑而是带一点青的石板灰,面板逐级提亮 3 档,强调色取终端绿系。
- **收敛**:中性 5 档L 0.09 / 0.13 / 0.17 / 0.24 / 0.65 / 0.96+ 1 个
有彩色(青绿 #35D6A4chroma 0.13)。
- **论证**:深色界面里最贵的资源是「亮」——把亮度留给三样东西:标题、
截图、路径条末端那个真正要点的按钮。其余全部压在 0.09-0.24 之间,读
者的眼睛自然被推到该看的地方。青绿而不是蓝紫:蓝紫在深底上是「廉价 AI
科技风」的签名色,青绿在终端/编辑器语境里有真实出处。
### 负约束(本模板明令不做的事)
- **不用蓝紫渐变、不用外发光。** 深色 + 发光是廉价科技风的固定搭配,本
模板的层级全部由**面板明度**给,一处发光都没有。
- 不画伪 UI。除了那一个截图位不虚构窗口标题栏、假按钮、假代码——虚构
的界面细节永远不可读,读者一眼就知道是假的。
- 不用 emoji 当图标;图标只用单色线性 lucide且只出现在路径分隔与占位。
- 不做圆角以外的装饰形状(不加斜切、不加网格纹理、不加扫描线)。
- 步骤最多 3 条。第 4 条就该拆成第二张卡,不缩字号硬塞。
- 说明句不写「点击此处即可轻松完成」这类零信息量的套话,每句都要带一个
可验证的具体值(倍数、格式、勾选项)。
硬契约:
- 内容距边缘 ≥72px这里 72
- 配色全部走 color_vars换主色只需改 c-accent 与 c-accent-ink 两个
- 正文与背景对比度 ≥2.0(实测表见文件末尾)
- **CJK 行高比西文全线高 0.2**display 1.2,标题 1.35,正文 1.7
- **CJK 负字距不超过 -0.02em**64px 标题 → -1.2px = -0.019em
- 汉字走 Noto Sans SC数字与拉丁走 Inter等价于「西文在前中文在后」的
fallback 链,在 .op 里按节点写死)
- 顶层 frame 显式写 x/y
- 截图位实测 936×3872.42:1——改上下任何一块的高度都要重量
- 截图位是 frame合法图片拖放目标提示做成它的子节点拖图后整块占位
连提示一起被替换
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from oplib import (Ids, color_vars, frame, icon_font, solid, stroke, text,
write_doc)
ids = Ids()
VARS = color_vars({
"c-bg": "#14171A",
"c-panel": "#1D2126",
"c-card": "#262C33",
"c-line": "#3A424B",
"c-ink": "#F3F6F8",
"c-muted": "#9AA6B0",
"c-accent": "#35D6A4",
# 强调块上的字。深底模板最容易翻车的一处:把 c-ink 直接放到 accent 上
# 只有 1.9:1。这里给一个专用的深字色。
"c-accent-ink": "#08231B",
})
CJK = "Noto Sans SC"
NUM = "Inter"
W, H = 1080, 1350
EDGE = 72
LH_DISPLAY, LH_HEAD, LH_BODY = 1.2, 1.35, 1.7
# 菜单路径。末尾那一段是真正要点的东西,所以只有它反白成实心 chip。
PATH = ["文件", "导出为", "PNG · 2x"]
# (序号, 一句话动作, 带具体值的说明)
STEPS = [
("1", "先把画板选中,再打开导出",
"没选中画板时导出的是整个画布,边上会多出一圈空白。"),
("2", "倍数选 2x格式选 PNG",
"1x 发出去在手机上发虚JPG 会把文字边缘压出灰边。"),
("3", "勾掉「包含背景」再导",
"留白交给发布平台去铺,导出时带底色会和它的底打架。"),
]
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=20, 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 chip(label, *, bg, fg, weight=600, size=26):
node = frame(ids, "路径段", width="fit_content", height="fit_content",
layout="horizontal", padding=[12, 22], cornerRadius=10,
alignItems="center", justifyContent="center", fill=solid(bg))
node["children"] = [
text(ids, "路径段文字", label, size, weight, fg, family=CJK,
width="fit_content", growth="auto", line_height=1.4),
]
return node
# ------------------------------------------------------------------ 页头
def header():
tag = frame(ids, "档位标签", width="fit_content", height="fit_content",
layout="horizontal", padding=[10, 20], cornerRadius=999,
alignItems="center", justifyContent="center",
fill=solid("$c-card"))
tag["children"] = [
text(ids, "档位文字", "操作教程 · 共 3 步", 24, 600, "$c-accent",
family=CJK, width="fit_content", growth="auto", line_height=1.4),
]
return col("页头", [
tag,
text(ids, "主标题", "三步把设计稿\n导成能直接发的图", 64, 700,
"$c-ink", family=CJK, line_height=LH_DISPLAY, spacing=-1.2),
text(ids, "副标题", "菜单藏得深,记住下面这条路径就够了。", 26, 400,
"$c-muted", family=CJK, line_height=LH_BODY),
], gap=20)
# ------------------------------------------------------------------ 路径条
def path_bar():
kids = []
for index, seg in enumerate(PATH):
if index:
kids.append(icon_font(ids, "路径分隔", "chevron-right", 22,
"$c-muted"))
last = index == len(PATH) - 1
kids.append(chip(seg,
bg="$c-accent" if last else "$c-panel",
fg="$c-accent-ink" if last else "$c-ink"))
bar = row("菜单路径条", kids, gap=12, padding=[16, 20])
bar["fill"] = solid("$c-panel")
bar["cornerRadius"] = 16
bar["stroke"] = stroke("$c-line", 2)
return bar
# ------------------------------------------------------------------ 截图位
def shot():
node = frame(ids, "截图占位", width="fill_container",
height="fill_container", layout="vertical", gap=16,
alignItems="center", justifyContent="center",
cornerRadius=20, fill=solid("$c-card"),
stroke=stroke("$c-line", 2))
node["children"] = [
icon_font(ids, "占位图标", "image", 56, "$c-muted"),
text(ids, "占位标题", "把导出面板的截图拖进来", 30, 600, "$c-ink",
family=CJK, align="center", line_height=LH_HEAD),
text(ids, "占位规格", "占位比例 2.4:1导出前记得关掉通知",
24, 400, "$c-muted", family=CJK, align="center",
line_height=LH_BODY),
]
return node
# ------------------------------------------------------------------ 步骤
def step_row(no, title, desc):
box = frame(ids, "序号底", width=48, height=48, layout="horizontal",
alignItems="center", justifyContent="center", cornerRadius=12,
fill=solid("$c-panel"))
box["children"] = [
text(ids, "序号", no, 26, 700, "$c-accent", family=NUM,
width="fit_content", growth="auto", line_height=1.0),
]
return row("步骤", [
box,
col("步骤文案", [
text(ids, "步骤标题", title, 28, 600, "$c-ink", family=CJK,
line_height=LH_HEAD),
text(ids, "步骤说明", desc, 24, 400, "$c-muted", family=CJK,
line_height=LH_BODY),
], gap=6),
], gap=18, align="start")
def steps():
return col("步骤区", [step_row(*s) for s in STEPS], gap=18)
# ------------------------------------------------------------------ 页脚
def footer():
return row("页脚", [
text(ids, "账号名", "@ 你的账号名", 24, 600, "$c-muted", family=CJK,
width="fit_content", growth="auto", line_height=1.4),
text(ids, "用法提示", "深色块是图片位,直接把截图拖上去", 22, 400,
"$c-muted", family=CJK, width="fit_content", growth="auto",
line_height=1.4),
], gap=16, align="center", justifyContent="space_between")
def build():
page = frame(ids, "软件操作步骤卡", width=W, height=H, layout="vertical",
padding=[64, EDGE], gap=32, fill=solid("$c-bg"),
clipContent=True)
page["children"] = [header(), path_bar(), shot(), steps(), footer()]
page["x"], page["y"] = 0, 0
return [page]
# 对比度WCAG 相对亮度比op-design-lint 门槛 2.0;数值由脚本实测):
# c-ink on c-bg 16.58 c-muted on c-bg 7.24
# c-ink on c-panel 14.91 c-muted on c-panel 6.52
# c-ink on c-card 12.98 c-muted on c-card 5.67
# c-accent on c-bg 9.69 c-accent on c-panel 8.71
# c-accent on c-card 7.59 c-accent-ink on c-accent 8.93
# 承载正文的最低一对是 5.67(占位规格压在 c-card 上),仍高于 AA 正文
# 门槛 4.5。c-line 只用于描边,是非文字图形,不参与文字对比度。
if __name__ == "__main__":
write_doc(sys.argv[1], VARS, build(), "软件操作步骤卡 · 4:5 单卡")