openpencil/templates/step0/_generators/tpl_minimal_keynote.py
Fini cc9b7889e2 feat(editor): grow the template library to five deck styles and a card tier
Adds a knowledge-card scene (vertical and square social cards) and
four presentation styles beside the original warm-white deck: a dark
pitch deck, a paper-white lecture deck, a minimal keynote and a
gradient-tech deck with glassmorphism built from opaque composited
stops — the linter drops alpha channels, and a truly translucent card
would exempt its text from contrast checking forever. Every palette
pair ships with measured WCAG ratios baked into the generators, and
low-contrast probe documents prove the audit actually fires on each
template family.

Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
2026-08-05 00:47:50 +08:00

208 lines
8.1 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
"""minimal-keynote.op — 极简 Keynote deck8 页 1920×1080
这套的难点是「少」。没有卡片、没有描边、没有底色块,一页只留 2-4 个元素,
版面全靠字阶落差和留白撑。元素越少,任何一处对不齐都无处遁形——所以整套
只有一条左基线EDGE=160每页的内容块都从这条线起排纵向居中。
跟已有三套的区别slide-deck 是暖白商务、pitch-deck-dark 是深底路演、
lecture-deck-light 是纸白课件,它们都靠「卡片」组织信息;这一套刻意不给
任何容器上色,是发布会舞台上那种一句话一屏的节奏。
排版遵循 skills/domains/slides.md 的硬契约:
- 每帧固定 1920×1080绝不 fit_content内容距边 ≥100这里 160
- 正文 ≥24取 28-32标题 ≥40关键数字 80-200数据页取 200
- 行高:展示字 1.05-1.15,正文 1.45+
- 最多 2 个字体族Noto Sans SC 排中文Inter 排数字),靠字重分层
- 强调色只用于强调
白底上的对比度WCAG实测见文件末尾最低一对是 5.33:1全部高于 4.5。
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from oplib import Ids, color_vars, frame, rect, solid, text, write_doc
ids = Ids()
# 纯白 + 近黑 + 一个克制的朱红。
#
# 强调色没有用常见的 #FF5A36那个橙在纯白上只有 3.10:1当文字用不到
# 4.5 的可读线(实测,见文件末尾)。降明度到 #C7340F 后是 5.35:1色相
# 仍在同一族,投影时反而更沉得住。整套只有这一个彩色,出现三次以内。
VARS = color_vars({
"c-bg": "#FFFFFF",
"c-ink": "#0A0A0A",
"c-muted": "#6B6B6B",
"c-accent": "#C7340F",
})
W, H = 1920, 1080
# 留白是这套模板唯一的「装饰」,所以边距给到 160 —— 契约下限 100 的 1.6 倍。
EDGE = 160
CJK = "Noto Sans SC"
NUM = "Inter"
def slide(name, *, gap=40, justify="center"):
"""一帧。固定 1920×1080无底色块、无描边 —— 白就是版面本身。"""
node = frame(ids, name, width=W, height=H, layout="vertical",
padding=[EDGE, EDGE], gap=gap, justifyContent=justify,
alignItems="start", fill=solid("$c-bg"), clipContent=True)
node["children"] = []
return node
def col(name, children, *, gap=16, width="fill_container",
height="fit_content", **props):
"""透明结构容器。这套模板里没有任何一个容器该有 fill。"""
node = frame(ids, name, width=width, height=height, layout="vertical",
gap=gap, alignItems="start", fill=[], **props)
node["children"] = children
return node
def row(name, children, *, gap=32, align="center", **props):
node = frame(ids, name, width="fill_container", height="fit_content",
layout="horizontal", gap=gap, alignItems=align, fill=[],
**props)
node["children"] = children
return node
def accent_rule(width=120, height=8):
"""整套唯一的图形元素。一条短线,替代所有本该是卡片边框的东西。"""
return rect(ids, "强调短线", width=width, height=height,
fill=solid("$c-accent"))
# ------------------------------------------------------------------ 01 封面
def cover():
lede = col("主张", [
text(ids, "主标题", "把复杂\n留在里面", 130, 700, "$c-ink",
family=CJK, line_height=1.08),
accent_rule(),
text(ids, "副标题", "第七代产品,今天发布。", 32, 400, "$c-muted",
family=CJK, line_height=1.45),
], gap=40)
meta = row("落款", [
text(ids, "场合", "2026 秋季发布会", 26, 500, "$c-ink", family=CJK,
width="fit_content", growth="auto"),
text(ids, "日期", "2026.10.08", 26, 400, "$c-muted", family=NUM,
width="fit_content", growth="auto"),
], gap=28, justifyContent="space_between")
s = slide("01 封面", gap=48, justify="space_between")
s["children"] = [lede, meta]
return s
# ------------------------------------------------------------ 02 一句话论点
def statement():
s = slide("02 论点", gap=44)
s["children"] = [
rect(ids, "强调点", width=18, height=18, cornerRadius=9,
fill=solid("$c-accent")),
text(ids, "论点", "我们删掉了三十七个按钮。", 76, 700, "$c-ink",
family=CJK, line_height=1.15),
]
return s
# ------------------------------------------------------------- 03-05 词页
# 一页一个字。注解只有一行,写不下就说明这个词选错了。
WORDS = [
("", "从三十七个按钮,到三个。"),
("", "整机一点零九公斤,比上一代少了两百四十克。"),
("", "一次充电,用满两天。"),
]
def word_page(index, word, note):
s = slide(f"0{index + 3} 词 · {word}", gap=36)
s["children"] = [
text(ids, "大词", word, 200, 700, "$c-ink", family=CJK,
line_height=1.05),
text(ids, "注解", note, 30, 400, "$c-muted", family=CJK,
line_height=1.5),
]
return s
# ------------------------------------------------------------------ 06 引用
def quote():
s = slide("06 引用", gap=32)
s["children"] = [
# 引号本身当图形用替掉一切边框和背景块。lineHeight 压到 0.5 是
# 量出来的:引号的墨迹只占 em 盒顶部约三分之一,默认行高会在它下面
# 留出一大片空行盒,看起来就像 gap 没设对(同 oplib 里 `●` 用
# DOT_LINE_HEIGHT 收行盒的做法)。
text(ids, "引号", "", 160, 700, "$c-accent", family=NUM,
line_height=0.5),
text(ids, "引文", "最好的设计,是让人忘了它存在。", 54, 600,
"$c-ink", family=CJK, line_height=1.3),
text(ids, "出处", "— 产品负责人 沈亦文", 28, 400, "$c-muted",
family=CJK, line_height=1.5),
]
return s
# ------------------------------------------------------------- 07 数据一句话
def one_number():
s = slide("07 数据", gap=32)
s["children"] = [
# 契约允许关键数字 80-200这里取上限整页只有它和一行注。
text(ids, "大数字", "0.4 秒", 200, 700, "$c-ink", family=NUM,
line_height=1.05),
text(ids, "注解", "从按下到出图的全部时间,上一代是 2.6 秒。",
32, 400, "$c-muted", family=CJK, line_height=1.5),
]
return s
# ------------------------------------------------------------------ 08 结尾
def closing():
s = slide("08 结尾", gap=40)
s["children"] = [
accent_rule(),
text(ids, "结语", "今天开始发货。", 96, 700, "$c-ink", family=CJK,
line_height=1.12),
text(ids, "补充", "全国门店同步开售,官网可预约体验。", 30, 400,
"$c-muted", family=CJK, line_height=1.5),
]
return s
# 顶层 frame 必须显式写 x/y缺省时每帧都落在原点八页会叠成一页前一批
# 模板 2026-08-02 的实录)。纵向 gap 比横向多留 240是给画布上固定在帧顶部
# 的帧名标签让位——标签是屏幕空间的,不随缩放变小。
BOARD_GAP_X = 120
BOARD_GAP_Y = BOARD_GAP_X + 240
BOARDS_PER_ROW = 3
def build():
boards = [cover(), statement()]
boards += [word_page(i, w, n) for i, (w, n) in enumerate(WORDS)]
boards += [quote(), one_number(), closing()]
for index, board in enumerate(boards):
board["x"] = (index % BOARDS_PER_ROW) * (W + BOARD_GAP_X)
board["y"] = (index // BOARDS_PER_ROW) * (H + BOARD_GAP_Y)
return boards
# 对比度WCAG 相对亮度比,投影场景自设门槛 4.5,远高于 op-design-lint 的 2.5
# c-ink on c-bg 19.80
# c-muted on c-bg 5.33 ← 最低一对
# c-accent on c-bg 5.35
# 换强调色时先量它在纯白上的比值:橙红一族很容易掉到 4.5 以下
# #FF5A36 = 3.10、#E5471F = 4.00 都不够,#C7340F = 5.35 才过)。
if __name__ == "__main__":
write_doc(sys.argv[1], VARS, build(), "极简 Keynote · 16:9")