Two carousel templates stack a full-bleed transparent shell (washi tape, punch holes, film sprockets) in front of their content. Every click over the board landed on that shell instead of the text behind it. The engine half is the jian bump: a container that paints nothing no longer claims its own body in the hit-test. The template half marks those shells `locked`, so the shell is unselectable as a layer while the marks inside it stay individually selectable — dragging a piece of tape no longer drags the whole layer, and a marquee no longer nets the board. Both templates are regenerated output, so they also carry the row wrap their generators gained in the same pass. Claude-Session: https://claude.ai/code/session_01FqKQqNj8exYwopGDpYUU7x
519 lines
24 KiB
Python
519 lines
24 KiB
Python
#!/usr/bin/env python3
|
||
"""tutorial-journal-carousel.op — 教程轮播(6 板 · 1080×1440 · 3:4)
|
||
|
||
叙事类型:**教程**,但和信息长图那一档的教程不是一回事。长图的教程可以把
|
||
八个步骤铺在一屏里让人上下找;轮播的教程必须**一板一步**——读者的手指就
|
||
是进度条,划一次等于做完一步。所以本套的节奏是「钩子 → 备料 → 第一步做
|
||
给你看 → 剩下三步递进 → 一句提醒 → 三个动作」,每板结束时读者手上都该多
|
||
出一个已完成的动作。
|
||
|
||
这也是本套和 `tutorial` 档模板的分野:那边是「查得到」,这边是「跟得上」。
|
||
|
||
### 主题:T6 奶油手帐 `cream-journal`(亮 / 安静 / 生活·教程)
|
||
|
||
**采样 → 收敛 → 论证**(沿用 card-system-0808 §3 T6)
|
||
|
||
- 采样:奶油色内页纸、三种低饱和纸胶带(杏 / 鼠尾草绿 / 淡紫)、2B 铅笔
|
||
灰、缝线的锈红。
|
||
- 收敛:底 = 奶油 L0.955 C0.022;三支胶带色 L 全锁在 0.86、chroma
|
||
0.045-0.070;中性 = 铅笔灰序列;点睛 = 锈红。
|
||
- 论证:**三支胶带色的明度锁死在同一档 L0.86** —— 胶带是同一种材料的不
|
||
同颜色,明度不齐就变成「三个色块」而不是「三条胶带」。材料一致性优先
|
||
于色彩对比,这是本主题的核心决定。选它承载教程:手帐是中文语境里
|
||
「一个人把方法记下来给自己看」的形状,教程要的正是这种「可照抄」的
|
||
亲切,而不是课堂的权威感。
|
||
|
||
**最近邻论证**:库里最近的是 `knowledge-card-vertical`(暖纸 #FFF7F0 +
|
||
单一朱红强调)。两者都是暖亮底,分野在**装饰语言**:那张卡整页零装饰、
|
||
靠序号圆和短线立层级;本套的层级由胶带、活页孔、缝线这套纸质语汇承担,
|
||
且底色 #F6F0E0 比它更黄一档(C0.022 vs C0.012)。缩略图里一张是「白卡」,
|
||
一张是「本子」。
|
||
|
||
### 母版规则(六板的硬约束)
|
||
|
||
**每板固定,不许变**
|
||
1. 画幅 1080×1440,安全边距 左右 80 / 上 96 / 下 128。左侧另留 56px 给
|
||
活页孔,所以正文实际左界是 136。
|
||
2. **活页孔**:左缘 6 个直径 28 的孔,间距 96,纵向居中。六板一个不差 ——
|
||
它是本套最强的一致性信号,也是「这是同一个本子的六页」的物理依据。
|
||
3. 页眉:左「跟着做 · 六板」,右「NN / 06」(Inter,Caption 32px)。
|
||
4. 页脚:一道 14 段的**缝线**(锈红短虚线)+ 署名。
|
||
5. 字族:汉字 Noto Sans SC / 数字与页码 Inter。
|
||
6. 每板 1-2 条胶带,旋转角只在 1-3°,且**不可平行等距**。
|
||
|
||
**允许变,且只有这些能变**
|
||
- 主体区的信息结构(下面六板各用一条配方)。
|
||
- 当板用哪支胶带色、贴在哪、转几度。
|
||
- **锈红点睛**:每板最多一处(页脚缝线不计,它是母版)。
|
||
|
||
### 配方编排(card-system-0808 §4.2)
|
||
|
||
01 A1 巨字压顶 → 02 C3 勾选清单 → 03 F2 图注混排
|
||
04 C2 卡片阶梯 → 05 B3 高亮切词 → 06 G2 三键引导
|
||
|
||
首板 A 族、末板 G 族;相邻两板不同族;覆盖 A/C/F/B/G 五族;B 族出现 1 次
|
||
且落在后 1/3(第 5 板)。C 族出现 2 次但不相连(第 2、4 板)。
|
||
本主题禁配 A2 / D 族 / E3 / C5,编排已全部避开。
|
||
|
||
### 负约束(本模板明令不做的事)
|
||
|
||
- **胶带不可平行等距**。两条胶带若角度相同、间距相同,纸质感立刻塌成
|
||
「模板里的两个装饰条」——手贴的东西不会那么齐。
|
||
- **旋转角只在 1-3°**。超过 3° 就从「随手贴」变成「歪了」,那是失误不是
|
||
质感。
|
||
- 不用手绘涂鸦、不用贴纸 emoji、不用彩虹配色。三支胶带色一板最多用两支。
|
||
- 不用阴影堆出来的「立体便签」。纸的层次由明度差和胶带给,不由投影给。
|
||
- 不做流程图与箭头链(C5 是本主题禁配)——手帐不画流程图,它列清单。
|
||
- 不写「亲测有效 / 保姆级 / 一看就会」这类标题党词。
|
||
- 每板正文不超过 4 行。
|
||
|
||
硬契约:
|
||
- 字号下限 32px;单板最多用 4 档字阶。
|
||
- CJK 行高:Display 1.15 / Title 1.3 / Body 1.7 / Caption 1.5。
|
||
- CJK 字距恒为 0。
|
||
- 正文与背景对比度 ≥2.0(实测表见文件末尾)。
|
||
- 顶层 frame 必须显式写 x/y。
|
||
- 文本节点绝不写 height。
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
|
||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
|
||
from oplib import (Ids, PLACEHOLDER_DISC, PLACEHOLDER_ICON, PLACEHOLDER_SPEC,
|
||
PLACEHOLDER_TITLE, color_vars, frame, group, icon_font,
|
||
path, rect, solid, text, upload_disc, write_doc)
|
||
|
||
ids = Ids()
|
||
|
||
VARS = color_vars({
|
||
"c-bg": "#F6F0E0", # cream 奶油内页
|
||
"c-deep": "#EAE1CB", # cream.deep 卡片 / 内页面
|
||
"c-apricot": "#F3C7A2", # tape.apricot 胶带 A
|
||
"c-sage": "#C2D9BD", # tape.sage 胶带 B
|
||
"c-lilac": "#D8CEEE", # tape.lilac 胶带 C
|
||
"c-ink": "#39342F", # ink.pencil 主文字
|
||
"c-soft": "#69625B", # ink.soft 次级文字
|
||
"c-rust": "#A65438", # thread.rust 点睛:一板一处
|
||
})
|
||
|
||
CJK = "Noto Sans SC"
|
||
NUM = "Inter"
|
||
|
||
W, H, GAP = 1080, 1440, 120
|
||
|
||
# 3 板一行 —— 与 deck 体系(deckkit.BOARDS_PER_ROW)同一约定:多板模板在画布上
|
||
# 分行铺开,而不是拖成一长排。行间距比列间距多 240 不是手滑:画布在帧上方以
|
||
# **屏幕空间**固定偏移画帧名,缩到能整屏看时 120 文档像素只剩十几个屏幕像素,
|
||
# 第二行的帧名会压到上一行的板上。
|
||
BOARDS_PER_ROW = 3
|
||
ROW_GAP = GAP + 240
|
||
EDGE = 80
|
||
RING_GUTTER = 56 # 活页孔占掉的左侧宽度
|
||
TEXT_EDGE = EDGE + RING_GUTTER
|
||
TOP, BOT = 96, 128
|
||
INNER = W - TEXT_EDGE - EDGE # 884
|
||
COLUMN, GUTTER = 62, 16
|
||
|
||
FS_DISPLAY, FS_T1, FS_T2 = 88, 64, 48
|
||
FS_BODY, FS_CAPTION = 40, 32
|
||
LH_DISPLAY, LH_TITLE, LH_BODY, LH_CAPTION = 1.15, 1.3, 1.7, 1.5
|
||
|
||
SERIES = "跟着做 · 六板"
|
||
TOTAL = 6
|
||
RING_D, RING_STEP, RING_N = 28, 96, 6
|
||
|
||
|
||
# ----------------------------------------------------------------- 结构原语
|
||
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=24, align="center", width="fill_container",
|
||
justify="start", **props):
|
||
node = frame(ids, name, width=width, height="fit_content",
|
||
layout="horizontal", gap=gap, alignItems=align,
|
||
justifyContent=justify, fill=[], **props)
|
||
node["children"] = children
|
||
return node
|
||
|
||
|
||
def caption(name, content, color="$c-soft", *, family=CJK, weight=400,
|
||
width="fit_content"):
|
||
return text(ids, name, content, FS_CAPTION, weight, color, family=family,
|
||
width=width, growth="auto" if width == "fit_content"
|
||
else "fixed-width", line_height=LH_CAPTION)
|
||
|
||
|
||
def body(name, content, color="$c-soft", *, weight=400,
|
||
width="fill_container"):
|
||
return text(ids, name, content, FS_BODY, weight, color, family=CJK,
|
||
width=width, growth="auto" if width == "fit_content"
|
||
else "fixed-width", line_height=LH_BODY)
|
||
|
||
|
||
def sheet(name, children, *, gap=20, pad=(32, 34), fill="$c-deep",
|
||
width="fill_container"):
|
||
"""内页面。纸上贴的一张纸,靠明度差立层级,不靠阴影。"""
|
||
node = frame(ids, name, width=width, height="fit_content",
|
||
layout="vertical", gap=gap, padding=list(pad),
|
||
cornerRadius=6, alignItems="start", fill=solid(fill))
|
||
node["children"] = children
|
||
return node
|
||
|
||
|
||
# ----------------------------------------------------------------- 纸质语汇
|
||
def tape_path_d(w, h, teeth=5, depth=9):
|
||
"""一条纸胶带的轮廓:上下两边平直,左右两端各 5 齿锯齿。
|
||
|
||
锯齿走 `path` 的 `d` 而不是拿几个小方块拼 —— 拼出来的齿是独立节点,用
|
||
户拖动胶带时会散架;一条 path 是一个整体,怎么挪都还是一条胶带。
|
||
"""
|
||
step = h / teeth
|
||
pts = [f"M 0 0", f"L {w} 0"]
|
||
for i in range(teeth): # 右端:外→内交替
|
||
y = round(step * (i + 1), 2)
|
||
x = w - depth if i % 2 == 0 else w
|
||
pts.append(f"L {x} {y}")
|
||
pts.append(f"L 0 {h}")
|
||
for i in range(teeth): # 左端:回程同样交替
|
||
y = round(h - step * (i + 1), 2)
|
||
x = depth if i % 2 == 0 else 0
|
||
pts.append(f"L {x} {y}")
|
||
pts.append("Z")
|
||
return " ".join(pts)
|
||
|
||
|
||
# 正文柱:胶带层压在内容之上,所以胶带**绝不能**落进这个矩形。
|
||
CONTENT_BOX = (TEXT_EDGE, TOP, W - EDGE, H - BOT)
|
||
# 1-3° 旋转会让长边的端点往外甩 w·sin(3°);300 宽的胶带甩出约 16px,留 24。
|
||
TILT_SLACK = 24
|
||
|
||
|
||
def tape(name, x, y, w, h, color, angle):
|
||
"""一条胶带。半透明 + 1-3° 旋转,贴在内容层**之上**。
|
||
|
||
透明度用节点 opacity 而不是把颜色调淡:胶带压住的是纸,纸纹要透出来才
|
||
像胶带。底色不受影响 —— lint 量对比度只往祖先链上走,装饰兄弟不参与。
|
||
|
||
**位置有硬约束**:胶带层在正文之上,一条贴进正文柱的胶带会直接盖掉字
|
||
(第一版就把第 4 板标题的最后两个字压没了,几何审计还查不出来 —— 它
|
||
只管文字压文字)。真实的本子上胶带也本来就贴在页边、压住纸角,所以这
|
||
里把它钉死在正文柱外,并在构建时断言,让「贴歪了」变成构建失败而不是
|
||
发出去之后才发现。
|
||
"""
|
||
left, top, right, bottom = CONTENT_BOX
|
||
box = (x - TILT_SLACK, y - TILT_SLACK, x + w + TILT_SLACK,
|
||
y + h + TILT_SLACK)
|
||
assert box[2] <= left or box[0] >= right or box[3] <= top \
|
||
or box[1] >= bottom, f"{name} 压进正文柱:{box} vs {CONTENT_BOX}"
|
||
assert 1 <= abs(angle) <= 3, f"{name} 旋转角 {angle} 超出 1-3°"
|
||
node = path(ids, name, tape_path_d(w, h), width=w, height=h,
|
||
fill=solid(color))
|
||
node["x"], node["y"] = x, y
|
||
node["rotation"] = angle
|
||
node["opacity"] = 0.85
|
||
return node
|
||
|
||
|
||
def ring_holes():
|
||
"""活页孔:左缘 6 个孔,纵向居中。六板逐板重建,位置由常量算出,不手写。"""
|
||
total = RING_D + RING_STEP * (RING_N - 1)
|
||
top = round((H - total) / 2)
|
||
holes = []
|
||
for i in range(RING_N):
|
||
hole = {
|
||
"type": "ellipse", "id": ids("e"), "name": f"活页孔 {i + 1}",
|
||
"width": RING_D, "height": RING_D, "fill": solid("$c-bg"),
|
||
"stroke": {"thickness": 2, "fill": solid("$c-deep")},
|
||
"x": round(EDGE - RING_D / 2 + 6), "y": top + RING_STEP * i,
|
||
}
|
||
holes.append(hole)
|
||
return holes
|
||
|
||
|
||
def stitch_line(color="$c-rust", segments=14):
|
||
"""缝线:14 段短虚线排成一行。
|
||
|
||
不用 `line` 节点加 dashPattern —— 那是描边属性,在 flex 行里宽度行为不
|
||
可预期。14 个等宽小方块用 space_between 排开,是纯 flex 的、量得准的。
|
||
"""
|
||
dashes = [rect(ids, f"缝 {i + 1}", width=22, height=3,
|
||
fill=solid(color)) for i in range(segments)]
|
||
return row("缝线", dashes, gap=0, justify="space_between",
|
||
width="fill_container")
|
||
|
||
|
||
# ----------------------------------------------------------------- 母版部件
|
||
def header(page):
|
||
return row("页眉", [
|
||
caption("系列名", SERIES, "$c-soft"),
|
||
caption("页码", f"{page:02d} / {TOTAL:02d}", "$c-soft", family=NUM),
|
||
], gap=24, justify="space_between")
|
||
|
||
|
||
def footer():
|
||
return col("页脚", [
|
||
stitch_line(),
|
||
caption("账号名", "@ 你的账号名", "$c-soft", weight=600),
|
||
], gap=20)
|
||
|
||
|
||
def board(page, name, main, tapes):
|
||
"""一板。活页孔在最底层,正文居中,胶带贴在最上层。
|
||
|
||
这里没用 oplib.stack:stack 只给「内容 + 一层装饰(在下)」,而胶带按
|
||
定义必须压在内容**上面**。所以外壳自己搭三层,靠 children 顺序定 z ——
|
||
jian 的兄弟顺序是 index 0 最上(canvas_viewport_paint_mask 反着遍历)。
|
||
胶带层用 group:group 不是图片投放目标(image_drop.rs 不匹配 Group),
|
||
所以它盖在整板上也不会截走用户拖进来的图。
|
||
|
||
两个装饰层都上 `locked`:它们是 fill_container 的满幅壳子,压在正文之
|
||
上,但自己一寸也不画(真正的墨在里面那几条胶带 / 六个孔上)。locked 让
|
||
壳子自身不可选,壳内的胶带和孔照常可选(hit_test_walk 的 locked 判定在
|
||
子节点遍历之后),于是拖动装饰不会拽走整层,框选也不会一网兜住整板。
|
||
"""
|
||
content = frame(ids, f"{name} · 内容", width="fill_container",
|
||
height="fill_container", layout="vertical",
|
||
padding=[TOP, EDGE, BOT, TEXT_EDGE], gap=0,
|
||
alignItems="start", fill=[])
|
||
content["children"] = [header(page), main, footer()]
|
||
|
||
tape_layer = group(ids, f"{name} · 胶带", width="fill_container",
|
||
height="fill_container", layout="none", fill=[],
|
||
locked=True)
|
||
tape_layer["children"] = tapes
|
||
|
||
holes = frame(ids, f"{name} · 活页孔", width="fill_container",
|
||
height="fill_container", layout="none", fill=[],
|
||
locked=True)
|
||
holes["children"] = ring_holes()
|
||
|
||
shell = frame(ids, f"{page:02d} {name}", width=W, height=H, layout="none",
|
||
fill=solid("$c-bg"), clipContent=True)
|
||
shell["children"] = [tape_layer, content, holes]
|
||
shell["x"] = ((page - 1) % BOARDS_PER_ROW) * (W + GAP)
|
||
shell["y"] = ((page - 1) // BOARDS_PER_ROW) * (H + ROW_GAP)
|
||
return shell
|
||
|
||
|
||
def zone(children, *, justify="center", gap=32, pad_y=48):
|
||
node = frame(ids, "主体", width="fill_container", height="fill_container",
|
||
layout="vertical", gap=gap, padding=[pad_y, 0],
|
||
justifyContent=justify, alignItems="start", fill=[])
|
||
node["children"] = children
|
||
return node
|
||
|
||
|
||
# ------------------------------------------------------- 01 封面 · A1 巨字压顶
|
||
def cover():
|
||
"""A1:主标压顶占约 38%,副标与眉标在下。一板一个钩子,不并列卖点。"""
|
||
main = zone([
|
||
caption("眉标", "四步整理法 · 每板一步", "$c-rust", weight=600),
|
||
text(ids, "主标", "十分钟\n把桌面清空", FS_DISPLAY, 700, "$c-ink",
|
||
family=CJK, width=INNER, growth="fixed-width",
|
||
line_height=LH_DISPLAY),
|
||
body("副标", "不是收纳技巧,是一套决定东西去哪的顺序。", "$c-soft"),
|
||
], justify="center", gap=36)
|
||
return board(1, "封面 · 巨字压顶", main, [
|
||
tape("胶带 · 杏", 742, -48, 300, 84, "$c-apricot", 2),
|
||
tape("胶带 · 淡紫", -58, 1336, 214, 76, "$c-lilac", -3),
|
||
])
|
||
|
||
|
||
# ------------------------------------------------------- 02 备料 · C3 勾选清单
|
||
CHECKS = [
|
||
"一个空纸箱,装待定的东西",
|
||
"一个垃圾袋,装确定要扔的",
|
||
"一块抹布,最后一步用",
|
||
"一个计时器,定十分钟",
|
||
"把手机放到另一个房间",
|
||
]
|
||
|
||
|
||
def prep():
|
||
"""C3:单列勾选框,5 条,每条 1 行。引导符一板只用一种 —— 这里是方框。"""
|
||
items = []
|
||
for line in CHECKS:
|
||
box = rect(ids, "勾选框", width=36, height=36, cornerRadius=4,
|
||
fill=[], stroke={"thickness": 3, "fill": solid("$c-ink")})
|
||
items.append(row("清单项", [
|
||
box,
|
||
body("清单文字", line, "$c-ink", width=INNER - 36 - 24 - 68),
|
||
], gap=24, align="center"))
|
||
|
||
main = zone([
|
||
text(ids, "小标题", "开始前,先备这五样", FS_T1, 700, "$c-ink",
|
||
family=CJK, line_height=LH_TITLE),
|
||
sheet("备料清单", [col("清单", items, gap=22)], pad=(34, 34)),
|
||
], justify="center", gap=40)
|
||
return board(2, "备料 · 勾选清单", main, [
|
||
tape("胶带 · 鼠尾草", 604, 1338, 336, 80, "$c-sage", -2),
|
||
])
|
||
|
||
|
||
# ------------------------------------------------------- 03 第一步 · F2 图注混排
|
||
def step_one():
|
||
"""F2:视觉块占约 44%,下面是标题 + 正文 + 图注。图位是内容不是装饰。"""
|
||
disc = upload_disc(ids, "上传占位", 116, PLACEHOLDER_DISC, 42,
|
||
PLACEHOLDER_ICON)
|
||
hint = col("占位说明", [
|
||
text(ids, "占位标题", "拍一张开始前的桌面", FS_BODY, 600,
|
||
PLACEHOLDER_TITLE, family=CJK, width="fit_content",
|
||
growth="auto", line_height=LH_TITLE),
|
||
text(ids, "占位规格", "建议 884×580 以上", FS_CAPTION, 400,
|
||
PLACEHOLDER_SPEC, family=CJK, width="fit_content",
|
||
growth="auto", line_height=LH_CAPTION),
|
||
], gap=8, width="fit_content", align="center")
|
||
slot = frame(ids, "照片位", width="fill_container", height=580,
|
||
layout="vertical", gap=22, alignItems="center",
|
||
justifyContent="center", cornerRadius=6,
|
||
fill=solid("$c-deep"))
|
||
slot["children"] = [disc, hint]
|
||
|
||
main = zone([
|
||
slot,
|
||
col("图注区", [
|
||
text(ids, "步骤标题", "第一步 · 全部拿下来", FS_T2, 700,
|
||
"$c-ink", family=CJK, line_height=LH_TITLE),
|
||
body("正文", "桌面清成一块空板,连你觉得本来就该在那儿的几样也拿"
|
||
"下来。留着的东西会替你做决定。", "$c-ink"),
|
||
caption("图注", "拍下来 · 十分钟后你会想看这张", "$c-soft"),
|
||
], gap=14),
|
||
], justify="center", gap=32)
|
||
return board(3, "第一步 · 图注混排", main, [
|
||
tape("胶带 · 杏", -62, -44, 262, 78, "$c-apricot", 3),
|
||
])
|
||
|
||
|
||
# ------------------------------------------------------- 04 递进 · C2 卡片阶梯
|
||
LADDER = [
|
||
("第二步", "分三堆", "常用、待定、不要。三堆,没有第四堆。",
|
||
"$c-bg"),
|
||
("第三步", "先处理不要那堆", "直接进垃圾袋封口,不要再看第二眼。",
|
||
"$c-deep"),
|
||
("第四步", "常用的才回桌面", "待定那箱封好,一个月没开就扔。",
|
||
"$c-apricot"),
|
||
]
|
||
|
||
|
||
def ladder():
|
||
"""C2:三张卡纵排,逐张左缩进一列、底色加深一档 —— 缩进本身就是递进。"""
|
||
cards = []
|
||
for index, (label, title, note, fill) in enumerate(LADDER):
|
||
card = sheet(f"阶梯卡 {index + 1}", [
|
||
caption("阶梯标签", label, "$c-rust" if index == 2 else "$c-soft",
|
||
weight=600),
|
||
text(ids, "阶梯标题", title, FS_T2, 700, "$c-ink", family=CJK,
|
||
line_height=LH_TITLE),
|
||
body("阶梯注解", note, "$c-soft"),
|
||
], gap=12, pad=(28, 30), fill=fill)
|
||
if fill == "$c-bg":
|
||
card["stroke"] = {"thickness": 2, "fill": solid("$c-deep")}
|
||
wrap = row(f"阶梯行 {index + 1}", [card], gap=0, align="start")
|
||
if index:
|
||
wrap["padding"] = [0, 0, 0, (COLUMN + GUTTER) * index]
|
||
cards.append(wrap)
|
||
|
||
main = zone([
|
||
text(ids, "小标题", "剩下三步,一层比一层狠", FS_T1, 700, "$c-ink",
|
||
family=CJK, line_height=LH_TITLE),
|
||
col("阶梯", cards, gap=20),
|
||
], justify="center", gap=36)
|
||
return board(4, "递进 · 卡片阶梯", main, [
|
||
tape("胶带 · 淡紫", 1032, 470, 252, 74, "$c-lilac", -2),
|
||
])
|
||
|
||
|
||
# ------------------------------------------------------- 05 提醒 · B3 高亮切词
|
||
def reminder():
|
||
"""B3:被击中的两个字先出来,整句在后,注解压到 Caption。
|
||
|
||
高亮在本主题里只有一种合法形态 —— 一小段胶带压在词底下。这里把它做成
|
||
行内的一个 `fit_content` 容器,而不是装饰层的绝对定位,因为它属于内容。
|
||
"""
|
||
hit = frame(ids, "被击中的词", width="fit_content", height="fit_content",
|
||
layout="horizontal", padding=[14, 26], cornerRadius=4,
|
||
alignItems="center", justifyContent="center",
|
||
fill=solid("$c-sage"))
|
||
hit["children"] = [
|
||
text(ids, "词", "待定", FS_DISPLAY, 700, "$c-ink", family=CJK,
|
||
width="fit_content", growth="auto", line_height=1.0),
|
||
]
|
||
|
||
main = zone([
|
||
hit,
|
||
text(ids, "整句", "最容易漏掉的是那一箱。", FS_T1, 700,
|
||
"$c-ink", family=CJK, width=INNER, growth="fixed-width",
|
||
line_height=LH_TITLE),
|
||
caption("注解", "在箱子上写好日期,一个月后不用再犹豫。", "$c-soft",
|
||
width=INNER),
|
||
], justify="center", gap=40)
|
||
return board(5, "提醒 · 高亮切词", main, [
|
||
tape("胶带 · 杏", 812, -42, 282, 76, "$c-apricot", 2),
|
||
])
|
||
|
||
|
||
# ------------------------------------------------------- 06 收尾 · G2 三键引导
|
||
ACTIONS = [
|
||
("bookmark", "收藏", "留着下次照做"),
|
||
("message-circle", "评论", "说说卡在第几步"),
|
||
("user-plus", "关注", "下一板讲抽屉"),
|
||
]
|
||
|
||
|
||
def closing():
|
||
"""G2:三块等分、等高,三个动作必须**不同**(收藏 / 评论 / 关注)。"""
|
||
blocks = []
|
||
for glyph, label, note in ACTIONS:
|
||
block = frame(ids, f"动作 · {label}", width="fill_container",
|
||
height="fill_container", layout="vertical", gap=14,
|
||
padding=[28, 20], cornerRadius=6, alignItems="center",
|
||
justifyContent="center", fill=solid("$c-deep"))
|
||
block["children"] = [
|
||
icon_font(ids, "动作图标", glyph, 44, "$c-ink"),
|
||
text(ids, "动作名", label, FS_T2, 700, "$c-ink", family=CJK,
|
||
width="fit_content", growth="auto", line_height=1.0),
|
||
text(ids, "动作说明", note, FS_CAPTION, 400, "$c-soft",
|
||
family=CJK, width="fill_container", growth="fixed-width",
|
||
align="center", line_height=LH_CAPTION),
|
||
]
|
||
blocks.append(block)
|
||
|
||
main = zone([
|
||
text(ids, "收束句", "先清空,\n再决定放什么回去。", FS_DISPLAY, 700,
|
||
"$c-ink", family=CJK, width=INNER, growth="fixed-width",
|
||
line_height=LH_DISPLAY),
|
||
row("三键", blocks, gap=20, align="start"),
|
||
], justify="center", gap=52)
|
||
return board(6, "收尾 · 三键引导", main, [
|
||
tape("胶带 · 鼠尾草", 386, 1340, 320, 82, "$c-sage", -3),
|
||
])
|
||
|
||
|
||
def build():
|
||
return [cover(), prep(), step_one(), ladder(), reminder(), closing()]
|
||
|
||
|
||
# 对比度(WCAG 相对亮度比,op-design-lint 门槛 2.0;本表逐对实算):
|
||
# c-ink on c-bg 10.82 c-soft on c-bg 5.28
|
||
# c-rust on c-bg 4.68 c-ink on c-deep 9.45
|
||
# c-soft on c-deep 4.61 c-rust on c-deep 4.09
|
||
# c-ink on c-apricot 7.91 c-soft on c-apricot 3.86
|
||
# c-ink on c-sage 8.18 c-ink on c-lilac 8.20
|
||
# c-deep on c-bg 1.14
|
||
# 承载正文的最低一对是 c-soft on c-apricot 3.86 —— 第 4 板最后一张阶梯卡
|
||
# 的注解,高出 lint 门槛 1.93 倍。其余承载正文处最低 4.61。三支胶带色的
|
||
# 明度锁在同一档 L0.86,所以 c-ink 压在它们上面永远落在 7.9-8.2 这个窄区
|
||
# 间 —— 换胶带色只要守住 L0.86,这张表就不用重量,这正是「材料一致性优先
|
||
# 于色彩对比」那条论证在对比度上的回报。c-deep on c-bg 1.14 低于门槛是对
|
||
# 的:那是纸上叠纸的明度差,属非文字图形,绝不拿它写字。
|
||
|
||
if __name__ == "__main__":
|
||
write_doc(sys.argv[1], VARS, build(), "教程轮播 · 3:4 六板")
|